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:
2026-03-19 21:37:32 +00:00
committed by xcaliber
parent b1266b0d7c
commit ed3e9363f2
42 changed files with 2527 additions and 129 deletions

View File

@@ -217,6 +217,10 @@
// -- Monitoring -------------------------------------------------------
SCAFFOLDING.dashboard =
'<div id="adminDashboardCards" class="stat-cards-grid"></div>' +
'<div id="adminDashboardContent"><div class="loading">Loading...</div></div>';
SCAFFOLDING.usage =
'<div id="adminUsageCards" class="stat-cards-grid"></div>' +
'<div id="adminUsageChart" style="margin-bottom:24px"></div>' +

View File

@@ -542,6 +542,7 @@ const API = {
getPublicSettings() { return this._get('/api/v1/settings/public'); },
adminUpdateSetting(key, value) { return this._put(`/api/v1/admin/settings/${key}`, value); },
adminGetStats() { return this._get('/api/v1/admin/stats'); },
adminGetDashboard() { return this._get('/api/v1/admin/dashboard'); },
adminListGlobalConfigs() { return this._get('/api/v1/admin/configs'); },
adminCreateGlobalConfig(name, provider, endpoint, apiKey, modelDefault, isPrivate) {

View File

@@ -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>';