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