Changeset 0.33.0 (#207)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ const ADMIN_SECTIONS = {
|
||||
workflows: ['workflows', 'tasks'],
|
||||
routing: ['health', 'routing', 'capabilities'],
|
||||
system: ['settings', 'storage', 'packages', 'channels', 'broadcast'],
|
||||
monitoring: ['usage', 'audit', 'stats'],
|
||||
monitoring: ['dashboard', 'usage', 'audit', 'stats'],
|
||||
};
|
||||
|
||||
const ADMIN_LABELS = {
|
||||
@@ -21,7 +21,7 @@ const ADMIN_LABELS = {
|
||||
health: 'Health', routing: 'Routing', capabilities: 'Capabilities',
|
||||
workflows: 'Workflows', tasks: 'Tasks',
|
||||
settings: 'Settings', storage: 'Storage', packages: 'Packages', channels: 'Channels', broadcast: 'Broadcast',
|
||||
usage: 'Usage', audit: 'Audit', stats: 'Stats',
|
||||
dashboard: 'Dashboard', usage: 'Usage', audit: 'Audit', stats: 'Stats',
|
||||
};
|
||||
|
||||
// Section → loader mapping (reuses all existing loadAdmin* functions)
|
||||
@@ -47,6 +47,7 @@ const ADMIN_LOADERS = {
|
||||
health: () => UI.loadAdminHealth(),
|
||||
routing: () => UI.loadAdminRouting(),
|
||||
capabilities: () => UI.loadAdminCapabilities(),
|
||||
dashboard: () => UI.loadAdminDashboard(),
|
||||
usage: () => UI.loadAdminUsage(),
|
||||
audit: () => UI.loadAuditLog(),
|
||||
stats: () => UI.loadAdminStats(),
|
||||
@@ -242,6 +243,120 @@ Object.assign(UI, {
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
// ── Admin: Dashboard (v0.33.0) ────────
|
||||
async loadAdminDashboard() {
|
||||
const el = document.getElementById('adminDashboardContent');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="loading">Loading dashboard...</div>';
|
||||
|
||||
try {
|
||||
const d = await API.adminGetDashboard();
|
||||
|
||||
// Stat cards row
|
||||
const providers = d.provider_health || [];
|
||||
const healthy = providers.filter(p => p.status === 'healthy').length;
|
||||
const degraded = providers.filter(p => p.status === 'degraded' || p.status === 'down' || p.status === 'unhealthy').length;
|
||||
const usage = d.usage_24h;
|
||||
const tokenStr = usage && usage.total_tokens ? Number(usage.total_tokens).toLocaleString() : '0';
|
||||
const reqStr = usage && usage.total_requests ? Number(usage.total_requests).toLocaleString() : '0';
|
||||
|
||||
const rt = d.runtime || {};
|
||||
|
||||
const cardsEl = document.getElementById('adminDashboardCards');
|
||||
if (cardsEl) {
|
||||
cardsEl.innerHTML = `
|
||||
<div class="stat-card"><div class="stat-card-label">Uptime</div><div class="stat-card-value">${esc(d.uptime || '—')}</div></div>
|
||||
<div class="stat-card"><div class="stat-card-label">WS Connections</div><div class="stat-card-value">${d.ws_connections || 0}</div></div>
|
||||
<div class="stat-card"><div class="stat-card-label">Providers</div><div class="stat-card-value" style="color:var(--success)">${healthy}</div><div class="stat-card-sub">${degraded ? degraded + ' degraded' : 'all healthy'}</div></div>
|
||||
<div class="stat-card"><div class="stat-card-label">Tokens (24h)</div><div class="stat-card-value">${tokenStr}</div><div class="stat-card-sub">${reqStr} requests</div></div>
|
||||
<div class="stat-card"><div class="stat-card-label">Goroutines</div><div class="stat-card-value">${rt.goroutines || '—'}</div></div>
|
||||
<div class="stat-card"><div class="stat-card-label">Heap</div><div class="stat-card-value">${rt.heap_mb != null ? rt.heap_mb + ' MB' : '—'}</div><div class="stat-card-sub">Sys: ${rt.sys_mb || '—'} MB</div></div>`;
|
||||
}
|
||||
|
||||
// Provider health cards
|
||||
let html = '';
|
||||
if (providers.length > 0) {
|
||||
html += '<h4 style="margin:0 0 12px">Provider Health</h4>';
|
||||
html += '<div class="dashboard-provider-grid">';
|
||||
providers.forEach(p => {
|
||||
const name = esc(p.provider_name || p.provider_config_id?.slice(0, 12) || '—');
|
||||
const statusCls = p.status === 'healthy' ? 'badge-healthy' : p.status === 'degraded' ? 'badge-degraded' : 'badge-unhealthy';
|
||||
const latency = p.avg_latency_ms != null ? Math.round(p.avg_latency_ms) + 'ms' : '—';
|
||||
html += `<div class="dashboard-provider-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<span style="font-weight:600;font-size:13px">${name}</span>
|
||||
<span class="${statusCls}">${esc(p.status || 'unknown')}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:16px;font-size:12px;color:var(--text-3)">
|
||||
<span>Latency: ${latency}</span>
|
||||
<span>Reqs: ${p.request_count || 0}</span>
|
||||
<span>Errors: ${p.error_count || 0}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// DB pool stats
|
||||
if (d.db_pool) {
|
||||
const pool = d.db_pool;
|
||||
const pct = pool.OpenConnections > 0 ? Math.round(pool.InUse / pool.OpenConnections * 100) : 0;
|
||||
html += '<h4 style="margin:20px 0 12px">DB Connection Pool</h4>';
|
||||
html += `<div class="dashboard-db-pool">
|
||||
<div class="dashboard-pool-bar"><div class="dashboard-pool-fill" style="width:${pct}%"></div></div>
|
||||
<div style="display:flex;gap:16px;font-size:12px;color:var(--text-3);margin-top:6px">
|
||||
<span>Open: ${pool.OpenConnections}</span>
|
||||
<span>In Use: ${pool.InUse}</span>
|
||||
<span>Idle: ${pool.Idle}</span>
|
||||
<span>Wait Count: ${pool.WaitCount}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Storage status (fetched in parallel)
|
||||
try {
|
||||
const storage = await API._get('/api/v1/admin/storage/status');
|
||||
if (storage && storage.configured) {
|
||||
html += '<h4 style="margin:20px 0 12px">Storage</h4>';
|
||||
html += '<div class="dashboard-db-pool">';
|
||||
html += `<div style="display:flex;gap:16px;font-size:12px;color:var(--text-3)">`;
|
||||
html += `<span>Backend: ${esc(storage.backend || '—')}</span>`;
|
||||
if (storage.total_bytes != null) html += `<span>Total: ${(storage.total_bytes / 1024 / 1024).toFixed(1)} MB</span>`;
|
||||
if (storage.file_count != null) html += `<span>Files: ${storage.file_count}</span>`;
|
||||
html += `<span style="color:${storage.healthy ? 'var(--success)' : 'var(--danger)'}">${storage.healthy ? 'Healthy' : 'Unhealthy'}</span>`;
|
||||
html += `</div></div>`;
|
||||
}
|
||||
} catch (_) { /* storage status optional */ }
|
||||
|
||||
// Recent errors
|
||||
const errors = d.recent_errors || [];
|
||||
if (errors.length > 0) {
|
||||
html += '<h4 style="margin:20px 0 12px">Recent Errors</h4>';
|
||||
html += '<div class="dashboard-errors">';
|
||||
errors.forEach(e => {
|
||||
const time = e.created_at ? new Date(e.created_at).toLocaleString() : '—';
|
||||
html += `<div class="dashboard-error-row">
|
||||
<span class="text-muted" style="font-size:11px;min-width:140px">${time}</span>
|
||||
<span style="font-size:12px">${esc(e.action || '')} ${esc(e.resource_type || '')} ${esc(e.resource_id || '')}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
el.innerHTML = html || '<div class="empty-hint">No data yet.</div>';
|
||||
|
||||
// Auto-refresh every 30s
|
||||
if (UI._dashboardRefreshTimer) clearInterval(UI._dashboardRefreshTimer);
|
||||
UI._dashboardRefreshTimer = setInterval(() => {
|
||||
if (document.getElementById('adminDashboardContent')) {
|
||||
UI.loadAdminDashboard();
|
||||
} else {
|
||||
clearInterval(UI._dashboardRefreshTimer);
|
||||
}
|
||||
}, 30000);
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadAdminStats() {
|
||||
const el = document.getElementById('adminStats');
|
||||
el.innerHTML = '<div class="loading">Loading...</div>';
|
||||
|
||||
Reference in New Issue
Block a user