Files
viewer/lab/preview.html
2025-12-02 16:25:22 +01:00

283 lines
8.6 KiB
HTML
Raw Permalink 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 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>