Compare commits

...

15 Commits

Author SHA1 Message Date
c69694031c test 2025-12-02 18:04:27 +01:00
05dbc38507 test 2025-12-02 18:03:22 +01:00
3233ce1998 test 2025-12-02 18:02:06 +01:00
7a12f25d0a test 2025-12-02 17:57:23 +01:00
bc7e0644e3 test 2025-12-02 17:54:15 +01:00
f6929726f4 test 2025-12-02 17:46:21 +01:00
55c4d88608 test 2025-12-02 17:43:41 +01:00
0f07e73d7d test 2025-12-02 17:41:40 +01:00
0c0b4effea test 2025-12-02 17:35:37 +01:00
ff6500e709 test 2025-12-02 17:34:49 +01:00
Gitea Deploy
eca11a82a9 Fix build script to copy static files 2025-12-02 17:30:01 +01:00
root
e3b67752b5 Test auto-deploy 2025-12-02 16:25:22 +01:00
3dd1e4d576 nTest auto-deploy 2025-12-02 16:05:12 +01:00
ce48826dd8 npm 2025-12-02 15:54:22 +01:00
cf7541b5ff npm 2025-12-02 15:52:23 +01:00
22 changed files with 7855 additions and 7862 deletions

1
.app-type Normal file
View File

@@ -0,0 +1 @@
type=static-fe

View File

@@ -1,42 +1,42 @@
# Virtual environments # Virtual environments
.venv/ .venv/
venv/ venv/
env/ env/
# Python cache # Python cache
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
*.so *.so
.Python .Python
# Git # Git
.git/ .git/
.gitignore .gitignore
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
*.swo *.swo
*~ *~
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Documentation (not needed in container) # Documentation (not needed in container)
*.md *.md
wiki/ wiki/
# Logs # Logs
logs/ logs/
*.log *.log
# Docker # Docker
docker-compose.yml docker-compose.yml
Dockerfile Dockerfile
# Output files (generated during build) # Output files (generated during build)
# *.png # *.png
# *.gv # *.gv

62
.gitignore vendored
View File

@@ -1,32 +1,32 @@
# Virtual Environment # Virtual Environment
.venv/ .venv/
venv/ venv/
env/ env/
# Python # Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
*.so *.so
.Python .Python
node_modules/ node_modules/
# Generated diagram files # Generated diagram files
*.pdf *.pdf
*.gv *.gv
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
*.swo *.swo
*~ *~
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Distribution // packaging # Distribution // packaging
dist/ dist/
build/ build/
*.egg-info/ *.egg-info/

View File

@@ -1,18 +1,23 @@
# Build stage # -- Build stage --
FROM node:20-alpine AS build FROM node:20-alpine AS build
WORKDIR /app
WORKDIR /app
# Alleen package-bestanden eerst (better caching)
# Copy package files COPY package*.json ./
COPY package*.json ./ RUN npm ci || npm install
RUN npm ci
# Dan de rest van de code
COPY . . COPY . .
RUN npm run build
# LET OP: pas dit aan als jouw build-script anders heet
# Runtime stage RUN npm run build
FROM nginx:alpine
# -- Serve stage --
COPY --from=build /app/dist/ /usr/share/nginx/html/ FROM nginx:alpine
WORKDIR /usr/share/nginx/html
EXPOSE 80
# Pas dit aan als jouw outputmap niet 'dist' is (bijv. 'build')
COPY --from=build /app/dist ./
# Optioneel: eigen nginx.conf voor SPA routing
# COPY nginx.conf /etc/nginx/conf.d/default.conf

164
README.md
View File

