876 lines
34 KiB
HTML
876 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Zero-Scale Platform Dashboard</title>
|
|
<meta name="description" content="Real-time operational telemetry and infrastructure monitoring for the Zero-Scale Platform on Kubernetes.">
|
|
<meta name="robots" content="index, follow">
|
|
<link rel="canonical" href="https://kstat.fairfaxmedia.net/">
|
|
|
|
<!-- Open Graph Metadata -->
|
|
<meta property="og:title" content="Zero-Scale Platform Dashboard">
|
|
<meta property="og:description" content="Real-time operational telemetry and infrastructure monitoring for the Zero-Scale Platform on Kubernetes.">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://kstat.fairfaxmedia.net/">
|
|
<meta property="og:site_name" content="Zero-Scale Platform">
|
|
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>">
|
|
<style>
|
|
:root {
|
|
--bg: #090d16;
|
|
--card: #111827;
|
|
--card-hover: #172238;
|
|
--border: #1f293d;
|
|
--text: #f1f5f9;
|
|
--text-muted: #94a3b8;
|
|
--blue: #38bdf8;
|
|
--blue-glow: rgba(56, 189, 248, 0.15);
|
|
--green: #22c55e;
|
|
--green-glow: rgba(34, 197, 94, 0.15);
|
|
--amber: #f59e0b;
|
|
--amber-glow: rgba(245, 158, 11, 0.15);
|
|
--purple: #a855f7;
|
|
--red: #ef4444;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
background: radial-gradient(circle at 50% 0%, #172554 0%, var(--bg) 40%);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
min-height: 100vh;
|
|
padding: 24px;
|
|
line-height: 1.5;
|
|
}
|
|
.container { max-width: 1400px; margin: 0 auto; }
|
|
|
|
/* Header */
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-bottom: 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 24px;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
.brand { display: flex; align-items: center; gap: 14px; }
|
|
.brand-icon { font-size: 32px; filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.4)); }
|
|
.brand h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
|
|
.brand p { font-size: 13px; color: var(--text-muted); }
|
|
.header-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
border: 1px solid var(--border);
|
|
background: var(--card);
|
|
color: var(--text);
|
|
}
|
|
.btn:hover { background: var(--card-hover); border-color: var(--blue); color: #fff; }
|
|
.btn-git {
|
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
|
border-color: #f97316;
|
|
color: #fb923c;
|
|
box-shadow: 0 0 12px rgba(249, 115, 22, 0.2);
|
|
}
|
|
.btn-git:hover {
|
|
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|
color: #fff;
|
|
border-color: #ea580c;
|
|
box-shadow: 0 0 16px rgba(249, 115, 22, 0.5);
|
|
}
|
|
.btn-primary { background: #0284c7; border-color: #38bdf8; color: #fff; }
|
|
.btn-primary:hover { background: #0369a1; }
|
|
|
|
/* Status Indicator */
|
|
.live-indicator {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
color: var(--green);
|
|
background: var(--green-glow);
|
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
padding: 6px 12px;
|
|
border-radius: 20px;
|
|
}
|
|
.pulse-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--green);
|
|
box-shadow: 0 0 8px var(--green);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
@keyframes pulse { 0% { opacity: 0.4; } 50% { opacity: 1; } 100% { opacity: 0.4; } }
|
|
|
|
/* Summary Metrics Cards */
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.metric-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 18px 20px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.metric-card::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0; height: 2px;
|
|
background: linear-gradient(90deg, transparent, var(--accent, var(--blue)), transparent);
|
|
}
|
|
.metric-title { font-size: 13px; color: var(--text-muted); text-transform: uppercase; font-weight: 600; letter-spacing: 0.05em; }
|
|
.metric-value { font-size: 28px; font-weight: 700; margin: 6px 0; color: #fff; }
|
|
.metric-sub { font-size: 12px; color: var(--text-muted); display: flex; justify-content: space-between; align-items: center; }
|
|
|
|
/* Progress Bar */
|
|
.progress-bg {
|
|
width: 100%;
|
|
height: 6px;
|
|
background: rgba(255,255,255,0.08);
|
|
border-radius: 3px;
|
|
margin-top: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: 3px;
|
|
background: var(--fill-color, var(--blue));
|
|
transition: width 0.4s ease;
|
|
}
|
|
|
|
/* Section Title */
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin: 32px 0 16px 0;
|
|
}
|
|
.section-title { font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
|
.badge-count { font-size: 11px; background: rgba(56, 189, 248, 0.15); color: var(--blue); padding: 2px 8px; border-radius: 12px; border: 1px solid rgba(56, 189, 248, 0.3); }
|
|
|
|
/* Nodes Grid */
|
|
.nodes-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.node-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
transition: transform 0.2s ease, border-color 0.2s ease;
|
|
}
|
|
.node-card:hover { transform: translateY(-2px); border-color: rgba(56, 189, 248, 0.4); }
|
|
.node-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
|
.node-name { font-size: 16px; font-weight: 600; color: #fff; }
|
|
.node-role { font-size: 11px; color: var(--text-muted); text-transform: uppercase; margin-top: 2px; }
|
|
.status-badge {
|
|
font-size: 11px;
|
|
padding: 3px 8px;
|
|
border-radius: 12px;
|
|
font-weight: 600;
|
|
}
|
|
.status-badge.ready { background: var(--green-glow); color: var(--green); border: 1px solid rgba(34, 197, 94, 0.3); }
|
|
.status-badge.notready { background: rgba(239, 68, 68, 0.15); color: var(--red); border: 1px solid rgba(239, 68, 68, 0.3); }
|
|
.status-badge.active { background: var(--green-glow); color: var(--green); border: 1px solid rgba(34, 197, 94, 0.3); }
|
|
.status-badge.idle { background: var(--amber-glow); color: var(--amber); border: 1px solid rgba(245, 158, 11, 0.3); }
|
|
|
|
.node-meta { font-size: 12px; color: var(--text-muted); display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 12px 0 16px 0; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; }
|
|
.node-metric { margin-top: 10px; }
|
|
.metric-label-row { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px; }
|
|
/* Operators Grid */
|
|
.operators-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.operator-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
transition: transform 0.2s ease, border-color 0.2s ease;
|
|
}
|
|
.operator-card:hover { transform: translateY(-2px); border-color: rgba(56, 189, 248, 0.4); }
|
|
.operator-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
|
.operator-title { font-size: 16px; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 8px; }
|
|
.operator-sub { font-size: 11.5px; color: var(--text-muted); margin-top: 3px; }
|
|
.operator-meta {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
margin: 12px 0 0 0;
|
|
background: rgba(0,0,0,0.2);
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
}
|
|
.operator-stat-box { display: flex; flex-direction: column; gap: 3px; }
|
|
.operator-stat-label { font-size: 10.5px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
.operator-stat-val { font-size: 13.5px; font-weight: 600; color: #fff; word-break: break-all; }
|
|
|
|
/* Zero-Scale Grid */
|
|
.zero-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.zero-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 18px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
.zero-host { font-size: 15px; font-weight: 600; color: #fff; text-align: center; margin-bottom: 8px; word-break: break-all; }
|
|
.zero-status-row { display: flex; justify-content: center; margin-bottom: 10px; }
|
|
.zero-desc { font-size: 12px; color: var(--text-muted); text-align: center; margin-bottom: 16px; min-height: 36px; }
|
|
.zero-actions { display: flex; gap: 8px; }
|
|
|
|
/* Site Cards */
|
|
.sites-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 14px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.site-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 16px;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: flex;
|
|
gap: 14px;
|
|
align-items: flex-start;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.site-card:hover {
|
|
background: var(--card-hover);
|
|
border-color: var(--blue);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
|
|
}
|
|
.site-icon { font-size: 26px; flex-shrink: 0; padding-top: 2px; }
|
|
.site-info { flex-grow: 1; }
|
|
.site-name { font-size: 14px; font-weight: 600; color: #fff; margin-bottom: 2px; }
|
|
.site-host { font-size: 12px; color: var(--blue); margin-bottom: 4px; word-break: break-all; }
|
|
.site-desc { font-size: 12px; color: var(--text-muted); }
|
|
.site-ns { font-size: 10px; background: rgba(255,255,255,0.06); padding: 2px 6px; border-radius: 4px; color: var(--text-muted); display: inline-block; margin-top: 6px; }
|
|
|
|
footer {
|
|
text-align: center;
|
|
padding: 32px 0 16px 0;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
border-top: 1px solid var(--border);
|
|
margin-top: 40px;
|
|
}
|
|
|
|
/* RBAC Badges & Modal */
|
|
.tag-locked {
|
|
color: var(--amber);
|
|
background: rgba(245, 158, 11, 0.12);
|
|
padding: 2px 7px;
|
|
border-radius: 4px;
|
|
border: 1px dashed rgba(245, 158, 11, 0.45);
|
|
font-size: 11.5px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.tag-locked:hover {
|
|
background: rgba(245, 158, 11, 0.22);
|
|
border-color: var(--amber);
|
|
}
|
|
.tag-unlocked {
|
|
color: var(--green);
|
|
background: rgba(34, 197, 94, 0.12);
|
|
padding: 2px 7px;
|
|
border-radius: 4px;
|
|
border: 1px solid rgba(34, 197, 94, 0.45);
|
|
font-size: 11.5px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(4, 7, 13, 0.85);
|
|
backdrop-filter: blur(4px);
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
padding: 16px;
|
|
}
|
|
.modal-overlay.active { display: flex; }
|
|
.modal-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
max-width: 520px;
|
|
width: 100%;
|
|
padding: 24px;
|
|
box-shadow: 0 16px 40px rgba(0,0,0,0.6);
|
|
}
|
|
.modal-card h3 { font-size: 18px; margin-bottom: 8px; color: var(--text); }
|
|
.modal-card p { font-size: 13px; color: var(--text-muted); margin-bottom: 16px; line-height: 1.5; }
|
|
.code-snippet {
|
|
background: rgba(0,0,0,0.4);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
color: var(--blue);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
.modal-textarea {
|
|
width: 100%;
|
|
height: 90px;
|
|
background: rgba(0,0,0,0.5);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
color: var(--text);
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
resize: vertical;
|
|
margin-bottom: 14px;
|
|
}
|
|
.modal-textarea:focus { outline: none; border-color: var(--blue); }
|
|
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
|
.modal-msg { font-size: 12px; margin-bottom: 12px; display: none; }
|
|
.modal-msg.error { color: var(--red); display: block; }
|
|
.modal-msg.success { color: var(--green); display: block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<noscript>
|
|
<div style="background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 14px 18px; margin-bottom: 20px; font-size: 13px; color: var(--text-muted);">
|
|
This dashboard monitors real-time compute telemetry, node utilization, and autonomous zero-scale service lifecycles across the Kubernetes cluster infrastructure. Active services and source repositories are available at <a href="https://git.fairfaxmedia.net/" style="color:var(--blue); text-decoration:none;">git.fairfaxmedia.net</a>.
|
|
</div>
|
|
</noscript>
|
|
<header>
|
|
<div class="brand">
|
|
<div class="brand-icon">⚡</div>
|
|
<div>
|
|
<h1>Zero-Scale Platform Dashboard</h1>
|
|
<p>Serverless Compute Runtime & Kubernetes Infrastructure Status</p>
|
|
<div style="font-size: 12.5px; margin-top: 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
|
<span style="color: var(--text-muted);">Core Components:</span>
|
|
<a href="https://git.fairfaxmedia.net/open-cream-cheese/zero-scale-platform" target="_blank" style="color: var(--blue); text-decoration: underline;">⚡ zero-scale-platform</a>
|
|
<span style="color: var(--text-muted);">•</span>
|
|
<a href="https://git.fairfaxmedia.net/open-cream-cheese/open-cream-cheese" target="_blank" style="color: var(--blue); text-decoration: underline;">🧀 open-cream-cheese</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<div class="live-indicator">
|
|
<span class="pulse-dot"></span>
|
|
<span id="update-status">Live Monitoring</span>
|
|
</div>
|
|
<a href="https://git.fairfaxmedia.net/open-cream-cheese/zero-scale-platform" target="_blank" class="btn btn-git" title="Zero-Scale Platform Repository">
|
|
<span>⚡</span> <strong>zero-scale-platform</strong>
|
|
</a>
|
|
<a href="https://git.fairfaxmedia.net/open-cream-cheese/open-cream-cheese" target="_blank" class="btn" title="Open-Cream-Cheese Repository">
|
|
<span>🧀</span> <strong>open-cream-cheese</strong>
|
|
</a>
|
|
<a href="https://kube.fairfaxmedia.net/" target="_blank" class="btn">
|
|
<span>☸️</span> Kube Dashboard
|
|
</a>
|
|
<button id="btn-auth" class="btn" onclick="toggleAuthModal()" style="border-color: var(--border);">
|
|
<span id="auth-icon">🔐</span> <span id="auth-label">RBAC Access</span>
|
|
</button>
|
|
<button class="btn btn-primary" onclick="loadStatus()">🔄 Refresh</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Cluster Overview Cards -->
|
|
<div class="metrics-grid">
|
|
<div class="metric-card" style="--accent: var(--green);">
|
|
<div class="metric-title">Cluster Nodes</div>
|
|
<div class="metric-value" id="val-nodes">-- / --</div>
|
|
<div class="metric-sub"><span>Nodes Status</span><span id="sub-nodes" style="color:var(--green)">Online</span></div>
|
|
</div>
|
|
<div class="metric-card" style="--accent: var(--blue);">
|
|
<div class="metric-title">CPU Utilization</div>
|
|
<div class="metric-value" id="val-cpu">-- %</div>
|
|
<div class="metric-sub"><span id="sub-cpu-detail">-- / -- Cores</span><span id="sub-cpu-pct">-- %</span></div>
|
|
<div class="progress-bg"><div class="progress-fill" id="fill-cpu" style="width: 0%; --fill-color: var(--blue);"></div></div>
|
|
</div>
|
|
<div class="metric-card" style="--accent: var(--purple);">
|
|
<div class="metric-title">RAM Utilization</div>
|
|
<div class="metric-value" id="val-ram">-- %</div>
|
|
<div class="metric-sub"><span id="sub-ram-detail">-- / -- GB</span><span id="sub-ram-pct">-- %</span></div>
|
|
<div class="progress-bg"><div class="progress-fill" id="fill-ram" style="width: 0%; --fill-color: var(--purple);"></div></div>
|
|
</div>
|
|
<div class="metric-card" style="--accent: var(--amber);">
|
|
<div class="metric-title">Zero-Scale Apps</div>
|
|
<div class="metric-value" id="val-zero">--</div>
|
|
<div class="metric-sub"><span id="sub-zero-active">-- Active</span><span>On-Demand Scaled</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Platform Operators Section -->
|
|
<div class="section-header">
|
|
<div class="section-title">
|
|
<span>⚙️ Core Platform Operators</span>
|
|
<span class="badge-count" id="count-operators">2 Active</span>
|
|
</div>
|
|
<div style="font-size:12px; color:var(--text-muted);">High-Availability Data & Distributed Storage Controllers</div>
|
|
</div>
|
|
<div class="operators-grid" id="operators-container">
|
|
<div style="color:var(--text-muted); font-size:13px;">Querying platform operators...</div>
|
|
</div>
|
|
|
|
<!-- Nodes Section -->
|
|
<div class="section-header">
|
|
<div class="section-title">
|
|
<span>🖥️ Cluster Nodes</span>
|
|
<span class="badge-count" id="count-nodes">0</span>
|
|
</div>
|
|
</div>
|
|
<div class="nodes-grid" id="nodes-container">
|
|
<div style="color:var(--text-muted); font-size:13px;">Connecting to cluster node metrics...</div>
|
|
</div>
|
|
|
|
<!-- Zero-Scale Deployments Section -->
|
|
<div class="section-header">
|
|
<div class="section-title">
|
|
<span>⚡ Zero-Scale Deployed Services</span>
|
|
<span class="badge-count" id="count-zero">0</span>
|
|
</div>
|
|
<div style="font-size:12px; color:var(--text-muted);">Consolidated systemd runner compute runtime • 10s idle scale-down</div>
|
|
</div>
|
|
<div class="zero-scale-notice" style="background: rgba(56, 189, 248, 0.07); border: 1px solid rgba(56, 189, 248, 0.25); border-radius: 8px; padding: 12px 16px; margin-bottom: 18px; font-size: 12.5px; color: #bae6fd; line-height: 1.5; display: flex; align-items: flex-start; gap: 12px;">
|
|
<span style="font-size: 20px; line-height: 1.2;">⚡</span>
|
|
<div>
|
|
<strong>On-Demand Serverless Compute:</strong> Services remain <em>"Scaled to Zero"</em> when inactive to conserve cluster resources. Opening a service instantly boots it in under 200ms. If you click <strong>Open App</strong> on an idle service and switch tabs back to this dashboard within <strong>10 seconds</strong>, the badge will show <strong style="color: var(--green);">🟢 Active</strong> before scaling down automatically after 10 seconds of idle!
|
|
</div>
|
|
</div>
|
|
<div class="zero-grid" id="zero-container">
|
|
<div style="color:var(--text-muted); font-size:13px;">Querying zero-scale runner runtime...</div>
|
|
</div>
|
|
|
|
<!-- Ingresses and Other Sites -->
|
|
<div id="sites-container">
|
|
<div style="color:var(--text-muted); font-size:13px;">Discovering cluster ingress services...</div>
|
|
</div>
|
|
|
|
<!-- RBAC Authentication Modal -->
|
|
<div class="modal-overlay" id="auth-modal" onclick="if(event.target === this) closeAuthModal()">
|
|
<div class="modal-card">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
|
<h3 style="margin:0;">🔐 Kubernetes RBAC Access</h3>
|
|
<button onclick="closeAuthModal()" style="background:none; border:none; color:var(--text-muted); font-size:22px; cursor:pointer; line-height:1;">×</button>
|
|
</div>
|
|
<p>
|
|
Enter a Kubernetes ServiceAccount Bearer token to unlock exact Linux kernel versions, cluster patch releases, and system runtime internals.
|
|
</p>
|
|
<div style="font-size:11.5px; color:var(--text-muted); margin-bottom:6px;">Generate token on workstation or cluster node:</div>
|
|
<div class="code-snippet">
|
|
<code>kubectl create token default</code>
|
|
<button class="btn" style="padding:2px 8px; font-size:11px;" onclick="navigator.clipboard.writeText('kubectl create token default'); alert('Copied to clipboard!')">Copy</button>
|
|
</div>
|
|
<textarea class="modal-textarea" id="token-input" placeholder="Paste Bearer Token (ey...)"></textarea>
|
|
<div class="modal-msg" id="auth-msg"></div>
|
|
<div class="modal-actions">
|
|
<button class="btn" onclick="clearToken()" id="btn-clear-token" style="display:none; color:var(--red); border-color: rgba(239,68,68,0.3);">Clear / Logout</button>
|
|
<button class="btn" onclick="closeAuthModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="submitToken()">Unlock Telemetry</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
Zero-Scale Platform Dashboard • Fairfax Media Infrastructure • <a href="https://git.fairfaxmedia.net/" style="color:var(--blue); text-decoration:none;">git.fairfaxmedia.net</a>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
function formatBytes(bytes) {
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function getAuthToken() {
|
|
return sessionStorage.getItem('kube_token') || '';
|
|
}
|
|
|
|
function toggleAuthModal() {
|
|
const modal = document.getElementById('auth-modal');
|
|
const input = document.getElementById('token-input');
|
|
const clearBtn = document.getElementById('btn-clear-token');
|
|
const msg = document.getElementById('auth-msg');
|
|
msg.className = 'modal-msg';
|
|
msg.innerText = '';
|
|
|
|
const token = getAuthToken();
|
|
if (token) {
|
|
input.value = token;
|
|
clearBtn.style.display = 'inline-flex';
|
|
} else {
|
|
input.value = '';
|
|
clearBtn.style.display = 'none';
|
|
}
|
|
modal.classList.add('active');
|
|
input.focus();
|
|
}
|
|
|
|
function closeAuthModal() {
|
|
document.getElementById('auth-modal').classList.remove('active');
|
|
}
|
|
|
|
async function submitToken() {
|
|
const token = document.getElementById('token-input').value.trim();
|
|
const msg = document.getElementById('auth-msg');
|
|
if (!token) {
|
|
msg.className = 'modal-msg error';
|
|
msg.innerText = 'Please enter a token';
|
|
return;
|
|
}
|
|
msg.className = 'modal-msg';
|
|
msg.innerText = 'Validating token with cluster apiserver...';
|
|
msg.style.display = 'block';
|
|
|
|
try {
|
|
const res = await fetch('/api/auth/verify', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok && data.authenticated) {
|
|
sessionStorage.setItem('kube_token', token);
|
|
msg.className = 'modal-msg success';
|
|
msg.innerText = 'Token verified! Telemetry unlocked.';
|
|
setTimeout(() => {
|
|
closeAuthModal();
|
|
loadStatus();
|
|
}, 500);
|
|
} else {
|
|
msg.className = 'modal-msg error';
|
|
msg.innerText = 'Invalid token: ' + (data.reason || 'Permission denied');
|
|
}
|
|
} catch (err) {
|
|
msg.className = 'modal-msg error';
|
|
msg.innerText = 'Verification request failed: ' + err.message;
|
|
}
|
|
}
|
|
|
|
function clearToken() {
|
|
sessionStorage.removeItem('kube_token');
|
|
document.getElementById('token-input').value = '';
|
|
closeAuthModal();
|
|
loadStatus();
|
|
}
|
|
|
|
async function loadStatus() {
|
|
const statusEl = document.getElementById('update-status');
|
|
statusEl.innerText = 'Updating...';
|
|
try {
|
|
const headers = {};
|
|
const token = getAuthToken();
|
|
if (token) {
|
|
headers['Authorization'] = 'Bearer ' + token;
|
|
}
|
|
const res = await fetch('/api/status', { headers });
|
|
if (!res.ok) throw new Error('API returned ' + res.status);
|
|
const data = await res.json();
|
|
renderDashboard(data);
|
|
statusEl.innerText = 'Updated ' + new Date().toLocaleTimeString();
|
|
} catch (err) {
|
|
console.error(err);
|
|
statusEl.innerText = 'Sync Error';
|
|
}
|
|
}
|
|
|
|
function renderDashboard(data) {
|
|
// Update Auth Button State
|
|
const authBtn = document.getElementById('btn-auth');
|
|
const authIcon = document.getElementById('auth-icon');
|
|
const authLabel = document.getElementById('auth-label');
|
|
const isAuthed = !!data.authenticated;
|
|
const token = getAuthToken();
|
|
|
|
if (isAuthed) {
|
|
authIcon.innerText = '🟢';
|
|
authLabel.innerText = 'RBAC Active';
|
|
authBtn.style.borderColor = 'var(--green)';
|
|
authBtn.style.color = 'var(--green)';
|
|
} else {
|
|
authIcon.innerText = '🔐';
|
|
authLabel.innerText = 'RBAC Access';
|
|
authBtn.style.borderColor = 'var(--border)';
|
|
authBtn.style.color = 'var(--text)';
|
|
}
|
|
|
|
// Summary Cards
|
|
const sum = data.summary;
|
|
document.getElementById('val-nodes').innerText = `${sum.ready_nodes} / ${sum.total_nodes}`;
|
|
document.getElementById('count-nodes').innerText = sum.total_nodes;
|
|
document.getElementById('sub-nodes').innerText = sum.ready_nodes === sum.total_nodes ? 'All Nodes Ready' : 'Degraded';
|
|
|
|
const cpuPct = Math.round(sum.cpu_usage_percent || 0);
|
|
document.getElementById('val-cpu').innerText = `${cpuPct}%`;
|
|
document.getElementById('sub-cpu-detail').innerText = `${sum.used_cores.toFixed(2)} / ${sum.total_cores} Cores`;
|
|
document.getElementById('sub-cpu-pct').innerText = `${cpuPct}%`;
|
|
document.getElementById('fill-cpu').style.width = `${Math.min(cpuPct, 100)}%`;
|
|
|
|
const ramPct = Math.round(sum.ram_usage_percent || 0);
|
|
document.getElementById('val-ram').innerText = `${ramPct}%`;
|
|
document.getElementById('sub-ram-detail').innerText = `${formatBytes(sum.used_ram_bytes)} / ${formatBytes(sum.total_ram_bytes)}`;
|
|
document.getElementById('sub-ram-pct').innerText = `${ramPct}%`;
|
|
document.getElementById('fill-ram').style.width = `${Math.min(ramPct, 100)}%`;
|
|
|
|
document.getElementById('val-zero').innerText = `${sum.total_zero_apps}`;
|
|
document.getElementById('count-zero').innerText = sum.total_zero_apps;
|
|
document.getElementById('sub-zero-active').innerText = `${sum.active_zero_apps} Active Now`;
|
|
|
|
// Render Platform Operators
|
|
const opsCont = document.getElementById('operators-container');
|
|
if (opsCont && data.operators) {
|
|
opsCont.innerHTML = '';
|
|
const pg = data.operators.postgres;
|
|
const lh = data.operators.longhorn;
|
|
|
|
if (pg) {
|
|
const pgCard = document.createElement('div');
|
|
pgCard.className = 'operator-card';
|
|
const pgReadyCls = pg.operator_ready > 0 ? 'ready' : 'notready';
|
|
pgCard.innerHTML = `
|
|
<div class="operator-top">
|
|
<div>
|
|
<div class="operator-title"><span>🐘</span> CloudNativePG Operator</div>
|
|
<div class="operator-sub">Enterprise PostgreSQL HA Controller • ns: ${pg.namespace || 'cnpg-system'}</div>
|
|
</div>
|
|
<span class="status-badge ${pgReadyCls}">${pg.status || 'Active'}</span>
|
|
</div>
|
|
<div class="operator-meta">
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">Operator Controller</span>
|
|
<span class="operator-stat-val" style="color: var(--green);">${pg.operator_ready} / ${pg.operator_replicas} Replicas Ready</span>
|
|
</div>
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">HA Database Cluster</span>
|
|
<span class="operator-stat-val">${pg.ready_instances} / ${pg.instances} Instances Ready</span>
|
|
</div>
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">Primary Instance</span>
|
|
<span class="operator-stat-val" style="color: var(--blue);">🟢 ${pg.primary || 'ha-postgres-1'}</span>
|
|
</div>
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">Cluster Target</span>
|
|
<span class="operator-stat-val">${pg.cluster_name || 'ha-postgres'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
opsCont.appendChild(pgCard);
|
|
}
|
|
|
|
if (lh) {
|
|
const lhCard = document.createElement('div');
|
|
lhCard.className = 'operator-card';
|
|
const lhReadyCls = lh.manager_ready > 0 && lh.manager_ready === lh.manager_desired ? 'ready' : 'notready';
|
|
lhCard.innerHTML = `
|
|
<div class="operator-top">
|
|
<div>
|
|
<div class="operator-title"><span>🐮</span> Longhorn Storage Operator</div>
|
|
<div class="operator-sub">Cloud-Native Distributed Block Storage • ns: ${lh.namespace || 'longhorn-system'}</div>
|
|
</div>
|
|
<span class="status-badge ${lhReadyCls}">${lh.status || 'Healthy'}</span>
|
|
</div>
|
|
<div class="operator-meta">
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">Storage Manager (DS)</span>
|
|
<span class="operator-stat-val" style="color: var(--green);">${lh.manager_ready} / ${lh.manager_desired} Nodes Ready</span>
|
|
</div>
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">Management UI</span>
|
|
<span class="operator-stat-val" style="color: var(--green);">${lh.ui_ready} / ${lh.ui_desired} Replicas Ready</span>
|
|
</div>
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">Distributed Volumes</span>
|
|
<span class="operator-stat-val" style="color: var(--blue);">${lh.total_volumes} Active PVs</span>
|
|
</div>
|
|
<div class="operator-stat-box">
|
|
<span class="operator-stat-label">Storage Class</span>
|
|
<span class="operator-stat-val">${lh.storage_class || 'longhorn-2-replicas'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
opsCont.appendChild(lhCard);
|
|
}
|
|
}
|
|
|
|
// Render Nodes
|
|
const nodesCont = document.getElementById('nodes-container');
|
|
nodesCont.innerHTML = '';
|
|
data.nodes.forEach(n => {
|
|
const card = document.createElement('div');
|
|
card.className = 'node-card';
|
|
const cpuPct = Math.round(n.cpu_usage_percent || 0);
|
|
const ramPct = Math.round(n.ram_usage_percent || 0);
|
|
const readyCls = n.status === 'Ready' ? 'ready' : 'notready';
|
|
|
|
const kernelHtml = isAuthed
|
|
? `<span class="tag-unlocked">🟢 ${n.kernel_version}</span>`
|
|
: `<span class="tag-locked" onclick="toggleAuthModal()" title="Click to unlock with RBAC token">${n.kernel_version}</span>`;
|
|
const kubeletHtml = isAuthed
|
|
? `<span class="tag-unlocked">🟢 ${n.kubelet_version}</span>`
|
|
: `<span class="tag-locked" onclick="toggleAuthModal()" title="Click to unlock with RBAC token">${n.kubelet_version}</span>`;
|
|
|
|
card.innerHTML = `
|
|
<div class="node-top">
|
|
<div>
|
|
<div class="node-name">${n.name}</div>
|
|
<div class="node-role">${n.roles.join(', ')} • ${n.uptime} uptime</div>
|
|
</div>
|
|
<span class="status-badge ${readyCls}">${n.status}</span>
|
|
</div>
|
|
<div class="node-meta">
|
|
<div><strong>OS:</strong> ${n.os_image}</div>
|
|
<div><strong>Runtime:</strong> ${n.container_engine.split('://')[0]}</div>
|
|
<div><strong>Kernel:</strong> ${kernelHtml}</div>
|
|
<div><strong>Kubelet:</strong> ${kubeletHtml}</div>
|
|
</div>
|
|
<div class="node-metric">
|
|
<div class="metric-label-row">
|
|
<span>CPU (${n.cpu_cores} Cores)</span>
|
|
<span>${n.cpu_used_cores.toFixed(2)} cores (${cpuPct}%)</span>
|
|
</div>
|
|
<div class="progress-bg">
|
|
<div class="progress-fill" style="width:${Math.min(cpuPct, 100)}%; --fill-color: var(--blue);"></div>
|
|
</div>
|
|
</div>
|
|
<div class="node-metric">
|
|
<div class="metric-label-row">
|
|
<span>RAM (${formatBytes(n.ram_total_bytes)})</span>
|
|
<span>${formatBytes(n.ram_used_bytes)} (${ramPct}%)</span>
|
|
</div>
|
|
<div class="progress-bg">
|
|
<div class="progress-fill" style="width:${Math.min(ramPct, 100)}%; --fill-color: var(--purple);"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
nodesCont.appendChild(card);
|
|
});
|
|
|
|
// Render Zero Scale
|
|
const zeroCont = document.getElementById('zero-container');
|
|
zeroCont.innerHTML = '';
|
|
data.zero_scale_apps.forEach(app => {
|
|
const card = document.createElement('div');
|
|
card.className = 'zero-card';
|
|
const stateCls = app.state === 'active' ? 'active' : 'idle';
|
|
const stateLabel = app.state === 'active' ? '🟢 Active' : '💤 Scaled to Zero';
|
|
const logsUrl = isAuthed && token ? `${app.logs_url}?token=${encodeURIComponent(token)}` : app.logs_url;
|
|
|
|
card.innerHTML = `
|
|
<div>
|
|
<div class="zero-host">${app.host}</div>
|
|
<div class="zero-status-row">
|
|
<span class="status-badge ${stateCls}">${stateLabel}</span>
|
|
</div>
|
|
<div class="zero-desc">${app.description || 'Zero-scale dynamic service'}</div>
|
|
</div>
|
|
<div class="zero-actions">
|
|
<a href="${app.url}" target="_blank" onclick="handleOpenApp()" class="btn btn-primary" style="flex:1; justify-content:center;">🌐 Open App</a>
|
|
<a href="${logsUrl}" target="_blank" class="btn" style="flex:1; justify-content:center;">🔐 Logs</a>
|
|
</div>
|
|
`;
|
|
zeroCont.appendChild(card);
|
|
});
|
|
|
|
// Render Other Sites Groups
|
|
const sitesCont = document.getElementById('sites-container');
|
|
sitesCont.innerHTML = '';
|
|
data.other_sites.forEach(group => {
|
|
const section = document.createElement('div');
|
|
section.innerHTML = `
|
|
<div class="section-header">
|
|
<div class="section-title">
|
|
<span>${group.category}</span>
|
|
<span class="badge-count">${group.sites.length}</span>
|
|
</div>
|
|
</div>
|
|
<div class="sites-grid" id="group-${group.category.replace(/\s+/g, '-')}"></div>
|
|
`;
|
|
sitesCont.appendChild(section);
|
|
|
|
const grid = section.querySelector('.sites-grid');
|
|
group.sites.forEach(site => {
|
|
const card = document.createElement('a');
|
|
card.className = 'site-card';
|
|
card.href = site.url;
|
|
card.target = '_blank';
|
|
card.innerHTML = `
|
|
<div class="site-icon">${site.icon || '🌐'}</div>
|
|
<div class="site-info">
|
|
<div class="site-name">${site.name}</div>
|
|
<div class="site-host">${site.host}</div>
|
|
<div class="site-desc">${site.description}</div>
|
|
<span class="site-ns">ns: ${site.namespace}</span>
|
|
</div>
|
|
`;
|
|
grid.appendChild(card);
|
|
});
|
|
});
|
|
}
|
|
|
|
function handleOpenApp() {
|
|
setTimeout(loadStatus, 400);
|
|
setTimeout(loadStatus, 1500);
|
|
}
|
|
|
|
// Automatically refresh status whenever the user switches back to this tab
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
loadStatus();
|
|
}
|
|
});
|
|
|
|
// Initial load & 10s auto-refresh
|
|
loadStatus();
|
|
setInterval(loadStatus, 10000);
|
|
</script>
|
|
</body>
|
|
</html>
|