Files
homelab-infra/apps/dashboard/dashboard.html
T

1203 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KalliLab Control Panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;500;600;700&family=Orbitron:wght@500;600;700;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
:root {
--bg-0: #040806;
--bg-1: #060b09;
--bg-2: #08120f;
--bg-3: #0d1c18;
--panel: rgba(7, 18, 15, 0.68);
--panel-strong: rgba(8, 20, 17, 0.82);
--panel-soft: rgba(10, 26, 21, 0.54);
--panel-border: rgba(0, 220, 140, 0.18);
--panel-border-strong: rgba(0, 220, 140, 0.3);
--text: #c6e0d8;
--text-dim: #7da397;
--text-soft: #58766b;
--white: #e9fff7;
--teal: #00dc8c;
--teal-strong: #00ffaa;
--teal-deep: #009c66;
--green: #67ffb3;
--red: #ff5a73;
--yellow: #ffca57;
--blue: #55b8ff;
--blue-soft: #85d5ff;
--glow: 0 0 20px rgba(0, 220, 140, 0.18);
--glow-strong: 0 0 32px rgba(0, 220, 140, 0.28);
--radius-lg: 20px;
--radius-md: 14px;
--radius-sm: 10px;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.42);
--font-display: "Orbitron", monospace;
--font-mono: "Share Tech Mono", monospace;
--font-body: "Exo 2", sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
color-scheme: dark;
font-size: 14px;
}
body {
min-height: 100vh;
font-family: var(--font-body);
color: var(--text);
background:
radial-gradient(circle at top left, rgba(0, 255, 170, 0.12), transparent 28%),
radial-gradient(circle at 85% 18%, rgba(0, 220, 140, 0.12), transparent 24%),
radial-gradient(circle at 50% 110%, rgba(0, 135, 100, 0.18), transparent 35%),
linear-gradient(160deg, rgba(9, 30, 24, 0.86), rgba(3, 8, 6, 0.96)),
linear-gradient(180deg, var(--bg-2), var(--bg-0));
overflow-x: hidden;
position: relative;
}
body::before {
content: "";
position: fixed;
inset: 0;
background:
linear-gradient(rgba(0, 220, 140, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 220, 140, 0.03) 1px, transparent 1px);
background-size: 46px 46px;
mask-image: radial-gradient(circle at center, black 45%, transparent 100%);
pointer-events: none;
z-index: 0;
}
body::after {
content: "";
position: fixed;
inset: 0;
background:
radial-gradient(circle at 30% 20%, rgba(0, 255, 170, 0.08), transparent 18%),
radial-gradient(circle at 80% 28%, rgba(0, 255, 170, 0.05), transparent 24%),
linear-gradient(transparent, rgba(0, 0, 0, 0.18));
pointer-events: none;
z-index: 0;
}
.hud-noise,
.hud-vignette {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
}
.hud-noise {
opacity: 0.08;
background-image:
radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.2) 1px, transparent 0);
background-size: 16px 16px;
mix-blend-mode: soft-light;
}
.hud-vignette {
box-shadow: inset 0 0 180px rgba(0, 0, 0, 0.7);
}
.app-shell {
position: relative;
z-index: 1;
max-width: 1800px;
margin: 0 auto;
padding: 16px 16px 20px;
}
.top-frame {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(340px, 0.78fr);
gap: 12px;
margin-bottom: 12px;
}
.panel {
position: relative;
background: linear-gradient(180deg, rgba(10, 23, 20, 0.78), rgba(6, 14, 12, 0.66));
border: 1px solid var(--panel-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.03);
backdrop-filter: blur(18px) saturate(1.22);
-webkit-backdrop-filter: blur(18px) saturate(1.22);
overflow: hidden;
}
.panel::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(130deg, rgba(255, 255, 255, 0.045), transparent 24%),
radial-gradient(circle at top right, rgba(0, 255, 170, 0.08), transparent 22%);
pointer-events: none;
}
.panel-inner {
position: relative;
z-index: 1;
padding: 18px 20px;
}
.hero {
min-height: 156px;
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(380px, 0.95fr);
gap: 12px;
align-items: stretch;
}
.eyebrow,
.section-label,
.card-label {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--font-display);
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--teal);
}
.eyebrow::before,
.section-label::before,
.card-label::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--teal-strong);
box-shadow: 0 0 14px rgba(0, 255, 170, 0.7);
}
.hero h1 {
margin-top: 12px;
font-family: var(--font-display);
font-size: clamp(2rem, 3.2vw, 3rem);
line-height: 0.92;
letter-spacing: 0.06em;
color: var(--white);
text-shadow: 0 0 26px rgba(0, 255, 170, 0.12);
}
.hero-copy {
margin-top: 12px;
max-width: 66ch;
color: var(--text-dim);
font-size: 0.92rem;
line-height: 1.42;
}
.hero-meta {
margin-top: 18px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.meta-chip {
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(0, 220, 140, 0.2);
background: rgba(5, 17, 13, 0.54);
color: var(--text);
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.05em;
}
.status-stack {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
align-content: start;
}
.status-tile {
padding: 14px 15px;
border-radius: 14px;
background: linear-gradient(180deg, rgba(8, 20, 17, 0.84), rgba(5, 14, 11, 0.8));
border: 1px solid rgba(0, 220, 140, 0.18);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), var(--glow);
}
.status-tile strong {
display: block;
margin-top: 8px;
font-family: var(--font-display);
font-size: 1.16rem;
color: var(--white);
letter-spacing: 0.06em;
}
.status-tile span {
display: block;
margin-top: 5px;
color: var(--text-dim);
font-size: 0.82rem;
}
.clock-panel {
min-height: 156px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.clock-readout {
display: flex;
flex-direction: column;
gap: 5px;
}
.clock-time {
font-family: var(--font-display);
font-size: clamp(2.15rem, 3.2vw, 3.2rem);
font-weight: 800;
line-height: 0.95;
letter-spacing: 0.16em;
color: var(--teal-strong);
text-shadow: 0 0 26px rgba(0, 255, 170, 0.28);
}
.clock-date,
.clock-meta {
font-family: var(--font-mono);
color: var(--text-dim);
font-size: 0.78rem;
letter-spacing: 0.08em;
}
.clock-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-top: 12px;
}
.mini-cell {
border-radius: 12px;
padding: 10px 12px;
background: rgba(5, 16, 13, 0.7);
border: 1px solid rgba(0, 220, 140, 0.12);
}
.mini-cell dt {
font-family: var(--font-display);
font-size: 0.6rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--text-soft);
}
.mini-cell dd {
margin-top: 5px;
font-family: var(--font-mono);
font-size: 0.88rem;
color: var(--white);
}
.section {
margin-top: 13px;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-bottom: 8px;
padding: 0 4px;
}
.section-head p {
color: var(--text-soft);
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.stats-grid,
.storage-grid,
.service-grid,
.quick-grid {
display: grid;
gap: 12px;
}
.stats-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.storage-grid {
grid-template-columns: minmax(340px, 0.8fr) minmax(0, 1.2fr);
}
.service-grid {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.quick-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.card {
position: relative;
min-height: 148px;
background: linear-gradient(180deg, rgba(8, 19, 16, 0.8), rgba(5, 12, 10, 0.76));
border: 1px solid rgba(0, 220, 140, 0.16);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.34);
backdrop-filter: blur(16px) saturate(1.18);
-webkit-backdrop-filter: blur(16px) saturate(1.18);
}
.card::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.05), transparent 24%),
radial-gradient(circle at 100% 0, rgba(0, 255, 170, 0.08), transparent 28%);
pointer-events: none;
}
.card:hover {
border-color: var(--panel-border-strong);
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.4), var(--glow-strong);
}
.card-inner {
position: relative;
z-index: 1;
height: 100%;
padding: 15px 16px 16px;
display: flex;
flex-direction: column;
}
.card-head {
display: flex;
align-items: start;
justify-content: space-between;
gap: 14px;
}
.signal {
width: 10px;
height: 10px;
border-radius: 50%;
box-shadow: 0 0 16px currentColor;
flex: 0 0 auto;
margin-top: 4px;
}
.signal.online { color: var(--teal-strong); background: var(--teal-strong); }
.signal.warning { color: var(--yellow); background: var(--yellow); }
.signal.offline { color: var(--red); background: var(--red); }
.signal.info { color: var(--blue-soft); background: var(--blue-soft); }
.card-title {
margin-top: 11px;
font-family: var(--font-display);
font-size: 1.04rem;
line-height: 1.1;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--white);
}
.card-value {
margin-top: auto;
font-family: var(--font-display);
font-size: clamp(1.9rem, 2.55vw, 2.55rem);
line-height: 0.95;
letter-spacing: 0.06em;
color: var(--teal-strong);
text-shadow: 0 0 22px rgba(0, 255, 170, 0.18);
}
.value-online {
color: var(--green);
text-shadow: 0 0 22px rgba(103, 255, 179, 0.18);
}
.value-warning {
color: var(--yellow);
text-shadow: 0 0 22px rgba(255, 202, 87, 0.16);
}
.value-danger {
color: var(--red);
text-shadow: 0 0 22px rgba(255, 90, 115, 0.16);
}
.value-info {
color: var(--blue-soft);
text-shadow: 0 0 22px rgba(85, 184, 255, 0.16);
}
.card-subvalue {
margin-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 0.76rem;
letter-spacing: 0.05em;
}
.metric-accent {
color: var(--white);
}
.metric-accent.online {
color: var(--green);
}
.metric-accent.warning {
color: var(--yellow);
}
.metric-accent.danger {
color: var(--red);
}
.metric-accent.info {
color: var(--blue-soft);
}
.metric-bar {
margin-top: 14px;
height: 7px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}
.metric-bar > span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--teal-deep), var(--teal-strong));
box-shadow: 0 0 18px rgba(0, 255, 170, 0.28);
}
.metric-bar.warning > span {
background: linear-gradient(90deg, #b88d2a, var(--yellow));
box-shadow: 0 0 18px rgba(255, 202, 87, 0.25);
}
.metric-bar.danger > span {
background: linear-gradient(90deg, #b93d58, var(--red));
box-shadow: 0 0 18px rgba(255, 90, 115, 0.24);
}
.metric-bar.info > span {
background: linear-gradient(90deg, #2679c6, var(--blue-soft));
box-shadow: 0 0 18px rgba(85, 184, 255, 0.22);
}
.root-card .card-value {
font-size: clamp(2.2rem, 2.75vw, 2.95rem);
}
.storage-list {
margin-top: 12px;
display: grid;
gap: 9px;
}
.storage-row {
display: grid;
grid-template-columns: minmax(110px, 140px) minmax(70px, 90px) 1fr minmax(70px, 80px);
gap: 12px;
align-items: center;
padding: 9px 11px;
border-radius: 12px;
background: rgba(5, 16, 13, 0.6);
border: 1px solid rgba(0, 220, 140, 0.11);
font-family: var(--font-mono);
}
.storage-row strong {
color: var(--white);
font-size: 0.84rem;
letter-spacing: 0.05em;
}
.storage-row span {
color: var(--text-dim);
font-size: 0.74rem;
}
.storage-row .status-pill {
justify-self: end;
}
.service-card {
min-height: 170px;
}
.service-card.primary {
grid-column: span 2;
}
.service-card.secondary {
grid-column: span 2;
}
.service-card.standard {
grid-column: span 2;
}
.service-card .card-copy {
margin-top: 10px;
color: var(--text-dim);
line-height: 1.42;
font-size: 0.82rem;
}
.service-meta {
margin-top: auto;
display: grid;
gap: 7px;
padding-top: 12px;
}
.service-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--text-dim);
}
.service-meta-row strong {
color: var(--white);
font-weight: 400;
}
.service-meta-row strong.online {
color: var(--green);
}
.service-meta-row strong.warning {
color: var(--yellow);
}
.service-meta-row strong.offline {
color: var(--red);
}
.service-meta-row strong.info {
color: var(--blue-soft);
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 8px;
border-radius: 999px;
background: rgba(7, 18, 15, 0.78);
border: 1px solid rgba(0, 220, 140, 0.14);
font-family: var(--font-display);
font-size: 0.58rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--text);
}
.status-pill::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 14px currentColor;
}
.status-pill.online {
color: var(--teal-strong);
}
.status-pill.warning {
color: var(--yellow);
}
.status-pill.offline {
color: var(--red);
}
.status-pill.info {
color: var(--blue-soft);
}
.quick-card {
min-height: 98px;
}
.quick-card a {
margin-top: auto;
text-decoration: none;
color: inherit;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 10px;
border-radius: 12px;
border: 1px solid rgba(0, 220, 140, 0.12);
background: rgba(4, 12, 10, 0.55);
font-family: var(--font-mono);
font-size: 0.74rem;
letter-spacing: 0.05em;
}
.quick-card .card-title {
font-size: 0.94rem;
margin-top: 9px;
}
.quick-card a span:last-child {
color: var(--teal-strong);
}
.footer-note {
margin-top: 12px;
padding: 0 4px;
color: var(--text-soft);
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.api-error {
color: var(--yellow);
}
@media (max-width: 1220px) {
.top-frame,
.hero-grid,
.storage-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.quick-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.status-stack {
grid-template-columns: 1fr;
}
.clock-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.service-card.primary,
.service-card.secondary,
.service-card.standard {
grid-column: span 2;
}
}
@media (max-width: 860px) {
.app-shell {
padding: 16px 14px 26px;
}
.stats-grid,
.quick-grid {
grid-template-columns: 1fr;
}
.service-grid {
grid-template-columns: 1fr;
}
.service-card.primary,
.service-card.secondary,
.service-card.standard {
grid-column: span 1;
}
.storage-row {
grid-template-columns: 1fr;
}
.clock-grid {
grid-template-columns: 1fr 1fr;
}
.status-stack {
grid-template-columns: 1fr;
}
}
.net-health-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.net-health-grid .card {
min-width: 0;
}
.adguard-stats,
.scrutiny-stats {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.adguard-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.82rem;
color: var(--text-dim);
}
@media (max-width: 700px) {
.net-health-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="hud-noise"></div>
<div class="hud-vignette"></div>
<main class="app-shell">
<section class="top-frame">
<article class="panel hero">
<div class="panel-inner hero-grid">
<div>
<span class="eyebrow">Homelab Command Surface</span>
<h1>KALLILAB<br>CONTROL PANEL</h1>
<p class="hero-copy" id="hero-copy">
Eigenstaendiges MVP-v1 Frontend-Mockup mit voller visueller Praesenz:
atmosphaerischer Dark-Tech-Look, neon teal Highlights, technische Typografie,
Glass Panels und dichte Control-Surface-Stimmung.
</p>
<div class="hero-meta">
<span class="meta-chip">MVP-V1 SCOPE LOCKED</span>
<span class="meta-chip">FASTAPI AGGREGATOR READY</span>
<span class="meta-chip">VANILLA JS FRONTEND</span>
</div>
</div>
<div class="status-stack">
<div class="status-tile" id="overall-status-tile">
<span class="card-label">Overall Cluster State</span>
<strong id="overall-status-label">NOMINAL</strong>
<span id="overall-status-summary">8 Services online, 2 degraded, 1 offline</span>
</div>
<div class="status-tile" id="home-assistant-tile">
<span class="card-label">Home Assistant</span>
<strong id="ha-status-label">ONLINE</strong>
<span id="ha-status-summary">Basisstatus aktiv, API-Mapping fuer MVP vorbereitet</span>
</div>
</div>
</div>
</article>
<aside class="panel clock-panel">
<div class="panel-inner">
<span class="eyebrow">Local Time Sync</span>
<div class="clock-readout">
<div class="clock-time" id="clock-time">00:00</div>
<div class="clock-date" id="clock-date">SAMSTAG / 04 APR 2026</div>
<div class="clock-meta" id="last-refresh">LAST RENDER 12:00:00 CET</div>
</div>
<dl class="clock-grid">
<div class="mini-cell">
<dt>Mode</dt>
<dd>CONTROL</dd>
</div>
<div class="mini-cell">
<dt>Refresh</dt>
<dd>15 SEC</dd>
</div>
<div class="mini-cell">
<dt>Theme</dt>
<dd>ATMOS DARK</dd>
</div>
<div class="mini-cell">
<dt>Profile</dt>
<dd>MVP-V1</dd>
</div>
</dl>
</div>
</aside>
</section>
<section class="section">
<div class="section-head">
<span class="section-label">System Stats</span>
<p>Beszel-backed host telemetry</p>
</div>
<div class="stats-grid">
<article class="card" id="cpu-card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">CPU</span>
<span class="signal online" id="cpu-signal"></span>
</div>
<h2 class="card-title">Compute Load</h2>
<div class="card-value value-online" id="cpu-value">23%</div>
<div class="card-subvalue" id="cpu-subvalue">
<span><span class="metric-accent online" id="cpu-cores">8 CORES</span> ACTIVE</span>
<span><span class="metric-accent info">L1</span> <span id="cpu-load">0.82</span></span>
</div>
<div class="metric-bar" id="cpu-bar"><span id="cpu-bar-fill" style="width:23%"></span></div>
</div>
</article>
<article class="card" id="ram-card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">RAM</span>
<span class="signal warning" id="ram-signal"></span>
</div>
<h2 class="card-title">Memory Pressure</h2>
<div class="card-value value-warning" id="ram-value">61%</div>
<div class="card-subvalue" id="ram-subvalue">
<span><span class="metric-accent warning" id="ram-used">19.6 GB</span> USED</span>
<span><span class="metric-accent info" id="ram-free">12.4 GB</span> FREE</span>
</div>
<div class="metric-bar warning" id="ram-bar"><span id="ram-bar-fill" style="width:61%"></span></div>
</div>
</article>
<article class="card" id="network-card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Network</span>
<span class="signal info" id="network-signal"></span>
</div>
<h2 class="card-title">Traffic Flow</h2>
<div class="card-value value-info" id="network-value">12.4</div>
<div class="card-subvalue" id="network-subvalue">
<span><span class="metric-accent info">RX</span> MBPS</span>
<span><span class="metric-accent online">TX</span> <span id="network-tx">3.1 MBPS</span></span>
</div>
<div class="metric-bar info" id="network-bar"><span id="network-bar-fill" style="width:48%"></span></div>
</div>
</article>
<article class="card" id="uptime-card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Uptime</span>
<span class="signal online" id="uptime-signal"></span>
</div>
<h2 class="card-title">Host Runtime</h2>
<div class="card-value value-online" id="uptime-value">10D</div>
<div class="card-subvalue" id="uptime-subvalue">
<span><span class="metric-accent info">NODE</span> <span id="uptime-host">HOMELAB-01</span></span>
<span><span class="metric-accent online" id="uptime-hours">240</span> HRS</span>
</div>
<div class="metric-bar" id="uptime-bar"><span id="uptime-bar-fill" style="width:84%"></span></div>
</div>
</article>
</div>
</section>
<section class="section">
<div class="section-head">
<span class="section-label">Storage</span>
<p>Root capacity plus disk overview</p>
</div>
<div class="storage-grid">
<article class="card root-card" id="root-storage-card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Root Storage</span>
<span class="status-pill online" id="root-storage-pill">Stable</span>
</div>
<h2 class="card-title">Primary Volume</h2>
<div class="card-value value-online" id="root-storage-value">48.7%</div>
<div class="card-subvalue" id="root-storage-subvalue">
<span><span class="metric-accent warning" id="root-storage-used">233.8 GB</span> USED</span>
<span><span class="metric-accent info" id="root-storage-free">246.2 GB</span> FREE</span>
</div>
<div class="metric-bar" id="root-storage-bar"><span id="root-storage-bar-fill" style="width:49%"></span></div>
</div>
</article>
<article class="card" id="disk-matrix-card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Disk Matrix</span>
<span class="signal warning" id="disk-matrix-signal"></span>
</div>
<h2 class="card-title">Mounted Volumes</h2>
<div class="storage-list" id="storage-list">
<div class="storage-row">
<strong>rootfs</strong>
<span>/</span>
<div class="metric-bar"><span style="width:49%"></span></div>
<span class="status-pill online">49%</span>
</div>
<div class="storage-row">
<strong>data</strong>
<span>/data</span>
<div class="metric-bar warning"><span style="width:71%"></span></div>
<span class="status-pill warning">71%</span>
</div>
<div class="storage-row">
<strong>backup</strong>
<span>/backup</span>
<div class="metric-bar info"><span style="width:10%"></span></div>
<span class="status-pill online">10%</span>
</div>
</div>
</div>
</article>
</div>
</section>
<section class="section">
<div class="section-head">
<span class="section-label">Service Grid</span>
<p>Core runtime and monitored services</p>
</div>
<div class="service-grid" id="service-grid">
<article class="card service-card primary">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Docker Summary</span>
<span class="status-pill online" id="docker-summary-pill">Online</span>
</div>
<h2 class="card-title">Container Runtime</h2>
<p class="card-copy">Secure proxy-backed Docker state for the MVP service layer.</p>
<div class="service-meta">
<div class="service-meta-row"><span>Running</span><strong class="online" id="docker-running">18</strong></div>
<div class="service-meta-row"><span>Stopped</span><strong class="offline" id="docker-stopped">2</strong></div>
<div class="service-meta-row"><span>Unhealthy</span><strong class="warning" id="docker-unhealthy">1</strong></div>
</div>
</div>
</article>
<article class="card service-card primary">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Uptime Kuma</span>
<span class="status-pill online" id="kuma-summary-pill">Synced</span>
</div>
<h2 class="card-title">Monitor Surface</h2>
<p class="card-copy">Availability layer for service health, latency and incident signals.</p>
<div class="service-meta">
<div class="service-meta-row"><span>Monitors Up</span><strong class="online" id="kuma-up">8</strong></div>
<div class="service-meta-row"><span>Monitors Down</span><strong class="offline" id="kuma-down">1</strong></div>
<div class="service-meta-row"><span>Paused</span><strong class="warning" id="kuma-paused">1</strong></div>
</div>
</div>
</article>
<article class="card service-card primary">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Home Assistant</span>
<span class="status-pill online" id="service-ha-pill">Reachable</span>
</div>
<h2 class="card-title">Core Automation Hub</h2>
<p class="card-copy">MVP scope limited to online and basis status, no entity rendering yet.</p>
<div class="service-meta">
<div class="service-meta-row"><span>Version</span><strong class="info" id="service-ha-version">2026.3.4</strong></div>
<div class="service-meta-row"><span>Latency</span><strong class="online" id="service-ha-latency">142 MS</strong></div>
<div class="service-meta-row"><span>Last Check</span><strong id="service-ha-last-check">11:59:58</strong></div>
</div>
</div>
</article>
<article class="card service-card secondary" id="service-card-fallback-1">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Immich</span>
<span class="status-pill warning">Degraded</span>
</div>
<h2 class="card-title">Photo Pipeline</h2>
<p class="card-copy">Container active, response times elevated beyond nominal threshold.</p>
<div class="service-meta">
<div class="service-meta-row"><span>Latency</span><strong class="warning">821 MS</strong></div>
<div class="service-meta-row"><span>Docker</span><strong class="online">RUNNING</strong></div>
</div>
</div>
</article>
<article class="card service-card secondary" id="service-card-fallback-2">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Gitea</span>
<span class="status-pill online">Healthy</span>
</div>
<h2 class="card-title">Git Platform</h2>
<p class="card-copy">Service healthy across monitor and container state layers.</p>
<div class="service-meta">
<div class="service-meta-row"><span>Latency</span><strong class="online">98 MS</strong></div>
<div class="service-meta-row"><span>Docker</span><strong class="online">RUNNING</strong></div>
</div>
</div>
</article>
<article class="card service-card secondary" id="service-card-fallback-3">
<div class="card-inner">
<div class="card-head">
<span class="card-label">AdGuard</span>
<span class="status-pill offline">Offline</span>
</div>
<h2 class="card-title">DNS Layer</h2>
<p class="card-copy">Placeholder service card within MVP grid and future optional adapter set.</p>
<div class="service-meta">
<div class="service-meta-row"><span>Latency</span><strong class="offline">N/A</strong></div>
<div class="service-meta-row"><span>Docker</span><strong class="offline">STOPPED</strong></div>
</div>
</div>
</article>
</div>
</section>
<section class="section">
<div class="section-head">
<span class="section-label">Network &amp; Health</span>
<p>DNS filtering · Disk integrity</p>
</div>
<div class="net-health-grid">
<!-- AdGuard DNS -->
<article class="card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">AdGuard Home</span>
<span class="status-pill offline" id="adguard-pill">Offline</span>
</div>
<h2 class="card-title">DNS Filtering</h2>
<div class="adguard-stats">
<div class="adguard-row">
<span>Total Queries</span>
<span class="metric-accent online" id="adguard-total">0</span>
</div>
<div class="adguard-row">
<span>Blocked</span>
<span class="metric-accent online" id="adguard-blocked">0</span>
</div>
<div class="adguard-row">
<span>Block Rate</span>
<span class="metric-accent online" id="adguard-blocked-pct">0%</span>
</div>
<div class="metric-bar">
<span id="adguard-bar-fill" style="width:0%"></span>
</div>
<div class="adguard-row">
<span>Avg Latency</span>
<span class="metric-accent online" id="adguard-latency">0 MS</span>
</div>
</div>
</div>
</article>
<!-- Scrutiny SMART -->
<article class="card">
<div class="card-inner">
<div class="card-head">
<span class="card-label">Scrutiny</span>
<span class="status-pill offline" id="scrutiny-pill">Offline</span>
</div>
<h2 class="card-title">Disk Health</h2>
<div class="scrutiny-stats" id="scrutiny-list">
<div class="storage-row"><strong style="color:var(--text-dim)">No data</strong><span></span><span></span><span></span></div>
</div>
</div>
</article>
</div>
</section>
<section class="section">
<div class="section-head">
<span class="section-label">Quick Access</span>
<p>Supplemental launch surface only</p>
</div>
<div class="quick-grid" id="quick-access-grid">
<article class="card quick-card">
<div class="card-inner">
<span class="card-label">Core</span>
<h2 class="card-title">Home Assistant</h2>
<a href="#"><span>OPEN CONTROL HUB</span><span>&gt;</span></a>
</div>
</article>
<article class="card quick-card">
<div class="card-inner">
<span class="card-label">Ops</span>
<h2 class="card-title">Uptime Kuma</h2>
<a href="#"><span>OPEN MONITORS</span><span>&gt;</span></a>
</div>
</article>
<article class="card quick-card">
<div class="card-inner">
<span class="card-label">Containers</span>
<h2 class="card-title">Portainer</h2>
<a href="#"><span>OPEN RUNTIME</span><span>&gt;</span></a>
</div>
</article>
<article class="card quick-card">
<div class="card-inner">
<span class="card-label">Media</span>
<h2 class="card-title">Immich</h2>
<a href="#"><span>OPEN GALLERY</span><span>&gt;</span></a>
</div>
</article>
</div>
</section>
<div class="footer-note" id="footer-note">Live dashboard connected to aggregator API.</div>
</main>
<script type="module" src="./assets/js/app.js"></script>
</body>
</html>