@@ -1,82 +1,82 @@
# Network Architecture Diagrams # Network Architecture Diagrams
Python scripts to generate network architecture diagrams using the [Diagrams](https://diagrams.mingrammer.com/) library. Python scripts to generate network architecture diagrams using the [Diagrams](https://diagrams.mingrammer.com/) library.
## Prerequisites ## Prerequisites
- Python 3.8+ - Python 3.8+
- Graphviz ([installation guide](https://graphviz.org/download/)) - Graphviz ([installation guide](https://graphviz.org/download/))
### Installing Graphviz ### Installing Graphviz
**Windows:** **Windows:**
```bash ```bash
choco install graphviz choco install graphviz
# or download from https://graphviz.org/download/ # or download from https://graphviz.org/download/
``` ```
**macOS:** **macOS:**
```bash ```bash
brew install graphviz brew install graphviz
``` ```
**Linux:** **Linux:**
```bash ```bash
sudo apt-get install graphviz # Debian/Ubuntu sudo apt-get install graphviz # Debian/Ubuntu
sudo yum install graphviz # RHEL/CentOS sudo yum install graphviz # RHEL/CentOS
``` ```
## Setup ## Setup
1. Create a virtual environment: 1. Create a virtual environment:
```bash ```bash
python -m venv .venv python -m venv .venv
``` ```
2. Activate the virtual environment: 2. Activate the virtual environment:
```bash ```bash
# Windows # Windows
.venv\Scripts\activate .venv\Scripts\activate
# macOS/Linux # macOS/Linux
source .venv/bin/activate source .venv/bin/activate
``` ```
3. Install dependencies: 3. Install dependencies:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
## Usage ## Usage
Run the diagram generation scripts: Run the diagram generation scripts:
```bash ```bash
# Generate LAN architecture diagram # Generate LAN architecture diagram
python lan_architecture.py python lan_architecture.py
# Generate main diagram # Generate main diagram
python main.py python main.py
``` ```
Output files will be generated in the current directory as PNG images. Output files will be generated in the current directory as PNG images.
## Available Diagrams ## Available Diagrams
- `lan_architecture.py` - Home Lab / Auction Stack Architecture diagram - `lan_architecture.py` - Home Lab / Auction Stack Architecture diagram
- `main.py` - Network architecture diagram - `main.py` - Network architecture diagram
## Deployment ## Deployment
To deploy or share this project: To deploy or share this project:
1. Ensure Graphviz is installed on the target system 1. Ensure Graphviz is installed on the target system
2. Clone the repository 2. Clone the repository
3. Follow the setup instructions above 3. Follow the setup instructions above
4. Run the desired diagram script 4. Run the desired diagram script
## Notes ## Notes
- Generated diagram files (`.png`, `.gv`) are excluded from version control - Generated diagram files (`.png`, `.gv`) are excluded from version control
- The diagrams use a left-to-right (`LR`) layout direction - The diagrams use a left-to-right (`LR`) layout direction
- Output files are saved with `show=False` to prevent automatic opening - Output files are saved with `show=False` to prevent automatic opening

View File

@@ -1,26 +1,26 @@
services: services:
viewer: viewer:
build: build:
context: /opt/apps/viewer context: /opt/apps/viewer
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: viewer container_name: viewer
restart: unless-stopped restart: unless-stopped
networks: networks:
- traefik_net - traefik_net
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)" - "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)"
- "traefik.http.routers.viewer.entrypoints=websecure" - "traefik.http.routers.viewer.entrypoints=websecure"
- "traefik.http.routers.viewer.tls=true" - "traefik.http.routers.viewer.tls=true"
- "traefik.http.services.viewer.loadbalancer.server.port=80" - "traefik.http.services.viewer.loadbalancer.server.port=80"
- "traefik.http.routers.viewer-http.rule=Host(`viewer.appmodel.nl`)" - "traefik.http.routers.viewer-http.rule=Host(`viewer.appmodel.nl`)"
- "traefik.http.routers.viewer-http.entrypoints=web" - "traefik.http.routers.viewer-http.entrypoints=web"
- "traefik.http.routers.viewer-http.middlewares=viewer-https" - "traefik.http.routers.viewer-http.middlewares=viewer-https"
- "traefik.http.middlewares.viewer-https.redirectscheme.scheme=https" - "traefik.http.middlewares.viewer-https.redirectscheme.scheme=https"
- "traefik.http.routers.auction.tls.certresolver=letsencrypt", - "traefik.http.routers.auction.tls.certresolver=letsencrypt",
- "traefik.http.middlewares.viewer-https.redirectscheme.permanent=true" - "traefik.http.middlewares.viewer-https.redirectscheme.permanent=true"
networks: networks:
traefik_net: traefik_net:
external: true external: true
name: traefik_net name: traefik_net

View File

@@ -1,218 +1,218 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Home-Lab viewer Mermaid + Kroki (Graphviz)</title> <title>Home-Lab viewer Mermaid + Kroki (Graphviz)</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <style>
:root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd} :root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd}
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)} body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)}
header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-size:1.1rem;font-weight:600} header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-size:1.1rem;font-weight:600}
.wrap{display:flex;height:calc(100vh - 40px)} .wrap{display:flex;height:calc(100vh - 40px)}
.panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd} .panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd}
.panel:last-child{border:none} .panel:last-child{border:none}
h3{margin:.4rem .6rem;font-size:1rem} h3{margin:.4rem .6rem;font-size:1rem}
.btn{margin:.2rem .6rem;padding:.3rem .6rem;border:1px solid #ccc;border-radius:4px;background:#fff;cursor:pointer} .btn{margin:.2rem .6rem;padding:.3rem .6rem;border:1px solid #ccc;border-radius:4px;background:#fff;cursor:pointer}
.btn:hover{background:#eee} .btn:hover{background:#eee}
#editor{flex:1} #editor{flex:1}
.diagram{flex:2;background:#fff;padding:.5rem;overflow:auto} .diagram{flex:2;background:#fff;padding:.5rem;overflow:auto}
svg{max-width:100%;height:auto} svg{max-width:100%;height:auto}
/* mermaid theme tweaks */ /* mermaid theme tweaks */
.mermaid{height:100%;display:flex;align-items:center;justify-content:center} .mermaid{height:100%;display:flex;align-items:center;justify-content:center}
</style> </style>
<!-- ========= 1. LIBRARIES ========= --> <!-- ========= 1. LIBRARIES ========= -->
<!-- Monaco editor --> <!-- Monaco editor -->
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script> <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
<!-- Mermaid --> <!-- Mermaid -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<!-- Kroki client (tiny wrapper) --> <!-- Kroki client (tiny wrapper) -->
<script src="https://cdn.jsdelivr.net/npm/kroki-client@1/dist/kroki-client.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/kroki-client@1/dist/kroki-client.min.js"></script>
<!-- html2canvas for PNG export --> <!-- html2canvas for PNG export -->
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
</head> </head>
<body> <body>
<header>🏠 Home-Lab viewer editable in browser (localStorage auto-saved)</header> <header>🏠 Home-Lab viewer editable in browser (localStorage auto-saved)</header>
<div class="wrap"> <div class="wrap">
<!-- ---- LEFT: EDITOR ---- --> <!-- ---- LEFT: EDITOR ---- -->
<div class="panel"> <div class="panel">
<h3>Source (Mermaid syntax)</h3> <h3>Source (Mermaid syntax)</h3>
<div id="editor"></div> <div id="editor"></div>
<div style="padding:.4rem .6rem"> <div style="padding:.4rem .6rem">
<button class="btn" onclick="render()">▶ Render both</button> <button class="btn" onclick="render()">▶ Render both</button>
<button class="btn" onclick="savePNG()">💾 Save PNG</button> <button class="btn" onclick="savePNG()">💾 Save PNG</button>
<button class="btn" onclick="saveSVG()">💾 Save SVG</button> <button class="btn" onclick="saveSVG()">💾 Save SVG</button>
<label class="btn"> <label class="btn">
<input type="checkbox" id="dark"> dark <input type="checkbox" id="dark"> dark
</label> </label>
</div> </div>
</div> </div>
<!-- ---- MIDDLE: MERMAID ---- --> <!-- ---- MIDDLE: MERMAID ---- -->
<div class="panel"> <div class="panel">
<h3>Mermaid render</h3> <h3>Mermaid render</h3>
<div id="mermaid" class="diagram"></div> <div id="mermaid" class="diagram"></div>
</div> </div>
<!-- ---- RIGHT: KROKI / GRAPHVIZ ---- --> <!-- ---- RIGHT: KROKI / GRAPHVIZ ---- -->
<div class="panel"> <div class="panel">
<h3>Kroki (Graphviz) render</h3> <h3>Kroki (Graphviz) render</h3>
<div id="kroki" class="diagram"></div> <div id="kroki" class="diagram"></div>
</div> </div>
</div> </div>
<script> <script>
/* ========= 2. DEFAULT SOURCE ========= */ /* ========= 2. DEFAULT SOURCE ========= */
const DEFAULT = `flowchart TD const DEFAULT = `flowchart TD
%% ---------- colours ---------- %% ---------- colours ----------
classDef internet fill:#e1f5ff,stroke:#007bff classDef internet fill:#e1f5ff,stroke:#007bff
classDef router fill:#fff3cd,stroke:#ffc107 classDef router fill:#fff3cd,stroke:#ffc107
classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px
classDef core fill:#ffe6e6,stroke:#dc3545 classDef core fill:#ffe6e6,stroke:#dc3545
classDef infra fill:#e6ffe6,stroke:#28a745 classDef infra fill:#e6ffe6,stroke:#28a745
classDef worker fill:#f0e6ff,stroke:#6f42c1 classDef worker fill:#f0e6ff,stroke:#6f42c1
classDef iot fill:#fff9e6,stroke:#fd7e14 classDef iot fill:#fff9e6,stroke:#fd7e14
%% ---------- nodes ---------- %% ---------- nodes ----------
internet(🌐 Internet / Cloud):::internet internet(🌐 Internet / Cloud):::internet
router{{🛜 Router<br/>hub.lan<br/>192.168.1.1}}:::router router{{🛜 Router<br/>hub.lan<br/>192.168.1.1}}:::router
subgraph LAN["🏠 LAN 192.168.1.0/24"]:::lan subgraph LAN["🏠 LAN 192.168.1.0/24"]:::lan
subgraph CORE["💻 Core server<br/>192.168.1.159"]:::core subgraph CORE["💻 Core server<br/>192.168.1.159"]:::core
traefik[🚦 Traefik]:::core traefik[🚦 Traefik]:::core
gitea[📚 Gitea]:::core gitea[📚 Gitea]:::core
dokku[🐳 Dokku]:::core dokku[🐳 Dokku]:::core
auction[🧱 Auction stack]:::core auction[🧱 Auction stack]:::core
mi50[🧠 MI50 / Ollama]:::core mi50[🧠 MI50 / Ollama]:::core
end end
subgraph INFRA["🧭 Infra & DNS<br/>192.168.1.163"]:::infra subgraph INFRA["🧭 Infra & DNS<br/>192.168.1.163"]:::infra
adguard[🛡️ AdGuard]:::infra adguard[🛡️ AdGuard]:::infra
artifactory[📦 Artifactory]:::infra artifactory[📦 Artifactory]:::infra
end end
ha[🏡 Home Assistant<br/>192.168.1.193]:::infra ha[🏡 Home Assistant<br/>192.168.1.193]:::infra
atlas[🧱 Atlas<br/>192.168.1.100]:::worker atlas[🧱 Atlas<br/>192.168.1.100]:::worker
iot1[📺 IoT-1]:::iot iot1[📺 IoT-1]:::iot
iot2[📟 IoT-2]:::iot iot2[📟 IoT-2]:::iot
end end
subgraph TETHER["📶 Tether 192.168.137.0/24"]:::lan subgraph TETHER["📶 Tether 192.168.137.0/24"]:::lan
hermes[🛰️ Hermes]:::worker hermes[🛰️ Hermes]:::worker
plato[🛰️ Plato]:::worker plato[🛰️ Plato]:::worker
end end
dev[👨‍💻 Dev laptop]:::internet dev[👨‍💻 Dev laptop]:::internet
%% ---------- edges ---------- %% ---------- edges ----------
internet ==> router internet ==> router
router --> CORE router --> CORE
router --> INFRA router --> INFRA
router --> ha router --> ha
router --> atlas router --> atlas
router --> iot1 router --> iot1
router --> iot2 router --> iot2
dev ==> gitea dev ==> gitea
dev ==> dokku dev ==> dokku
dev ==> mi50 dev ==> mi50
traefik --> gitea traefik --> gitea
traefik --> auction traefik --> auction
traefik --> dokku traefik --> dokku
CORE -.->|DNS| adguard CORE -.->|DNS| adguard
ha -.->|DNS| adguard ha -.->|DNS| adguard
atlas-.->|DNS| adguard atlas-.->|DNS| adguard
hermes-.->|DNS| adguard hermes-.->|DNS| adguard
plato-.->|DNS| adguard plato-.->|DNS| adguard
CORE === TETHER CORE === TETHER
`; `;
/* ========= 3. EDITOR SETUP ========= */ /* ========= 3. EDITOR SETUP ========= */
let editor; let editor;
require.config({paths:{vs:'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs'}}); require.config({paths:{vs:'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs'}});
require(['vs/editor/editor.main'], () => { require(['vs/editor/editor.main'], () => {
editor = monaco.editor.create(document.getElementById('editor'), { editor = monaco.editor.create(document.getElementById('editor'), {
value: localStorage.getItem('diagramSrc') || DEFAULT, value: localStorage.getItem('diagramSrc') || DEFAULT,
language: 'markdown', language: 'markdown',
theme: 'vs-dark', theme: 'vs-dark',
minimap: {enabled: false}, minimap: {enabled: false},
wordWrap: 'on' wordWrap: 'on'
}); });
editor.onDidChangeModelContent(() => { editor.onDidChangeModelContent(() => {
localStorage.setItem('diagramSrc', editor.getValue()); localStorage.setItem('diagramSrc', editor.getValue());
}); });
render(); render();
}); });
/* ========= 4. RENDERERS ========= */ /* ========= 4. RENDERERS ========= */
mermaid.initialize({startOnLoad: false, theme: 'default'}); mermaid.initialize({startOnLoad: false, theme: 'default'});
function render() { function render() {
const src = editor.getValue(); const src = editor.getValue();
// 4a mermaid // 4a mermaid
document.getElementById('mermaid').innerHTML = '<div class="mermaid">'+src+'</div>'; document.getElementById('mermaid').innerHTML = '<div class="mermaid">'+src+'</div>';
mermaid.init(); mermaid.init();
// 4b kroki graphviz // 4b kroki graphviz
Kroki.render('graphviz', dotFromMermaid(src)).then(svg => { Kroki.render('graphviz', dotFromMermaid(src)).then(svg => {
document.getElementById('kroki').innerHTML = svg; document.getElementById('kroki').innerHTML = svg;
}).catch(err => { }).catch(err => {
document.getElementById('kroki').innerHTML = '<pre>'+err+'</pre>'; document.getElementById('kroki').innerHTML = '<pre>'+err+'</pre>';
}); });
} }
/* crude converter good enough for this topology */ /* crude converter good enough for this topology */
function dotFromMermaid(m) { function dotFromMermaid(m) {
let dot = 'digraph G {\nbgcolor=transparent;rankdir=TB;node [shape=rect,style=rounded];\n'; let dot = 'digraph G {\nbgcolor=transparent;rankdir=TB;node [shape=rect,style=rounded];\n';
const lines = m.split('\n'); const lines = m.split('\n');
let inSub = false; let inSub = false;
lines.forEach(l => { lines.forEach(l => {
l = l.trim(); l = l.trim();
if (l.startsWith('subgraph')) { if (l.startsWith('subgraph')) {
const name = (l.match(/subgraph\s+(\w+)/) || [, 'cluster'])[1]; const name = (l.match(/subgraph\s+(\w+)/) || [, 'cluster'])[1];
dot += 'subgraph '+name+' {\nlabel="'+l.split('"')[1]+'";\nstyle=filled;fillcolor=lightgrey;\n'; dot += 'subgraph '+name+' {\nlabel="'+l.split('"')[1]+'";\nstyle=filled;fillcolor=lightgrey;\n';
inSub = true; inSub = true;
} else if (l === 'end') { } else if (l === 'end') {
dot += '}\n'; dot += '}\n';
inSub = false; inSub = false;
} else if (l.includes('[') && l.includes(']')) { } else if (l.includes('[') && l.includes(']')) {
const id = l.split('[')[0].trim(); const id = l.split('[')[0].trim();
const label = (l.match(/\[(.*?)\]/) || [, id])[1]; const label = (l.match(/\[(.*?)\]/) || [, id])[1];
dot += id + ' [label="' + label.replace(/:::.*$/,'') + '"];\n'; dot += id + ' [label="' + label.replace(/:::.*$/,'') + '"];\n';
} else if (l.includes('-->') || l.includes('==>') || l.includes('-.->')) { } else if (l.includes('-->') || l.includes('==>') || l.includes('-.->')) {
const arr = l.replace(/[~=>\-.]+/g,'->').split('->').map(x=>x.trim()).filter(Boolean); const arr = l.replace(/[~=>\-.]+/g,'->').split('->').map(x=>x.trim()).filter(Boolean);
if (arr.length === 2) dot += arr[0] + ' -> ' + arr[1] + ';\n'; if (arr.length === 2) dot += arr[0] + ' -> ' + arr[1] + ';\n';
} }
}); });
dot += '}'; dot += '}';
return dot; return dot;
} }
/* ========= 5. EXPORT ========= */ /* ========= 5. EXPORT ========= */
function savePNG() { function savePNG() {
html2canvas(document.querySelector('#mermaid svg')).then(canvas => { html2canvas(document.querySelector('#mermaid svg')).then(canvas => {
download(canvas.toDataURL(), 'home-lab.png'); download(canvas.toDataURL(), 'home-lab.png');
}); });
} }
function saveSVG() { function saveSVG() {
const svg = document.querySelector('#mermaid svg'); const svg = document.querySelector('#mermaid svg');
const url = 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg.outerHTML); const url = 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg.outerHTML);
download(url, 'home-lab.svg'); download(url, 'home-lab.svg');
} }
function download(href, name) { function download(href, name) {
const a = Object.assign(document.createElement('a'), {href, download: name}); const a = Object.assign(document.createElement('a'), {href, download: name});
document.body.appendChild(a); a.click(); a.remove(); document.body.appendChild(a); a.click(); a.remove();
} }
/* dark toggle */ /* dark toggle */
document.getElementById('dark').onchange = e => { document.getElementById('dark').onchange = e => {
document.body.style.background = e.target.checked ? '#121212' : '#f7f9fc'; document.body.style.background = e.target.checked ? '#121212' : '#f7f9fc';
document.body.style.color = e.target.checked ? '#eee' : '#222'; document.body.style.color = e.target.checked ? '#eee' : '#222';
}; };
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,283 +1,283 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Home-Lab diagram live editor</title> <title>Home-Lab diagram live editor</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <style>
:root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd} :root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd}
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)} body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)}
header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-weight:600;display:flex;gap:.5rem;align-items:center} header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-weight:600;display:flex;gap:.5rem;align-items:center}
header button{padding:.2rem .6rem;border:none;border-radius:4px;background:#fff;color:var(--accent);cursor:pointer} header button{padding:.2rem .6rem;border:none;border-radius:4px;background:#fff;color:var(--accent);cursor:pointer}
.wrap{display:flex;height:calc(100vh - 40px)} .wrap{display:flex;height:calc(100vh - 40px)}
.panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd;transition:flex .3s ease} .panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd;transition:flex .3s ease}
.panel:last-child{border:none} .panel:last-child{border:none}
h3{margin:.4rem .6rem;font-size:1rem} h3{margin:.4rem .6rem;font-size:1rem}
#src, #preview{flex:1;padding:.5rem;overflow:auto;background:#fff;font-family:"Consolas","Monaco",monospace} #src, #preview{flex:1;padding:.5rem;overflow:auto;background:#fff;font-family:"Consolas","Monaco",monospace}
#src{border:none;outline:none;background:#1e1e1e;color:#d4d4d4;white-space:pre-wrap} #src{border:none;outline:none;background:#1e1e1e;color:#d4d4d4;white-space:pre-wrap}
#preview{display:flex;align-items:flex-start;justify-content:center;min-height:0} #preview{display:flex;align-items:flex-start;justify-content:center;min-height:0}
svg{max-width:100%;height:auto} svg{max-width:100%;height:auto}
#error{color:#d32f2f;background:#fff3cd;padding:.5rem;margin:.5rem;border-left:4px solid #d32f2f;display:none} #error{color:#d32f2f;background:#fff3cd;padding:.5rem;margin:.5rem;border-left:4px solid #d32f2f;display:none}
#zoomVal{margin-left:.5rem;font-size:.9rem;color:#fff} #zoomVal{margin-left:.5rem;font-size:.9rem;color:#fff}
/* collapsed state */ /* collapsed state */
#srcPanel.collapsed{flex:0} #srcPanel.collapsed{flex:0}
#srcPanel.collapsed #src{display:none} #srcPanel.collapsed #src{display:none}
</style> </style>
<!-- ONLY Mermaid NO Monaco, NO AMD loader --> <!-- ONLY Mermaid NO Monaco, NO AMD loader -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
</head> </head>
<body> <body>
<header> <header>
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button> <button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
Home-Lab diagram live Mermaid editor Home-Lab diagram live Mermaid editor
<button onclick="saveSVG()">💾 SVG</button> <button onclick="saveSVG()">💾 SVG</button>
<button onclick="savePNG()">💾 PNG</button> <button onclick="savePNG()">💾 PNG</button>
<button onclick="reset()">🔄 Reset</button> <button onclick="reset()">🔄 Reset</button>
<label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;color:#fff"> <label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;color:#fff">
Zoom: <input type="range" id="zoom" min="50" max="300" value="100" style="width:100px"><span id="zoomVal">100%</span> Zoom: <input type="range" id="zoom" min="50" max="300" value="100" style="width:100px"><span id="zoomVal">100%</span>
</label> </label>
</header> </header>
<div class="wrap"> <div class="wrap">
<div class="panel" id="srcPanel"> <div class="panel" id="srcPanel">
<h3>Source (edit here)</h3> <h3>Source (edit here)</h3>
<textarea id="src" spellcheck="false"></textarea> <textarea id="src" spellcheck="false"></textarea>
</div> </div>
<div class="panel"> <div class="panel">
<h3>Live preview</h3> <h3>Live preview</h3>
<div id="preview"></div> <div id="preview"></div>
<div id="error"></div> <div id="error"></div>
</div> </div>
</div> </div>
<script> <script>
/* ---------- DEFAULT SOURCE guaranteed valid Mermaid 10.6.1 ---------- */ /* ---------- DEFAULT SOURCE guaranteed valid Mermaid 10.6.1 ---------- */
const DEFAULT = `flowchart TD const DEFAULT = `flowchart TD
%% ---------- styles ---------- %% ---------- styles ----------
classDef internet fill:#e1f5ff,stroke:#007bff classDef internet fill:#e1f5ff,stroke:#007bff
classDef router fill:#fff3cd,stroke:#ffc107 classDef router fill:#fff3cd,stroke:#ffc107
classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px
classDef core fill:#ffe6e6,stroke:#dc3545 classDef core fill:#ffe6e6,stroke:#dc3545
classDef infra fill:#e6ffe6,stroke:#28a745 classDef infra fill:#e6ffe6,stroke:#28a745
classDef worker fill:#f0e6ff,stroke:#6f42c1 classDef worker fill:#f0e6ff,stroke:#6f42c1
classDef iot fill:#fff9e6,stroke:#fd7e14 classDef iot fill:#fff9e6,stroke:#fd7e14
%% ---------- nodes ---------- %% ---------- nodes ----------
internet(🌐 Internet / Cloud):::internet internet(🌐 Internet / Cloud):::internet
router[🛜 Router\nhub.lan\n192.168.1.1]:::router router[🛜 Router\nhub.lan\n192.168.1.1]:::router
subgraph LAN [🏠 LAN 192.168.1.0/24] subgraph LAN [🏠 LAN 192.168.1.0/24]
subgraph CORE [💻 Core server\n192.168.1.159] subgraph CORE [💻 Core server\n192.168.1.159]
traefik[🚦 Traefik]:::core traefik[🚦 Traefik]:::core
gitea[📚 Gitea]:::core gitea[📚 Gitea]:::core
dokku[🐳 Dokku]:::core dokku[🐳 Dokku]:::core
auction[🧱 Auction stack]:::core auction[🧱 Auction stack]:::core
mi50[🧠 MI50 / Ollama]:::core mi50[🧠 MI50 / Ollama]:::core
end end
subgraph INFRA [🧭 Infra & DNS\n192.168.1.163] subgraph INFRA [🧭 Infra & DNS\n192.168.1.163]
adguard[🛡️ AdGuard]:::infra adguard[🛡️ AdGuard]:::infra
artifactory[📦 Artifactory]:::infra artifactory[📦 Artifactory]:::infra
end end
ha[🏡 Home Assistant\n192.168.1.193]:::infra ha[🏡 Home Assistant\n192.168.1.193]:::infra
atlas[🧱 Atlas\n192.168.1.100]:::worker atlas[🧱 Atlas\n192.168.1.100]:::worker
iot1[📺 IoT-1]:::iot iot1[📺 IoT-1]:::iot
iot2[📟 IoT-2]:::iot iot2[📟 IoT-2]:::iot
end end
subgraph TETHER [📶 Tether 192.168.137.0/24] subgraph TETHER [📶 Tether 192.168.137.0/24]
hermes[🛰️ Hermes]:::worker hermes[🛰️ Hermes]:::worker
plato[🛰️ Plato]:::worker plato[🛰️ Plato]:::worker
end end
dev[👨‍💻 Dev laptop]:::internet dev[👨‍💻 Dev laptop]:::internet
%% ---------- edges ---------- %% ---------- edges ----------
internet ==> router internet ==> router
router --> CORE router --> CORE
router --> INFRA router --> INFRA
router --> ha router --> ha
router --> atlas router --> atlas
router --> iot1 router --> iot1
router --> iot2 router --> iot2
dev ==> gitea dev ==> gitea
dev ==> dokku dev ==> dokku
dev ==> mi50 dev ==> mi50
traefik --> gitea traefik --> gitea
traefik --> auction traefik --> auction
traefik --> dokku traefik --> dokku
CORE -.->|DNS| adguard CORE -.->|DNS| adguard
ha -.->|DNS| adguard ha -.->|DNS| adguard
atlas-.->|DNS| adguard atlas-.->|DNS| adguard
hermes-.->|DNS| adguard hermes-.->|DNS| adguard
plato-.->|DNS| adguard plato-.->|DNS| adguard
CORE === TETHER CORE === TETHER
`; `;
/* ---------- safe localStorage access ---------- */ /* ---------- safe localStorage access ---------- */
function getStorage(key, fallback) { function getStorage(key, fallback) {
try { try {
const val = localStorage.getItem(key); const val = localStorage.getItem(key);
return val !== null ? val : fallback; return val !== null ? val : fallback;
} catch (e) { } catch (e) {
// IntelliJ preview blocks localStorage // IntelliJ preview blocks localStorage
console.warn('localStorage unavailable:', e); console.warn('localStorage unavailable:', e);
return fallback; return fallback;
} }
} }
function setStorage(key, val) { function setStorage(key, val) {
try { try {
localStorage.setItem(key, val); localStorage.setItem(key, val);
} catch (e) { } catch (e) {
console.warn('localStorage save failed:', e); console.warn('localStorage save failed:', e);
} }
} }
/* ---------- load state ---------- */ /* ---------- load state ---------- */
const srcEl = document.getElementById('src'); const srcEl = document.getElementById('src');
const errEl = document.getElementById('error'); const errEl = document.getElementById('error');
srcEl.value = getStorage('labDiagram', DEFAULT); srcEl.value = getStorage('labDiagram', DEFAULT);
const srcPanel = document.getElementById('srcPanel'); const srcPanel = document.getElementById('srcPanel');
const toggleBtn = document.getElementById('toggleBtn'); const toggleBtn = document.getElementById('toggleBtn');
if (getStorage('panelCollapsed', 'false') === 'true') { if (getStorage('panelCollapsed', 'false') === 'true') {
srcPanel.classList.add('collapsed'); srcPanel.classList.add('collapsed');
toggleBtn.textContent = '🗔 Show source'; toggleBtn.textContent = '🗔 Show source';
} }
/* ---------- panel toggle ---------- */ /* ---------- panel toggle ---------- */
function togglePanel() { function togglePanel() {
srcPanel.classList.toggle('collapsed'); srcPanel.classList.toggle('collapsed');
const collapsed = srcPanel.classList.contains('collapsed'); const collapsed = srcPanel.classList.contains('collapsed');
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source'; toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
setStorage('panelCollapsed', collapsed); setStorage('panelCollapsed', collapsed);
} }
/* ---------- Mermaid init ---------- */ /* ---------- Mermaid init ---------- */
mermaid.initialize({startOnLoad: false, theme: 'default'}); mermaid.initialize({startOnLoad: false, theme: 'default'});
/* ---------- render ---------- */ /* ---------- render ---------- */
function render() { function render() {
const src = srcEl.value; const src = srcEl.value;
setStorage('labDiagram', src); setStorage('labDiagram', src);
showError(''); showError('');
const preview = document.getElementById('preview'); const preview = document.getElementById('preview');
preview.innerHTML = ''; preview.innerHTML = '';
const mermaidDiv = document.createElement('div'); const mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid'; mermaidDiv.className = 'mermaid';
mermaidDiv.textContent = src; mermaidDiv.textContent = src;
preview.appendChild(mermaidDiv); preview.appendChild(mermaidDiv);
// Render then zoom // Render then zoom
mermaid.init(undefined, mermaidDiv).then(() => { mermaid.init(undefined, mermaidDiv).then(() => {
const svg = preview.querySelector('svg'); const svg = preview.querySelector('svg');
if (svg) { if (svg) {
svg.style.maxWidth = 'none'; svg.style.maxWidth = 'none';
svg.style.width = 'auto'; svg.style.width = 'auto';
svg.style.height = 'auto'; svg.style.height = 'auto';
svg.style.transformOrigin = 'top left'; svg.style.transformOrigin = 'top left';
applyZoom(); applyZoom();
} }
}).catch(e => { }).catch(e => {
showError('Mermaid error: ' + e.message); showError('Mermaid error: ' + e.message);
}); });
} }
function showError(msg) { function showError(msg) {
errEl.textContent = msg; errEl.textContent = msg;
errEl.style.display = msg ? 'block' : 'none'; errEl.style.display = msg ? 'block' : 'none';
} }
/* ---------- auto-render + zoom ---------- */ /* ---------- auto-render + zoom ---------- */
srcEl.addEventListener('input', () => { srcEl.addEventListener('input', () => {
clearTimeout(srcEl._t); clearTimeout(srcEl._t);
srcEl._t = setTimeout(render, 300); srcEl._t = setTimeout(render, 300);
}); });
const zoomEl = document.getElementById('zoom'); const zoomEl = document.getElementById('zoom');
zoomEl.addEventListener('input', () => { zoomEl.addEventListener('input', () => {
document.getElementById('zoomVal').textContent = zoomEl.value + '%'; document.getElementById('zoomVal').textContent = zoomEl.value + '%';
applyZoom(); applyZoom();
}); });
function applyZoom() { function applyZoom() {
const svg = document.querySelector('#preview svg'); const svg = document.querySelector('#preview svg');
if (svg) { if (svg) {
svg.style.transform = `scale(${zoomEl.value / 100})`; svg.style.transform = `scale(${zoomEl.value / 100})`;
} }
} }
/* ---------- actions ---------- */ /* ---------- actions ---------- */
function reset() { function reset() {
if (confirm('Reset to default diagram?')) { if (confirm('Reset to default diagram?')) {
srcEl.value = DEFAULT; srcEl.value = DEFAULT;
zoomEl.value = 100; zoomEl.value = 100;
document.getElementById('zoomVal').textContent = '100%'; document.getElementById('zoomVal').textContent = '100%';
setStorage('labDiagram', DEFAULT); setStorage('labDiagram', DEFAULT);
render(); render();
} }
} }
function saveSVG() { function saveSVG() {
const svg = document.querySelector('#preview svg'); const svg = document.querySelector('#preview svg');
if (!svg) return alert('Nothing to save yet.'); if (!svg) return alert('Nothing to save yet.');
const clone = svg.cloneNode(true); const clone = svg.cloneNode(true);
clone.removeAttribute('style'); clone.removeAttribute('style');
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'}); const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = 'home-lab.svg'; a.download = 'home-lab.svg';
a.click(); a.click();
setTimeout(() => URL.revokeObjectURL(url), 100); setTimeout(() => URL.revokeObjectURL(url), 100);
} }
function savePNG() { function savePNG() {
const svg = document.querySelector('#preview svg'); const svg = document.querySelector('#preview svg');
if (!svg) return alert('Nothing to save yet.'); if (!svg) return alert('Nothing to save yet.');
const bbox = svg.getBBox(); const bbox = svg.getBBox();
const width = Math.max(bbox.width, 1200); const width = Math.max(bbox.width, 1200);
const height = Math.max(bbox.height, 800); const height = Math.max(bbox.height, 800);
const svgData = new XMLSerializer().serializeToString(svg); const svgData = new XMLSerializer().serializeToString(svg);
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height); ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(blob => { canvas.toBlob(blob => {
const a = document.createElement('a'); const a = document.createElement('a');
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.download = 'home-lab.png'; a.download = 'home-lab.png';
a.click(); a.click();
}); });
}; };
img.onerror = () => alert('PNG conversion failed. Try SVG instead.'); img.onerror = () => alert('PNG conversion failed. Try SVG instead.');
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData); img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
} }
// Initial render // Initial render
render(); render();
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,121 +1,121 @@
from diagrams import Diagram, Cluster, Edge from diagrams import Diagram, Cluster, Edge
from diagrams.onprem.network import Internet from diagrams.onprem.network import Internet
from diagrams.onprem.compute import Server from diagrams.onprem.compute import Server
from diagrams.onprem.iac import Ansible from diagrams.onprem.iac import Ansible
from diagrams.generic.network import Router, Switch from diagrams.generic.network import Router, Switch
from diagrams.generic.device import Mobile, Tablet from diagrams.generic.device import Mobile, Tablet
from diagrams.generic.blank import Blank from diagrams.generic.blank import Blank
from diagrams.onprem.client import Users from diagrams.onprem.client import Users
from diagrams.onprem.container import Docker from diagrams.onprem.container import Docker
from diagrams.custom import Custom from diagrams.custom import Custom
# Tip: run this in a venv: # Tip: run this in a venv:
# pip install diagrams graphviz # pip install diagrams graphviz
with Diagram("Home Lab / Auction Stack Architecture", show=False, filename="home_lab_architecture", direction="LR"): with Diagram("Home Lab / Auction Stack Architecture", show=False, filename="home_lab_architecture", direction="LR"):
internet = Internet("Internet / Cloud") internet = Internet("Internet / Cloud")
dev = Users("Dev laptop(s)\nWindows / WSL") dev = Users("Dev laptop(s)\nWindows / WSL")
# -------------------- LAN 192.168.1.x -------------------- # -------------------- LAN 192.168.1.x --------------------
with Cluster("LAN 192.168.1.0/24"): with Cluster("LAN 192.168.1.0/24"):
router = Router("hub.lan\n192.168.1.1\nRouter / Gateway") router = Router("hub.lan\n192.168.1.1\nRouter / Gateway")
# -------- Core server / desktop (Tour / hephaestus / dokku / ollama) -------- # -------- Core server / desktop (Tour / hephaestus / dokku / ollama) --------
with Cluster("Core server / desktop\nTour / hephaestus / dokku.lan / ollama.lan\n192.168.1.159"): with Cluster("Core server / desktop\nTour / hephaestus / dokku.lan / ollama.lan\n192.168.1.159"):
core_os = Server("Ubuntu host") core_os = Server("Ubuntu host")
traefik = Docker("Traefik\nReverse Proxy") traefik = Docker("Traefik\nReverse Proxy")
gitea = Docker("Gitea\n git.appmodel.nl") gitea = Docker("Gitea\n git.appmodel.nl")
dokku = Docker("Dokku\nPaaS / app hosting") dokku = Docker("Dokku\nPaaS / app hosting")
auction_fe = Docker("Auction Frontend\n auction.appmodel.nl") auction_fe = Docker("Auction Frontend\n auction.appmodel.nl")
aupi_be = Docker("Aupi API Backend\n aupi.appmodel.nl") aupi_be = Docker("Aupi API Backend\n aupi.appmodel.nl")
mi50 = Server("MI50 GPU / Ollama\nAI workloads") mi50 = Server("MI50 GPU / Ollama\nAI workloads")
# -------- Infra & DNS (odroid / dns.lan) -------- # -------- Infra & DNS (odroid / dns.lan) --------
with Cluster("Infra & DNS\nodroid / dns.lan\n192.168.1.163"): with Cluster("Infra & DNS\nodroid / dns.lan\n192.168.1.163"):
dns_host = Server("Odroid host") dns_host = Server("Odroid host")
adguard = Docker("AdGuard Home\nDNS / *.lan / *.appmodel.nl") adguard = Docker("AdGuard Home\nDNS / *.lan / *.appmodel.nl")
artifactory = Docker("Artifactory (future)") artifactory = Docker("Artifactory (future)")
runner = Docker("CI / Build runner (future)") runner = Docker("CI / Build runner (future)")
# -------- Home Assistant -------- # -------- Home Assistant --------
with Cluster("Home Automation\nha.lan\n192.168.1.193"): with Cluster("Home Automation\nha.lan\n192.168.1.193"):
ha_host = Server("HomeAssistant host") ha_host = Server("HomeAssistant host")
hass = Docker("Home Assistant") hass = Docker("Home Assistant")
# -------- Extra node / worker -------- # -------- Extra node / worker --------
atlas = Server("atlas.lan\n192.168.1.100\n(extra node / worker)") atlas = Server("atlas.lan\n192.168.1.100\n(extra node / worker)")
# -------- IoT / devices -------- # -------- IoT / devices --------
with Cluster("IoT / Clients"): with Cluster("IoT / Clients"):
iot_hof = Tablet("hof-E402NA\n192.168.1.214") iot_hof = Tablet("hof-E402NA\n192.168.1.214")
iot_s380 = Tablet("S380HB\n192.168.1.59") iot_s380 = Tablet("S380HB\n192.168.1.59")
iot_ecb5 = Tablet("ecb5faa56c90\n192.168.1.49") iot_ecb5 = Tablet("ecb5faa56c90\n192.168.1.49")
iot_unknown = Tablet("Unknown\n192.168.1.240") iot_unknown = Tablet("Unknown\n192.168.1.240")
# -------------------- Tether subnet 192.168.137.x -------------------- # -------------------- Tether subnet 192.168.137.x --------------------
with Cluster("Tether subnet 192.168.137.0/24"): with Cluster("Tether subnet 192.168.137.0/24"):
hermes = Server("hermes.lan\n192.168.137.239\nworker / node") hermes = Server("hermes.lan\n192.168.137.239\nworker / node")
plato = Server("plato.lan\n192.168.137.163\nworker / node") plato = Server("plato.lan\n192.168.137.163\nworker / node")
# -------------------- Edges / flows -------------------- # -------------------- Edges / flows --------------------
# Internet naar router + DNS host # Internet naar router + DNS host
internet >> router internet >> router
internet >> dns_host internet >> dns_host
# Dev naar Gitea / Traefik / Dokku # Dev naar Gitea / Traefik / Dokku
dev >> Edge(label="git / HTTPS") >> traefik dev >> Edge(label="git / HTTPS") >> traefik
dev >> Edge(label="SSH / HTTPS") >> gitea dev >> Edge(label="SSH / HTTPS") >> gitea
dev >> Edge(label="Dokku deploys") >> dokku dev >> Edge(label="Dokku deploys") >> dokku
# Router naar alle LAN-nodes # Router naar alle LAN-nodes
router >> core_os router >> core_os
router >> dns_host router >> dns_host
router >> ha_host router >> ha_host
router >> atlas router >> atlas
router >> iot_hof router >> iot_hof
router >> iot_s380 router >> iot_s380
router >> iot_ecb5 router >> iot_ecb5
router >> iot_unknown router >> iot_unknown
# Core server services # Core server services
core_os >> traefik core_os >> traefik
core_os >> gitea core_os >> gitea
core_os >> dokku core_os >> dokku
core_os >> auction_fe core_os >> auction_fe
core_os >> aupi_be core_os >> aupi_be
core_os >> mi50 core_os >> mi50
# Infra/DNS services # Infra/DNS services
dns_host >> adguard dns_host >> adguard
dns_host >> artifactory dns_host >> artifactory
dns_host >> runner dns_host >> runner
# Home Assistant # Home Assistant
ha_host >> hass ha_host >> hass
# DNS-queries # DNS-queries
core_os >> Edge(label="DNS") >> adguard core_os >> Edge(label="DNS") >> adguard
ha_host >> Edge(label="DNS") >> adguard ha_host >> Edge(label="DNS") >> adguard
atlas >> Edge(label="DNS") >> adguard atlas >> Edge(label="DNS") >> adguard
hermes >> Edge(label="DNS") >> adguard hermes >> Edge(label="DNS") >> adguard
plato >> Edge(label="DNS") >> adguard plato >> Edge(label="DNS") >> adguard
# Web traffic / reverse proxy flows # Web traffic / reverse proxy flows
internet >> Edge(label="HTTP/HTTPS") >> traefik internet >> Edge(label="HTTP/HTTPS") >> traefik
traefik >> Edge(label="git.appmodel.nl") >> gitea traefik >> Edge(label="git.appmodel.nl") >> gitea
traefik >> Edge(label="auction.appmodel.nl") >> auction_fe traefik >> Edge(label="auction.appmodel.nl") >> auction_fe
traefik >> Edge(label="aupi.appmodel.nl") >> aupi_be traefik >> Edge(label="aupi.appmodel.nl") >> aupi_be
traefik >> Edge(label="dokku.lan / apps") >> dokku traefik >> Edge(label="dokku.lan / apps") >> dokku
# App-level flow # App-level flow
auction_fe >> Edge(label="REST API") >> aupi_be auction_fe >> Edge(label="REST API") >> aupi_be
# AI workloads # AI workloads
dev >> Edge(label="LLM / Tuning / Inference") >> mi50 dev >> Edge(label="LLM / Tuning / Inference") >> mi50
# Tether workers verbonden met core (bv. jobs / agents) # Tether workers verbonden met core (bv. jobs / agents)
core_os >> Edge(label="jobs / ssh") >> hermes core_os >> Edge(label="jobs / ssh") >> hermes
core_os >> Edge(label="jobs / ssh") >> plato core_os >> Edge(label="jobs / ssh") >> plato

