init
This commit is contained in:
282
public/deployment.html
Normal file
282
public/deployment.html
Normal file
@@ -0,0 +1,282 @@
|
||||
<!-- test321 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab diagram – live editor</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-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}
|
||||
.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:last-child{border:none}
|
||||
h3{margin:.4rem .6rem;font-size:1rem}
|
||||
#src{flex:1;padding:.5rem;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-family:"Consolas","Monaco",monospace;border:none;outline:none;white-space:pre-wrap}
|
||||
#preview{flex:1;padding:.5rem;overflow:auto;background:#fff}
|
||||
/* IMPORTANT: Let the SVG flow naturally and scroll if needed */
|
||||
#preview svg{display:block;margin:0 auto;max-width:100%;height:auto}
|
||||
#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}
|
||||
/* collapsed state */
|
||||
#srcPanel.collapsed{flex:0}
|
||||
#srcPanel.collapsed #src{display:none}
|
||||
/* Debug: uncomment to see element bounds */
|
||||
/* #preview{outline:2px solid red} #preview svg{outline:2px solid blue} */
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
|
||||
Home-Lab diagram – live Mermaid editor
|
||||
<button onclick="saveSVG()">💾 SVG</button>
|
||||
<button onclick="savePNG()">💾 PNG</button>
|
||||
<button onclick="reset()">🔄 Reset</button>
|
||||
<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>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="panel" id="srcPanel">
|
||||
<h3>Source (edit here)</h3>
|
||||
<textarea id="src" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Live preview</h3>
|
||||
<div id="preview"></div>
|
||||
<div id="error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const DEFAULT = `flowchart LR
|
||||
%% ============ Internet ============
|
||||
subgraph WAN[🌐 Internet / Cloud]
|
||||
extDNS[(📡 Public DNS)]
|
||||
extGit[(☁️ Externe registries / Git)]
|
||||
end
|
||||
|
||||
%% ============ LAN 192.168.1.x ============
|
||||
subgraph LAN[🏠 LAN 192.168.1.0/24]
|
||||
hub[🛜 Router / Gateway\\nhub.lan\\n192.168.1.1]
|
||||
|
||||
subgraph core[💻 Hoofdserver / Desktop\\nTour / hephaestus / ollama / dokku.lan\\n192.168.1.159]
|
||||
traefik[🚦 Traefik\\nReverse Proxy]
|
||||
gitea[📚 Gitea\\n git.appmodel.nl]
|
||||
dokku[🐳 Dokku\\nPaaS / build]
|
||||
auctionFE[🧱 Auction Frontend\\n auction.appmodel.nl]
|
||||
aupiAPI[🧱 Auction Backend API\\n aupi.appmodel.nl]
|
||||
mi50[🧠 MI50 / Ollama\\nAI workloads]
|
||||
end
|
||||
|
||||
subgraph infraDNS[🧭 Infra & DNS\\nodroid / dns.lan\\n192.168.1.163]
|
||||
adguard[🧭 AdGuard Home\\nDNS / *.lan / *.appmodel.nl]
|
||||
artifactory[📦 Artifactory]
|
||||
runner[⚙️ Build runners]
|
||||
end
|
||||
|
||||
subgraph ha[🏡 Home Automation\\nha.lan\\n192.168.1.193]
|
||||
hass[🏠 Home Assistant]
|
||||
end
|
||||
|
||||
atlas[🧱 atlas.lan\\n192.168.1.100\\n]
|
||||
|
||||
iot1[📺 hof-E402NA\\n192.168.1.214]
|
||||
iot2[🎧 S380HB\\n192.168.1.59]
|
||||
iot3[📟 ecb5faa56c90\\n192.168.1.49]
|
||||
iot4[❓ Unknown\\n192.168.1.240]
|
||||
end
|
||||
|
||||
%% ============ Tether subnet ============
|
||||
subgraph TETHER[📶 Tether subnet 192.168.137.0/24]
|
||||
hermes[🛰️ hermes.lan\\n192.168.137.239\\nworker / node]
|
||||
plato[🛰️ plato.lan\\n192.168.137.163\\nworker / node]
|
||||
end
|
||||
|
||||
%% ============ Verkeer ============
|
||||
|
||||
%% Basis LAN connecties
|
||||
hub --- core
|
||||
hub --- infraDNS
|
||||
hub --- ha
|
||||
hub --- atlas
|
||||
hub --- iot1
|
||||
hub --- iot2
|
||||
hub --- iot3
|
||||
hub --- iot4
|
||||
|
||||
%% WAN koppeling
|
||||
hub --> WAN
|
||||
infraDNS --> WAN
|
||||
|
||||
%% DNS-resolutie
|
||||
core --> adguard
|
||||
ha --> adguard
|
||||
atlas --> adguard
|
||||
TETHER --> adguard
|
||||
|
||||
%% Websites / reverse proxy
|
||||
extDNS --> traefik
|
||||
traefik --> gitea
|
||||
traefik --> auctionFE
|
||||
traefik --> aupiAPI
|
||||
traefik --> dokku
|
||||
|
||||
%% App flow
|
||||
auctionFE --> aupiAPI
|
||||
aupiAPI --> adguard
|
||||
|
||||
%% AI workloads
|
||||
core --> mi50
|
||||
|
||||
%% Tether workers
|
||||
core --- TETHER
|
||||
`;
|
||||
|
||||
function getStorage(key, fallback) {
|
||||
try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; }
|
||||
}
|
||||
function setStorage(key, val) {
|
||||
try { localStorage.setItem(key, val); } catch {}
|
||||
}
|
||||
|
||||
const srcEl = document.getElementById('src');
|
||||
const errEl = document.getElementById('error');
|
||||
const zoomEl = document.getElementById('zoom');
|
||||
const zoomVal = document.getElementById('zoomVal');
|
||||
const srcPanel = document.getElementById('srcPanel');
|
||||
const toggleBtn = document.getElementById('toggleBtn');
|
||||
|
||||
srcEl.value = getStorage('labDiagram', DEFAULT);
|
||||
if (getStorage('panelCollapsed', 'false') === 'true') {
|
||||
srcPanel.classList.add('collapsed');
|
||||
toggleBtn.textContent = '🗔 Show source';
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
srcPanel.classList.toggle('collapsed');
|
||||
const collapsed = srcPanel.classList.contains('collapsed');
|
||||
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
|
||||
setStorage('panelCollapsed', collapsed);
|
||||
}
|
||||
|
||||
mermaid.initialize({startOnLoad: false, theme: 'default'});
|
||||
|
||||
function render() {
|
||||
const src = srcEl.value;
|
||||
setStorage('labDiagram', src);
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = src;
|
||||
preview.appendChild(mermaidDiv);
|
||||
|
||||
// Render and fix sizing
|
||||
mermaid.init(undefined, mermaidDiv).then(() => {
|
||||
const svg = preview.querySelector('svg');
|
||||
if (svg) {
|
||||
// CRITICAL: Ensure SVG has viewBox for proper sizing
|
||||
if (!svg.hasAttribute('viewBox')) {
|
||||
const {width, height} = svg.getBBox();
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
}
|
||||
svg.style.maxWidth = '100%';
|
||||
svg.style.height = 'auto';
|
||||
applyZoom();
|
||||
}
|
||||
}).catch(e => {
|
||||
errEl.textContent = 'Mermaid error: ' + e.message;
|
||||
errEl.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
srcEl.addEventListener('input', () => {
|
||||
clearTimeout(srcEl._t);
|
||||
srcEl._t = setTimeout(render, 300);
|
||||
});
|
||||
|
||||
zoomEl.addEventListener('input', () => {
|
||||
zoomVal.textContent = zoomEl.value + '%';
|
||||
applyZoom();
|
||||
});
|
||||
|
||||
function applyZoom() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${zoomEl.value / 100})`;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (confirm('Reset to default diagram?')) {
|
||||
srcEl.value = DEFAULT;
|
||||
zoomEl.value = 100;
|
||||
zoomVal.textContent = '100%';
|
||||
setStorage('labDiagram', DEFAULT);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSVG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const clone = svg.cloneNode(true);
|
||||
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 a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'home-lab.svg';
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
|
||||
function savePNG() {
|
||||
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 canvasH = Math.max(height, 800);
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvasW;
|
||||
canvas.height = canvasH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||
ctx.drawImage(img, 0, 0, canvasW, canvasH);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
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);
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!-- Auto-deployed at $(date122`) -->
|
||||
218
public/dia-kimi.html
Normal file
218
public/dia-kimi.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab nex – 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 nex – 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>
|
||||
107
public/dia.html
Normal file
107
public/dia.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Netwerk architectuur</title>
|
||||
<!-- Mermaid via CDN -->
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({ startOnLoad: true, theme: "default" });
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 1rem; }
|
||||
.mermaid { max-width: 100%; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Netwerk architectuur</h1>
|
||||
|
||||
<div class="mermaid">
|
||||
flowchart LR
|
||||
|
||||
%% ============ Internet ============
|
||||
subgraph WAN[🌐 Internet / Cloud]
|
||||
extDNS[(📡 Public DNS)]
|
||||
extGit[(☁️ Externe registries / Git)]
|
||||
end
|
||||
|
||||
%% ============ LAN 192.168.1.x ============
|
||||
subgraph LAN[🏠 LAN 192.168.1.0/24]
|
||||
hub[🛜 Router / Gateway\nhub.lan\n192.168.1.1]
|
||||
|
||||
subgraph core[💻 Hoofdserver / Desktop\nTour / hephaestus / ollama / dokku.lan\n192.168.1.159]
|
||||
traefik[🚦 Traefik\nReverse Proxy]
|
||||
gitea[📚 Gitea\n git.appmodel.nl]
|
||||
dokku[🐳 Dokku\nPaaS / build]
|
||||
auctionFE[🧱 Auction Frontend\n auction.appmodel.nl]
|
||||
aupiAPI[🧱 Auction Backend API\n aupi.appmodel.nl]
|
||||
mi50[🧠 MI50 / Ollama\nAI workloads]
|
||||
end
|
||||
|
||||
subgraph infraDNS[🧭 Infra & DNS\nodroid / dns.lan\n192.168.1.163]
|
||||
adguard[🧭 AdGuard Home\nDNS / *.lan / *.appmodel.nl]
|
||||
artifactory[📦 Artifactory]
|
||||
runner[⚙️ Build runners]
|
||||
end
|
||||
|
||||
subgraph ha[🏡 Home Automation\nha.lan\n192.168.1.193]
|
||||
hass[🏠 Home Assistant]
|
||||
end
|
||||
|
||||
atlas[🧱 atlas.lan\n192.168.1.100\n]
|
||||
|
||||
iot1[📺 hof-E402NA\n192.168.1.214]
|
||||
iot2[🎧 S380HB\n192.168.1.59]
|
||||
iot3[📟 ecb5faa56c90\n192.168.1.49]
|
||||
iot4[❓ Unknown\n192.168.1.240]
|
||||
end
|
||||
|
||||
%% ============ Tether subnet ============
|
||||
subgraph TETHER[📶 Tether subnet 192.168.137.0/24]
|
||||
hermes[🛰️ hermes.lan\n192.168.137.239\nworker / node]
|
||||
plato[🛰️ plato.lan\n192.168.137.163\nworker / node]
|
||||
end
|
||||
|
||||
%% ============ Verkeer ============
|
||||
|
||||
%% Basis LAN connecties
|
||||
hub --- core
|
||||
hub --- infraDNS
|
||||
hub --- ha
|
||||
hub --- atlas
|
||||
hub --- iot1
|
||||
hub --- iot2
|
||||
hub --- iot3
|
||||
hub --- iot4
|
||||
|
||||
%% WAN koppeling
|
||||
hub --> WAN
|
||||
infraDNS --> WAN
|
||||
|
||||
%% DNS-resolutie
|
||||
core --> adguard
|
||||
ha --> adguard
|
||||
atlas --> adguard
|
||||
TETHER --> adguard
|
||||
|
||||
%% Websites / reverse proxy
|
||||
extDNS --> traefik
|
||||
traefik --> gitea
|
||||
traefik --> auctionFE
|
||||
traefik --> aupiAPI
|
||||
traefik --> dokku
|
||||
|
||||
%% App flow
|
||||
auctionFE --> aupiAPI
|
||||
aupiAPI --> adguard
|
||||
|
||||
%% AI workloads
|
||||
core --> mi50
|
||||
|
||||
%% Tether workers
|
||||
core --- TETHER
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
7
public/img/favicon.svg
Normal file
7
public/img/favicon.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#2563eb" rx="15"/>
|
||||
<path d="M25 40 L50 20 L75 40 L75 70 L25 70 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2"/>
|
||||
<circle cx="50" cy="45" r="8" fill="#2563eb"/>
|
||||
<rect x="40" y="55" width="20" height="3" fill="#2563eb"/>
|
||||
<text x="50" y="90" font-family="Arial" font-size="12" fill="#ffffff" text-anchor="middle" font-weight="bold">AUCTION</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 465 B |
BIN
public/img/home.png
Normal file
BIN
public/img/home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
BIN
public/img/home_lab_architecture.png
Normal file
BIN
public/img/home_lab_architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
BIN
public/img/img.png
Normal file
BIN
public/img/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
public/img/network-architecture.gv.png
Normal file
BIN
public/img/network-architecture.gv.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
266
public/index.html
Normal file
266
public/index.html
Normal file
@@ -0,0 +1,266 @@
|
||||
<!-- test321 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab diagram – live editor</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-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}
|
||||
.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:last-child{border:none}
|
||||
h3{margin:.4rem .6rem;font-size:1rem}
|
||||
#src{flex:1;padding:.5rem;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-family:"Consolas","Monaco",monospace;border:none;outline:none;white-space:pre-wrap}
|
||||
#preview{flex:1;padding:.5rem;overflow:auto;background:#fff}
|
||||
/* IMPORTANT: Let the SVG flow naturally and scroll if needed */
|
||||
#preview svg{display:block;margin:0 auto;max-width:100%;height:auto}
|
||||
#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}
|
||||
/* collapsed state */
|
||||
#srcPanel.collapsed{flex:0}
|
||||
#srcPanel.collapsed #src{display:none}
|
||||
/* Debug: uncomment to see element bounds */
|
||||
/* #preview{outline:2px solid red} #preview svg{outline:2px solid blue} */
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
|
||||
Home-Lab diagram – live Mermaid editor
|
||||
<button onclick="saveSVG()">💾 SVG</button>
|
||||
<button onclick="savePNG()">💾 PNG</button>
|
||||
<button onclick="reset()">🔄 Reset</button>
|
||||
<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>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="panel" id="srcPanel">
|
||||
<h3>Source (edit here)</h3>
|
||||
<textarea id="src" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Live preview</h3>
|
||||
<div id="preview"></div>
|
||||
<div id="error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const DEFAULT = `flowchart TD
|
||||
%% ---------- styles ----------
|
||||
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\nhub.lan\n192.168.1.1]:::router
|
||||
|
||||
subgraph LAN [🏠 LAN 192.168.1.0/24]
|
||||
subgraph CORE [💻 Core server\n192.168.1.159]
|
||||
traefik[🚦 Traefik]:::core
|
||||
gitea[📚 Gitea]:::core
|
||||
dokku[🐳 Dokku]:::core
|
||||
auction[🧱 Auction stack]:::core
|
||||
mi50[🧠 MI50 / Ollama]:::core
|
||||
end
|
||||
|
||||
subgraph INFRA [🧭 Infra & DNS\n192.168.1.163]
|
||||
adguard[🛡️ AdGuard]:::infra
|
||||
artifactory[📦 Artifactory]:::infra
|
||||
end
|
||||
|
||||
ha[🏡 Home Assistant\n192.168.1.193]:::infra
|
||||
atlas[🧱 Atlas\n192.168.1.100]:::worker
|
||||
|
||||
iot1[📺 IoT-1]:::iot
|
||||
iot2[📟 IoT-2]:::iot
|
||||
end
|
||||
|
||||
subgraph TETHER [📶 Tether 192.168.137.0/24]
|
||||
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
|
||||
`;
|
||||
|
||||
function getStorage(key, fallback) {
|
||||
try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; }
|
||||
}
|
||||
function setStorage(key, val) {
|
||||
try { localStorage.setItem(key, val); } catch {}
|
||||
}
|
||||
|
||||
const srcEl = document.getElementById('src');
|
||||
const errEl = document.getElementById('error');
|
||||
const zoomEl = document.getElementById('zoom');
|
||||
const zoomVal = document.getElementById('zoomVal');
|
||||
const srcPanel = document.getElementById('srcPanel');
|
||||
const toggleBtn = document.getElementById('toggleBtn');
|
||||
|
||||
srcEl.value = getStorage('labDiagram', DEFAULT);
|
||||
if (getStorage('panelCollapsed', 'false') === 'true') {
|
||||
srcPanel.classList.add('collapsed');
|
||||
toggleBtn.textContent = '🗔 Show source';
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
srcPanel.classList.toggle('collapsed');
|
||||
const collapsed = srcPanel.classList.contains('collapsed');
|
||||
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
|
||||
setStorage('panelCollapsed', collapsed);
|
||||
}
|
||||
|
||||
mermaid.initialize({startOnLoad: false, theme: 'default'});
|
||||
|
||||
function render() {
|
||||
const src = srcEl.value;
|
||||
setStorage('labDiagram', src);
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = src;
|
||||
preview.appendChild(mermaidDiv);
|
||||
|
||||
// Render and fix sizing
|
||||
mermaid.init(undefined, mermaidDiv).then(() => {
|
||||
const svg = preview.querySelector('svg');
|
||||
if (svg) {
|
||||
// CRITICAL: Ensure SVG has viewBox for proper sizing
|
||||
if (!svg.hasAttribute('viewBox')) {
|
||||
const {width, height} = svg.getBBox();
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
}
|
||||
svg.style.maxWidth = '100%';
|
||||
svg.style.height = 'auto';
|
||||
applyZoom();
|
||||
}
|
||||
}).catch(e => {
|
||||
errEl.textContent = 'Mermaid error: ' + e.message;
|
||||
errEl.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
srcEl.addEventListener('input', () => {
|
||||
clearTimeout(srcEl._t);
|
||||
srcEl._t = setTimeout(render, 300);
|
||||
});
|
||||
|
||||
zoomEl.addEventListener('input', () => {
|
||||
zoomVal.textContent = zoomEl.value + '%';
|
||||
applyZoom();
|
||||
});
|
||||
|
||||
function applyZoom() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${zoomEl.value / 100})`;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (confirm('Reset to default diagram?')) {
|
||||
srcEl.value = DEFAULT;
|
||||
zoomEl.value = 100;
|
||||
zoomVal.textContent = '100%';
|
||||
setStorage('labDiagram', DEFAULT);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSVG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const clone = svg.cloneNode(true);
|
||||
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 a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'home-lab.svg';
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
|
||||
function savePNG() {
|
||||
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 canvasH = Math.max(height, 800);
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvasW;
|
||||
canvas.height = canvasH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||
ctx.drawImage(img, 0, 0, canvasW, canvasH);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
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);
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!-- Auto-deployed at $(date122`) -->
|
||||
283
public/preview.html
Normal file
283
public/preview.html
Normal file
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home-Lab diagram – live editor</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-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}
|
||||
.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:last-child{border:none}
|
||||
h3{margin:.4rem .6rem;font-size:1rem}
|
||||
#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}
|
||||
#preview{display:flex;align-items:flex-start;justify-content:center;min-height:0}
|
||||
svg{max-width:100%;height:auto}
|
||||
#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}
|
||||
/* collapsed state */
|
||||
#srcPanel.collapsed{flex:0}
|
||||
#srcPanel.collapsed #src{display:none}
|
||||
</style>
|
||||
<!-- ONLY Mermaid – NO Monaco, NO AMD loader -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<button id="toggleBtn" onclick="togglePanel()">🗔 Hide source</button>
|
||||
Home-Lab diagram – live Mermaid editor
|
||||
<button onclick="saveSVG()">💾 SVG</button>
|
||||
<button onclick="savePNG()">💾 PNG</button>
|
||||
<button onclick="reset()">🔄 Reset</button>
|
||||
<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>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="panel" id="srcPanel">
|
||||
<h3>Source (edit here)</h3>
|
||||
<textarea id="src" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Live preview</h3>
|
||||
<div id="preview"></div>
|
||||
<div id="error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ---------- DEFAULT SOURCE – guaranteed valid Mermaid 10.6.1 ---------- */
|
||||
const DEFAULT = `flowchart TD
|
||||
%% ---------- styles ----------
|
||||
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\nhub.lan\n192.168.1.1]:::router
|
||||
|
||||
subgraph LAN [🏠 LAN 192.168.1.0/24]
|
||||
subgraph CORE [💻 Core server\n192.168.1.159]
|
||||
traefik[🚦 Traefik]:::core
|
||||
gitea[📚 Gitea]:::core
|
||||
dokku[🐳 Dokku]:::core
|
||||
auction[🧱 Auction stack]:::core
|
||||
mi50[🧠 MI50 / Ollama]:::core
|
||||
end
|
||||
|
||||
subgraph INFRA [🧭 Infra & DNS\n192.168.1.163]
|
||||
adguard[🛡️ AdGuard]:::infra
|
||||
artifactory[📦 Artifactory]:::infra
|
||||
end
|
||||
|
||||
ha[🏡 Home Assistant\n192.168.1.193]:::infra
|
||||
atlas[🧱 Atlas\n192.168.1.100]:::worker
|
||||
|
||||
iot1[📺 IoT-1]:::iot
|
||||
iot2[📟 IoT-2]:::iot
|
||||
end
|
||||
|
||||
subgraph TETHER [📶 Tether 192.168.137.0/24]
|
||||
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
|
||||
`;
|
||||
|
||||
/* ---------- safe localStorage access ---------- */
|
||||
function getStorage(key, fallback) {
|
||||
try {
|
||||
const val = localStorage.getItem(key);
|
||||
return val !== null ? val : fallback;
|
||||
} catch (e) {
|
||||
// IntelliJ preview blocks localStorage
|
||||
console.warn('localStorage unavailable:', e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
function setStorage(key, val) {
|
||||
try {
|
||||
localStorage.setItem(key, val);
|
||||
} catch (e) {
|
||||
console.warn('localStorage save failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- load state ---------- */
|
||||
const srcEl = document.getElementById('src');
|
||||
const errEl = document.getElementById('error');
|
||||
srcEl.value = getStorage('labDiagram', DEFAULT);
|
||||
|
||||
const srcPanel = document.getElementById('srcPanel');
|
||||
const toggleBtn = document.getElementById('toggleBtn');
|
||||
if (getStorage('panelCollapsed', 'false') === 'true') {
|
||||
srcPanel.classList.add('collapsed');
|
||||
toggleBtn.textContent = '🗔 Show source';
|
||||
}
|
||||
|
||||
/* ---------- panel toggle ---------- */
|
||||
function togglePanel() {
|
||||
srcPanel.classList.toggle('collapsed');
|
||||
const collapsed = srcPanel.classList.contains('collapsed');
|
||||
toggleBtn.textContent = collapsed ? '🗔 Show source' : '🗔 Hide source';
|
||||
setStorage('panelCollapsed', collapsed);
|
||||
}
|
||||
|
||||
/* ---------- Mermaid init ---------- */
|
||||
mermaid.initialize({startOnLoad: false, theme: 'default'});
|
||||
|
||||
/* ---------- render ---------- */
|
||||
function render() {
|
||||
const src = srcEl.value;
|
||||
setStorage('labDiagram', src);
|
||||
showError('');
|
||||
|
||||
const preview = document.getElementById('preview');
|
||||
preview.innerHTML = '';
|
||||
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = src;
|
||||
preview.appendChild(mermaidDiv);
|
||||
|
||||
// Render then zoom
|
||||
mermaid.init(undefined, mermaidDiv).then(() => {
|
||||
const svg = preview.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.maxWidth = 'none';
|
||||
svg.style.width = 'auto';
|
||||
svg.style.height = 'auto';
|
||||
svg.style.transformOrigin = 'top left';
|
||||
applyZoom();
|
||||
}
|
||||
}).catch(e => {
|
||||
showError('Mermaid error: ' + e.message);
|
||||
});
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errEl.textContent = msg;
|
||||
errEl.style.display = msg ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/* ---------- auto-render + zoom ---------- */
|
||||
srcEl.addEventListener('input', () => {
|
||||
clearTimeout(srcEl._t);
|
||||
srcEl._t = setTimeout(render, 300);
|
||||
});
|
||||
|
||||
const zoomEl = document.getElementById('zoom');
|
||||
zoomEl.addEventListener('input', () => {
|
||||
document.getElementById('zoomVal').textContent = zoomEl.value + '%';
|
||||
applyZoom();
|
||||
});
|
||||
|
||||
function applyZoom() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${zoomEl.value / 100})`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- actions ---------- */
|
||||
function reset() {
|
||||
if (confirm('Reset to default diagram?')) {
|
||||
srcEl.value = DEFAULT;
|
||||
zoomEl.value = 100;
|
||||
document.getElementById('zoomVal').textContent = '100%';
|
||||
setStorage('labDiagram', DEFAULT);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSVG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const clone = svg.cloneNode(true);
|
||||
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 a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'home-lab.svg';
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
|
||||
function savePNG() {
|
||||
const svg = document.querySelector('#preview svg');
|
||||
if (!svg) return alert('Nothing to save yet.');
|
||||
|
||||
const bbox = svg.getBBox();
|
||||
const width = Math.max(bbox.width, 1200);
|
||||
const height = Math.max(bbox.height, 800);
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
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);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user