639 lines
38 KiB
HTML
639 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>KiteStacks Homelab — Architecture Map</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { background: #030810; font-family: sans-serif; overflow-x: hidden; }
|
||
|
||
@keyframes twinkle { 0%,100%{opacity:1} 50%{opacity:0.1} }
|
||
@keyframes shoot { 0%{stroke-dashoffset:200;opacity:0} 15%{opacity:1} 85%{opacity:0.9} 100%{stroke-dashoffset:0;opacity:0} }
|
||
@keyframes pulse-ring { 0%,100%{r:50;opacity:0.35} 50%{r:62;opacity:0.04} }
|
||
@keyframes pulse-core2 { 0%,100%{r:42} 50%{r:46} }
|
||
@keyframes drift { 0%,100%{transform:translate(0,0)} 33%{transform:translate(2px,-4px)} 66%{transform:translate(-3px,-2px)} }
|
||
@keyframes line-glow { 0%,100%{opacity:0.3} 50%{opacity:0.85} }
|
||
@keyframes nebula-pulse { 0%,100%{opacity:0.07} 50%{opacity:0.15} }
|
||
@keyframes gitops-flow {
|
||
0% { stroke-dashoffset: 60; opacity: 0.2; }
|
||
50% { opacity: 1; }
|
||
100% { stroke-dashoffset: 0; opacity: 0.2; }
|
||
}
|
||
@keyframes glow-orbit {
|
||
from { transform-origin: 340px 310px; transform: rotate(0deg); }
|
||
to { transform-origin: 340px 310px; transform: rotate(360deg); }
|
||
}
|
||
|
||
.star-dot { animation: twinkle var(--d,2s) ease-in-out infinite var(--delay,0s); fill: #fff; }
|
||
.shoot { stroke-dasharray: 90 200; animation: shoot var(--sd,5s) ease-in-out infinite var(--sdelay,0s); }
|
||
.conn { animation: line-glow var(--ld,3s) ease-in-out infinite var(--ldelay,0s); }
|
||
.float-n { animation: drift var(--fd,4.5s) ease-in-out infinite var(--fdelay,0s); }
|
||
.nebula { animation: nebula-pulse 5s ease-in-out infinite; }
|
||
.gitops { stroke-dasharray: 10 8; animation: gitops-flow 2s linear infinite; }
|
||
|
||
.node-btn { cursor: pointer; }
|
||
.node-btn:hover circle { filter: brightness(1.35); }
|
||
|
||
.legend-item { font-size: 11px; fill: #8090b0; }
|
||
|
||
/* ── Modal overlay ── */
|
||
#kite-modal-overlay {
|
||
display: none;
|
||
position: fixed; inset: 0;
|
||
background: rgba(2,6,18,0.82);
|
||
backdrop-filter: blur(3px);
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
#kite-modal-overlay.open { display: flex; }
|
||
|
||
#kite-modal {
|
||
background: linear-gradient(145deg, #0d1e3a, #071428);
|
||
border: 1px solid #2a4878;
|
||
border-radius: 12px;
|
||
padding: 24px 28px 20px;
|
||
max-width: 480px;
|
||
width: 92%;
|
||
box-shadow: 0 0 40px rgba(74,144,217,0.18), 0 8px 32px rgba(0,0,0,0.6);
|
||
position: relative;
|
||
color: #c8dff8;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
#kite-modal-close {
|
||
position: absolute; top: 12px; right: 14px;
|
||
background: none; border: none;
|
||
color: #4a6898; font-size: 18px; cursor: pointer;
|
||
line-height: 1;
|
||
transition: color 0.2s;
|
||
}
|
||
#kite-modal-close:hover { color: #80b8f8; }
|
||
|
||
#kite-modal-title {
|
||
font-size: 13px; font-weight: 700;
|
||
color: #80b8f8;
|
||
letter-spacing: 1.5px;
|
||
text-transform: uppercase;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #1a3060;
|
||
}
|
||
|
||
#kite-modal-body {
|
||
font-size: 12px;
|
||
line-height: 1.75;
|
||
color: #9ab8d8;
|
||
white-space: pre-wrap;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.kite-path-section {
|
||
margin-top: 14px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #1a3060;
|
||
}
|
||
.kite-path-label {
|
||
font-size: 10px;
|
||
color: #4a6898;
|
||
letter-spacing: 1.5px;
|
||
text-transform: uppercase;
|
||
margin-bottom: 8px;
|
||
}
|
||
.kite-path-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
.kite-path-text {
|
||
flex: 1;
|
||
background: #040d1e;
|
||
border: 1px solid #1e3660;
|
||
border-radius: 5px;
|
||
padding: 5px 10px;
|
||
font-size: 11px;
|
||
color: #60b0f0;
|
||
font-family: 'Courier New', monospace;
|
||
overflow-x: auto;
|
||
white-space: nowrap;
|
||
cursor: text;
|
||
user-select: all;
|
||
}
|
||
.kite-copy-btn {
|
||
background: #0d2248;
|
||
border: 1px solid #2a4878;
|
||
border-radius: 5px;
|
||
color: #60a8e8;
|
||
font-size: 10px;
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: background 0.15s, color 0.15s;
|
||
font-family: sans-serif;
|
||
}
|
||
.kite-copy-btn:hover { background: #1a3a68; color: #a0d0ff; }
|
||
.kite-copy-btn.copied { background: #0d3020; color: #40d090; border-color: #1a5040; }
|
||
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<svg width="100%" viewBox="0 0 900 840" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<radialGradient id="bg" cx="50%" cy="50%" r="60%">
|
||
<stop offset="0%" stop-color="#0c1830"/>
|
||
<stop offset="100%" stop-color="#030810"/>
|
||
</radialGradient>
|
||
</defs>
|
||
|
||
<!-- BG -->
|
||
<rect width="900" height="780" fill="url(#bg)"/>
|
||
|
||
<!-- STARS -->
|
||
<g>
|
||
<circle class="star-dot" style="--d:1.2s;--delay:0s" cx="48" cy="30" r="1.5"/>
|
||
<circle class="star-dot" style="--d:2.1s;--delay:.4s" cx="105" cy="70" r="1"/>
|
||
<circle class="star-dot" style="--d:1.8s;--delay:.7s" cx="170" cy="38" r="1.5"/>
|
||
<circle class="star-dot" style="--d:3.0s;--delay:.1s" cx="245" cy="20" r="1"/>
|
||
<circle class="star-dot" style="--d:1.5s;--delay:.5s" cx="320" cy="50" r="1.5"/>
|
||
<circle class="star-dot" style="--d:2.4s;--delay:.9s" cx="410" cy="22" r="1"/>
|
||
<circle class="star-dot" style="--d:1.9s;--delay:.2s" cx="480" cy="45" r="1.5"/>
|
||
<circle class="star-dot" style="--d:2.7s;--delay:.6s" cx="555" cy="18" r="1"/>
|
||
<circle class="star-dot" style="--d:1.6s;--delay:.4s" cx="630" cy="55" r="1.5"/>
|
||
<circle class="star-dot" style="--d:2.2s;--delay:.8s" cx="700" cy="28" r="1"/>
|
||
<circle class="star-dot" style="--d:3.1s;--delay:.1s" cx="760" cy="80" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.4s;--delay:.3s" cx="820" cy="40" r="1"/>
|
||
<circle class="star-dot" style="--d:2.6s;--delay:.7s" cx="870" cy="90" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.9s;--delay:.2s" cx="880" cy="200" r="1"/>
|
||
<circle class="star-dot" style="--d:2.3s;--delay:.5s" cx="30" cy="380" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.7s;--delay:.9s" cx="55" cy="480" r="1"/>
|
||
<circle class="star-dot" style="--d:2.8s;--delay:.3s" cx="30" cy="580" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.3s;--delay:.6s" cx="70" cy="680" r="1"/>
|
||
<circle class="star-dot" style="--d:2.0s;--delay:.1s" cx="160" cy="740" r="1.5"/>
|
||
<circle class="star-dot" style="--d:3.2s;--delay:.4s" cx="250" cy="760" r="1"/>
|
||
<circle class="star-dot" style="--d:1.6s;--delay:.8s" cx="370" cy="755" r="1.5"/>
|
||
<circle class="star-dot" style="--d:2.5s;--delay:.2s" cx="460" cy="765" r="1"/>
|
||
<circle class="star-dot" style="--d:1.8s;--delay:.6s" cx="570" cy="750" r="1.5"/>
|
||
<circle class="star-dot" style="--d:2.9s;--delay:.0s" cx="660" cy="740" r="1"/>
|
||
<circle class="star-dot" style="--d:1.4s;--delay:.3s" cx="750" cy="760" r="1.5"/>
|
||
<circle class="star-dot" style="--d:2.3s;--delay:.7s" cx="840" cy="720" r="1"/>
|
||
<circle class="star-dot" style="--d:1.7s;--delay:.1s" cx="880" cy="620" r="1.5"/>
|
||
<circle class="star-dot" style="--d:3.0s;--delay:.5s" cx="870" cy="500" r="1"/>
|
||
<circle class="star-dot" style="--d:2.1s;--delay:.9s" cx="860" cy="380" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.5s;--delay:.2s" cx="845" cy="290" r="1"/>
|
||
<circle class="star-dot" style="--d:2.4s;--delay:.4s" cx="190" cy="140" r="1.5" fill="#e8f4ff"/>
|
||
<circle class="star-dot" style="--d:1.9s;--delay:.8s" cx="640" cy="120" r="2" fill="#c0e0ff"/>
|
||
<circle class="star-dot" style="--d:2.6s;--delay:.0s" cx="780" cy="340" r="1.5" fill="#c8e0ff"/>
|
||
<circle class="star-dot" style="--d:1.3s;--delay:.6s" cx="120" cy="500" r="1.5" fill="#e8f4ff"/>
|
||
<!-- CENTER FIELD -->
|
||
<circle class="star-dot" style="--d:1.7s;--delay:.2s" cx="280" cy="180" r="1"/>
|
||
<circle class="star-dot" style="--d:2.4s;--delay:.5s" cx="340" cy="220" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.9s;--delay:.8s" cx="400" cy="160" r="1"/>
|
||
<circle class="star-dot" style="--d:3.0s;--delay:.1s" cx="450" cy="240" r="1.5" fill="#c8e0ff"/>
|
||
<circle class="star-dot" style="--d:1.5s;--delay:.4s" cx="300" cy="280" r="1"/>
|
||
<circle class="star-dot" style="--d:2.7s;--delay:.7s" cx="260" cy="340" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.6s;--delay:.3s" cx="320" cy="360" r="1"/>
|
||
<circle class="star-dot" style="--d:2.2s;--delay:.9s" cx="380" cy="300" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.8s;--delay:.6s" cx="340" cy="395" r="1" fill="#e8f4ff"/>
|
||
<circle class="star-dot" style="--d:2.9s;--delay:.2s" cx="290" cy="420" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.4s;--delay:.5s" cx="410" cy="380" r="1"/>
|
||
<circle class="star-dot" style="--d:2.5s;--delay:.8s" cx="430" cy="430" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.9s;--delay:.1s" cx="470" cy="320" r="1"/>
|
||
<circle class="star-dot" style="--d:2.3s;--delay:.4s" cx="500" cy="260" r="1.5" fill="#c8e0ff"/>
|
||
<circle class="star-dot" style="--d:1.7s;--delay:.7s" cx="540" cy="350" r="1"/>
|
||
<circle class="star-dot" style="--d:2.8s;--delay:.3s" cx="560" cy="280" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.5s;--delay:.6s" cx="610" cy="320" r="1"/>
|
||
<circle class="star-dot" style="--d:2.6s;--delay:.9s" cx="640" cy="380" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.8s;--delay:.2s" cx="600" cy="420" r="1" fill="#e8f4ff"/>
|
||
<circle class="star-dot" style="--d:2.1s;--delay:.5s" cx="660" cy="300" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.6s;--delay:.8s" cx="250" cy="240" r="1"/>
|
||
<circle class="star-dot" style="--d:2.4s;--delay:.1s" cx="370" cy="450" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.9s;--delay:.4s" cx="480" cy="450" r="1"/>
|
||
<circle class="star-dot" style="--d:2.7s;--delay:.7s" cx="540" cy="430" r="1.5" fill="#c8e0ff"/>
|
||
<circle class="star-dot" style="--d:1.4s;--delay:.3s" cx="220" cy="300" r="1"/>
|
||
<circle class="star-dot" style="--d:2.5s;--delay:.6s" cx="700" cy="350" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.7s;--delay:.9s" cx="430" cy="200" r="1"/>
|
||
<circle class="star-dot" style="--d:2.2s;--delay:.2s" cx="360" cy="500" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.8s;--delay:.5s" cx="280" cy="480" r="1" fill="#e8f4ff"/>
|
||
<circle class="star-dot" style="--d:2.9s;--delay:.8s" cx="500" cy="500" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.5s;--delay:.1s" cx="620" cy="450" r="1"/>
|
||
<circle class="star-dot" style="--d:2.3s;--delay:.4s" cx="320" cy="150" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.9s;--delay:.7s" cx="560" cy="200" r="1"/>
|
||
<circle class="star-dot" style="--d:2.6s;--delay:.3s" cx="240" cy="400" r="1.5" fill="#c8e0ff"/>
|
||
<circle class="star-dot" style="--d:1.6s;--delay:.6s" cx="660" cy="240" r="1"/>
|
||
<circle class="star-dot" style="--d:2.4s;--delay:.9s" cx="450" cy="350" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.8s;--delay:.2s" cx="390" cy="340" r="1"/>
|
||
<circle class="star-dot" style="--d:2.1s;--delay:.5s" cx="520" cy="390" r="1.5"/>
|
||
<circle class="star-dot" style="--d:1.7s;--delay:.8s" cx="310" cy="320" r="1" fill="#e8f4ff"/>
|
||
</g>
|
||
|
||
<!-- SHOOTING STARS -->
|
||
<line class="shoot" style="--sd:5s;--sdelay:0s" x1="60" y1="42" x2="200" y2="100" stroke="#fff" stroke-width="1.8" opacity="0"/>
|
||
<line class="shoot" style="--sd:8s;--sdelay:2.5s" x1="520" y1="28" x2="660" y2="82" stroke="#c8d8ff" stroke-width="1.4" opacity="0"/>
|
||
<line class="shoot" style="--sd:6s;--sdelay:1.2s" x1="720" y1="200" x2="810" y2="270" stroke="#fff" stroke-width="1.2" opacity="0"/>
|
||
<line class="shoot" style="--sd:9s;--sdelay:3.8s" x1="25" y1="300" x2="120" y2="370" stroke="#d0e0ff" stroke-width="1.5" opacity="0"/>
|
||
<line class="shoot" style="--sd:7s;--sdelay:0.8s" x1="380" y1="12" x2="490" y2="65" stroke="#fff" stroke-width="1.3" opacity="0"/>
|
||
<line class="shoot" style="--sd:11s;--sdelay:5s" x1="60" y1="680" x2="180" y2="620" stroke="#e0c8ff" stroke-width="1.2" opacity="0"/>
|
||
<line class="shoot" style="--sd:8.5s;--sdelay:1.8s" x1="250" y1="200" x2="430" y2="320" stroke="#fff" stroke-width="1.4" opacity="0"/>
|
||
<line class="shoot" style="--sd:10s;--sdelay:4.2s" x1="650" y1="250" x2="480" y2="400" stroke="#c8d8ff" stroke-width="1.3" opacity="0"/>
|
||
<line class="shoot" style="--sd:7.5s;--sdelay:2.8s" x1="400" y1="180" x2="560" y2="380" stroke="#fff" stroke-width="1.2" opacity="0"/>
|
||
<line class="shoot" style="--sd:9.5s;--sdelay:6s" x1="300" y1="450" x2="500" y2="300" stroke="#d0e0ff" stroke-width="1.3" opacity="0"/>
|
||
|
||
<!-- ================================================================
|
||
TAILSCALE FLOW ARROWS (dashed animated — monk ↔ kscloud1)
|
||
================================================================ -->
|
||
<!-- monk core → kscloud1 (Tailscale VPN overlay) -->
|
||
<path class="gitops" d="M 340,352 Q 340,460 340,540" fill="none" stroke="#4a90d9" stroke-width="1.5"/>
|
||
<!-- Portainer → manages Docker containers on monk -->
|
||
<path class="gitops" d="M 530,310 Q 490,310 450,310" fill="none" stroke="#4a90d9" stroke-width="1.5"/>
|
||
<!-- kscloud1 ↔ Authentik DB (Tailscale) -->
|
||
<path d="M 340,540 Q 340,430 185,280 Q 160,250 185,210" fill="none" stroke="#70b0f0" stroke-width="1" stroke-dasharray="5 6" opacity="0.35"/>
|
||
|
||
<!-- ================================================================
|
||
MAIN CONNECTION LINES (Core monk → clusters)
|
||
================================================================ -->
|
||
<!-- Core → Auth cluster -->
|
||
<line class="conn" style="--ld:2.2s;--ldelay:0s" x1="310" y1="278" x2="185" y2="195" stroke="#7aaee8" stroke-width="1.8" opacity="0.45"/>
|
||
<!-- Core → Observability cluster -->
|
||
<line class="conn" style="--ld:2.8s;--ldelay:.4s" x1="310" y1="320" x2="155" y2="390" stroke="#6ab0f0" stroke-width="1.8" opacity="0.45"/>
|
||
<!-- Core → AI cluster -->
|
||
<line class="conn" style="--ld:3.2s;--ldelay:.8s" x1="310" y1="340" x2="185" y2="480" stroke="#80b8f8" stroke-width="1.8" opacity="0.45"/>
|
||
<!-- Core → Apps cluster -->
|
||
<line class="conn" style="--ld:2.5s;--ldelay:.2s" x1="370" y1="340" x2="510" y2="480" stroke="#50a8f0" stroke-width="1.8" opacity="0.45"/>
|
||
<!-- Core → Portal -->
|
||
<line class="conn" style="--ld:3.5s;--ldelay:.6s" x1="380" y1="290" x2="510" y2="195" stroke="#60b8f0" stroke-width="1.8" opacity="0.45"/>
|
||
|
||
<!-- sub-node lines -->
|
||
<!-- auth children -->
|
||
<line x1="185" y1="175" x2="100" y2="130" stroke="#4a88d0" stroke-width="0.8" opacity="0.35"/>
|
||
<line x1="185" y1="195" x2="88" y2="210" stroke="#4a88d0" stroke-width="0.8" opacity="0.35"/>
|
||
<line x1="185" y1="205" x2="115" y2="268" stroke="#4a88d0" stroke-width="0.8" opacity="0.35"/>
|
||
<!-- obs children -->
|
||
<line x1="155" y1="380" x2="68" y2="318" stroke="#5090d8" stroke-width="0.8" opacity="0.35"/>
|
||
<line x1="155" y1="400" x2="55" y2="435" stroke="#5090d8" stroke-width="0.8" opacity="0.35"/>
|
||
<!-- ai children -->
|
||
<line x1="185" y1="480" x2="122" y2="510" stroke="#6898e0" stroke-width="0.8" opacity="0.35"/>
|
||
<line x1="185" y1="500" x2="110" y2="590" stroke="#6898e0" stroke-width="0.8" opacity="0.35"/>
|
||
<!-- apps children -->
|
||
<line x1="510" y1="480" x2="600" y2="445" stroke="#4a90d9" stroke-width="0.8" opacity="0.35"/>
|
||
<line x1="510" y1="495" x2="608" y2="530" stroke="#4a90d9" stroke-width="0.8" opacity="0.35"/>
|
||
<line x1="510" y1="495" x2="595" y2="575" stroke="#4a90d9" stroke-width="0.8" opacity="0.35"/>
|
||
<!-- kscloud1 ↔ apps line -->
|
||
<line x1="608" y1="545" x2="608" y2="600" stroke="#70b0f0" stroke-width="0.9" opacity="0.5" stroke-dasharray="4 4"/>
|
||
|
||
<!-- ================================================================
|
||
CORE NODE — monk (primary Docker host)
|
||
================================================================ -->
|
||
<!-- orbiting dot -->
|
||
<circle r="4" fill="#b0d8ff" opacity="0.9">
|
||
<animateMotion dur="9s" repeatCount="indefinite">
|
||
<mpath href="#orb"/>
|
||
</animateMotion>
|
||
</circle>
|
||
<path id="orb" d="M 340,268 A 42,42 0 1 1 339.99,268" fill="none"/>
|
||
<!-- body -->
|
||
<circle cx="340" cy="310" r="42" fill="transparent" stroke="#60a0f0" stroke-width="2"/>
|
||
<circle cx="340" cy="310" r="30" fill="none" stroke="#4a80d8" stroke-width="0.8" opacity="0.55"/>
|
||
<!-- text -->
|
||
<text x="340" y="303" text-anchor="middle" font-size="14" font-weight="700" fill="#e8f4ff" letter-spacing="2.5">Kite AO</text>
|
||
<text x="340" y="319" text-anchor="middle" font-size="8" fill="#80a8d8" letter-spacing="1">monk • Docker</text>
|
||
|
||
|
||
<!-- ================================================================
|
||
PORTAINER NODE (right of core — replaces FluxCD)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:4s;--fdelay:.3s">
|
||
<g class="node-btn" onclick="alert('Portainer CE — Docker Management UI\nURL: portainer.kitestacks.com\nManages: all Docker containers on monk\nAuth: Authentik OAuth2 SSO\nData: portainer_data Docker volume\nCompose: ~/kitestacks-live/docker/portainer/\nAdmin user must be pre-created via API before first OAuth login.')">
|
||
<circle cx="580" cy="310" r="34" fill="transparent" stroke="#4a90d9" stroke-width="1.8"/>
|
||
<circle cx="580" cy="310" r="24" fill="none" stroke="#4a90d9" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="580" y="302" text-anchor="middle" font-size="11" fill="#ffffff">⊞</text>
|
||
<text x="580" y="315" text-anchor="middle" font-size="9" font-weight="700" fill="#e8f4ff" letter-spacing="1">PORTAINER</text>
|
||
<text x="580" y="326" text-anchor="middle" font-size="7.5" fill="#a0c8f0">Docker UI • OAuth2</text>
|
||
</g>
|
||
</g>
|
||
<!-- Portainer ↔ core connection -->
|
||
<line class="conn" style="--ld:2s;--ldelay:0s" x1="546" y1="310" x2="382" y2="310" stroke="#4a90d9" stroke-width="1.8" opacity="0.5"/>
|
||
|
||
<!-- ================================================================
|
||
FORGEJO NODE (top-right)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:3.8s;--fdelay:.8s">
|
||
<g class="node-btn" onclick="alert('Forgejo Git Server — Self-hosted at gitforge.kitestacks.com\nHosts all homelab repos (private)\nSSH git on port 2222\nWeb UI at gitforge.kitestacks.com\nCompose: ~/kitestacks-live/docker/forgejo/\nAPI token for automation: generate via forgejo admin CLI\nSSO: Authentik OIDC')">
|
||
<circle cx="700" cy="240" r="32" fill="transparent" stroke="#5a9ee8" stroke-width="1.8"/>
|
||
<circle cx="700" cy="240" r="22" fill="none" stroke="#5a9ee8" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="700" y="232" text-anchor="middle" font-size="11" fill="#ffffff">⎇</text>
|
||
<text x="700" y="245" text-anchor="middle" font-size="9" font-weight="700" fill="#e8f4ff" letter-spacing="1">FORGEJO</text>
|
||
<text x="700" y="256" text-anchor="middle" font-size="7.5" fill="#a0c0e8">gitforge.kitestacks.com</text>
|
||
</g>
|
||
</g>
|
||
<!-- Forgejo → Portainer -->
|
||
<line class="conn" style="--ld:2.4s;--ldelay:.5s" x1="672" y1="248" x2="607" y2="287" stroke="#5a9ee8" stroke-width="1.5" opacity="0.5"/>
|
||
|
||
<!-- ================================================================
|
||
PORTAL NODE (top-right of core)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:4.2s;--fdelay:0s">
|
||
<g class="node-btn" onclick="alert('KiteStacks Portal — www.kitestacks.com\nCustom cyberpunk-themed homepage\nnginx serves static HTML/CSS/JS\nMetrics API: FastAPI (Python) at /api/*\nMetrics API reads: CPU, RAM, disk, network via psutil\nWeather: wttr.in API\nGit activity: Forgejo API\nCompose: ~/kitestacks-live/docker/kitestacks-portal/\nnetwork_mode: host for real system stats')">
|
||
<circle cx="520" cy="185" r="32" fill="transparent" stroke="#60b8f0" stroke-width="1.8"/>
|
||
<circle cx="520" cy="185" r="22" fill="none" stroke="#60b8f0" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="520" y="177" text-anchor="middle" font-size="11" fill="#ffffff">⌂</text>
|
||
<text x="520" y="190" text-anchor="middle" font-size="9" font-weight="700" fill="#e8f4ff" letter-spacing="1">PORTAL</text>
|
||
<text x="520" y="201" text-anchor="middle" font-size="7.5" fill="#a0c8f0">www.kitestacks.com</text>
|
||
</g>
|
||
</g>
|
||
|
||
<!-- ================================================================
|
||
AUTH CLUSTER — Authentik (top-left)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:4.8s;--fdelay:.5s">
|
||
<g class="node-btn" onclick="alert('Authentik — Identity Provider (SSO)\nURL: auth.kitestacks.com\nProvides: OIDC / OAuth2 for all services\nServer + Worker on monk\nDatabase: PostgreSQL on kscloud1 (Tailscale)\nRedis: on kscloud1 (Tailscale)\nCompose: ~/kitestacks-live/docker/authentik/\nBookStack issuer_mode must be per_provider (not global)')">
|
||
<circle cx="185" cy="185" r="32" fill="transparent" stroke="#7aaee8" stroke-width="1.8"/>
|
||
<circle cx="185" cy="185" r="22" fill="none" stroke="#7aaee8" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="185" y="177" text-anchor="middle" font-size="10" fill="#ffffff">⬡</text>
|
||
<text x="185" y="190" text-anchor="middle" font-size="9" font-weight="700" fill="#e8f4ff" letter-spacing="1">AUTHENTIK</text>
|
||
<text x="185" y="201" text-anchor="middle" font-size="7.5" fill="#a0c0e8">SSO • OIDC / OAuth2</text>
|
||
</g>
|
||
</g>
|
||
<!-- auth sub: postgres on kscloud1 -->
|
||
<g class="node-btn" onclick="alert('PostgreSQL — Authentik database\nRunning on: kscloud1 (Hetzner VPS)\nAccess: Tailscale overlay only (never public)\nBoth monk and kscloud1 Authentik containers connect via Tailscale IP\nCompose: /opt/kitestacks/docker/authentik/ on kscloud1')">
|
||
<circle cx="98" cy="122" r="22" fill="transparent" stroke="#4a88d0" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="98" cy="118" y="118" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Postgres</text>
|
||
<text x="98" y="129" text-anchor="middle" font-size="7.5" fill="#b8d8ff">kscloud1</text>
|
||
</g>
|
||
<!-- auth sub: BookStack -->
|
||
<g class="node-btn" onclick="alert('BookStack — Internal Wiki\nURL: wiki.kitestacks.com\nRunning on: monk + kscloud1 (both)\nAuth: Authentik OIDC SSO\nFix: OIDC_ISSUER must point to per-app URL\nFix: OIDC_ISSUER_DISCOVER=true\nFix: chown -R abc:users /config/www/framework/cache/\nCompose: ~/kitestacks-live/docker/bookstack/')">
|
||
<circle cx="80" cy="210" r="22" fill="transparent" stroke="#4a88d0" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="80" y="206" text-anchor="middle" font-size="7.5" fill="#b8d8ff">BookStack</text>
|
||
<text x="80" y="217" text-anchor="middle" font-size="7.5" fill="#b8d8ff">wiki</text>
|
||
</g>
|
||
<!-- auth sub: Redis on kscloud1 -->
|
||
<g class="node-btn" onclick="alert('Redis — Authentik session cache\nRunning on: kscloud1 (Hetzner VPS)\nAccess: Tailscale overlay only\nBoth monk and kscloud1 Authentik containers use this shared Redis\nIf kscloud1 goes down: Authentik logins will fail until kscloud1 is restored')">
|
||
<circle cx="110" cy="268" r="22" fill="transparent" stroke="#4a88d0" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="110" y="264" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Redis</text>
|
||
<text x="110" y="275" text-anchor="middle" font-size="7.5" fill="#b8d8ff">kscloud1</text>
|
||
</g>
|
||
|
||
<!-- ================================================================
|
||
OBSERVABILITY CLUSTER — Grafana + Prometheus (left)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:3.9s;--fdelay:1s">
|
||
<g class="node-btn" onclick="alert('Grafana — Metrics Dashboards\nURL: grafana.kitestacks.com\nData source: Prometheus (http://prometheus:9090)\nAuth: Authentik OAuth2 SSO\nDashboards: host OS stats (CPU, RAM, disk, network)\nCompose: ~/kitestacks-live/docker/grafana/')">
|
||
<circle cx="155" cy="390" r="32" fill="transparent" stroke="#6ab0f0" stroke-width="1.8"/>
|
||
<circle cx="155" cy="390" r="22" fill="none" stroke="#6ab0f0" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="155" y="382" text-anchor="middle" font-size="10" fill="#ffffff">◈</text>
|
||
<text x="155" y="395" text-anchor="middle" font-size="9" font-weight="700" fill="#e8f4ff" letter-spacing="1">GRAFANA</text>
|
||
<text x="155" y="406" text-anchor="middle" font-size="7.5" fill="#a0c8f0">Observability</text>
|
||
</g>
|
||
</g>
|
||
<g class="node-btn" onclick="alert('Prometheus — Metrics Scraper\nScrapes: node-exporter on monk (localhost:9100)\nScrapes: node-exporter on kscloud1 (Tailscale IP:9100)\nRetention: local Docker volume\nCompose: ~/kitestacks-live/docker/prometheus/')">
|
||
<circle cx="62" cy="318" r="22" fill="transparent" stroke="#5090d8" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="62" y="314" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Prometheus</text>
|
||
<text x="62" y="325" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Scraper</text>
|
||
</g>
|
||
<g class="node-btn" onclick="alert('Node Exporter — Host OS metrics\nRunning on: monk (network_mode: host, pid: host)\nExposes: CPU, RAM, disk, network to Prometheus\nAlso deployed on kscloud1 for cloud host metrics\nMust use host network to see real OS stats (not container stats)')">
|
||
<circle cx="50" cy="435" r="22" fill="transparent" stroke="#5090d8" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="50" y="431" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Node</text>
|
||
<text x="50" y="442" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Exporter</text>
|
||
</g>
|
||
|
||
<!-- ================================================================
|
||
AI CLUSTER — Open WebUI + LiteLLM (bottom-left)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:5.2s;--fdelay:.2s">
|
||
<g class="node-btn" onclick="alert('Open WebUI — AI Chat Interface\nURL: ai.kitestacks.com\nConnects to: LiteLLM proxy for model routing\nSupports: GPT-4, Claude, local Ollama models\nAuth: Authentik OIDC SSO\nCompose: ~/kitestacks-live/docker/kite-openwebui/')">
|
||
<circle cx="185" cy="490" r="32" fill="transparent" stroke="#80b8f8" stroke-width="1.8"/>
|
||
<circle cx="185" cy="490" r="22" fill="none" stroke="#80b8f8" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="185" y="482" text-anchor="middle" font-size="10" fill="#e8f4ff">✦</text>
|
||
<text x="185" y="495" text-anchor="middle" font-size="8" font-weight="700" fill="#e8f4ff" letter-spacing="1">OPEN WEBUI</text>
|
||
<text x="185" y="506" text-anchor="middle" font-size="7.5" fill="#a0c0e8">AI Interface</text>
|
||
</g>
|
||
</g>
|
||
<g class="node-btn" onclick="alert('LiteLLM — LLM API Proxy / Router\nRoutes AI requests to multiple backends\nBackends: OpenAI, Anthropic, local Ollama\nUsed by: Open WebUI\nCompose: ~/kitestacks-live/docker/kite-litellm/')">
|
||
<circle cx="100" cy="520" r="22" fill="transparent" stroke="#6898e0" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="100" y="516" text-anchor="middle" font-size="7.5" fill="#b8d8ff">LiteLLM</text>
|
||
<text x="100" y="527" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Proxy</text>
|
||
</g>
|
||
<g class="node-btn" onclick="alert('LLM Models — AI model backends\nRouted via LiteLLM proxy:\n• OpenAI (GPT-4o, GPT-4)\n• Anthropic (Claude Sonnet)\n• Local Ollama (runs on monk GPU/CPU)\nModel selection handled by Open WebUI dropdown')">
|
||
<circle cx="88" cy="600" r="22" fill="transparent" stroke="#6898e0" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="88" y="596" text-anchor="middle" font-size="7.5" fill="#b8d8ff">LLM</text>
|
||
<text x="88" y="607" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Models</text>
|
||
</g>
|
||
|
||
<!-- ================================================================
|
||
APPS CLUSTER — Kavita, Karakeep, OSTicket (bottom-right)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:4.4s;--fdelay:.7s">
|
||
<g class="node-btn" onclick="alert('Kavita — Digital Library / Ebook Reader\nURL: kavita.kitestacks.com\nRunning as: plain Docker container\nMounts: /mnt/books (host path)\nCompose: ~/kitestacks-live/docker/kavita/')">
|
||
<circle cx="510" cy="490" r="32" fill="transparent" stroke="#50a8f0" stroke-width="1.8"/>
|
||
<circle cx="510" cy="490" r="22" fill="none" stroke="#50a8f0" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="510" y="482" text-anchor="middle" font-size="10" fill="#ffffff">📚</text>
|
||
<text x="510" y="495" text-anchor="middle" font-size="9" font-weight="700" fill="#e8f4ff" letter-spacing="1">KAVITA</text>
|
||
<text x="510" y="506" text-anchor="middle" font-size="7.5" fill="#a0c0e8">Docker • Library</text>
|
||
</g>
|
||
</g>
|
||
<!-- apps sub: Karakeep -->
|
||
<g class="node-btn" onclick="alert('Karakeep — Bookmark Manager\nURL: links.kitestacks.com\nIncludes: karakeep-chrome (headless browser for screenshots)\nIncludes: karakeep-meilisearch (full-text search)\nCompose: ~/kitestacks-live/docker/karakeep/')">
|
||
<circle cx="600" cy="440" r="22" fill="transparent" stroke="#4a90d9" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="600" y="436" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Karakeep</text>
|
||
<text x="600" y="447" text-anchor="middle" font-size="7.5" fill="#b8d8ff">Bookmarks</text>
|
||
</g>
|
||
<!-- apps sub: OSTicket -->
|
||
<g class="node-btn" onclick="alert('OSTicket — Help Desk\nURL: tasks.kitestacks.com\nSMTP: smtp.gmail.com:587, STARTTLS\nFrom: kitestacks.helpdesk@gmail.com\nIncludes: osticket-db (MariaDB)\nCompose: ~/kitestacks-live/docker/osticket/')">
|
||
<circle cx="608" cy="575" r="25" fill="transparent" stroke="#70b0f0" stroke-width="1.5" opacity="0.9"/>
|
||
<text x="608" y="569" text-anchor="middle" font-size="7.5" fill="#e8f4ff">OSTicket</text>
|
||
<text x="608" y="580" text-anchor="middle" font-size="7.5" fill="#c8e0ff">Help Desk</text>
|
||
<text x="608" y="590" text-anchor="middle" font-size="7" fill="#a0c0e8">tasks.kitestacks.com</text>
|
||
</g>
|
||
<!-- apps sub: Uptime Kuma -->
|
||
<g class="node-btn" onclick="alert('Uptime Kuma — Service Uptime Monitor\nURL: status.kitestacks.com\nMonitors: all homelab subdomains + external ping\nAlerts via: ntfy push notifications\nCompose: ~/kitestacks-live/docker/uptime-kuma/')">
|
||
<circle cx="590" cy="640" r="20" fill="transparent" stroke="#4a80c8" stroke-width="1.2" opacity="0.9"/>
|
||
<text x="590" y="636" text-anchor="middle" font-size="7" fill="#a0c0e8">Uptime</text>
|
||
<text x="590" y="647" text-anchor="middle" font-size="7" fill="#a0c0e8">Kuma</text>
|
||
</g>
|
||
<line x1="608" y1="600" x2="595" y2="620" stroke="#70b0f0" stroke-width="0.8" opacity="0.5" stroke-dasharray="3 4"/>
|
||
|
||
<!-- ================================================================
|
||
kscloud1 NODE — Hetzner VPS cloud replica (bottom-center)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:3.6s;--fdelay:.4s">
|
||
<g class="node-btn" onclick="alert('kscloud1 — Hetzner VPS (Cloud Replica)\nOS: Ubuntu 24.04 LTS\nHardware: CAX11 / 3 vCPU ARM, 3.7GB RAM\nPrivate access: Tailscale overlay\nHosts: cloudflared (same token as monk)\nHosts: authentik-postgresql + authentik-redis (SHARED)\nHosts: bookstack, portal replica, kite-monitor\nActive-active CF Tunnel: Cloudflare load-balances monk + kscloud1\nSSH: ssh -i ~/.ssh/id_ed25519_kscloud1 root@<KSCLOUD1_TAILSCALE_IP>')">
|
||
<circle cx="340" cy="570" r="30" fill="transparent" stroke="#70b0f0" stroke-width="1.8"/>
|
||
<circle cx="340" cy="570" r="20" fill="none" stroke="#70b0f0" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="340" y="562" text-anchor="middle" font-size="9" fill="#ffffff">☁</text>
|
||
<text x="340" y="575" text-anchor="middle" font-size="8" font-weight="700" fill="#e8f4ff" letter-spacing="1">kscloud1</text>
|
||
<text x="340" y="586" text-anchor="middle" font-size="7.5" fill="#a0c0e8">Hetzner • Tailscale</text>
|
||
</g>
|
||
</g>
|
||
<!-- kscloud1 connections -->
|
||
<line class="conn" style="--ld:4s;--ldelay:1s" x1="340" y1="540" x2="340" y2="382" stroke="#70b0f0" stroke-width="1" opacity="0.3"/>
|
||
<!-- kscloud1 → Forgejo (Tailscale overlay) -->
|
||
<line x1="360" y1="558" x2="668" y2="250" stroke="#70b0f0" stroke-width="0.8" opacity="0.2" stroke-dasharray="5 7"/>
|
||
<!-- kscloud1 → Authentik shared DB -->
|
||
<line x1="340" y1="540" x2="275" y2="500" stroke="#70b0f0" stroke-width="0.7" opacity="0.25" stroke-dasharray="4 5"/>
|
||
<!-- kscloud1 → BookStack replica -->
|
||
<line x1="340" y1="548" x2="488" y2="502" stroke="#70b0f0" stroke-width="0.7" opacity="0.25" stroke-dasharray="4 5"/>
|
||
<line x1="330" y1="548" x2="145" y2="402" stroke="#70b0f0" stroke-width="0.7" opacity="0.2" stroke-dasharray="4 5"/>
|
||
|
||
<!-- ================================================================
|
||
CF TUNNEL INDICATOR (top-center, above core)
|
||
================================================================ -->
|
||
<g class="float-n" style="--fd:5s;--fdelay:1.2s">
|
||
<g class="node-btn" onclick="alert('Cloudflare Zero Trust Tunnel — Active-Active\nSame TUNNEL_TOKEN on monk + kscloud1\nCloudflare load-balances across both connectors\nNo ports open on home router or kscloud1 firewall\nAll public subdomains (*.kitestacks.com) route through here\nFailover: if monk goes down, kscloud1 takes all traffic within seconds\nCompose: ~/kitestacks-live/docker/cloudflared/')">
|
||
<circle cx="340" cy="155" r="28" fill="transparent" stroke="#38a8f8" stroke-width="1.8"/>
|
||
<circle cx="340" cy="155" r="19" fill="none" stroke="#38a8f8" stroke-width="0.6" opacity="0.5"/>
|
||
<text x="340" y="148" text-anchor="middle" font-size="9" fill="#ffffff">☁</text>
|
||
<text x="340" y="160" text-anchor="middle" font-size="8" font-weight="700" fill="#e8f4ff" letter-spacing="1">CF TUNNEL</text>
|
||
<text x="340" y="170" text-anchor="middle" font-size="7" fill="#80c8f8">active-active</text>
|
||
</g>
|
||
</g>
|
||
<line class="conn" style="--ld:2s;--ldelay:.2s" x1="340" y1="183" x2="340" y2="268" stroke="#38a8f8" stroke-width="1.5" opacity="0.5"/>
|
||
|
||
<!-- ================================================================
|
||
LEGEND — full width, two columns
|
||
================================================================ -->
|
||
<rect x="20" y="788" width="860" height="38" rx="6" fill="transparent" stroke="#1e3460" stroke-width="0.8" opacity="0.7"/>
|
||
<text x="36" y="803" font-size="8" font-weight="700" fill="#4a5888" letter-spacing="2">LEGEND</text>
|
||
|
||
<!-- col 1 -->
|
||
<line x1="90" y1="800" x2="108" y2="800" stroke="#4a90d9" stroke-width="1.5"/>
|
||
<text class="legend-item" x="113" y="804">CF Tunnel / Portainer connection</text>
|
||
|
||
<line x1="90" y1="816" x2="108" y2="816" stroke="#4a90d9" stroke-width="1.5" stroke-dasharray="8 5"/>
|
||
<text class="legend-item" x="113" y="820">Tailscale flow (monk ↔ kscloud1)</text>
|
||
|
||
<!-- col 2 -->
|
||
<line x1="280" y1="800" x2="298" y2="800" stroke="#70b0f0" stroke-width="1.2" stroke-dasharray="5 5"/>
|
||
<text class="legend-item" x="303" y="804">Tailscale overlay (private)</text>
|
||
|
||
<circle cx="288" cy="816" r="6" fill="transparent" stroke="#70b0f0" stroke-width="1.2"/>
|
||
<text class="legend-item" x="303" y="820">Cloud replica / kscloud1</text>
|
||
|
||
<!-- col 3 -->
|
||
<circle cx="458" cy="800" r="7" fill="transparent" stroke="#4a90d9" stroke-width="1.2"/>
|
||
<text class="legend-item" x="471" y="804">Management / infra node</text>
|
||
|
||
<circle cx="458" cy="816" r="7" fill="transparent" stroke="#60b8f0" stroke-width="1.2"/>
|
||
<text class="legend-item" x="471" y="820">Docker app (Compose)</text>
|
||
|
||
<!-- col 4 -->
|
||
<circle cx="598" cy="800" r="7" fill="transparent" stroke="#50a8f0" stroke-width="1.2"/>
|
||
<text class="legend-item" x="611" y="804">Application service</text>
|
||
|
||
<circle cx="598" cy="816" r="7" fill="transparent" stroke="#5a9ee8" stroke-width="1.2"/>
|
||
<text class="legend-item" x="611" y="820">Forgejo Git server</text>
|
||
|
||
<!-- click hint -->
|
||
<text x="450" y="780" text-anchor="middle" font-size="8.5" fill="#2a3460" letter-spacing="1">click any node for details</text>
|
||
</svg>
|
||
|
||
<!-- ── Modal ── -->
|
||
<div id="kite-modal-overlay">
|
||
<div id="kite-modal">
|
||
<button id="kite-modal-close" title="Close">✕</button>
|
||
<div id="kite-modal-title"></div>
|
||
<div id="kite-modal-body"></div>
|
||
<div id="kite-modal-paths"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ── Star twinkle ── */
|
||
document.querySelectorAll('.star-dot').forEach(s => {
|
||
s.style.setProperty('--d', (Math.random()*3+0.8).toFixed(2)+'s');
|
||
s.style.setProperty('--delay', (Math.random()*4).toFixed(2)+'s');
|
||
});
|
||
|
||
/* ── Path detection ── */
|
||
const PATH_RE = /(?:^|\s)((?:~|\.\.?)?(?:\/[\w.\-+@]+)+(?:\/[\w.\-+@*]*)*|apps\/[\w.\-/]+|\/var\/run\/docker\.sock)/gm;
|
||
|
||
function extractPaths(text) {
|
||
const found = new Set();
|
||
let m;
|
||
while ((m = PATH_RE.exec(text)) !== null) {
|
||
found.add(m[1].trim());
|
||
}
|
||
const relRe = /\b(apps\/[^\s,\n•]+)/g;
|
||
while ((m = relRe.exec(text)) !== null) {
|
||
found.add(m[1].trim());
|
||
}
|
||
return [...found];
|
||
}
|
||
|
||
function buildFullPath(p) { return p; }
|
||
|
||
/* ── Modal logic ── */
|
||
const overlay = document.getElementById('kite-modal-overlay');
|
||
const mTitle = document.getElementById('kite-modal-title');
|
||
const mBody = document.getElementById('kite-modal-body');
|
||
const mPaths = document.getElementById('kite-modal-paths');
|
||
const mClose = document.getElementById('kite-modal-close');
|
||
|
||
function showModal(raw) {
|
||
const lines = raw.split('\n');
|
||
const title = lines[0].split('—')[0].split('–')[0].trim();
|
||
const body = lines.join('\n');
|
||
|
||
mTitle.textContent = title;
|
||
mBody.textContent = body;
|
||
mPaths.innerHTML = '';
|
||
|
||
const paths = extractPaths(raw);
|
||
if (paths.length) {
|
||
const section = document.createElement('div');
|
||
section.className = 'kite-path-section';
|
||
const lbl = document.createElement('div');
|
||
lbl.className = 'kite-path-label';
|
||
lbl.textContent = '📁 File Paths';
|
||
section.appendChild(lbl);
|
||
|
||
paths.forEach(p => {
|
||
const full = buildFullPath(p);
|
||
const row = document.createElement('div');
|
||
row.className = 'kite-path-row';
|
||
|
||
const span = document.createElement('div');
|
||
span.className = 'kite-path-text';
|
||
span.textContent = full;
|
||
span.title = 'Click to select';
|
||
|
||
const btn = document.createElement('button');
|
||
btn.className = 'kite-copy-btn';
|
||
btn.textContent = 'Copy';
|
||
btn.onclick = () => {
|
||
navigator.clipboard.writeText(full).then(() => {
|
||
btn.textContent = '✓ Copied';
|
||
btn.classList.add('copied');
|
||
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
|
||
});
|
||
};
|
||
|
||
row.appendChild(span);
|
||
row.appendChild(btn);
|
||
section.appendChild(row);
|
||
});
|
||
|
||
mPaths.appendChild(section);
|
||
}
|
||
|
||
overlay.classList.add('open');
|
||
}
|
||
|
||
mClose.addEventListener('click', () => overlay.classList.remove('open'));
|
||
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.classList.remove('open'); });
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') overlay.classList.remove('open'); });
|
||
|
||
/* ── Replace all alert() calls on node-btn elements ── */
|
||
document.querySelectorAll('.node-btn').forEach(el => {
|
||
const oc = el.getAttribute('onclick') || '';
|
||
const m = oc.match(/alert\('([\s\S]*?)'\)/);
|
||
if (m) {
|
||
const text = m[1].replace(/\\n/g, '\n');
|
||
el.removeAttribute('onclick');
|
||
el.addEventListener('click', (e) => { e.stopPropagation(); showModal(text); });
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|