318
main.py
View File

@@ -1,159 +1,159 @@
from graphviz import Digraph from graphviz import Digraph
def make_network_diagram(output_name: str = "network-architecture"): def make_network_diagram(output_name: str = "network-architecture"):
g = Digraph( g = Digraph(
"network", "network",
filename=f"{output_name}.gv", filename=f"{output_name}.gv",
format="png", format="png",
) )
# Globale stijl # Globale stijl
g.attr(rankdir="LR", fontname="Segoe UI") g.attr(rankdir="LR", fontname="Segoe UI")
# ---------- WAN / Internet ---------- # ---------- WAN / Internet ----------
with g.subgraph(name="cluster_wan") as wan: with g.subgraph(name="cluster_wan") as wan:
wan.attr( wan.attr(
label="🌐 Internet / Cloud", label="🌐 Internet / Cloud",
style="rounded", style="rounded",
color="lightgrey", color="lightgrey",
fontsize="16", fontsize="16",
) )
wan.node("extDNS", "📡 Public DNS", shape="rectangle") wan.node("extDNS", "📡 Public DNS", shape="rectangle")
wan.node("extGit", "☁️ Externe registries / Git\n(GitHub / Docker Hub)", shape="rectangle") wan.node("extGit", "☁️ Externe registries / Git\n(GitHub / Docker Hub)", shape="rectangle")
# ---------- LAN 192.168.1.0/24 ---------- # ---------- LAN 192.168.1.0/24 ----------
with g.subgraph(name="cluster_lan") as lan: with g.subgraph(name="cluster_lan") as lan:
lan.attr( lan.attr(
label="🏠 LAN 192.168.1.0/24", label="🏠 LAN 192.168.1.0/24",
style="rounded", style="rounded",
color="lightgrey", color="lightgrey",
fontsize="16", fontsize="16",
) )
# Router / gateway # Router / gateway
lan.node( lan.node(
"hub", "hub",
"🛜 Router / Gateway\nhub.lan\n192.168.1.1", "🛜 Router / Gateway\nhub.lan\n192.168.1.1",
shape="rectangle", shape="rectangle",
style="filled", style="filled",
fillcolor="#f0f0ff", fillcolor="#f0f0ff",
) )
# ---- Core server / desktop ---- # ---- Core server / desktop ----
with lan.subgraph(name="cluster_core") as core: with lan.subgraph(name="cluster_core") as core:
core.attr( core.attr(
label="💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159", label="💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159",
style="rounded", style="rounded",
color="#aaccee", color="#aaccee",
) )
core.node("traefik", "🚦 Traefik\nReverse Proxy", shape="rectangle") core.node("traefik", "🚦 Traefik\nReverse Proxy", shape="rectangle")
core.node("gitea", "📚 Gitea\ngit.appmodel.nl", shape="rectangle") core.node("gitea", "📚 Gitea\ngit.appmodel.nl", shape="rectangle")
core.node("dokku", "🐳 Dokku\nPaaS / build", shape="rectangle") core.node("dokku", "🐳 Dokku\nPaaS / build", shape="rectangle")
core.node("auctionFE", "🧱 Auction Frontend\nauction.appmodel.nl", shape="rectangle") core.node("auctionFE", "🧱 Auction Frontend\nauction.appmodel.nl", shape="rectangle")
core.node("aupiAPI", "🧱 Auction Backend API\naupi.appmodel.nl", shape="rectangle") core.node("aupiAPI", "🧱 Auction Backend API\naupi.appmodel.nl", shape="rectangle")
core.node("mi50", "🧠 MI50 / Ollama\nAI workloads", shape="rectangle") core.node("mi50", "🧠 MI50 / Ollama\nAI workloads", shape="rectangle")
# Aanvulling: monitoring / logging # Aanvulling: monitoring / logging
core.node("monitoring", "📈 Monitoring / Logging\nPrometheus / Loki / Grafana", shape="rectangle") core.node("monitoring", "📈 Monitoring / Logging\nPrometheus / Loki / Grafana", shape="rectangle")
# ---- Infra & DNS ---- # ---- Infra & DNS ----
with lan.subgraph(name="cluster_infra_dns") as infra: with lan.subgraph(name="cluster_infra_dns") as infra:
infra.attr( infra.attr(
label="🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163", label="🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163",
style="rounded", style="rounded",
color="#aaddaa", color="#aaddaa",
) )
infra.node("adguard", "🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl", shape="rectangle") infra.node("adguard", "🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl", shape="rectangle")
infra.node("artifactory", "📦 Artifactory", shape="rectangle") infra.node("artifactory", "📦 Artifactory", shape="rectangle")
infra.node("runner", "⚙️ Build runners\nCI/CD", shape="rectangle") infra.node("runner", "⚙️ Build runners\nCI/CD", shape="rectangle")
# ---- Home Automation ---- # ---- Home Automation ----
with lan.subgraph(name="cluster_ha") as ha: with lan.subgraph(name="cluster_ha") as ha:
ha.attr( ha.attr(
label="🏡 Home Automation\nha.lan\n192.168.1.193", label="🏡 Home Automation\nha.lan\n192.168.1.193",
style="rounded", style="rounded",
color="#ffddaa", color="#ffddaa",
) )
ha.node("hass", "🏠 Home Assistant", shape="rectangle") ha.node("hass", "🏠 Home Assistant", shape="rectangle")
# Overige LAN-hosts / IoT # Overige LAN-hosts / IoT
lan.node("atlas", "🧱 atlas.lan\n192.168.1.100", shape="rectangle") lan.node("atlas", "🧱 atlas.lan\n192.168.1.100", shape="rectangle")
lan.node("iot1", "📺 hof-E402NA\n192.168.1.214", shape="rectangle") lan.node("iot1", "📺 hof-E402NA\n192.168.1.214", shape="rectangle")
lan.node("iot2", "🎧 S380HB\n192.168.1.59", shape="rectangle") lan.node("iot2", "🎧 S380HB\n192.168.1.59", shape="rectangle")
lan.node("iot3", "📟 ecb5faa56c90\n192.168.1.49", shape="rectangle") lan.node("iot3", "📟 ecb5faa56c90\n192.168.1.49", shape="rectangle")
lan.node("iot4", "❓ Unknown\n192.168.1.240", shape="rectangle") lan.node("iot4", "❓ Unknown\n192.168.1.240", shape="rectangle")
# ---------- Tether subnet ---------- # ---------- Tether subnet ----------
with g.subgraph(name="cluster_tether") as tether: with g.subgraph(name="cluster_tether") as tether:
tether.attr( tether.attr(
label="📶 Tether subnet 192.168.137.0/24", label="📶 Tether subnet 192.168.137.0/24",
style="rounded", style="rounded",
color="lightgrey", color="lightgrey",
fontsize="16", fontsize="16",
) )
tether.node("hermes", "🛰️ hermes.lan\n192.168.137.239\nworker / node", shape="rectangle") tether.node("hermes", "🛰️ hermes.lan\n192.168.137.239\nworker / node", shape="rectangle")
tether.node("plato", "🛰️ plato.lan\n192.168.137.163\nworker / node", shape="rectangle") tether.node("plato", "🛰️ plato.lan\n192.168.137.163\nworker / node", shape="rectangle")
# ---------- Externe gebruikers (aanvulling) ---------- # ---------- Externe gebruikers (aanvulling) ----------
g.node( g.node(
"users", "users",
"👨‍💻 Developers / Users\nlaptops / mobiel", "👨‍💻 Developers / Users\nlaptops / mobiel",
shape="rectangle", shape="rectangle",
style="dashed", style="dashed",
) )
# ==================== VERBINDINGEN ==================== # ==================== VERBINDINGEN ====================
# Basis LAN connecties (ongeveer jouw '---' links) # Basis LAN connecties (ongeveer jouw '---' links)
for target in ["core", "infraDNS", "ha", "atlas", "iot1", "iot2", "iot3", "iot4"]: for target in ["core", "infraDNS", "ha", "atlas", "iot1", "iot2", "iot3", "iot4"]:
# We linken naar representatieve node binnen subgraph # We linken naar representatieve node binnen subgraph
if target == "core": if target == "core":
g.edge("hub", "traefik", dir="both", label="LAN") g.edge("hub", "traefik", dir="both", label="LAN")
elif target == "infraDNS": elif target == "infraDNS":
g.edge("hub", "adguard", dir="both", label="LAN") g.edge("hub", "adguard", dir="both", label="LAN")
elif target == "ha": elif target == "ha":
g.edge("hub", "hass", dir="both", label="LAN") g.edge("hub", "hass", dir="both", label="LAN")
else: else:
g.edge("hub", target, dir="both", label="LAN") g.edge("hub", target, dir="both", label="LAN")
# WAN koppeling # WAN koppeling
g.edge("hub", "extDNS", label="📶 Internet") g.edge("hub", "extDNS", label="📶 Internet")
g.edge("adguard", "extDNS", label="Upstream DNS") g.edge("adguard", "extDNS", label="Upstream DNS")
# DNS-resolutie (core / ha / atlas / tether -> AdGuard) # DNS-resolutie (core / ha / atlas / tether -> AdGuard)
for client in ["traefik", "hass", "atlas", "hermes", "plato"]: for client in ["traefik", "hass", "atlas", "hermes", "plato"]:
g.edge(client, "adguard", label="DNS", style="dashed") g.edge(client, "adguard", label="DNS", style="dashed")
# Websites / reverse proxy # Websites / reverse proxy
g.edge("extDNS", "traefik", label="DNS → Traefik") g.edge("extDNS", "traefik", label="DNS → Traefik")
g.edge("traefik", "gitea", label="HTTP(S)") g.edge("traefik", "gitea", label="HTTP(S)")
g.edge("traefik", "auctionFE", label="HTTP(S)") g.edge("traefik", "auctionFE", label="HTTP(S)")
g.edge("traefik", "aupiAPI", label="HTTP(S)") g.edge("traefik", "aupiAPI", label="HTTP(S)")
g.edge("traefik", "dokku", label="Apps / Deploy") g.edge("traefik", "dokku", label="Apps / Deploy")
# App flow # App flow
g.edge("auctionFE", "aupiAPI", label="API calls") g.edge("auctionFE", "aupiAPI", label="API calls")
g.edge("aupiAPI", "adguard", label="DNS lookups", style="dashed") g.edge("aupiAPI", "adguard", label="DNS lookups", style="dashed")
# AI workloads # AI workloads
g.edge("traefik", "mi50", label="AI / inference", style="dotted") g.edge("traefik", "mi50", label="AI / inference", style="dotted")
# Tether workers # Tether workers
g.edge("traefik", "hermes", dir="both", label="Jobs / tasks") g.edge("traefik", "hermes", dir="both", label="Jobs / tasks")
g.edge("traefik", "plato", dir="both", label="Jobs / tasks") g.edge("traefik", "plato", dir="both", label="Jobs / tasks")
# Monitoring / logging (aanvulling) # Monitoring / logging (aanvulling)
for observed in ["traefik", "gitea", "dokku", "auctionFE", "aupiAPI", "mi50", "adguard", "atlas", "hass"]: for observed in ["traefik", "gitea", "dokku", "auctionFE", "aupiAPI", "mi50", "adguard", "atlas", "hass"]:
g.edge(observed, "monitoring", style="dotted", label="metrics / logs") g.edge(observed, "monitoring", style="dotted", label="metrics / logs")
# Developers / gebruikers # Developers / gebruikers
g.edge("users", "traefik", label="HTTPS") g.edge("users", "traefik", label="HTTPS")
g.edge("users", "gitea", style="dashed", label="Git / HTTP") g.edge("users", "gitea", style="dashed", label="Git / HTTP")
# Genereer bestanden (PNG + .gv) # Genereer bestanden (PNG + .gv)
g.render(cleanup=True) g.render(cleanup=True)
print(f"Diagram gegenereerd als {output_name}.png") print(f"Diagram gegenereerd als {output_name}.png")
if __name__ == "__main__": if __name__ == "__main__":
make_network_diagram() make_network_diagram()

