Compare commits

...

23 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
b55fdb532b npm 2025-12-02 15:45:09 +01:00
8653c510b7 npm 2025-12-02 15:44:35 +01:00
bd3eb14edc viewer 2025-12-02 14:08:53 +01:00
f6c2cb0a8a test.. 2025-12-02 13:50:28 +01:00
c2646d9680 test.. 2025-12-02 13:40:22 +01:00
2ac206ebd9 test.. 2025-12-02 13:27:26 +01:00
f6ccae25ca wiki 2025-12-02 13:21:59 +01:00
eb12cc4dd5 Images 2025-12-02 13:21:42 +01:00
26 changed files with 7861 additions and 2012 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

63
.gitignore vendored
View File

@@ -1,33 +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
# Generated diagram files node_modules/
*.png # Generated diagram files
*.pdf *.pdf
*.svg *.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,61 +1,23 @@
# Multi-stage Dockerfile for static frontend deployment # -- Build stage --
# Stage 1: Build the diagrams FROM node:20-alpine AS build
FROM python:3.11-slim AS builder WORKDIR /app
# Install system dependencies for Graphviz # Alleen package-bestanden eerst (better caching)
RUN apt-get update && \ COPY package*.json ./
apt-get install -y --no-install-recommends \ RUN npm ci || npm install
graphviz \
&& rm -rf /var/lib/apt/lists/* # Dan de rest van de code
COPY . .
# Set working directory
WORKDIR /app # LET OP: pas dit aan als jouw build-script anders heet
RUN npm run build
# Copy requirements and install Python dependencies
COPY requirements.txt . # -- Serve stage --
RUN pip install --no-cache-dir -r requirements.txt FROM nginx:alpine
WORKDIR /usr/share/nginx/html
# Copy source files
COPY *.py . # Pas dit aan als jouw outputmap niet 'dist' is (bijv. 'build')
COPY --from=build /app/dist ./
# Generate diagrams (outputs will be in /app)
RUN python lan_architecture.py && \ # Optioneel: eigen nginx.conf voor SPA routing
python main.py # COPY nginx.conf /etc/nginx/conf.d/default.conf
# Verify generated files exist
RUN ls -la *.png || echo "Warning: No PNG files generated"
# Stage 2: Serve with Nginx
FROM nginx:alpine
# Remove default nginx static assets
RUN rm -rf /usr/share/nginx/html/*
# Copy generated diagram files from builder stage
COPY --from=builder /app/*.png /usr/share/nginx/html/
# Copy any HTML/CSS files for the frontend
COPY public/ /usr/share/nginx/html/
# Create a simple nginx config for serving static files
RUN echo 'server { \
listen 80; \
server_name _; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
# Enable CORS if needed \
add_header Access-Control-Allow-Origin *; \
}' > /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

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,28 +1,26 @@
version: '3.8' services:
viewer:
services: build:
diagram: context: /opt/apps/viewer
build: dockerfile: Dockerfile
context: . container_name: viewer
dockerfile: Dockerfile restart: unless-stopped
container_name: diagram-viewer networks:
restart: unless-stopped - traefik_net
networks: labels:
- traefik_net - "traefik.enable=true"
labels: - "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)"
- "traefik.enable=true" - "traefik.http.routers.viewer.entrypoints=websecure"
- "traefik.http.routers.diagram.rule=Host(`diagram.appmodel.nl`)" - "traefik.http.routers.viewer.tls=true"
- "traefik.http.routers.diagram.entrypoints=websecure" - "traefik.http.services.viewer.loadbalancer.server.port=80"
- "traefik.http.routers.diagram.tls=true" - "traefik.http.routers.viewer-http.rule=Host(`viewer.appmodel.nl`)"
- "traefik.http.routers.diagram.tls.certresolver=letsencrypt" - "traefik.http.routers.viewer-http.entrypoints=web"
- "traefik.http.services.diagram.loadbalancer.server.port=80" - "traefik.http.routers.viewer-http.middlewares=viewer-https"
# Optional: mount logs - "traefik.http.middlewares.viewer-https.redirectscheme.scheme=https"
volumes: - "traefik.http.routers.auction.tls.certresolver=letsencrypt",
- ./logs:/var/log/nginx - "traefik.http.middlewares.viewer-https.redirectscheme.permanent=true"
# Optional: environment variables
environment: networks:
- TZ=Europe/Amsterdam traefik_net:
external: true
networks: name: traefik_net
traefik_net:
external: true

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 diagram 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 diagram 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

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()

4651
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

9
package.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "viewer",
"version": "1.0.0",
"description": "",
"maintainers": ["michael@appmodel.nl"],
"scripts": {
"build": "mkdir -p dist && cp -r public/* dist/"
}
}

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>

BIN
public/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
public/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

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`) -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

View File

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

192
wiki/DEPLOYMENT_GUIDE.md Normal file
View File

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

502
wiki/DEPLOY_SERVER_SETUP.md Normal file
View File

@@ -0,0 +1,502 @@
# Deploy Server Setup - Docker Build Pipeline
Complete guide for setting up and using the Docker-based deployment pipeline on the build server.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Deployment Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Dev Machine Gitea Server │
│ ┌──────────┐ ┌──────┐ ┌─────────┐ │
│ │ git │ │ Repo │ │ /opt/ │ │
│ │ commit ├─────push────▶│Tour/ ├──hook────▶ │ apps/ │ │
│ │ push │ │<app> │ │ <app>/ │ │
│ └──────────┘ └──────┘ └────┬────┘ │
│ │ │
│ git pull │
│ │ │
│ ┌───────────▼────────┐ │
│ │ app-deploy <app> │ │
│ │ (as user: git) │ │
│ └───────────┬────────┘ │
│ │ │
│ docker compose up -d │
│ --build │
│ │ │
│ ┌───────────▼────────┐ │
│ │ ~/infra/<app>/ │ │
│ │ docker-compose.yml │ │
│ └───────────┬────────┘ │
│ │ │
│ rebuild │
│ │ │
│ ┌───────────▼────────┐ │
│ │ 🐳 Container │ │
│ │ <app>:latest │ │
│ └───────────┬────────┘ │
│ │ │
│ traefik_net │
│ │ │
│ ┌───────────▼────────┐ │
│ │ 🚦 Traefik │ │
│ │ Reverse Proxy │ │
│ └───────────┬────────┘ │
│ │ │
│ https://<app>.appmodel.nl│
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Build Pipeline Workflow
### Key Principles
1. **Source of Truth**: Gitea repository `Tour/<app>` is the authoritative source
2. **Build Server**: Server is only for building and deployment, not development
3. **Automated Deployment**: Git push triggers automatic rebuild and deployment
4. **Isolation**: Each app runs in its own Docker container
5. **Shared Networking**: All apps connect via `traefik_net` for reverse proxy
### Pipeline Steps
#### 1. Git Push (Developer)
```bash
git push origin main
```
#### 2. Post-Receive Hook (Gitea)
Located in: **Settings → Git Hooks → post-receive**
```bash
#!/usr/bin/env bash
/usr/local/bin/app-deploy viewer
```
#### 3. App Deploy Script (Server)
Runs as Linux user `git`:
```bash
app-deploy viewer
```
This script performs:
1. `cd /opt/apps/viewer`
2. `git pull --ff-only` from `git@git.appmodel.nl:Tour/viewer.git`
3. `cd /home/tour/infra/viewer`
4. `docker compose up -d --build viewer`
#### 4. Docker Build (Server)
Multi-stage build process:
- **Stage 1**: Build viewer using Python + Graphviz
- **Stage 2**: Serve with Nginx
#### 5. Traefik Routing (Server)
Traefik automatically detects the container via labels and publishes:
```
https://viewer.appmodel.nl
```
#### 6. DNS Resolution (AdGuard)
AdGuard resolves `viewer.appmodel.nl` → Build server IP
Result: Both internal and external clients use the same URL.
## Server Directory Structure
```
/opt/apps/
└── viewer/ # Git repository (pulled from Gitea)
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── lan_architecture.py
├── main.py
└── public/
└── index.html
/home/tour/infra/
└── viewer/
├── docker-compose.yml # Docker Compose config (may differ from repo)
└── logs/ # Nginx logs (optional)
/var/log/
└── app-deploy-viewer.log # Deployment logs
```
## Setup Instructions
### 1. Create Repository in Gitea
1. Navigate to: https://git.appmodel.nl
2. Create new repository:
- **Owner**: `Tour`
- **Name**: `viewer`
- **Visibility**: Private
### 2. Initialize Local Repository
```bash
# On dev machine
git clone git@git.appmodel.nl:Tour/viewer.git
cd viewer
# Add project files
git add Dockerfile docker-compose.yml requirements.txt *.py public/
git commit -m "Initial commit: viewer"
git push origin main
```
### 3. Setup Server Directories
```bash
# On server as appropriate user
sudo -u git mkdir -p /opt/apps/viewer
sudo -u git git clone git@git.appmodel.nl:Tour/viewer.git /opt/apps/viewer
mkdir -p /home/tour/infra/viewer
cp /opt/apps/viewer/docker-compose.yml /home/tour/infra/viewer/
```
### 4. Configure Gitea Post-Receive Hook
1. Go to: https://git.appmodel.nl/Tour/viewer
2. Navigate to: **Settings → Git Hooks**
3. Select: **post-receive**
4. Add script:
```bash
#!/usr/bin/env bash
/usr/local/bin/app-deploy viewer
```
5. Save and test
### 5. Configure DNS
Add DNS record in AdGuard:
```
viewer.appmodel.nl → <server-ip>
```
### 6. Test Deployment
```bash
# Manual test on server
app-deploy viewer
# Verify container is running
cd /home/tour/infra/viewer
docker compose ps
# Check logs
docker compose logs -f viewer
# Test endpoint
curl -I https://viewer.appmodel.nl
```
## Docker Configuration
### Dockerfile Explained
```dockerfile
# Stage 1: Build diagrams
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y graphviz
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY *.py .
RUN python lan_architecture.py && python main.py
# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=builder /app/*.png /usr/share/nginx/html/
COPY public/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
**Benefits**:
- Small final image (only Nginx + static files)
- Build tools not included in production image
- Automatic diagram generation on build
### docker-compose.yml Configuration
```yaml
version: '3.8'
services:
viewer:
build:
context: .
dockerfile: Dockerfile
container_name: viewer
restart: unless-stopped
networks:
- traefik_net
labels:
- "traefik.enable=true"
- "traefik.http.routers.viewer.rule=Host(`viewer.appmodel.nl`)"
- "traefik.http.routers.viewer.entrypoints=websecure"
- "traefik.http.routers.viewer.tls=true"
- "traefik.http.routers.viewer.tls.certresolver=letsencrypt"
- "traefik.http.services.viewer.loadbalancer.server.port=80"
networks:
traefik_net:
external: true
```
**Key Points**:
- Connects to external `traefik_net` network
- Traefik labels configure automatic HTTPS with Let's Encrypt
- Container restarts automatically unless stopped manually
## Development Workflow
### Local Development
```bash
# Edit files locally
vim lan_architecture.py
vim public/index.html
# Test locally (optional)
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -r requirements.txt
python lan_architecture.py
# Commit and push
git add .
git commit -m "Update viewer architecture"
git push
```
### Automatic Deployment
After `git push`, the server automatically:
1. Receives post-receive hook trigger
2. Pulls latest code
3. Rebuilds Docker image
4. Restarts container
5. Updates live site
**Timeline**: Usually completes in 30-60 seconds.
### Manual Deployment
If needed, manually trigger deployment:
```bash
# On server
app-deploy viewer
# Or manually
cd /opt/apps/viewer
git pull
cd /home/tour/infra/viewer
docker compose up -d --build viewer
```
## Monitoring & Troubleshooting
### View Logs
```bash
# Deployment logs
tail -f /var/log/app-deploy-viewer.log
# Container logs
cd /home/tour/infra/viewer
docker compose logs -f viewer
# Nginx access logs (if volume mounted)
tail -f /home/tour/infra/viewer/logs/access.log
```
### Check Container Status
```bash
cd /home/tour/infra/viewer
# List running containers
docker compose ps
# Inspect container
docker compose exec viewer sh
# Test nginx config
docker compose exec viewer nginx -t
```
### Debug Build Issues
```bash
# Rebuild without cache
docker compose build --no-cache viewer
# View build output
docker compose up --build viewer
# Check image layers
docker image history diagram-viewer
```
### Common Issues
#### 1. Build Fails - Missing Dependencies
**Symptom**: Build fails at pip install step
**Solution**: Verify `requirements.txt` is correct
```bash
cd /opt/apps/viewer
cat requirements.txt
```
#### 2. Nginx 404 Error
**Symptom**: Container runs but shows 404
**Solution**: Check if files were copied correctly
```bash
docker compose exec viewer ls -la /usr/share/nginx/html/
```
#### 3. Traefik Not Routing
**Symptom**: Container runs but domain not accessible
**Solution**:
- Verify container is on `traefik_net`: `docker network inspect traefik_net`
- Check Traefik labels: `docker inspect viewer | grep traefik`
- Verify DNS: `nslookup viewer.appmodel.nl`
#### 4. Git Pull Fails
**Symptom**: app-deploy fails at git pull
**Solution**:
- Check SSH keys for git user: `sudo -u git ssh -T git@git.appmodel.nl`
- Verify remote: `cd /opt/apps/viewer && git remote -v`
## Security Considerations
### User Permissions
- **git user**: Owns `/opt/apps/*` directories, runs app-deploy
- **tour user**: Owns `/home/tour/infra/*`, runs Docker Compose
- Principle: Separate concerns between git operations and container management
### Network Isolation
- Containers only exposed via Traefik
- No direct port exposure to host
- `traefik_net` provides isolation between services
### Secrets Management
For apps requiring secrets:
```yaml
# In docker-compose.yml
services:
viewer:
environment:
- SECRET_KEY=${SECRET_KEY}
env_file:
- .env # Not committed to git
```
Create `.env` file on server:
```bash
echo "SECRET_KEY=your-secret-here" > /home/tour/infra/viewer/.env
chmod 600 /home/tour/infra/viewer/.env
```
## Scaling & Future Improvements
### Multi-Container Apps
For apps with multiple services:
```yaml
services:
frontend:
build: ./frontend
labels:
- "traefik.enable=true"
- "traefik.http.routers.app-fe.rule=Host(`app.appmodel.nl`)"
backend:
build: ./backend
labels:
- "traefik.enable=true"
- "traefik.http.routers.app-be.rule=Host(`api.app.appmodel.nl`)"
database:
image: postgres:15
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
```
### CI/CD Integration
Consider adding:
- Automated tests before deployment
- Rollback mechanism
- Blue-green deployments
- Health checks before switching traffic
### Monitoring
Add monitoring stack:
- Prometheus for metrics
- Grafana for dashboards
- Loki for log aggregation
- Alertmanager for notifications
## Quick Reference
### Key Paths
| Path | Purpose |
|------|---------|
| `/opt/apps/<app>/` | Git repository checkout |
| `/home/tour/infra/<app>/` | Docker Compose directory |
| `/var/log/app-deploy-<app>.log` | Deployment logs |
### Key Commands
| Command | Purpose |
|---------|---------|
| `app-deploy <app>` | Deploy/redeploy application |
| `docker compose ps` | List running containers |
| `docker compose logs -f <app>` | Follow container logs |
| `docker compose restart <app>` | Restart service |
| `docker compose build --no-cache` | Force rebuild |
### URLs
| Service | URL |
|---------|-----|
| Gitea | https://git.appmodel.nl |
| Diagram Viewer | https://viewer.appmodel.nl |
| Traefik Dashboard | https://traefik.appmodel.nl/dashboard/ |
## Support
For issues or questions:
1. Check deployment logs: `/var/log/app-deploy-<app>.log`
2. Check container logs: `docker compose logs -f`
3. Review Gitea webhook history in repository settings
4. Test manual deployment: `app-deploy <app>`

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/diagram.git git clone git@git.appmodel.nl:Tour/viewer.git
cd diagram 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
``` ```
diagram/ 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 diagram static-fe apps-create viewer static-fe
# This creates: # This creates:
# - /opt/apps/diagram (git repository) # - /opt/apps/viewer (git repository)
# - /home/tour/infra/diagram/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 diagram /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://diagram.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 diagram app-deploy viewer
# Or step-by-step # Or step-by-step
cd /opt/apps/diagram cd /opt/apps/viewer
git pull git pull
cd /home/tour/infra/diagram cd /home/tour/infra/viewer
docker compose up -d --build diagram docker compose up -d --build viewer
``` ```
### Monitoring ### Monitoring
```bash ```bash
# View deployment logs # View deployment logs
tail -f /var/log/app-deploy-diagram.log tail -f /var/log/app-deploy-viewer.log
# View container logs # View container logs
cd /home/tour/infra/diagram cd /home/tour/infra/viewer
docker compose logs -f diagram 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/diagram │ │ │ │ Tour/viewer │ │
│ └──────┬───────┘ │ │ └──────┬───────┘ │
│ │ git push │ │ │ git push │
│ ↓ │ │ ↓ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ Post-Receive │ (Auto-trigger) │ │ │ Post-Receive │ (Auto-trigger) │
│ │ Hook │ │ │ │ Hook │ │
│ └──────┬───────┘ │ │ └──────┬───────┘ │
│ │ app-deploy diagram │ │ app-deploy viewer
│ ↓ │ │ ↓ │
│ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │
│ │ /opt/apps/diagram/ │ │ │ │ /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 │
│ ↓ │ │ ↓ │
diagram.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://diagram.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-diagram.log tail -f /var/log/app-deploy-viewer.log
# Rebuild manually with verbose output # Rebuild manually with verbose output
cd /home/tour/infra/diagram cd /home/tour/infra/viewer
docker compose build --no-cache --progress=plain diagram 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 diagram.appmodel.nl` 1. Check DNS: `nslookup viewer.appmodel.nl`
2. Verify Traefik labels: `docker inspect diagram-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/diagram cd /home/tour/infra/viewer
docker compose down docker compose down
docker compose up -d --build --force-recreate diagram 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/diagram.git git clone git@git.appmodel.nl:Tour/viewer.git
cd diagram 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://diagram.appmodel.nl https://viewer.appmodel.nl
``` ```
### Manual Deployment ### Manual Deployment
On the server: On the server:
```bash ```bash
# Trigger deployment # Trigger deployment
app-deploy diagram app-deploy viewer
# View logs # View logs
tail -f /var/log/app-deploy-diagram.log tail -f /var/log/app-deploy-viewer.log
# Check status # Check status
cd /home/tour/infra/diagram 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-diagram.log tail -f /var/log/app-deploy-viewer.log
# Container logs # Container logs
cd /home/tour/infra/diagram cd /home/tour/infra/viewer
docker compose logs -f diagram docker compose logs -f viewer
# Last 100 lines # Last 100 lines
docker compose logs --tail=100 diagram docker compose logs --tail=100 viewer
``` ```
### Restart Service ### Restart Service
```bash ```bash
cd /home/tour/infra/diagram cd /home/tour/infra/viewer
# Restart container # Restart container
docker compose restart diagram docker compose restart viewer
# Full restart (rebuild) # Full restart (rebuild)
docker compose down docker compose down
docker compose up -d --build diagram 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 diagram-viewer | grep -A 10 Health docker inspect viewer | grep -A 10 Health
# Test endpoint # Test endpoint
curl -I https://diagram.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-diagram.log tail -f /var/log/app-deploy-viewer.log
# Rebuild with verbose output # Rebuild with verbose output
cd /home/tour/infra/diagram cd /home/tour/infra/viewer
docker compose build --no-cache --progress=plain diagram 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 diagram docker logs traefik | grep viewer
# Test DNS # Test DNS
nslookup diagram.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/diagram cd /home/tour/infra/viewer
docker compose up -d --build --force-recreate diagram 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://diagram.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.

361
wiki/WIKI_INTEGRATION.md Normal file
View File

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

31
wiki/build-pipeline.md Normal file
View File

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

141
wiki/readme-pipe.md Normal file
View File

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