aboutsummaryrefslogtreecommitdiff
path: root/downloads/dashboard.html
diff options
context:
space:
mode:
Diffstat (limited to 'downloads/dashboard.html')
-rw-r--r--downloads/dashboard.html643
1 files changed, 643 insertions, 0 deletions
diff --git a/downloads/dashboard.html b/downloads/dashboard.html
new file mode 100644
index 0000000..d4275f2
--- /dev/null
+++ b/downloads/dashboard.html
@@ -0,0 +1,643 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>newOS4 Dashboard</title>
+ <style>
+ :root {
+ --bg: #0a0f0a;
+ --surface: #0f1a0f;
+ --border: #1a3320;
+ --accent: #00ff41;
+ --accent-dim: #00cc33;
+ --accent-glow: rgba(0, 255, 65, 0.15);
+ --text: #e0ffe6;
+ --text-dim: #88aa88;
+ --warn: #ffcc00;
+ --danger: #ff4444;
+ --info: #44aaff;
+ }
+ * { box-sizing: border-box; margin: 0; padding: 0; }
+ body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: 'Segoe UI', system-ui, sans-serif;
+ line-height: 1.5;
+ min-height: 100vh;
+ }
+ header {
+ border-bottom: 1px solid var(--border);
+ padding: 1rem 1.5rem;
+ background: var(--surface);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ }
+ header h1 {
+ font-size: 1.2rem;
+ color: var(--accent);
+ text-shadow: 0 0 8px var(--accent-glow);
+ }
+ header .meta {
+ font-size: 0.8rem;
+ color: var(--text-dim);
+ }
+ .header-controls {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ }
+ .header-controls button {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: 0.3rem 0.8rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.8rem;
+ font-family: inherit;
+ }
+ .header-controls button:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ }
+ .header-controls .status-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: 4px;
+ }
+ .status-dot.green { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
+ .status-dot.yellow { background: var(--warn); box-shadow: 0 0 6px var(--warn); }
+ .status-dot.red { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
+ .status-dot.gray { background: var(--text-dim); }
+
+ .container { max-width: 1200px; margin: 0 auto; padding: 1rem; }
+
+ .grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ }
+ .panel {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 1rem;
+ transition: border-color 0.3s;
+ }
+ .panel:hover { border-color: var(--accent-dim); }
+ .panel h3 {
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-dim);
+ margin-bottom: 0.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ }
+ .panel .value {
+ font-size: 1.8rem;
+ font-weight: 700;
+ color: var(--text);
+ }
+ .panel .sub {
+ font-size: 0.75rem;
+ color: var(--text-dim);
+ margin-top: 0.2rem;
+ }
+ .panel .progress {
+ height: 4px;
+ background: #1a3320;
+ border-radius: 2px;
+ overflow: hidden;
+ margin-top: 0.5rem;
+ }
+ .panel .progress-fill {
+ height: 100%;
+ border-radius: 2px;
+ transition: width 0.5s;
+ }
+ .progress-fill.green { background: var(--accent); }
+ .progress-fill.yellow { background: var(--warn); }
+ .progress-fill.red { background: var(--danger); }
+
+ .section {
+ margin: 1.5rem 0;
+ padding: 1rem;
+ background: var(--surface);
+ border-left: 3px solid var(--accent);
+ border-radius: 0 6px 6px 0;
+ }
+ .section h2 {
+ font-size: 1rem;
+ color: var(--accent);
+ margin-bottom: 0.75rem;
+ }
+ .section.warn { border-left-color: var(--warn); }
+ .section.danger { border-left-color: var(--danger); }
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.85rem;
+ }
+ th, td {
+ text-align: left;
+ padding: 0.4rem 0.6rem;
+ border-bottom: 1px solid var(--border);
+ }
+ th {
+ color: var(--accent);
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 0.7rem;
+ letter-spacing: 0.05em;
+ }
+ td { color: var(--text); }
+ tr:hover td { background: rgba(0,255,65,0.04); }
+
+ .badge {
+ display: inline-block;
+ padding: 0.1rem 0.4rem;
+ border: 1px solid var(--border);
+ font-size: 0.7rem;
+ border-radius: 3px;
+ color: var(--text-dim);
+ }
+ .badge.green { border-color: var(--accent); color: var(--accent); }
+ .badge.yellow { border-color: var(--warn); color: var(--warn); }
+ .badge.red { border-color: var(--danger); color: var(--danger); }
+ .badge.blue { border-color: var(--info); color: var(--info); }
+
+ .agent-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.4rem 0;
+ border-bottom: 1px dashed var(--border);
+ font-size: 0.85rem;
+ }
+ .agent-row:last-child { border-bottom: none; }
+ .agent-row .name { color: var(--text); }
+ .agent-row .status { font-size: 0.75rem; }
+ .agent-row .caps { font-size: 0.7rem; color: var(--text-dim); }
+
+ .metric-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.3rem 0;
+ border-bottom: 1px dashed var(--border);
+ font-size: 0.85rem;
+ }
+ .metric-row:last-child { border-bottom: none; }
+
+ .loading {
+ text-align: center;
+ padding: 3rem;
+ color: var(--text-dim);
+ }
+ .loading .spinner {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ border: 2px solid var(--border);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin-bottom: 0.5rem;
+ }
+ @keyframes spin { to { transform: rotate(360deg); } }
+
+ .error-banner {
+ background: rgba(255,68,68,0.1);
+ border: 1px solid var(--danger);
+ color: var(--danger);
+ padding: 0.75rem;
+ border-radius: 6px;
+ margin-bottom: 1rem;
+ font-size: 0.85rem;
+ }
+
+ .last-updated {
+ text-align: center;
+ font-size: 0.7rem;
+ color: var(--text-dim);
+ padding: 1rem;
+ border-top: 1px solid var(--border);
+ margin-top: 1rem;
+ }
+
+ @media (max-width: 640px) {
+ header { padding: 0.75rem; flex-direction: column; align-items: flex-start; }
+ .container { padding: 0.75rem; }
+ .grid { grid-template-columns: 1fr 1fr; }
+ .panel .value { font-size: 1.4rem; }
+ }
+ @media (max-width: 400px) {
+ .grid { grid-template-columns: 1fr; }
+ }
+ </style>
+</head>
+<body>
+<header>
+ <div>
+ <h1>⬡ newOS4 Dashboard</h1>
+ <div class="meta">Live system telemetry · <span id="version-display">v?</span></div>
+ </div>
+ <div class="header-controls">
+ <span id="connection-status"><span class="status-dot gray"></span> Loading...</span>
+ <button onclick="refreshNow()">⟳ Refresh</button>
+ <button onclick="document.querySelector('.container').classList.toggle('compact')">⊞ Toggle</button>
+ </div>
+</header>
+
+<div class="container">
+ <div id="error-banner" class="error-banner" style="display:none"></div>
+
+ <div id="loading" class="loading">
+ <div class="spinner"></div>
+ <div>Connecting to newOS4...</div>
+ </div>
+
+ <div id="dashboard-content" style="display:none">
+ <!-- Metric Panels -->
+ <div class="grid" id="metric-grid">
+ <div class="panel" id="panel-modules">
+ <h3>🧩 Modules</h3>
+ <div class="value" id="val-modules">--</div>
+ <div class="sub" id="sub-modules"></div>
+ <div class="progress"><div class="progress-fill green" id="bar-modules" style="width:0%"></div></div>
+ </div>
+ <div class="panel" id="panel-agents">
+ <h3>🤖 Agents</h3>
+ <div class="value" id="val-agents">--</div>
+ <div class="sub" id="sub-agents"></div>
+ <div class="progress"><div class="progress-fill" id="bar-agents" style="width:0%"></div></div>
+ </div>
+ <div class="panel" id="panel-memory">
+ <h3>💾 Memory</h3>
+ <div class="value" id="val-memory">--</div>
+ <div class="sub" id="sub-memory"></div>
+ <div class="progress"><div class="progress-fill green" id="bar-memory" style="width:0%"></div></div>
+ </div>
+ <div class="panel" id="panel-vfs">
+ <h3>📁 VFS</h3>
+ <div class="value" id="val-vfs">--</div>
+ <div class="sub" id="sub-vfs"></div>
+ <div class="progress"><div class="progress-fill green" id="bar-vfs" style="width:0%"></div></div>
+ </div>
+ <div class="panel" id="panel-ollama">
+ <h3>🧠 Ollama</h3>
+ <div class="value" id="val-ollama">--</div>
+ <div class="sub" id="sub-ollama"></div>
+ <div class="progress"><div class="progress-fill green" id="bar-ollama" style="width:0%"></div></div>
+ </div>
+ <div class="panel" id="panel-skills">
+ <h3>📚 Skills</h3>
+ <div class="value" id="val-skills">--</div>
+ <div class="sub" id="sub-skills"></div>
+ <div class="progress"><div class="progress-fill green" id="bar-skills" style="width:0%"></div></div>
+ </div>
+ </div>
+
+ <!-- Agent Pool -->
+ <div class="section" id="section-agents">
+ <h2>🤖 Agent Pool</h2>
+ <div id="agent-list"><div class="metric-row">No agents spawned</div></div>
+ </div>
+
+ <!-- Token Usage -->
+ <div class="section" id="section-tokens">
+ <h2>📊 LLM Token Usage</h2>
+ <div id="token-summary"></div>
+ <div style="margin-top:0.5rem">
+ <table>
+ <thead><tr><th>Model</th><th>Requests</th><th>Tokens</th><th>%</th></tr></thead>
+ <tbody id="token-by-model"></tbody>
+ </table>
+ </div>
+ </div>
+
+ <!-- Tool Latency -->
+ <div class="section warn" id="section-latency">
+ <h2>⏱ Tool Latency (Top 10)</h2>
+ <table>
+ <thead><tr><th>Tool</th><th>Avg</th><th>Max</th><th>Calls</th><th>Errors</th></tr></thead>
+ <tbody id="latency-table"></tbody>
+ </table>
+ </div>
+
+ <!-- World Model -->
+ <div class="section" id="section-world-model">
+ <h2>🌐 World Model</h2>
+ <div id="world-model-metrics">
+ <div class="metric-row"><span>Not synthesized yet</span><span>—</span></div>
+ </div>
+ </div>
+
+ <!-- System Health -->
+ <div class="section" id="section-health">
+ <h2>🩺 System Health</h2>
+ <div id="health-metrics"></div>
+ </div>
+ </div>
+
+ <div class="last-updated">
+ Last updated: <span id="last-updated-time">--</span> · Auto-refresh every 5s
+ </div>
+</div>
+
+<script>
+(function() {
+ 'use strict';
+
+ const DASHBOARD_KEY = 'webx11_dashboard_data';
+ const REFRESH_INTERVAL = 5000;
+ let refreshTimer = null;
+ let lastData = null;
+
+ function showError(msg) {
+ const banner = document.getElementById('error-banner');
+ banner.textContent = '⚠️ ' + msg;
+ banner.style.display = 'block';
+ }
+
+ function hideError() {
+ document.getElementById('error-banner').style.display = 'none';
+ }
+
+ function showLoading() {
+ document.getElementById('loading').style.display = 'block';
+ document.getElementById('dashboard-content').style.display = 'none';
+ }
+
+ function showContent() {
+ document.getElementById('loading').style.display = 'none';
+ document.getElementById('dashboard-content').style.display = 'block';
+ }
+
+ function updateConnectionStatus(ok) {
+ const el = document.getElementById('connection-status');
+ if (ok) {
+ el.innerHTML = '<span class="status-dot green"></span> Connected';
+ } else {
+ el.innerHTML = '<span class="status-dot red"></span> Disconnected';
+ }
+ }
+
+ function fetchDashboardData() {
+ // Try to get data from localStorage (set by the AI tool or terminal command)
+ try {
+ const raw = localStorage.getItem(DASHBOARD_KEY);
+ if (raw) {
+ const data = JSON.parse(raw);
+ if (data && data.timestamp && (Date.now() - data.timestamp < 10000)) {
+ return data; // fresh enough
+ }
+ }
+ } catch (e) {}
+ return null;
+ }
+
+ function renderDashboard(data) {
+ if (!data) {
+ showError('No dashboard data available. Open newOS4 terminal and run: dashboard');
+ updateConnectionStatus(false);
+ return;
+ }
+
+ hideError();
+ updateConnectionStatus(true);
+ lastData = data;
+
+ // Version
+ document.getElementById('version-display').textContent = data.version || 'v?';
+
+ // ── Metric Panels ──
+ // Modules
+ const modCount = data.modules?.loaded || 0;
+ const modTotal = data.modules?.total || modCount;
+ document.getElementById('val-modules').textContent = modCount + '/' + modTotal;
+ document.getElementById('sub-modules').textContent = (data.modules?.modifiedCount || 0) + ' modified';
+ document.getElementById('bar-modules').style.width = (modTotal > 0 ? (modCount / modTotal * 100) : 0) + '%';
+
+ // Agents
+ const agents = data.agents || {};
+ const agentTotal = agents.total || 0;
+ const agentBusy = agents.busy || 0;
+ const agentIdle = agents.idle || 0;
+ document.getElementById('val-agents').textContent = agentTotal;
+ document.getElementById('sub-agents').textContent = agentBusy + ' busy, ' + agentIdle + ' idle' + (agents.sleeping ? ', ' + agents.sleeping + ' sleeping' : '');
+ const barAgents = document.getElementById('bar-agents');
+ barAgents.style.width = (agentTotal > 0 ? (agentBusy / agentTotal * 100) : 0) + '%';
+ barAgents.className = 'progress-fill ' + (agentBusy > 0 ? 'yellow' : 'green');
+
+ // Memory
+ const memStr = data.memory?.heap || 'N/A';
+ const memNum = parseFloat(memStr) || 0;
+ document.getElementById('val-memory').textContent = memStr;
+ document.getElementById('sub-memory').textContent = data.memory?.graphNodes ? data.memory.graphNodes + ' graph nodes' : '';
+ const barMem = document.getElementById('bar-memory');
+ barMem.style.width = Math.min(memNum / 2, 100) + '%';
+ barMem.className = 'progress-fill ' + (memNum > 100 ? 'yellow' : (memNum > 200 ? 'red' : 'green'));
+
+ // VFS
+ const vfs = data.vfs || {};
+ document.getElementById('val-vfs').textContent = vfs.files || 0;
+ document.getElementById('sub-vfs').textContent = (vfs.dirs || 0) + ' dirs, ' + (vfs.size || '0KB');
+ document.getElementById('bar-vfs').style.width = Math.min((vfs.files || 0) / 2, 100) + '%';
+
+ // Ollama
+ const ollama = data.ollama || {};
+ const ollamaOk = ollama.connected || false;
+ document.getElementById('val-ollama').textContent = ollamaOk ? (ollama.model || 'Connected') : 'Disconnected';
+ document.getElementById('sub-ollama').textContent = ollamaOk ? (ollama.provider || '') : '';
+ const barOllama = document.getElementById('bar-ollama');
+ barOllama.style.width = ollamaOk ? '100%' : '0%';
+ barOllama.className = 'progress-fill ' + (ollamaOk ? 'green' : 'red');
+
+ // Skills
+ const skillCount = data.skills?.count || 0;
+ document.getElementById('val-skills').textContent = skillCount;
+ document.getElementById('sub-skills').textContent = data.skills?.note || '';
+ document.getElementById('bar-skills').style.width = Math.min(skillCount * 2.5, 100) + '%';
+
+ // ── Agent Pool ──
+ const agentList = document.getElementById('agent-list');
+ if (agents.list && agents.list.length > 0) {
+ let html = '';
+ for (const a of agents.list) {
+ const statusClass = a.status === 'busy' ? 'yellow' : a.status === 'idle' ? 'green' : a.status === 'sleeping' ? 'blue' : 'red';
+ html += '<div class="agent-row">' +
+ '<div><span class="name">' + a.id + '</span> <span class="caps">[' + (a.role || 'worker') + ']</span></div>' +
+ '<div><span class="badge ' + statusClass + '">' + a.status + '</span></div>' +
+ '</div>';
+ }
+ agentList.innerHTML = html;
+ } else {
+ agentList.innerHTML = '<div class="metric-row">No agents spawned</div>';
+ }
+
+ // ── Token Usage ──
+ const tokenSection = document.getElementById('token-summary');
+ const usage = data.usage || {};
+ if (usage.total && usage.total > 0) {
+ const avgLat = usage.avgLatency || 0;
+ const totalTok = (usage.totalPrompt || 0) + (usage.totalCompletion || 0);
+ tokenSection.innerHTML =
+ '<div class="metric-row"><span>Total requests</span><span>' + usage.total + '</span></div>' +
+ '<div class="metric-row"><span>Total tokens</span><span>' + totalTok.toLocaleString() + '</span></div>' +
+ '<div class="metric-row"><span>Avg latency</span><span>' + avgLat + 'ms</span></div>';
+
+ // Per-model breakdown
+ const tbody = document.getElementById('token-by-model');
+ const byModel = usage.byModel || {};
+ const modelKeys = Object.keys(byModel);
+ if (modelKeys.length > 0) {
+ let html = '';
+ for (const model of modelKeys) {
+ const m = byModel[model];
+ const pct = totalTok > 0 ? (m.tokens / totalTok * 100).toFixed(1) : '0';
+ html += '<tr><td>' + model + '</td><td>' + m.requests + '</td><td>' + m.tokens.toLocaleString() + '</td><td>' + pct + '%</td></tr>';
+ }
+ tbody.innerHTML = html;
+ } else {
+ tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">No usage data yet</td></tr>';
+ }
+ } else {
+ tokenSection.innerHTML = '<div class="metric-row" style="color:var(--text-dim)">No usage data yet</div>';
+ document.getElementById('token-by-model').innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">Run some AI queries to see usage</td></tr>';
+ }
+
+ // ── Tool Latency ──
+ const latencyTbody = document.getElementById('latency-table');
+ const latency = data.latency || [];
+ if (latency.length > 0) {
+ let html = '';
+ for (const t of latency.slice(0, 10)) {
+ const errClass = t.errors > 0 ? ' class="yellow"' : '';
+ html += '<tr' + errClass + '><td>' + t.name + '</td><td>' + t.avg + 'ms</td><td>' + t.max + 'ms</td><td>' + t.calls + '</td><td>' + (t.errors > 0 ? '⚠️ ' + t.errors : '0') + '</td></tr>';
+ }
+ latencyTbody.innerHTML = html;
+ } else {
+ latencyTbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim)">No latency data yet</td></tr>';
+ }
+
+ // ── World Model ──
+ const wmDiv = document.getElementById('world-model-metrics');
+ const wmData = data.worldModel;
+ if (wmData) {
+ const synTime = wmData.synthesized ? new Date(wmData.synthesized).toLocaleTimeString() : '?';
+ let wmHtml = '';
+ wmHtml += '<div class="metric-row"><span>Version</span><span>' + (wmData.version || '?') + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Last synthesized</span><span>' + synTime + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Modules</span><span>' + (wmData.modules || '?') + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Skills</span><span>' + (wmData.skills || 0) + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Files</span><span>' + (wmData.files || 0) + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Agents</span><span>' + (wmData.agents || 0) + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Memory graph</span><span>' + (wmData.memoryNodes || 0) + ' nodes, ' + (wmData.memoryEdges || 0) + ' edges</span></div>';
+ wmHtml += '<div class="metric-row"><span>Health</span><span>' + (wmData.health || '?') + '</span></div>';
+ if (wmData.activeProjects && wmData.activeProjects.length > 0) {
+ wmHtml += '<div class="metric-row"><span>Active projects</span><span>' + wmData.activeProjects.join(', ') + '</span></div>';
+ }
+ if (wmData.recent && wmData.recent.length > 0) {
+ wmHtml += '<div class="metric-row"><span>Recent changes</span><span>' + wmData.recent.join('; ') + '</span></div>';
+ }
+ wmDiv.innerHTML = wmHtml;
+ } else {
+ wmDiv.innerHTML = '<div class="metric-row" style="color:var(--text-dim)">World model not synthesized — run world-model-refresh in terminal</div>';
+ }
+
+ // ── System Health ──
+ const healthDiv = document.getElementById('health-metrics');
+ const health = data.health || {};
+ let healthHtml = '';
+ healthHtml += '<div class="metric-row"><span>Modules loaded</span><span>' + (health.modules || '?') + '</span></div>';
+ healthHtml += '<div class="metric-row"><span>VFS size</span><span>' + (health.vfsSize || '?') + '</span></div>';
+ healthHtml += '<div class="metric-row"><span>Online</span><span>' + (health.online ? '✅ Online' : '🔴 Offline') + '</span></div>';
+ healthHtml += '<div class="metric-row"><span>Errors</span><span>' + (health.errors > 0 ? '⚠️ ' + health.errors : '✅ 0') + '</span></div>';
+ healthHtml += '<div class="metric-row"><span>Predictor accuracy</span><span>' + (health.predictorAccuracy || 'N/A') + '</span></div>';
+ healthDiv.innerHTML = healthHtml;
+
+ // Timestamp
+ document.getElementById('last-updated-time').textContent = new Date().toLocaleTimeString();
+ }
+
+ function refreshNow() {
+ // Signal the terminal to update dashboard data
+ try {
+ localStorage.setItem(DASHBOARD_KEY + '_request', Date.now().toString());
+ } catch (e) {}
+ // Try reading fresh data
+ const data = fetchDashboardData();
+ if (data) {
+ renderDashboard(data);
+ } else {
+ // If no data, try to fetch via the newOS4 API
+ fetchDashboardFromAPI();
+ }
+ }
+
+ async function fetchDashboardFromAPI() {
+ showLoading();
+ try {
+ // Try the proxy API endpoint
+ const resp = await fetch('/api/dashboard', { signal: AbortSignal.timeout(5000) });
+ if (resp.ok) {
+ const data = await resp.json();
+ localStorage.setItem(DASHBOARD_KEY, JSON.stringify({...data, timestamp: Date.now()}));
+ renderDashboard(data);
+ showContent();
+ return;
+ }
+ } catch (e) {
+ // API not available — try localStorage fallback
+ }
+
+ // Fallback: check localStorage
+ const data = fetchDashboardData();
+ if (data) {
+ renderDashboard(data);
+ showContent();
+ } else {
+ showError('Cannot connect to newOS4. Open the terminal and run: dashboard');
+ updateConnectionStatus(false);
+ showContent();
+ }
+ }
+
+ // Auto-refresh loop
+ function startAutoRefresh() {
+ if (refreshTimer) clearInterval(refreshTimer);
+ refreshTimer = setInterval(() => {
+ const data = fetchDashboardData();
+ if (data) renderDashboard(data);
+ }, REFRESH_INTERVAL);
+ }
+
+ // Init
+ const initialData = fetchDashboardData();
+ if (initialData) {
+ renderDashboard(initialData);
+ showContent();
+ } else {
+ fetchDashboardFromAPI();
+ }
+ startAutoRefresh();
+
+ // Expose refresh for the button
+ window.refreshNow = refreshNow;
+})();
+</script>
+</body>
+</html>