9302
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,9 @@
{ {
"name": "viewer", "name": "viewer",
"version": "0.0.1", "version": "1.0.0",
"description": "", "description": "",
"private": true, "maintainers": ["michael@appmodel.nl"],
"keywords": [
""
],
"license": "",
"author": "",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "build": "mkdir -p dist && cp -r public/* dist/"
"start": "webpack serve --open --config webpack.config.dev.js",
"build": "webpack --config webpack.config.prod.js"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"html-webpack-plugin": "^5.6.0",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
} }
} }

View File

@@ -1,107 +1,107 @@
<!doctype html> <!doctype html>
<html lang="nl"> <html lang="nl">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Netwerk architectuur</title> <title>Netwerk architectuur</title>
<!-- Mermaid via CDN --> <!-- Mermaid via CDN -->
<script type="module"> <script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs"; import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: true, theme: "default" }); mermaid.initialize({ startOnLoad: true, theme: "default" });
</script> </script>
<style> <style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 1rem; } body { font-family: system-ui, sans-serif; margin: 0; padding: 1rem; }
.mermaid { max-width: 100%; overflow: auto; } .mermaid { max-width: 100%; overflow: auto; }
</style> </style>
</head> </head>
<body> <body>
<h1>Netwerk architectuur</h1> <h1>Netwerk architectuur</h1>
<div class="mermaid"> <div class="mermaid">
flowchart LR flowchart LR
%% ============ Internet ============ %% ============ Internet ============
subgraph WAN[🌐 Internet / Cloud] subgraph WAN[🌐 Internet / Cloud]
extDNS[(📡 Public DNS)] extDNS[(📡 Public DNS)]
extGit[(☁️ Externe registries / Git)] extGit[(☁️ Externe registries / Git)]
end end
%% ============ LAN 192.168.1.x ============ %% ============ LAN 192.168.1.x ============
subgraph LAN[🏠 LAN 192.168.1.0/24] subgraph LAN[🏠 LAN 192.168.1.0/24]
hub[🛜 Router / Gateway\nhub.lan\n192.168.1.1] hub[🛜 Router / Gateway\nhub.lan\n192.168.1.1]
subgraph core[💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159] subgraph core[💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159]
traefik[🚦 Traefik\nReverse Proxy] traefik[🚦 Traefik\nReverse Proxy]
gitea[📚 Gitea\n git.appmodel.nl] gitea[📚 Gitea\n git.appmodel.nl]
dokku[🐳 Dokku\nPaaS / build] dokku[🐳 Dokku\nPaaS / build]
auctionFE[🧱 Auction Frontend\n auction.appmodel.nl] auctionFE[🧱 Auction Frontend\n auction.appmodel.nl]
aupiAPI[🧱 Auction Backend API\n aupi.appmodel.nl] aupiAPI[🧱 Auction Backend API\n aupi.appmodel.nl]
mi50[🧠 MI50 / Ollama\nAI workloads] mi50[🧠 MI50 / Ollama\nAI workloads]
end end
subgraph infraDNS[🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163] subgraph infraDNS[🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163]
adguard[🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl] adguard[🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl]
artifactory[📦 Artifactory] artifactory[📦 Artifactory]
runner[⚙️ Build runners] runner[⚙️ Build runners]
end end
subgraph ha[🏡 Home Automation\nha.lan\n192.168.1.193] subgraph ha[🏡 Home Automation\nha.lan\n192.168.1.193]
hass[🏠 Home Assistant] hass[🏠 Home Assistant]
end end
atlas[🧱 atlas.lan\n192.168.1.100\n] atlas[🧱 atlas.lan\n192.168.1.100\n]
iot1[📺 hof-E402NA\n192.168.1.214] iot1[📺 hof-E402NA\n192.168.1.214]
iot2[🎧 S380HB\n192.168.1.59] iot2[🎧 S380HB\n192.168.1.59]
iot3[📟 ecb5faa56c90\n192.168.1.49] iot3[📟 ecb5faa56c90\n192.168.1.49]
iot4[❓ Unknown\n192.168.1.240] iot4[❓ Unknown\n192.168.1.240]
end end
%% ============ Tether subnet ============ %% ============ Tether subnet ============
subgraph TETHER[📶 Tether subnet 192.168.137.0/24] subgraph TETHER[📶 Tether subnet 192.168.137.0/24]
hermes[🛰️ hermes.lan\n192.168.137.239\nworker / node] hermes[🛰️ hermes.lan\n192.168.137.239\nworker / node]
plato[🛰️ plato.lan\n192.168.137.163\nworker / node] plato[🛰️ plato.lan\n192.168.137.163\nworker / node]
end end
%% ============ Verkeer ============ %% ============ Verkeer ============
%% Basis LAN connecties %% Basis LAN connecties
hub --- core hub --- core
hub --- infraDNS hub --- infraDNS
hub --- ha hub --- ha
hub --- atlas hub --- atlas
hub --- iot1 hub --- iot1
hub --- iot2 hub --- iot2
hub --- iot3 hub --- iot3
hub --- iot4 hub --- iot4
%% WAN koppeling %% WAN koppeling
hub --> WAN hub --> WAN
infraDNS --> WAN infraDNS --> WAN
%% DNS-resolutie %% DNS-resolutie
core --> adguard core --> adguard
ha --> adguard ha --> adguard
atlas --> adguard atlas --> adguard
TETHER --> adguard TETHER --> adguard
%% Websites / reverse proxy %% Websites / reverse proxy
extDNS --> traefik extDNS --> traefik
traefik --> gitea traefik --> gitea
traefik --> auctionFE traefik --> auctionFE
traefik --> aupiAPI traefik --> aupiAPI
traefik --> dokku traefik --> dokku
%% App flow %% App flow
auctionFE --> aupiAPI auctionFE --> aupiAPI
aupiAPI --> adguard aupiAPI --> adguard
%% AI workloads %% AI workloads
core --> mi50 core --> mi50
%% Tether workers %% Tether workers
core --- TETHER core --- TETHER
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,264 +1,266 @@
<!DOCTYPE html> <!-- test321 -->
<html lang="en"> <!DOCTYPE html>
<head> <html lang="en">
<meta charset="utf-8"> <head>
<title>Home-Lab diagram live editor</title> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <title>Home-Lab diagram live editor</title>
<style> <meta name="viewport" content="width=device-width, initial-scale=1">
:root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd} <style>
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)} :root{--bg:#f7f9fc;--text:#222;--accent:#0d6efd}
header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-weight:600;display:flex;gap:.5rem;align-items:center} body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;font-size:14px;background:var(--bg);color:var(--text)}
header button{padding:.2rem .6rem;border:none;border-radius:4px;background:#fff;color:var(--accent);cursor:pointer} header{background:var(--accent);color:#fff;padding:.6rem 1rem;font-weight:600;display:flex;gap:.5rem;align-items:center}
.wrap{display:flex;height:calc(100vh - 40px)} header button{padding:.2rem .6rem;border:none;border-radius:4px;background:#fff;color:var(--accent);cursor:pointer}
.panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd;transition:flex .3s ease} .wrap{display:flex;height:calc(100vh - 40px)}
.panel:last-child{border:none} .panel{flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #ddd;transition:flex .3s ease}
h3{margin:.4rem .6rem;font-size:1rem} .panel:last-child{border:none}
#src{flex:1;padding:.5rem;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-family:"Consolas","Monaco",monospace;border:none;outline:none;white-space:pre-wrap} h3{margin:.4rem .6rem;font-size:1rem}
#preview{flex:1;padding:.5rem;overflow:auto;background:#fff} #src{flex:1;padding:.5rem;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-family:"Consolas","Monaco",monospace;border:none;outline:none;white-space:pre-wrap}
/* IMPORTANT: Let the SVG flow naturally and scroll if needed */ #preview{flex:1;padding:.5rem;overflow:auto;background:#fff}
#preview svg{display:block;margin:0 auto;max-width:100%;height:auto} /* IMPORTANT: Let the SVG flow naturally and scroll if needed */
#error{color:#d32f2f;background:#fff3cd;padding:.5rem;margin:.5rem;border-left:4px solid #d32f2f;display:none} #preview svg{display:block;margin:0 auto;max-width:100%;height:auto}
#zoomVal{margin-left:.5rem;font-size:.9rem;color:#fff} #error{color:#d32f2f;background:#fff3cd;padding:.5rem;margin:.5rem;border-left:4px solid #d32f2f;display:none}
/* collapsed state */ #zoomVal{margin-left:.5rem;font-size:.9rem;color:#fff}
#srcPanel.collapsed{flex:0} /* collapsed state */
#srcPanel.collapsed #src{display:none} #srcPanel.collapsed{flex:0}
/* Debug: uncomment to see element bounds */ #srcPanel.collapsed #src{display:none}
/* #preview{outline:2px solid red} #preview svg{outline:2px solid blue} */ /* Debug: uncomment to see element bounds */
</style> /* #preview{outline:2px solid red} #preview svg{outline:2px solid blue} */
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script> </style>
</head> <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<body> </head>
<header> <body>
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button> <header>
Home-Lab diagram live Mermaid editor <button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
<button onclick="saveSVG()">💾 SVG</button> Home-Lab diagram live Mermaid editor
<button onclick="savePNG()">💾 PNG</button> <button onclick="saveSVG()">💾 SVG</button>
<button onclick="reset()">🔄 Reset</button> <button onclick="savePNG()">💾 PNG</button>
<label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;color:#fff"> <button onclick="reset()">🔄 Reset</button>
Zoom: <input type="range" id="zoom" min="50" max="300" value="100" style="width:100px"><span id="zoomVal">100%</span> <label style="margin-left:auto;display:flex;align-items:center;gap:.3rem;color:#fff">
</label> Zoom: <input type="range" id="zoom" min="50" max="300" value="100" style="width:100px"><span id="zoomVal">100%</span>
</header> </label>
</header>
<div class="wrap">
<div class="panel" id="srcPanel"> <div class="wrap">
<h3>Source (edit here)</h3> <div class="panel" id="srcPanel">
<textarea id="src" spellcheck="false"></textarea> <h3>Source (edit here)</h3>
</div> <textarea id="src" spellcheck="false"></textarea>
<div class="panel"> </div>
<h3>Live preview</h3> <div class="panel">
<div id="preview"></div> <h3>Live preview</h3>
<div id="error"></div> <div id="preview"></div>
</div> <div id="error"></div>
</div> </div>
</div>
<script>
const DEFAULT = `flowchart TD <script>
%% ---------- styles ---------- const DEFAULT = `flowchart TD
classDef internet fill:#e1f5ff,stroke:#007bff %% ---------- styles ----------
classDef router fill:#fff3cd,stroke:#ffc107 classDef internet fill:#e1f5ff,stroke:#007bff
classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px classDef router fill:#fff3cd,stroke:#ffc107
classDef core fill:#ffe6e6,stroke:#dc3545 classDef lan fill:#f8f9ff,stroke:#6c757d,stroke-width:2px
classDef infra fill:#e6ffe6,stroke:#28a745 classDef core fill:#ffe6e6,stroke:#dc3545
classDef worker fill:#f0e6ff,stroke:#6f42c1 classDef infra fill:#e6ffe6,stroke:#28a745
classDef iot fill:#fff9e6,stroke:#fd7e14 classDef worker fill:#f0e6ff,stroke:#6f42c1
classDef iot fill:#fff9e6,stroke:#fd7e14
%% ---------- nodes ----------
internet(🌐 Internet / Cloud):::internet %% ---------- nodes ----------
router[🛜 Router\nhub.lan\n192.168.1.1]:::router internet(🌐 Internet / Cloud):::internet
router[🛜 Router\nhub.lan\n192.168.1.1]:::router
subgraph LAN [🏠 LAN 192.168.1.0/24]
subgraph CORE [💻 Core server\n192.168.1.159] subgraph LAN [🏠 LAN 192.168.1.0/24]
traefik[🚦 Traefik]:::core subgraph CORE [💻 Core server\n192.168.1.159]
gitea[📚 Gitea]:::core traefik[🚦 Traefik]:::core
dokku[🐳 Dokku]:::core gitea[📚 Gitea]:::core
auction[🧱 Auction stack]:::core dokku[🐳 Dokku]:::core
mi50[🧠 MI50 / Ollama]:::core auction[🧱 Auction stack]:::core
end mi50[🧠 MI50 / Ollama]:::core
end
subgraph INFRA [🧭 Infra & DNS\n192.168.1.163]
adguard[🛡️ AdGuard]:::infra subgraph INFRA [🧭 Infra & DNS\n192.168.1.163]
artifactory[📦 Artifactory]:::infra adguard[🛡️ AdGuard]:::infra
end artifactory[📦 Artifactory]:::infra
end
ha[🏡 Home Assistant\n192.168.1.193]:::infra
atlas[🧱 Atlas\n192.168.1.100]:::worker ha[🏡 Home Assistant\n192.168.1.193]:::infra
atlas[🧱 Atlas\n192.168.1.100]:::worker
iot1[📺 IoT-1]:::iot
iot2[📟 IoT-2]:::iot iot1[📺 IoT-1]:::iot
end iot2[📟 IoT-2]:::iot
end
subgraph TETHER [📶 Tether 192.168.137.0/24]
hermes[🛰️ Hermes]:::worker subgraph TETHER [📶 Tether 192.168.137.0/24]
plato[🛰️ Plato]:::worker hermes[🛰️ Hermes]:::worker
end plato[🛰️ Plato]:::worker
end
dev[👨‍💻 Dev laptop]:::internet
dev[👨‍💻 Dev laptop]:::internet
%% ---------- edges ----------
internet ==> router %% ---------- edges ----------
router --> CORE internet ==> router
router --> INFRA router --> CORE
router --> ha router --> INFRA
router --> atlas router --> ha
router --> iot1 router --> atlas
router --> iot2 router --> iot1
router --> iot2
dev ==> gitea
dev ==> dokku dev ==> gitea
dev ==> mi50 dev ==> dokku
dev ==> mi50
traefik --> gitea
traefik --> auction traefik --> gitea
traefik --> dokku traefik --> auction
traefik --> dokku
CORE -.->|DNS| adguard
ha -.->|DNS| adguard CORE -.->|DNS| adguard
atlas-.->|DNS| adguard ha -.->|DNS| adguard
hermes-.->|DNS| adguard atlas-.->|DNS| adguard
plato-.->|DNS| adguard hermes-.->|DNS| adguard
plato-.->|DNS| adguard
CORE === TETHER
`; CORE === TETHER
`;
function getStorage(key, fallback) {
try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; } function getStorage(key, fallback) {
} try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; }
function setStorage(key, val) { }
try { localStorage.setItem(key, val); } catch {} function setStorage(key, val) {
} try { localStorage.setItem(key, val); } catch {}
}
const srcEl = document.getElementById('src');
const errEl = document.getElementById('error'); const srcEl = document.getElementById('src');
const zoomEl = document.getElementById('zoom'); const errEl = document.getElementById('error');
const zoomVal = document.getElementById('zoomVal'); const zoomEl = document.getElementById('zoom');
const srcPanel = document.getElementById('srcPanel'); const zoomVal = document.getElementById('zoomVal');
const toggleBtn = document.getElementById('toggleBtn'); const srcPanel = document.getElementById('srcPanel');
const toggleBtn = document.getElementById('toggleBtn');
srcEl.value = getStorage('labDiagram', DEFAULT);
if (getStorage('panelCollapsed', 'false') === 'true') { srcEl.value = getStorage('labDiagram', DEFAULT);
srcPanel.classList.add('collapsed'); if (getStorage('panelCollapsed', 'false') === 'true') {
toggleBtn.textContent = '🗔 Show source'; srcPanel.classList.add('collapsed');
} toggleBtn.textContent = '🗔 Show source';
}
function togglePanel() {
srcPanel.classList.toggle('collapsed'); function togglePanel() {
const collapsed = srcPanel.classList.contains('collapsed'); srcPanel.classList.toggle('collapsed');
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source'; const collapsed = srcPanel.classList.contains('collapsed');
setStorage('panelCollapsed', collapsed); toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
} setStorage('panelCollapsed', collapsed);
}
mermaid.initialize({startOnLoad: false, theme: 'default'});
mermaid.initialize({startOnLoad: false, theme: 'default'});
function render() {
const src = srcEl.value; function render() {
setStorage('labDiagram', src); const src = srcEl.value;
errEl.style.display = 'none'; setStorage('labDiagram', src);
errEl.style.display = 'none';
const preview = document.getElementById('preview');
preview.innerHTML = ''; const preview = document.getElementById('preview');
preview.innerHTML = '';
const mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid'; const mermaidDiv = document.createElement('div');
mermaidDiv.textContent = src; mermaidDiv.className = 'mermaid';
preview.appendChild(mermaidDiv); mermaidDiv.textContent = src;
preview.appendChild(mermaidDiv);
// Render and fix sizing
mermaid.init(undefined, mermaidDiv).then(() => { // Render and fix sizing
const svg = preview.querySelector('svg'); mermaid.init(undefined, mermaidDiv).then(() => {
if (svg) { const svg = preview.querySelector('svg');
// CRITICAL: Ensure SVG has viewBox for proper sizing if (svg) {
if (!svg.hasAttribute('viewBox')) { // CRITICAL: Ensure SVG has viewBox for proper sizing
const {width, height} = svg.getBBox(); if (!svg.hasAttribute('viewBox')) {
svg.setAttribute('viewBox', `0 0 ${width} ${height}`); const {width, height} = svg.getBBox();
} svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
svg.style.maxWidth = '100%'; }
svg.style.height = 'auto'; svg.style.maxWidth = '100%';
applyZoom(); svg.style.height = 'auto';
} applyZoom();
}).catch(e => { }
errEl.textContent = 'Mermaid error: ' + e.message; }).catch(e => {
errEl.style.display = 'block'; errEl.textContent = 'Mermaid error: ' + e.message;
}); errEl.style.display = 'block';
} });
}
srcEl.addEventListener('input', () => {
clearTimeout(srcEl._t); srcEl.addEventListener('input', () => {
srcEl._t = setTimeout(render, 300); clearTimeout(srcEl._t);
}); srcEl._t = setTimeout(render, 300);
});
zoomEl.addEventListener('input', () => {
zoomVal.textContent = zoomEl.value + '%'; zoomEl.addEventListener('input', () => {
applyZoom(); zoomVal.textContent = zoomEl.value + '%';
}); applyZoom();
});
function applyZoom() {
const svg = document.querySelector('#preview svg'); function applyZoom() {
if (svg) { const svg = document.querySelector('#preview svg');
svg.style.transform = `scale(${zoomEl.value / 100})`; if (svg) {
} svg.style.transform = `scale(${zoomEl.value / 100})`;
} }
}
function reset() {
if (confirm('Reset to default diagram?')) { function reset() {
srcEl.value = DEFAULT; if (confirm('Reset to default diagram?')) {
zoomEl.value = 100; srcEl.value = DEFAULT;
zoomVal.textContent = '100%'; zoomEl.value = 100;
setStorage('labDiagram', DEFAULT); zoomVal.textContent = '100%';
render(); setStorage('labDiagram', DEFAULT);
} render();
} }
}
function saveSVG() {
const svg = document.querySelector('#preview svg'); function saveSVG() {
if (!svg) return alert('Nothing to save yet.'); const svg = document.querySelector('#preview svg');
if (!svg) return alert('Nothing to save yet.');
const clone = svg.cloneNode(true);
clone.removeAttribute('style'); const clone = svg.cloneNode(true);
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); clone.removeAttribute('style');
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
const url = URL.createObjectURL(blob); const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
const a = document.createElement('a'); const url = URL.createObjectURL(blob);
a.href = url; const a = document.createElement('a');
a.download = 'home-lab.svg'; a.href = url;
a.click(); a.download = 'home-lab.svg';
setTimeout(() => URL.revokeObjectURL(url), 100); a.click();
} setTimeout(() => URL.revokeObjectURL(url), 100);
}
function savePNG() {
const svg = document.querySelector('#preview svg'); function savePNG() {
if (!svg) return alert('Nothing to save yet.'); const svg = document.querySelector('#preview svg');
if (!svg) return alert('Nothing to save yet.');
const {width, height} = svg.getBBox();
const canvasW = Math.max(width, 1200); const {width, height} = svg.getBBox();
const canvasH = Math.max(height, 800); const canvasW = Math.max(width, 1200);
const canvasH = Math.max(height, 800);
const svgData = new XMLSerializer().serializeToString(svg);
const img = new Image(); const svgData = new XMLSerializer().serializeToString(svg);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas'); img.onload = () => {
canvas.width = canvasW; const canvas = document.createElement('canvas');
canvas.height = canvasH; canvas.width = canvasW;
const ctx = canvas.getContext('2d'); canvas.height = canvasH;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvasW, canvasH); ctx.fillStyle = 'white';
ctx.drawImage(img, 0, 0, canvasW, canvasH); ctx.fillRect(0, 0, canvasW, canvasH);
ctx.drawImage(img, 0, 0, canvasW, canvasH);
canvas.toBlob(blob => {
const a = document.createElement('a'); canvas.toBlob(blob => {
a.href = URL.createObjectURL(blob); const a = document.createElement('a');
a.download = 'home-lab.png'; a.href = URL.createObjectURL(blob);
a.click(); a.download = 'home-lab.png';
}); a.click();
}; });
};
img.onerror = () => alert('PNG conversion failed. Try SVG instead.');
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData); img.onerror = () => alert('PNG conversion failed. Try SVG instead.');
} img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
}
render();
</script> render();
</body> </script>
</html> </body>
</html>
<!-- Auto-deployed at $(date122`) -->

View File

@@ -1,2 +1,2 @@
diagrams==0.25.1 diagrams==0.25.1
graphviz==0.20.3 graphviz==0.20.3

View File

@@ -1,192 +1,192 @@
# Deployment Guide - App Pipeline # Deployment Guide - App Pipeline
Complete guide for deploying applications using the custom deployment pipeline with Gitea, Docker, and Traefik. Complete guide for deploying applications using the custom deployment pipeline with Gitea, Docker, and Traefik.
## 1. Create New Application ## 1. Create New Application
### 1.1 Gitea Repository ### 1.1 Gitea Repository
1. Log in to Gitea: https://git.appmodel.nl 1. Log in to Gitea: https://git.appmodel.nl
2. Create a new repository: 2. Create a new repository:
- **Owner**: Tour - **Owner**: Tour
- **Name**: `viewer` (or your app name) - **Name**: `viewer` (or your app name)
### 1.2 Generate Skeleton on Server ### 1.2 Generate Skeleton on Server
On the server, run: On the server, run:
```bash ```bash
apps-create viewer static-fe apps-create viewer static-fe
``` ```
This command: This command:
- Sets up `/opt/apps/viewer` (attempts to clone from `git@git.appmodel.nl:Tour/viewer.git`) - Sets up `/opt/apps/viewer` (attempts to clone from `git@git.appmodel.nl:Tour/viewer.git`)
- Generates a multi-stage Dockerfile for a Node-based static frontend - Generates a multi-stage Dockerfile for a Node-based static frontend
- Creates `~/infra/viewer/docker-compose.yml` with: - Creates `~/infra/viewer/docker-compose.yml` with:
- Service `viewer` - Service `viewer`
- Connection to `traefik_net` - Connection to `traefik_net`
- Traefik route: `https://viewer.appmodel.nl` - Traefik route: `https://viewer.appmodel.nl`
## 2. Development & Build ## 2. Development & Build
On your development machine: On your development machine:
```bash ```bash
# Clone the repository # Clone the repository
git clone git@git.appmodel.nl:Tour/viewer.git git clone git@git.appmodel.nl:Tour/viewer.git
cd viewer cd viewer
# Build your app as usual # Build your app as usual
npm install npm install
npm run build # output goes to dist/ npm run build # output goes to dist/
# Commit and push # Commit and push
git add . git add .
git commit -m "First version" git commit -m "First version"
git push git push
``` ```
**Note**: The Dockerfile expects a `dist/` directory containing static files. **Note**: The Dockerfile expects a `dist/` directory containing static files.
## 3. Deployment Pipeline ## 3. Deployment Pipeline
### 3.1 Manual Deploy with `app-deploy` ### 3.1 Manual Deploy with `app-deploy`
On the server, use the generic deploy command: On the server, use the generic deploy command:
```bash ```bash
app-deploy viewer app-deploy viewer
``` ```
This command performs: This command performs:
1. `cd /opt/apps/viewer` 1. `cd /opt/apps/viewer`
2. `git pull --ff-only` 2. `git pull --ff-only`
3. `cd /home/tour/infra/viewer` 3. `cd /home/tour/infra/viewer`
4. `docker compose up -d --build viewer` 4. `docker compose up -d --build viewer`
Logs are saved to: `/var/log/app-deploy-viewer.log` Logs are saved to: `/var/log/app-deploy-viewer.log`
### 3.2 Automatic Deployment via Gitea Hook ### 3.2 Automatic Deployment via Gitea Hook
Set up automatic deployment on every push: Set up automatic deployment on every push:
1. In Gitea, go to repository `Tour/viewer` 1. In Gitea, go to repository `Tour/viewer`
2. Navigate to **Settings → Git Hooks** 2. Navigate to **Settings → Git Hooks**
3. Select **post-receive** 3. Select **post-receive**
4. Add the following script: 4. Add the following script:
```bash ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
/usr/local/bin/app-deploy viewer /usr/local/bin/app-deploy viewer
``` ```
**Result**: Every `git push` to `Tour/viewer` automatically triggers a deployment. **Result**: Every `git push` to `Tour/viewer` automatically triggers a deployment.
## 4. Traefik & DNS Configuration ## 4. Traefik & DNS Configuration
### Traefik Setup ### Traefik Setup
Traefik runs in the traefik stack on the same server and manages: Traefik runs in the traefik stack on the same server and manages:
- `git.appmodel.nl` - `git.appmodel.nl`
- `auction.appmodel.nl` - `auction.appmodel.nl`
- `aupi.appmodel.nl` - `aupi.appmodel.nl`
- ... (new apps via labels) - ... (new apps via labels)
### Auto-Generated Labels ### Auto-Generated Labels
The `apps-create` command automatically adds the correct Traefik labels: The `apps-create` command automatically adds the correct Traefik labels:
```yaml ```yaml
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)" - "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)"
- "traefik.http.routers.viewer.entrypoints=websecure" - "traefik.http.routers.viewer.entrypoints=websecure"
- "traefik.http.routers.viewer.tls=true" - "traefik.http.routers.viewer.tls=true"
- "traefik.http.routers.viewer.tls.certresolver=letsencrypt" - "traefik.http.routers.viewer.tls.certresolver=letsencrypt"
- "traefik.http.services.viewer.loadbalancer.server.port=80" - "traefik.http.services.viewer.loadbalancer.server.port=80"
``` ```
### DNS Configuration ### DNS Configuration
Add a DNS record pointing to your server's public IP: Add a DNS record pointing to your server's public IP:
``` ```
viewer.appmodel.nl → <server-public-ip> viewer.appmodel.nl → <server-public-ip>
``` ```
Traefik + Let's Encrypt will automatically handle SSL certificate generation. Traefik + Let's Encrypt will automatically handle SSL certificate generation.
## 5. Future Application Types ## 5. Future Application Types
The `apps-create` script currently supports: The `apps-create` script currently supports:
- **`static-fe`** Node build → Nginx → static frontend - **`static-fe`** Node build → Nginx → static frontend
### Planned Types ### Planned Types
Future app types can be added: Future app types can be added:
```bash ```bash
# Python API # Python API
apps-create stats api-py apps-create stats api-py
# Background worker/crawler # Background worker/crawler
apps-create crawler worker-py apps-create crawler worker-py
``` ```
Each type will have: Each type will have:
- Custom Dockerfile template - Custom Dockerfile template
- Standard `docker-compose.yml` configuration - Standard `docker-compose.yml` configuration
- Same deployment pipeline: `app-deploy <name>` - Same deployment pipeline: `app-deploy <name>`
## Quick Reference ## Quick Reference
### Create and Deploy New App ### Create and Deploy New App
```bash ```bash
# On server: create skeleton # On server: create skeleton
apps-create viewer static-fe apps-create viewer static-fe
# On dev machine: develop and push # On dev machine: develop and push
git clone git@git.appmodel.nl:Tour/viewer.git git clone git@git.appmodel.nl:Tour/viewer.git
cd viewer cd viewer
npm install npm install
npm run build npm run build
git add . git add .
git commit -m "Initial commit" git commit -m "Initial commit"
git push git push
# On server: deploy (or auto-deploys via hook) # On server: deploy (or auto-deploys via hook)
app-deploy viewer app-deploy viewer
``` ```
### Existing Infrastructure ### Existing Infrastructure
| Service | URL | Description | | Service | URL | Description |
|---------|-----|-------------| |---------|-----|-------------|
| Gitea | https://git.appmodel.nl | Git repository hosting | | Gitea | https://git.appmodel.nl | Git repository hosting |
| Auction | https://auction.appmodel.nl | Auction frontend | | Auction | https://auction.appmodel.nl | Auction frontend |
| Aupi API | https://aupi.appmodel.nl | Auction backend API | | Aupi API | https://aupi.appmodel.nl | Auction backend API |
| Traefik | - | Reverse proxy & SSL | | Traefik | - | Reverse proxy & SSL |
### Log Locations ### Log Locations
- Deployment logs: `/var/log/app-deploy-<app-name>.log` - Deployment logs: `/var/log/app-deploy-<app-name>.log`
- Docker logs: `docker compose logs -f <service-name>` - Docker logs: `docker compose logs -f <service-name>`
### Useful Commands ### Useful Commands
```bash ```bash
# View deployment logs # View deployment logs
tail -f /var/log/app-deploy-viewer.log tail -f /var/log/app-deploy-viewer.log
# View container logs # View container logs
cd ~/infra/viewer cd ~/infra/viewer
docker compose logs -f viewer docker compose logs -f viewer
# Restart service # Restart service
docker compose restart viewer docker compose restart viewer
# Rebuild without cache # Rebuild without cache
docker compose up -d --build --no-cache viewer docker compose up -d --build --no-cache viewer
``` ```

File diff suppressed because it is too large Load Diff

View File

@@ -1,378 +1,378 @@
# Diagram Viewer Wiki # Diagram Viewer Wiki
Welcome to the **Diagram Viewer** project documentation. Welcome to the **Diagram Viewer** project documentation.
This project provides a static web interface for viewing and managing network architecture diagrams generated with Python's `diagrams` library. This project provides a static web interface for viewing and managing network architecture diagrams generated with Python's `diagrams` library.
## 📚 Contents ## 📚 Contents
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Development Guide](#development-guide) - [Development Guide](#development-guide)
- [Deployment Guide](#deployment-guide) - [Deployment Guide](#deployment-guide)
- [Architecture Overview](#architecture-overview) - [Architecture Overview](#architecture-overview)
--- ---
## Getting Started ## Getting Started
### Prerequisites ### Prerequisites
- Python 3.8+ - Python 3.8+
- Graphviz - Graphviz
- Docker (for deployment) - Docker (for deployment)
### Quick Start ### Quick Start
```bash ```bash
# Clone repository # Clone repository
git clone git@git.appmodel.nl:Tour/viewer.git git clone git@git.appmodel.nl:Tour/viewer.git
cd viewer cd viewer
# Create virtual environment # Create virtual environment
python -m venv .venv python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install dependencies # Install dependencies
pip install -r requirements.txt pip install -r requirements.txt
# Generate diagrams # Generate diagrams
python lan_architecture.py python lan_architecture.py
python main.py python main.py
``` ```
--- ---
## Development Guide ## Development Guide
### Project Structure ### Project Structure
``` ```
viewer/ viewer/
├── Dockerfile # Multi-stage build for production ├── Dockerfile # Multi-stage build for production
├── docker-compose.yml # Docker Compose configuration ├── docker-compose.yml # Docker Compose configuration
├── requirements.txt # Python dependencies ├── requirements.txt # Python dependencies
├── lan_architecture.py # LAN architecture diagram generator ├── lan_architecture.py # LAN architecture diagram generator
├── main.py # Main diagram generator ├── main.py # Main diagram generator
├── public/ ├── public/
│ └── index.html # Live Mermaid diagram editor │ └── index.html # Live Mermaid diagram editor
└── wiki/ # Documentation (this wiki) └── wiki/ # Documentation (this wiki)
``` ```
### Adding New Diagrams ### Adding New Diagrams
1. Create a new Python file (e.g., `network_diagram.py`) 1. Create a new Python file (e.g., `network_diagram.py`)
2. Import required components from `diagrams` library 2. Import required components from `diagrams` library
3. Define your architecture using context managers 3. Define your architecture using context managers
4. Run the script to generate PNG output 4. Run the script to generate PNG output
Example: Example:
```python ```python
from diagrams import Diagram, Cluster from diagrams import Diagram, Cluster
from diagrams.onprem.network import Router from diagrams.onprem.network import Router
from diagrams.generic.device import Mobile from diagrams.generic.device import Mobile
with Diagram("My Network", show=False, filename="my_network"): with Diagram("My Network", show=False, filename="my_network"):
router = Router("Gateway") router = Router("Gateway")
device = Mobile("Phone") device = Mobile("Phone")
device >> router device >> router
``` ```
### Testing Locally ### Testing Locally
```bash ```bash
# Generate diagrams # Generate diagrams
python lan_architecture.py python lan_architecture.py
# View output # View output
ls -la *.png ls -la *.png
# Test with local web server # Test with local web server
python -m http.server 8000 --directory public/ python -m http.server 8000 --directory public/
# Open: http://localhost:8000 # Open: http://localhost:8000
``` ```
### Available Icons ### Available Icons
The `diagrams` library provides icons from multiple providers: The `diagrams` library provides icons from multiple providers:
- **OnPrem**: Network, compute, storage, etc. - **OnPrem**: Network, compute, storage, etc.
- **Cloud**: AWS, Azure, GCP services - **Cloud**: AWS, Azure, GCP services
- **Generic**: Network devices, blank nodes - **Generic**: Network devices, blank nodes
- **Custom**: Use your own images - **Custom**: Use your own images
See: [Diagrams Documentation](https://diagrams.mingrammer.com/docs/nodes/overview) See: [Diagrams Documentation](https://diagrams.mingrammer.com/docs/nodes/overview)
--- ---
## Deployment Guide ## Deployment Guide
### Overview ### Overview
The project uses a fully automated Docker-based deployment pipeline: The project uses a fully automated Docker-based deployment pipeline:
1. **Develop** on local machine 1. **Develop** on local machine
2. **Commit & Push** to Gitea 2. **Commit & Push** to Gitea
3. **Auto-Deploy** via post-receive hook 3. **Auto-Deploy** via post-receive hook
4. **Build** multi-stage Docker image 4. **Build** multi-stage Docker image
5. **Serve** via Traefik reverse proxy 5. **Serve** via Traefik reverse proxy
### Deployment Steps ### Deployment Steps
#### 1. Initial Setup #### 1. Initial Setup
```bash ```bash
# On server: Create app structure # On server: Create app structure
apps-create viewer static-fe apps-create viewer static-fe
# This creates: # This creates:
# - /opt/apps/viewer (git repository) # - /opt/apps/viewer (git repository)
# - /home/tour/infra/viewer/docker-compose.yml # - /home/tour/infra/viewer/docker-compose.yml
``` ```
#### 2. Configure Gitea Hook #### 2. Configure Gitea Hook
In repository **Settings → Git Hooks → post-receive**: In repository **Settings → Git Hooks → post-receive**:
```bash ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
/usr/local/bin/app-deploy viewer /usr/local/bin/app-deploy viewer
``` ```
#### 3. Deploy #### 3. Deploy
Simply push to Gitea: Simply push to Gitea:
```bash ```bash
git push origin main git push origin main
``` ```
The server automatically: The server automatically:
- Pulls latest code - Pulls latest code
- Rebuilds Docker image - Rebuilds Docker image
- Restarts container - Restarts container
- Updates live site at https://viewer.appmodel.nl - Updates live site at https://viewer.appmodel.nl
### Manual Deployment ### Manual Deployment
If needed: If needed:
```bash ```bash
# On server # On server
app-deploy viewer app-deploy viewer
# Or step-by-step # Or step-by-step
cd /opt/apps/viewer cd /opt/apps/viewer
git pull git pull
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose up -d --build viewer docker compose up -d --build viewer
``` ```
### Monitoring ### Monitoring
```bash ```bash
# View deployment logs # View deployment logs
tail -f /var/log/app-deploy-viewer.log tail -f /var/log/app-deploy-viewer.log
# View container logs # View container logs
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose logs -f viewer docker compose logs -f viewer
# Check container status # Check container status
docker compose ps docker compose ps
``` ```
--- ---
## Architecture Overview ## Architecture Overview
### Infrastructure ### Infrastructure
``` ```
┌─────────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐
│ Production Architecture │ │ Production Architecture │
├─────────────────────────────────────────────┤ ├─────────────────────────────────────────────┤
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ Gitea │ (Source of Truth) │ │ │ Gitea │ (Source of Truth) │
│ │ Tour/viewer │ │ │ │ Tour/viewer │ │
│ └──────┬───────┘ │ │ └──────┬───────┘ │
│ │ git push │ │ │ git push │
│ ↓ │ │ ↓ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ Post-Receive │ (Auto-trigger) │ │ │ Post-Receive │ (Auto-trigger) │
│ │ Hook │ │ │ │ Hook │ │
│ └──────┬───────┘ │ │ └──────┬───────┘ │
│ │ app-deploy viewer │ │ │ app-deploy viewer │
│ ↓ │ │ ↓ │
│ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │
│ │ /opt/apps/viewer/ │ │ │ │ /opt/apps/viewer/ │ │
│ │ (Git Repository) │ │ │ │ (Git Repository) │ │
│ └──────┬───────────────┘ │ │ └──────┬───────────────┘ │
│ │ git pull │ │ │ git pull │
│ ↓ │ │ ↓ │
│ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │
│ │ Docker Build Process │ │ │ │ Docker Build Process │ │
│ │ ┌─────────────────┐ │ │ │ │ ┌─────────────────┐ │ │
│ │ │ Stage 1: Build │ │ │ │ │ │ Stage 1: Build │ │ │
│ │ │ - Python │ │ │ │ │ │ - Python │ │ │
│ │ │ - Graphviz │ │ │ │ │ │ - Graphviz │ │ │
│ │ │ - Generate PNGs │ │ │ │ │ │ - Generate PNGs │ │ │
│ │ └─────────────────┘ │ │ │ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │ │ │ ┌─────────────────┐ │ │
│ │ │ Stage 2: Serve │ │ │ │ │ │ Stage 2: Serve │ │ │
│ │ │ - Nginx │ │ │ │ │ │ - Nginx │ │ │
│ │ │ - Static files │ │ │ │ │ │ - Static files │ │ │
│ │ └─────────────────┘ │ │ │ │ └─────────────────┘ │ │
│ └──────┬───────────────┘ │ │ └──────┬───────────────┘ │
│ │ container starts │ │ │ container starts │
│ ↓ │ │ ↓ │
│ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │
│ │ diagram-viewer │ │ │ │ diagram-viewer │ │
│ │ Container :80 │ │ │ │ Container :80 │ │
│ └──────┬───────────────┘ │ │ └──────┬───────────────┘ │
│ │ traefik_net │ │ │ traefik_net │
│ ↓ │ │ ↓ │
│ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │
│ │ Traefik │ │ │ │ Traefik │ │
│ │ Reverse Proxy │ │ │ │ Reverse Proxy │ │
│ │ + Let's Encrypt │ │ │ │ + Let's Encrypt │ │
│ └──────┬───────────────┘ │ │ └──────┬───────────────┘ │
│ │ HTTPS │ │ │ HTTPS │
│ ↓ │ │ ↓ │
│ viewer.appmodel.nl │ │ viewer.appmodel.nl │
│ │ │ │
└─────────────────────────────────────────────┘ └─────────────────────────────────────────────┘
``` ```
### Network Topology ### Network Topology
The diagram viewer documents our home lab infrastructure: The diagram viewer documents our home lab infrastructure:
- **LAN 192.168.1.0/24**: Main network - **LAN 192.168.1.0/24**: Main network
- Core server (192.168.1.159): Traefik, Gitea, Dokku, Auction stack, MI50/Ollama - Core server (192.168.1.159): Traefik, Gitea, Dokku, Auction stack, MI50/Ollama
- DNS server (192.168.1.163): AdGuard, Artifactory, CI runner - DNS server (192.168.1.163): AdGuard, Artifactory, CI runner
- Home Assistant (192.168.1.193) - Home Assistant (192.168.1.193)
- Atlas worker (192.168.1.100) - Atlas worker (192.168.1.100)
- IoT devices - IoT devices
- **Tether 192.168.137.0/24**: Isolated subnet - **Tether 192.168.137.0/24**: Isolated subnet
- Hermes worker (192.168.137.239) - Hermes worker (192.168.137.239)
- Plato worker (192.168.137.163) - Plato worker (192.168.137.163)
### Services ### Services
| Service | URL | Description | | Service | URL | Description |
|---------|-----|-------------| |---------|-----|-------------|
| Diagram Viewer | https://viewer.appmodel.nl | This application | | Diagram Viewer | https://viewer.appmodel.nl | This application |
| Gitea | https://git.appmodel.nl | Git hosting | | Gitea | https://git.appmodel.nl | Git hosting |
| Auction Frontend | https://auction.appmodel.nl | Auction platform | | Auction Frontend | https://auction.appmodel.nl | Auction platform |
| Aupi API | https://aupi.appmodel.nl | Backend API | | Aupi API | https://aupi.appmodel.nl | Backend API |
| Dokku | https://dokku.lan | PaaS platform | | Dokku | https://dokku.lan | PaaS platform |
--- ---
## Additional Resources ## Additional Resources
### Related Documentation ### Related Documentation
- [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md) - Detailed deployment guide - [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md) - Detailed deployment guide
- [DEPLOYMENT_GUIDE.md](../DEPLOYMENT_GUIDE.md) - General deployment pipeline - [DEPLOYMENT_GUIDE.md](../DEPLOYMENT_GUIDE.md) - General deployment pipeline
- [README.md](../README.md) - Project overview - [README.md](../README.md) - Project overview
### External Links ### External Links
- [Diagrams Library](https://diagrams.mingrammer.com/) - [Diagrams Library](https://diagrams.mingrammer.com/)
- [Graphviz](https://graphviz.org/) - [Graphviz](https://graphviz.org/)
- [Mermaid.js](https://mermaid.js.org/) (used in live editor) - [Mermaid.js](https://mermaid.js.org/) (used in live editor)
- [Traefik Documentation](https://doc.traefik.io/traefik/) - [Traefik Documentation](https://doc.traefik.io/traefik/)
- [Docker Compose](https://docs.docker.com/compose/) - [Docker Compose](https://docs.docker.com/compose/)
### Contributing ### Contributing
To contribute to this project: To contribute to this project:
1. Create a feature branch 1. Create a feature branch
2. Make your changes 2. Make your changes
3. Test locally 3. Test locally
4. Submit a pull request in Gitea 4. Submit a pull request in Gitea
5. Wait for automatic deployment after merge 5. Wait for automatic deployment after merge
--- ---
## Troubleshooting ## Troubleshooting
### Common Issues ### Common Issues
#### Diagrams Not Generating #### Diagrams Not Generating
**Problem**: Python script fails to generate PNG files **Problem**: Python script fails to generate PNG files
**Solution**: **Solution**:
```bash ```bash
# Check Graphviz installation # Check Graphviz installation
dot -V dot -V
# Reinstall if needed # Reinstall if needed
# macOS: brew install graphviz # macOS: brew install graphviz
# Ubuntu: sudo apt-get install graphviz # Ubuntu: sudo apt-get install graphviz
# Windows: choco install graphviz # Windows: choco install graphviz
# Verify Python dependencies # Verify Python dependencies
pip list | grep diagrams pip list | grep diagrams
pip install --upgrade diagrams pip install --upgrade diagrams
``` ```
#### Container Build Fails #### Container Build Fails
**Problem**: Docker build fails during deployment **Problem**: Docker build fails during deployment
**Solution**: **Solution**:
```bash ```bash
# Check deployment logs # Check deployment logs
tail -f /var/log/app-deploy-viewer.log tail -f /var/log/app-deploy-viewer.log
# Rebuild manually with verbose output # Rebuild manually with verbose output
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose build --no-cache --progress=plain viewer docker compose build --no-cache --progress=plain viewer
``` ```
#### Site Not Accessible #### Site Not Accessible
**Problem**: Container runs but site not reachable **Problem**: Container runs but site not reachable
**Solution**: **Solution**:
1. Check DNS: `nslookup viewer.appmodel.nl` 1. Check DNS: `nslookup viewer.appmodel.nl`
2. Verify Traefik labels: `docker inspect viewer-viewer | grep traefik` 2. Verify Traefik labels: `docker inspect viewer-viewer | grep traefik`
3. Check Traefik logs: `docker logs traefik` 3. Check Traefik logs: `docker logs traefik`
4. Test container directly: `curl http://localhost:PORT` 4. Test container directly: `curl http://localhost:PORT`
#### Changes Not Reflected #### Changes Not Reflected
**Problem**: Pushed changes but site unchanged **Problem**: Pushed changes but site unchanged
**Solution**: **Solution**:
```bash ```bash
# Force rebuild on server # Force rebuild on server
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose down docker compose down
docker compose up -d --build --force-recreate viewer docker compose up -d --build --force-recreate viewer
# Clear browser cache # Clear browser cache
# Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (macOS) # Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (macOS)
``` ```
--- ---
## Contact & Support ## Contact & Support
For issues, questions, or suggestions: For issues, questions, or suggestions:
1. Check this wiki 1. Check this wiki
2. Review [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md) 2. Review [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md)
3. Check container logs 3. Check container logs
4. Contact the infrastructure team 4. Contact the infrastructure team
--- ---
**Last Updated**: 2025-12-02 **Last Updated**: 2025-12-02
**Version**: 1.0.0 **Version**: 1.0.0
**Maintainer**: Tour **Maintainer**: Tour

View File

@@ -1,239 +1,239 @@
# Quick Start Guide # Quick Start Guide
Get up and running with the Diagram Viewer in minutes. Get up and running with the Diagram Viewer in minutes.
## For Developers ## For Developers
### 1. Clone Repository ### 1. Clone Repository
```bash ```bash
git clone git@git.appmodel.nl:Tour/viewer.git git clone git@git.appmodel.nl:Tour/viewer.git
cd viewer cd viewer
``` ```
### 2. Setup Environment ### 2. Setup Environment
```bash ```bash
# Create virtual environment # Create virtual environment
python -m venv .venv python -m venv .venv
# Activate (choose your platform) # Activate (choose your platform)
source .venv/bin/activate # macOS/Linux source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows CMD .venv\Scripts\activate # Windows CMD
source .venv/Scripts/activate # Windows Git Bash source .venv/Scripts/activate # Windows Git Bash
``` ```
### 3. Install Dependencies ### 3. Install Dependencies
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
### 4. Generate Diagrams ### 4. Generate Diagrams
```bash ```bash
# Generate LAN architecture diagram # Generate LAN architecture diagram
python lan_architecture.py python lan_architecture.py
# Generate main diagram # Generate main diagram
python main.py python main.py
# View generated files # View generated files
ls -la *.png ls -la *.png
``` ```
### 5. Test Locally ### 5. Test Locally
```bash ```bash
# Start local web server # Start local web server
python -m http.server 8000 --directory public/ python -m http.server 8000 --directory public/
# Open browser # Open browser
# Navigate to: http://localhost:8000 # Navigate to: http://localhost:8000
``` ```
### 6. Make Changes & Deploy ### 6. Make Changes & Deploy
```bash ```bash
# Edit diagram code # Edit diagram code
vim lan_architecture.py vim lan_architecture.py
# Commit changes # Commit changes
git add . git add .
git commit -m "Update network diagram" git commit -m "Update network diagram"
# Push to deploy (auto-deploys to production) # Push to deploy (auto-deploys to production)
git push origin main git push origin main
``` ```
--- ---
## For Operators ## For Operators
### View Live Site ### View Live Site
Simply navigate to: Simply navigate to:
``` ```
https://viewer.appmodel.nl https://viewer.appmodel.nl
``` ```
### Manual Deployment ### Manual Deployment
On the server: On the server:
```bash ```bash
# Trigger deployment # Trigger deployment
app-deploy viewer app-deploy viewer
# View logs # View logs
tail -f /var/log/app-deploy-viewer.log tail -f /var/log/app-deploy-viewer.log
# Check status # Check status
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose ps docker compose ps
``` ```
### View Logs ### View Logs
```bash ```bash
# Deployment logs # Deployment logs
tail -f /var/log/app-deploy-viewer.log tail -f /var/log/app-deploy-viewer.log
# Container logs # Container logs
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose logs -f viewer docker compose logs -f viewer
# Last 100 lines # Last 100 lines
docker compose logs --tail=100 viewer docker compose logs --tail=100 viewer
``` ```
### Restart Service ### Restart Service
```bash ```bash
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
# Restart container # Restart container
docker compose restart viewer docker compose restart viewer
# Full restart (rebuild) # Full restart (rebuild)
docker compose down docker compose down
docker compose up -d --build viewer docker compose up -d --build viewer
``` ```
--- ---
## Common Tasks ## Common Tasks
### Add New Diagram ### Add New Diagram
1. Create new Python file: 1. Create new Python file:
```python ```python
# my_diagram.py # my_diagram.py
from diagrams import Diagram from diagrams import Diagram
from diagrams.onprem.network import Router from diagrams.onprem.network import Router
with Diagram("My Network", show=False, filename="my_network"): with Diagram("My Network", show=False, filename="my_network"):
Router("Gateway") Router("Gateway")
``` ```
2. Update Dockerfile if needed (add to RUN command): 2. Update Dockerfile if needed (add to RUN command):
```dockerfile ```dockerfile
RUN python lan_architecture.py && \ RUN python lan_architecture.py && \
python main.py && \ python main.py && \
python my_diagram.py python my_diagram.py
``` ```
3. Commit and push: 3. Commit and push:
```bash ```bash
git add my_diagram.py Dockerfile git add my_diagram.py Dockerfile
git commit -m "Add my network diagram" git commit -m "Add my network diagram"
git push git push
``` ```
### Update Frontend ### Update Frontend
1. Edit HTML: 1. Edit HTML:
```bash ```bash
vim public/index.html vim public/index.html
``` ```
2. Test locally: 2. Test locally:
```bash ```bash
python -m http.server 8000 --directory public/ python -m http.server 8000 --directory public/
``` ```
3. Deploy: 3. Deploy:
```bash ```bash
git add public/index.html git add public/index.html
git commit -m "Update frontend" git commit -m "Update frontend"
git push git push
``` ```
### Check Container Health ### Check Container Health
```bash ```bash
# Container status # Container status
docker compose ps docker compose ps
# Health check # Health check
docker inspect viewer | grep -A 10 Health docker inspect viewer | grep -A 10 Health
# Test endpoint # Test endpoint
curl -I https://viewer.appmodel.nl curl -I https://viewer.appmodel.nl
``` ```
--- ---
## Troubleshooting ## Troubleshooting
### Build Fails ### Build Fails
```bash ```bash
# Check logs # Check logs
tail -f /var/log/app-deploy-viewer.log tail -f /var/log/app-deploy-viewer.log
# Rebuild with verbose output # Rebuild with verbose output
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose build --no-cache --progress=plain viewer docker compose build --no-cache --progress=plain viewer
``` ```
### Site Not Loading ### Site Not Loading
```bash ```bash
# Check container # Check container
docker compose ps docker compose ps
# Check Traefik routing # Check Traefik routing
docker logs traefik | grep viewer docker logs traefik | grep viewer
# Test DNS # Test DNS
nslookup viewer.appmodel.nl nslookup viewer.appmodel.nl
# Test directly # Test directly
curl http://localhost:PORT curl http://localhost:PORT
``` ```
### Changes Not Visible ### Changes Not Visible
```bash ```bash
# Force rebuild # Force rebuild
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose up -d --build --force-recreate viewer docker compose up -d --build --force-recreate viewer
# Clear browser cache # Clear browser cache
# Ctrl+Shift+R (Windows/Linux) # Ctrl+Shift+R (Windows/Linux)
# Cmd+Shift+R (macOS) # Cmd+Shift+R (macOS)
``` ```
--- ---
## Next Steps ## Next Steps
- Read the [Home](Home.md) page for comprehensive documentation - Read the [Home](Home.md) page for comprehensive documentation
- Review [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md) for deployment details - Review [DEPLOY_SERVER_SETUP.md](../DEPLOY_SERVER_SETUP.md) for deployment details
- Check [Diagrams Library docs](https://diagrams.mingrammer.com/) for icon options - Check [Diagrams Library docs](https://diagrams.mingrammer.com/) for icon options
- Explore the [Mermaid editor](https://viewer.appmodel.nl) for live diagram editing - Explore the [Mermaid editor](https://viewer.appmodel.nl) for live diagram editing
--- ---
**Need Help?** Check the [Troubleshooting](Home.md#troubleshooting) section or review deployment logs. **Need Help?** Check the [Troubleshooting](Home.md#troubleshooting) section or review deployment logs.

View File

@@ -1,361 +1,361 @@
# Gitea Wiki Integration Guide # Gitea Wiki Integration Guide
This guide explains how to integrate the local `wiki/` folder with Gitea's wiki system. This guide explains how to integrate the local `wiki/` folder with Gitea's wiki system.
## How Gitea Wikis Work ## How Gitea Wikis Work
Gitea stores wiki pages in a **separate Git repository** with the naming pattern: Gitea stores wiki pages in a **separate Git repository** with the naming pattern:
``` ```
<repository>.wiki.git <repository>.wiki.git
``` ```
For the `viewer` repository: For the `viewer` repository:
- Main repo: `git@git.appmodel.nl:Tour/viewer.git` - Main repo: `git@git.appmodel.nl:Tour/viewer.git`
- Wiki repo: `git@git.appmodel.nl:Tour/viewer.wiki.git` - Wiki repo: `git@git.appmodel.nl:Tour/viewer.wiki.git`
Wiki pages are **markdown files** (`.md`) stored in the root of the wiki repository. Wiki pages are **markdown files** (`.md`) stored in the root of the wiki repository.
## Integration Options ## Integration Options
### Option 1: Push Local Wiki to Gitea (Recommended) ### Option 1: Push Local Wiki to Gitea (Recommended)
This approach replaces the current Gitea wiki with your local content. This approach replaces the current Gitea wiki with your local content.
```bash ```bash
# 1. Clone the Gitea wiki repository # 1. Clone the Gitea wiki repository
git clone git@git.appmodel.nl:Tour/viewer.wiki.git viewer-wiki git clone git@git.appmodel.nl:Tour/viewer.wiki.git viewer-wiki
# 2. Copy your wiki files to the wiki repository # 2. Copy your wiki files to the wiki repository
cd viewer-wiki cd viewer-wiki
cp ../viewer/wiki/*.md . cp ../viewer/wiki/*.md .
# 3. Commit and push # 3. Commit and push
git add . git add .
git commit -m "Initialize wiki with documentation" git commit -m "Initialize wiki with documentation"
git push origin master git push origin master
# 4. View on Gitea # 4. View on Gitea
# Navigate to: https://git.appmodel.nl/Tour/viewer/wiki # Navigate to: https://git.appmodel.nl/Tour/viewer/wiki
``` ```
### Option 2: Use Git Submodule (Bidirectional Sync) ### Option 2: Use Git Submodule (Bidirectional Sync)
Keep the wiki as a submodule in the main repository for easy synchronization. Keep the wiki as a submodule in the main repository for easy synchronization.
```bash ```bash
# In the main viewer repository # In the main viewer repository
cd /c/vibe/viewer cd /c/vibe/viewer
# Remove local wiki folder # Remove local wiki folder
rm -rf wiki/ rm -rf wiki/
# Add Gitea wiki as submodule # Add Gitea wiki as submodule
git submodule add git@git.appmodel.nl:Tour/viewer.wiki.git wiki git submodule add git@git.appmodel.nl:Tour/viewer.wiki.git wiki
# Copy your documentation # Copy your documentation
cp ../temp-wiki-backup/*.md wiki/ cp ../temp-wiki-backup/*.md wiki/
# Commit to wiki submodule # Commit to wiki submodule
cd wiki cd wiki
git add . git add .
git commit -m "Add documentation" git commit -m "Add documentation"
git push origin master git push origin master
# Update main repository # Update main repository
cd .. cd ..
git add wiki git add wiki
git commit -m "Add wiki as submodule" git commit -m "Add wiki as submodule"
git push origin main git push origin main
``` ```
Now changes can be made either: Now changes can be made either:
- **In Gitea UI**: Edit wiki pages directly - **In Gitea UI**: Edit wiki pages directly
- **Locally**: Edit in `wiki/` folder, commit, and push - **Locally**: Edit in `wiki/` folder, commit, and push
To sync changes from Gitea: To sync changes from Gitea:
```bash ```bash
cd wiki cd wiki
git pull origin master git pull origin master
cd .. cd ..
git add wiki git add wiki
git commit -m "Update wiki submodule" git commit -m "Update wiki submodule"
git push git push
``` ```
### Option 3: Git Subtree (Advanced) ### Option 3: Git Subtree (Advanced)
Similar to submodule but integrates the wiki history into the main repository. Similar to submodule but integrates the wiki history into the main repository.
```bash ```bash
# Add wiki as subtree # Add wiki as subtree
git subtree add --prefix=wiki git@git.appmodel.nl:Tour/viewer.wiki.git master --squash git subtree add --prefix=wiki git@git.appmodel.nl:Tour/viewer.wiki.git master --squash
# Push changes to wiki # Push changes to wiki
git subtree push --prefix=wiki git@git.appmodel.nl:Tour/viewer.wiki.git master git subtree push --prefix=wiki git@git.appmodel.nl:Tour/viewer.wiki.git master
# Pull changes from wiki # Pull changes from wiki
git subtree pull --prefix=wiki git@git.appmodel.nl:Tour/viewer.wiki.git master --squash git subtree pull --prefix=wiki git@git.appmodel.nl:Tour/viewer.wiki.git master --squash
``` ```
### Option 4: Automated Sync Script ### Option 4: Automated Sync Script
Create a script to automatically sync wiki changes. Create a script to automatically sync wiki changes.
Create `sync-wiki.sh`: Create `sync-wiki.sh`:
```bash ```bash
#!/bin/bash #!/bin/bash
# sync-wiki.sh - Sync local wiki with Gitea # sync-wiki.sh - Sync local wiki with Gitea
WIKI_REPO="git@git.appmodel.nl:Tour/viewer.wiki.git" WIKI_REPO="git@git.appmodel.nl:Tour/viewer.wiki.git"
WIKI_DIR="wiki" WIKI_DIR="wiki"
TEMP_WIKI="/tmp/gitea-wiki-$$" TEMP_WIKI="/tmp/gitea-wiki-$$"
# Clone Gitea wiki # Clone Gitea wiki
git clone "$WIKI_REPO" "$TEMP_WIKI" git clone "$WIKI_REPO" "$TEMP_WIKI"
# Copy local changes to temp wiki # Copy local changes to temp wiki
cp -r "$WIKI_DIR"/*.md "$TEMP_WIKI/" cp -r "$WIKI_DIR"/*.md "$TEMP_WIKI/"
# Commit and push # Commit and push
cd "$TEMP_WIKI" cd "$TEMP_WIKI"
git add . git add .
if git diff-staged --quiet; then if git diff-staged --quiet; then
echo "No changes to sync" echo "No changes to sync"
else else
git commit -m "Sync wiki from main repository" git commit -m "Sync wiki from main repository"
git push origin master git push origin master
echo "Wiki synced successfully" echo "Wiki synced successfully"
fi fi
# Cleanup # Cleanup
cd - cd -
rm -rf "$TEMP_WIKI" rm -rf "$TEMP_WIKI"
``` ```
Usage: Usage:
```bash ```bash
chmod +x sync-wiki.sh chmod +x sync-wiki.sh
./sync-wiki.sh ./sync-wiki.sh
``` ```
## Recommended Workflow ## Recommended Workflow
For your deployment pipeline, I recommend **Option 1** with a manual push: For your deployment pipeline, I recommend **Option 1** with a manual push:
### Step-by-Step Setup ### Step-by-Step Setup
1. **Create wiki repository on Gitea** (if not exists): 1. **Create wiki repository on Gitea** (if not exists):
- Go to: https://git.appmodel.nl/Tour/viewer - Go to: https://git.appmodel.nl/Tour/viewer
- Click "Wiki" tab - Click "Wiki" tab
- Create first page (triggers wiki repo creation) - Create first page (triggers wiki repo creation)
2. **Push local wiki content**: 2. **Push local wiki content**:
```bash ```bash
# Clone wiki repository # Clone wiki repository
cd /c/vibe cd /c/vibe
git clone git@git.appmodel.nl:Tour/viewer.wiki.git git clone git@git.appmodel.nl:Tour/viewer.wiki.git
# Copy your wiki files # Copy your wiki files
cd viewer.wiki cd viewer.wiki
cp ../viewer/wiki/*.md . cp ../viewer/wiki/*.md .
# Verify files # Verify files
ls -la ls -la
# Commit and push # Commit and push
git add . git add .
git commit -m "Initialize wiki documentation git commit -m "Initialize wiki documentation
Added: Added:
- Home.md: Main documentation page - Home.md: Main documentation page
- Quick-Start.md: Quick start guide - Quick-Start.md: Quick start guide
" "
git push origin master git push origin master
``` ```
3. **Verify on Gitea**: 3. **Verify on Gitea**:
- Navigate to: https://git.appmodel.nl/Tour/viewer/wiki - Navigate to: https://git.appmodel.nl/Tour/viewer/wiki
- You should see your wiki pages - You should see your wiki pages
4. **Keep local wiki in main repo** (optional): 4. **Keep local wiki in main repo** (optional):
- Keep `wiki/` folder in main repo as "source of truth" - Keep `wiki/` folder in main repo as "source of truth"
- When updating wiki, manually sync to Gitea wiki repo - When updating wiki, manually sync to Gitea wiki repo
- Or set up automated sync via CI/CD - Or set up automated sync via CI/CD
## Wiki Page Naming ## Wiki Page Naming
Gitea wikis follow these conventions: Gitea wikis follow these conventions:
| Filename | URL | Purpose | | Filename | URL | Purpose |
|----------|-----|---------| |----------|-----|---------|
| `Home.md` | `/wiki/` or `/wiki/Home` | Main wiki page | | `Home.md` | `/wiki/` or `/wiki/Home` | Main wiki page |
| `Quick-Start.md` | `/wiki/Quick-Start` | Quick start guide | | `Quick-Start.md` | `/wiki/Quick-Start` | Quick start guide |
| `API-Reference.md` | `/wiki/API-Reference` | API documentation | | `API-Reference.md` | `/wiki/API-Reference` | API documentation |
**Rules**: **Rules**:
- Use PascalCase or kebab-case for multi-word pages - Use PascalCase or kebab-case for multi-word pages
- Spaces in filenames become hyphens in URLs - Spaces in filenames become hyphens in URLs
- First page is always `Home.md` - First page is always `Home.md`
## Linking Between Pages ## Linking Between Pages
In your markdown files: In your markdown files:
```markdown ```markdown
<!-- Link to another wiki page --> <!-- Link to another wiki page -->
[Quick Start Guide](Quick-Start) [Quick Start Guide](Quick-Start)
<!-- Link with custom text --> <!-- Link with custom text -->
[Get Started](Quick-Start) [Get Started](Quick-Start)
<!-- Link to main repository --> <!-- Link to main repository -->
[View Code](../) [View Code](../)
<!-- Link to specific file --> <!-- Link to specific file -->
[Dockerfile](../src/master/Dockerfile) [Dockerfile](../src/master/Dockerfile)
<!-- External link --> <!-- External link -->
[Diagrams Library](https://diagrams.mingrammer.com/) [Diagrams Library](https://diagrams.mingrammer.com/)
``` ```
## Automation with Post-Receive Hook ## Automation with Post-Receive Hook
To automatically sync wiki on deployment, add to Gitea post-receive hook: To automatically sync wiki on deployment, add to Gitea post-receive hook:
```bash ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
# Deploy main app # Deploy main app
/usr/local/bin/app-deploy viewer /usr/local/bin/app-deploy viewer
# Sync wiki (if wiki/ folder exists in repo) # Sync wiki (if wiki/ folder exists in repo)
if [ -d "/opt/apps/viewer/wiki" ]; then if [ -d "/opt/apps/viewer/wiki" ]; then
cd /tmp cd /tmp
git clone git@git.appmodel.nl:Tour/viewer.wiki.git gitea-wiki-$$ git clone git@git.appmodel.nl:Tour/viewer.wiki.git gitea-wiki-$$
cp /opt/apps/viewer/wiki/*.md /tmp/gitea-wiki-$$/ cp /opt/apps/viewer/wiki/*.md /tmp/gitea-wiki-$$/
cd /tmp/gitea-wiki-$$ cd /tmp/gitea-wiki-$$
git add . git add .
if ! git diff --staged --quiet; then if ! git diff --staged --quiet; then
git commit -m "Auto-sync from main repository" git commit -m "Auto-sync from main repository"
git push origin master git push origin master
fi fi
cd /tmp cd /tmp
rm -rf /tmp/gitea-wiki-$$ rm -rf /tmp/gitea-wiki-$$
fi fi
``` ```
## Current Setup Instructions ## Current Setup Instructions
Based on your current setup, here's what to do: Based on your current setup, here's what to do:
```bash ```bash
# 1. Navigate to your viewer project # 1. Navigate to your viewer project
cd /c/vibe/viewer cd /c/vibe/viewer
# 2. Clone the wiki repository (for viewer, adjust as needed) # 2. Clone the wiki repository (for viewer, adjust as needed)
cd .. cd ..
git clone git@git.appmodel.nl:Tour/viewer.wiki.git git clone git@git.appmodel.nl:Tour/viewer.wiki.git
# 3. Copy wiki files # 3. Copy wiki files
cp viewer/wiki/*.md viewer.wiki/ cp viewer/wiki/*.md viewer.wiki/
# 4. Push to Gitea # 4. Push to Gitea
cd viewer.wiki cd viewer.wiki
git add . git add .
git commit -m "Initialize wiki with deployment documentation" git commit -m "Initialize wiki with deployment documentation"
git push origin master git push origin master
# 5. Verify # 5. Verify
# Open: https://git.appmodel.nl/Tour/viewer/wiki # Open: https://git.appmodel.nl/Tour/viewer/wiki
``` ```
## Maintaining Both Wikis ## Maintaining Both Wikis
If you want to keep both local and Gitea wikis in sync: If you want to keep both local and Gitea wikis in sync:
### Manual Sync (Simple) ### Manual Sync (Simple)
```bash ```bash
# After editing local wiki files # After editing local wiki files
cd /c/vibe/viewer/wiki cd /c/vibe/viewer/wiki
cp *.md /c/vibe/viewer.wiki/ cp *.md /c/vibe/viewer.wiki/
cd /c/vibe/viewer.wiki cd /c/vibe/viewer.wiki
git add . git add .
git commit -m "Update documentation" git commit -m "Update documentation"
git push git push
``` ```
### Automated Sync (Add to package.json or Makefile) ### Automated Sync (Add to package.json or Makefile)
```json ```json
{ {
"scripts": { "scripts": {
"sync-wiki": "cp wiki/*.md ../viewer.wiki/ && cd ../viewer.wiki && git add . && git commit -m 'Sync wiki' && git push" "sync-wiki": "cp wiki/*.md ../viewer.wiki/ && cd ../viewer.wiki && git add . && git commit -m 'Sync wiki' && git push"
} }
} }
``` ```
Or create a `Makefile`: Or create a `Makefile`:
```makefile ```makefile
.PHONY: sync-wiki .PHONY: sync-wiki
sync-wiki: sync-wiki:
@echo "Syncing wiki to Gitea..." @echo "Syncing wiki to Gitea..."
@cp wiki/*.md ../viewer.wiki/ @cp wiki/*.md ../viewer.wiki/
@cd ../viewer.wiki && git add . && git commit -m "Sync wiki from main repo" && git push @cd ../viewer.wiki && git add . && git commit -m "Sync wiki from main repo" && git push
@echo "Wiki synced successfully!" @echo "Wiki synced successfully!"
``` ```
Usage: `make sync-wiki` Usage: `make sync-wiki`
## Best Practices ## Best Practices
1. **Single Source of Truth**: Choose one location (local or Gitea) as authoritative 1. **Single Source of Truth**: Choose one location (local or Gitea) as authoritative
2. **Version Control**: Keep wiki changes in Git history 2. **Version Control**: Keep wiki changes in Git history
3. **Review Process**: Use pull requests for significant documentation changes 3. **Review Process**: Use pull requests for significant documentation changes
4. **Link Checking**: Regularly verify internal links work 4. **Link Checking**: Regularly verify internal links work
5. **Consistent Formatting**: Use markdown linting tools 5. **Consistent Formatting**: Use markdown linting tools
6. **Table of Contents**: Keep `Home.md` as navigation hub 6. **Table of Contents**: Keep `Home.md` as navigation hub
## Troubleshooting ## Troubleshooting
### Wiki Repository Doesn't Exist ### Wiki Repository Doesn't Exist
Create the wiki first in Gitea UI: Create the wiki first in Gitea UI:
1. Go to repository: https://git.appmodel.nl/Tour/viewer 1. Go to repository: https://git.appmodel.nl/Tour/viewer
2. Click "Wiki" tab 2. Click "Wiki" tab
3. Click "Create New Page" 3. Click "Create New Page"
4. Create a dummy page (will be overwritten) 4. Create a dummy page (will be overwritten)
5. Now you can clone `viewer.wiki.git` 5. Now you can clone `viewer.wiki.git`
### Permission Denied ### Permission Denied
Ensure your SSH key is added to Gitea: Ensure your SSH key is added to Gitea:
```bash ```bash
ssh -T git@git.appmodel.nl ssh -T git@git.appmodel.nl
``` ```
### Merge Conflicts ### Merge Conflicts
If both local and Gitea wiki are edited: If both local and Gitea wiki are edited:
```bash ```bash
cd viewer.wiki cd viewer.wiki
git pull origin master git pull origin master
# Resolve conflicts manually # Resolve conflicts manually
git add . git add .
git commit -m "Merge local and remote changes" git commit -m "Merge local and remote changes"
git push git push
``` ```
## Summary ## Summary
**Recommended approach for your setup**: **Recommended approach for your setup**:
1. Keep `wiki/` in main repository for version control 1. Keep `wiki/` in main repository for version control
2. Manually push to Gitea wiki repository when documentation is ready 2. Manually push to Gitea wiki repository when documentation is ready
3. Optionally add automated sync in post-receive hook 3. Optionally add automated sync in post-receive hook
4. Edit wiki locally, sync to Gitea for visibility 4. Edit wiki locally, sync to Gitea for visibility
This gives you the best of both worlds: This gives you the best of both worlds:
- ✅ Documentation versioned with code - ✅ Documentation versioned with code
- ✅ Visible wiki in Gitea UI - ✅ Visible wiki in Gitea UI
- ✅ Easy to maintain and update - ✅ Easy to maintain and update
- ✅ Works with existing deployment pipeline - ✅ Works with existing deployment pipeline

View File

@@ -1,31 +1,31 @@
# Appmodel Home Lab Build & Deploy Pipeline # Appmodel Home Lab Build & Deploy Pipeline
Dit document beschrijft hoe een nieuwe applicatie in het home lab wordt aangemaakt en hoe de build- & deploy-pipeline werkt. Dit document beschrijft hoe een nieuwe applicatie in het home lab wordt aangemaakt en hoe de build- & deploy-pipeline werkt.
## Overzicht ## Overzicht
- **Broncode**: Gitea (`https://git.appmodel.nl`) - **Broncode**: Gitea (`https://git.appmodel.nl`)
- **Build & runtime**: Docker containers op netwerk `traefik_net` - **Build & runtime**: Docker containers op netwerk `traefik_net`
- **Routing & TLS**: Traefik (`https://traefik.appmodel.nl`) - **Routing & TLS**: Traefik (`https://traefik.appmodel.nl`)
- **Automatische deploy**: Gitea `post-receive` hooks → `app-deploy <app>` - **Automatische deploy**: Gitea `post-receive` hooks → `app-deploy <app>`
## Pipeline in één diagram ## Pipeline in één diagram
```mermaid ```mermaid
flowchart LR flowchart LR
Dev[💻 Dev machine\nVS Code / Git] -->|git push| Gitea[📚 Gitea\nTour/<app>] Dev[💻 Dev machine\nVS Code / Git] -->|git push| Gitea[📚 Gitea\nTour/<app>]
subgraph Server[🏠 Home lab server\n192.168.1.159] subgraph Server[🏠 Home lab server\n192.168.1.159]
Gitea --> Hook[🔔 post-receive hook\n/app-deploy <app>] Gitea --> Hook[🔔 post-receive hook\n/app-deploy <app>]
Hook --> Deploy[⚙️ app-deploy <app>\n/git pull + docker compose up -d --build <app>] Hook --> Deploy[⚙️ app-deploy <app>\n/git pull + docker compose up -d --build <app>]
subgraph Docker[🐳 Docker / traefik_net] subgraph Docker[🐳 Docker / traefik_net]
AppC[🧱 App container\n<app>.appmodel.nl] AppC[🧱 App container\n<app>.appmodel.nl]
Traefik[🚦 Traefik\nReverse Proxy] Traefik[🚦 Traefik\nReverse Proxy]
end end
end end
Deploy --> AppC Deploy --> AppC
Traefik --> AppC Traefik --> AppC
Client[🌐 Browser / API client] -->|https://<app>.appmodel.nl| Traefik Client[🌐 Browser / API client] -->|https://<app>.appmodel.nl| Traefik

View File

@@ -1,141 +1,141 @@
1. Nieuwe app aanmaken 1. Nieuwe app aanmaken
1.1 Gitea repo 1.1 Gitea repo
Log in op Gitea: https://git.appmodel.nl Log in op Gitea: https://git.appmodel.nl
Maak een nieuwe repo aan, bijvoorbeeld: Maak een nieuwe repo aan, bijvoorbeeld:
Owner: Tour Owner: Tour
Name: viewer Name: viewer
1.2 Skeleton genereren op de server 1.2 Skeleton genereren op de server
Op de server: Op de server:
apps-create viewer static-fe apps-create viewer static-fe
Dit doet: Dit doet:
/opt/apps/viewer klaarzetten (proberen te clonen uit git@git.appmodel.nl:Tour/viewer.git) /opt/apps/viewer klaarzetten (proberen te clonen uit git@git.appmodel.nl:Tour/viewer.git)
een multi-stage Dockerfile voor een Node-based static frontend aanmaken een multi-stage Dockerfile voor een Node-based static frontend aanmaken
~/infra/viewer/docker-compose.yml aanmaken met: ~/infra/viewer/docker-compose.yml aanmaken met:
service viewer service viewer
koppeling aan traefik_net koppeling aan traefik_net
Traefik route: https://viewer.appmodel.nl Traefik route: https://viewer.appmodel.nl
2. Develop & build 2. Develop & build
Op je dev machine: Op je dev machine:
git clone git@git.appmodel.nl:Tour/viewer.git git clone git@git.appmodel.nl:Tour/viewer.git
cd viewer cd viewer
# bouw je app zoals normaal # bouw je app zoals normaal
npm install npm install
npm run build # output in dist/ npm run build # output in dist/
git add . git add .
git commit -m "First version" git commit -m "First version"
git push git push
De Dockerfile verwacht een dist/ map met static files. De Dockerfile verwacht een dist/ map met static files.
3. Deploy pipeline 3. Deploy pipeline
3.1 app-deploy <app> 3.1 app-deploy <app>
Op de server verzorgt app-deploy de generieke deploy: Op de server verzorgt app-deploy de generieke deploy:
app-deploy viewer app-deploy viewer
Doet: Doet:
cd /opt/apps/viewer cd /opt/apps/viewer
git pull --ff-only git pull --ff-only
cd /home/tour/infra/viewer cd /home/tour/infra/viewer
docker compose up -d --build viewer docker compose up -d --build viewer
Logs komen in: Logs komen in:
/var/log/app-deploy-viewer.log /var/log/app-deploy-viewer.log
3.2 Automatisch deployen via Gitea hook 3.2 Automatisch deployen via Gitea hook
In Gitea (repo Tour/viewer): In Gitea (repo Tour/viewer):
Ga naar Instellingen → Git Hooks Ga naar Instellingen → Git Hooks
Kies post-receive Kies post-receive
Gebruik: Gebruik:
#!/usr/bin/env bash #!/usr/bin/env bash
/usr/local/bin/app-deploy viewer /usr/local/bin/app-deploy viewer
Vanaf nu: Vanaf nu:
Elke git push naar Tour/viewer triggert automatisch een deploy. Elke git push naar Tour/viewer triggert automatisch een deploy.
4. Traefik & DNS 4. Traefik & DNS
Traefik draait in de traefik stack op dezelfde server en beheert: Traefik draait in de traefik stack op dezelfde server en beheert:
git.appmodel.nl git.appmodel.nl
auction.appmodel.nl auction.appmodel.nl
aupi.appmodel.nl aupi.appmodel.nl
… (nieuwe apps via labels) … (nieuwe apps via labels)
Nieuwe app viewer krijgt via apps-create al de juiste labels: Nieuwe app viewer krijgt via apps-create al de juiste labels:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)" - "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)"
- "traefik.http.routers.viewer.entrypoints=websecure" - "traefik.http.routers.viewer.entrypoints=websecure"
- "traefik.http.routers.viewer.tls=true" - "traefik.http.routers.viewer.tls=true"
- "traefik.http.routers.viewer.tls.certresolver=letsencrypt" - "traefik.http.routers.viewer.tls.certresolver=letsencrypt"
- "traefik.http.services.viewer.loadbalancer.server.port=80" - "traefik.http.services.viewer.loadbalancer.server.port=80"
Je moet alleen in DNS nog een record maken: Je moet alleen in DNS nog een record maken:
viewer.appmodel.nl → publieke IP van de server viewer.appmodel.nl → publieke IP van de server
Traefik + Lets Encrypt regelen het certificaat automatisch. Traefik + Lets Encrypt regelen het certificaat automatisch.
5. Nieuwe app types (toekomst) 5. Nieuwe app types (toekomst)
Het apps-create script ondersteunt nu: Het apps-create script ondersteunt nu:
static-fe Node build → Nginx → static frontend static-fe Node build → Nginx → static frontend
Later kun je extra types toevoegen, bijvoorbeeld: Later kun je extra types toevoegen, bijvoorbeeld:
api-py Python (Flask/FastAPI) API api-py Python (Flask/FastAPI) API
worker-py background worker / crawler worker-py background worker / crawler
Door per type een eigen Dockerfile-sjabloon en standaard docker-compose.yml te genereren, wordt een nieuw project neerzetten: Door per type een eigen Dockerfile-sjabloon en standaard docker-compose.yml te genereren, wordt een nieuw project neerzetten:
apps-create stats api-py apps-create stats api-py
apps-create crawler worker-py apps-create crawler worker-py
en blijft de pipeline (app-deploy <naam>) identiek. en blijft de pipeline (app-deploy <naam>) identiek.
Je kunt nu: Je kunt nu:
apps-create viewer static-fe apps-create viewer static-fe
app-deploy viewer app-deploy viewer