This repository has been archived on 2026-06-19. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
homelab-mastery/architecture/mindmap.html

639 lines
38 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"/>
<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>