Files
viewer/lab/dia-kimi.html
2025-12-02 14:08:53 +01:00

218 lines
7.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home-Lab viewer 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 viewer 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>