App._renderOverviewShell = function() {
return ''
+ '<div class="card overview-status">'
+ ' <div class="overview-meta" id="ov-meta"></div>'
+ '</div>'
+ '<div class="card overview-attention" id="ov-attention" style="display:none"></div>'
+ '<div class="card overview-onboarding" id="ov-onboarding" style="display:none"></div>'
+ '<div class="overview-grid" id="overview-grid">'
+ ' <div id="ov-card-cost"></div>'
+ ' <div id="ov-card-tokens"></div>'
+ ' <div id="ov-card-cache"></div>'
+ ' <div id="ov-card-wallet"></div>'
+ ' <div id="ov-card-latency"></div>'
+ ' <div id="ov-card-sessions"></div>'
+ ' <div id="ov-card-system"></div>'
+ ' <div id="ov-card-errors"></div>'
+ ' <div id="ov-card-fleet" class="overview-fleet-slot" style="display:none"></div>'
+ '</div>'
+ '<div class="card" id="ov-integrations" style="margin-top:1rem"></div>'
+ '<div class="card" id="ov-throttle" style="margin-top:1rem"></div>';
};
App._applyOverviewCards = function(cards) {
var self = this;
var slotMap = {
cost: 'ov-card-cost',
tokens: 'ov-card-tokens',
cache: 'ov-card-cache',
wallet: 'ov-card-wallet',
latency: 'ov-card-latency',
sessions: 'ov-card-sessions',
system: 'ov-card-system',
errors: 'ov-card-errors',
fleet: 'ov-card-fleet'
};
var meta = cards && cards.__meta ? cards.__meta : {};
var attention = cards && cards.__attention ? cards.__attention : [];
var failures = cards && cards.__failures ? cards.__failures : [];
var ovMeta = document.getElementById('ov-meta');
if (ovMeta) {
var staleBadge = failures.length > 0
? '<span class="badge warning">Partial data (' + failures.length + ' source' + (failures.length > 1 ? 's' : '') + ' unavailable)</span>'
: '<span class="badge success">All sources healthy</span>';
var updated = meta.updated_at ? esc(meta.updated_at) : 'unknown';
setHtml(ovMeta, staleBadge + '<span class="badge muted">Last updated: ' + updated + '</span>');
}
var ovAttention = document.getElementById('ov-attention');
if (ovAttention) {
if (attention.length > 0) {
var items = attention.map(function(item) {
return '<li>' + esc(item) + '</li>';
}).join('');
setHtml(ovAttention, '<strong>Attention needed</strong><ul style="margin:0.5rem 0 0 1rem">' + items + '</ul>');
ovAttention.style.display = '';
} else {
ovAttention.style.display = 'none';
}
}
var ovOnboarding = document.getElementById('ov-onboarding');
if (ovOnboarding && hintsEnabled() && !window.localStorage.getItem('ic_dash_onboarding_dismissed')) {
setHtml(
ovOnboarding,
'<div style="display:flex;justify-content:space-between;gap:1rem;align-items:flex-start">'
+ '<div><strong>Quick Start</strong><div style="margin-top:0.35rem;color:var(--muted);font-size:0.85rem">1) Open <em>Sessions</em> and start a conversation. 2) Use <em>Context</em> to inspect token and reasoning traces. 3) Use <em>Prompt Performance</em> to tune latency/cost.</div></div>'
+ '<button class="btn secondary" id="dismiss-onboarding" title="Dismiss hint" aria-label="Dismiss hint">x</button>'
+ '</div>'
);
ovOnboarding.style.display = '';
} else if (ovOnboarding) {
ovOnboarding.style.display = 'none';
}
Object.keys(slotMap).forEach(function(key) {
var el = document.getElementById(slotMap[key]);
if (!el) return;
var html = cards && cards[key] ? cards[key] : '';
if (html) {
setHtml(el, html);
el.style.display = '';
} else {
setHtml(el, '');
if (key === 'fleet') el.style.display = 'none';
}
});
// Render integrations panel
var intEl = document.getElementById('ov-integrations');
if (intEl) {
var chStatuses = cards && cards.__channelStatuses ? cards.__channelStatuses : [];
var intHtml = '<div class="card-title">Integrations</div>';
if (Array.isArray(chStatuses) && chStatuses.length > 0) {
intHtml += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:0.75rem">';
chStatuses.forEach(function(ch) {
var dot = ch.connected ? 'background:#22c55e' : 'background:#ef4444';
var statusText = ch.connected ? 'Connected' : (ch.last_error || 'Disconnected');
var stats = '';
if (ch.messages_received != null) stats += '<div style="font-size:0.65rem;color:var(--muted)">In: ' + (ch.messages_received || 0) + ' / Out: ' + (ch.messages_sent || 0) + '</div>';
intHtml += '<div style="padding:0.65rem;border:1px solid var(--border);border-radius:6px">'
+ '<div style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.3rem">'
+ '<span style="width:8px;height:8px;border-radius:50%;display:inline-block;' + dot + '"></span>'
+ '<span style="font-weight:600;font-size:0.8rem">' + esc(ch.name || 'unknown') + '</span>'
+ '</div>'
+ '<div style="font-size:0.7rem;color:' + (ch.connected ? 'var(--text)' : 'var(--warning, #f59e0b)') + '">' + esc(statusText.substring(0, 80)) + '</div>'
+ stats
+ '<button class="btn secondary channel-test-btn" data-test-channel="' + esc(ch.name || '') + '" style="font-size:0.65rem;padding:0.2rem 0.5rem;margin-top:0.35rem">Test</button>'
+ '</div>';
});
intHtml += '</div>';
} else {
intHtml += '<div style="font-size:0.75rem;color:var(--muted)">No integrations configured. Add channels in Settings.</div>';
}
setHtml(intEl, intHtml);
}
// Render throttle / rate-limit panel
var thrEl = document.getElementById('ov-throttle');
if (thrEl) {
var thr = cards && cards.__throttle ? cards.__throttle : {};
var thrHtml = '<div class="card-title">Rate Limiting</div>';
var gCount = Number(thr.global_count) || 0;
var gCap = Number(thr.global_capacity) || 1;
var pct = Math.min(100, Math.round((gCount / gCap) * 100));
var barColor = pct >= 90 ? '#ef4444' : (pct >= 70 ? '#f59e0b' : '#22c55e');
thrHtml += '<div style="margin-bottom:0.75rem">'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.25rem">Window utilization (' + esc(String(gCount)) + ' / ' + esc(String(gCap)) + ')</div>'
+ '<div style="background:var(--border);border-radius:4px;height:8px;overflow:hidden">'
+ '<div style="width:' + pct + '%;height:100%;background:' + barColor + ';border-radius:4px;transition:width 0.3s"></div>'
+ '</div>'
+ '</div>';
var activeIps = Number(thr.active_ips) || 0;
var activeActors = Number(thr.active_actors) || 0;
var throttledGlobal = Number(thr.throttled_global) || 0;
var windowSecs = Number(thr.window_secs) || 60;
thrHtml += '<div style="display:flex;gap:1.5rem;margin-bottom:0.75rem;font-size:0.75rem">'
+ '<div><span style="color:var(--muted)">Active IPs:</span> ' + esc(String(activeIps)) + '</div>'
+ '<div><span style="color:var(--muted)">Active Actors:</span> ' + esc(String(activeActors)) + '</div>'
+ '<div><span style="color:var(--muted)">Throttled (global):</span> ' + esc(String(throttledGlobal)) + '</div>'
+ '<div><span style="color:var(--muted)">Window:</span> ' + esc(String(windowSecs)) + 's</div>'
+ '</div>';
var topIps = Array.isArray(thr.top_throttled_ips) ? thr.top_throttled_ips : [];
var topActors = Array.isArray(thr.top_throttled_actors) ? thr.top_throttled_actors : [];
thrHtml += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">';
thrHtml += '<div>';
thrHtml += '<div style="font-size:0.7rem;font-weight:600;margin-bottom:0.35rem">Top Throttled IPs</div>';
if (topIps.length > 0) {
thrHtml += '<table style="width:100%;font-size:0.7rem;border-collapse:collapse">';
thrHtml += '<tr style="color:var(--muted)"><td style="padding:0.15rem 0.5rem 0.15rem 0">IP</td><td style="padding:0.15rem 0;text-align:right">Count</td></tr>';
topIps.forEach(function(entry) {
thrHtml += '<tr><td style="padding:0.15rem 0.5rem 0.15rem 0">' + esc(String(entry[0])) + '</td><td style="padding:0.15rem 0;text-align:right">' + esc(String(entry[1])) + '</td></tr>';
});
thrHtml += '</table>';
} else {
thrHtml += '<div style="font-size:0.7rem;color:var(--muted)">No throttled IPs</div>';
}
thrHtml += '</div>';
thrHtml += '<div>';
thrHtml += '<div style="font-size:0.7rem;font-weight:600;margin-bottom:0.35rem">Top Throttled Actors</div>';
if (topActors.length > 0) {
thrHtml += '<table style="width:100%;font-size:0.7rem;border-collapse:collapse">';
thrHtml += '<tr style="color:var(--muted)"><td style="padding:0.15rem 0.5rem 0.15rem 0">Actor</td><td style="padding:0.15rem 0;text-align:right">Count</td></tr>';
topActors.forEach(function(entry) {
thrHtml += '<tr><td style="padding:0.15rem 0.5rem 0.15rem 0">' + esc(String(entry[0])) + '</td><td style="padding:0.15rem 0;text-align:right">' + esc(String(entry[1])) + '</td></tr>';
});
thrHtml += '</table>';
} else {
thrHtml += '<div style="font-size:0.7rem;color:var(--muted)">No throttled actors</div>';
}
thrHtml += '</div>';
thrHtml += '</div>';
setHtml(thrEl, thrHtml);
}
};
App.renderOverview = function() {
return Promise.all([
fetchWithFallback('/api/agent/status', {}, 'agent status'),
fetchWithFallback('/api/health', {}, 'health'),
fetchWithFallback('/api/sessions', { sessions: [] }, 'sessions'),
fetchWithFallback('/api/skills', { skills: [] }, 'skills'),
fetchWithFallback('/api/cron/jobs', { jobs: [] }, 'cron jobs'),
fetchWithFallback('/api/stats/cache', { hit_rate: 0 }, 'cache stats'),
fetchWithFallback('/api/stats/timeseries?hours=24', { series: {}, labels: [] }, 'timeseries'),
fetchWithFallback('/api/wallet/balance', {}, 'wallet'),
fetchWithFallback('/api/breaker/status', {}, 'breaker'),
fetchWithFallback('/api/workspace/state', { agents: [] }, 'workspace'),
fetchWithFallback('/api/stats/costs', { costs: [] }, 'cost stats'),
fetchWithFallback('/api/channels/status', [], 'channel status'),
fetchWithFallback('/api/stats/throttle', {}, 'throttle stats')
]).then(function(arr) {
sparklineId = 0; stackedId = 0;
var failures = arr.filter(function(r) { return !r.ok; });
var agent = arr[0].data, health = arr[1].data, sessions = arr[2].data, skills = arr[3].data;
var cron = arr[4].data, cache = arr[5].data, timeseries = arr[6].data, wallet = arr[7].data, breaker = arr[8].data;
var wsState = arr[9].data, costData = arr[10].data, channelStatuses = arr[11].data || [];
var throttleData = arr[12].data || {};
_cachedWorkspace = wsState;
var state = (agent.state || 'unknown').toLowerCase();
applySidebarIdentity(agent, health);
if (agent.agent_id) App._activeAgentId = String(agent.agent_id);
var sessionCount = (sessions.sessions || []).length;
var skillList = skills.skills || [];
var skillCount = skillList.length;
var enabledSkills = skillList.filter(function(s) { return s.enabled; }).length;
var jobCount = (cron.jobs || []).length;
var hitRate = cache.hit_rate != null ? cache.hit_rate : 0;
var balance = Number(wallet.balance) || 0;
var costs = costData.costs || [];
var totalCost = costs.reduce(function(s, c) { return s + (Number(c.cost) || 0); }, 0);
var totalTokens = costs.reduce(function(s, c) { return s + (Number(c.tokens_in) || 0) + (Number(c.tokens_out) || 0); }, 0);
var cronErrors = (cron.jobs || []).reduce(function(s, j) { return s + (j.consecutive_errors || 0); }, 0);
var agents = wsState.agents || [];
function agentActivityScore(a) {
var activity = (a.activity || '').toString().toLowerCase();
var state = (a.state || '').toString().toLowerCase();
if (activity === 'idle' || activity === 'standby') return 0;
if (activity === 'inference' || activity === 'working' || activity === 'tool_execution' || activity === 'tooling') return 1;
if (activity === 'walking' || activity === 'moving' || activity === 'talking') return 0.5;
if (state === 'running' && activity) return 0.6;
return 0;
}
var agentRunning = agents.filter(function(a) { return (a.state || '').toLowerCase() === 'running'; });
var agentActivity = {};
agents.forEach(function(a) { agentActivity[a.name || a.id] = agentActivityScore(a); });
var agentActiveNow = Object.keys(agentActivity).filter(function(k) { return (agentActivity[k] || 0) > 0; }).length;
var costPerHr = totalCost > 0 ? totalCost / 24 : 0;
var tokPerHr = totalTokens > 0 ? totalTokens / 24 : 0;
var agentLoadVal = agents.length > 0
? agents.reduce(function(sum, a) { return sum + agentActivityScore(a); }, 0) / agents.length
: 0;
var cronSuccessVal = cronErrors > 0 ? 0.9 : (jobCount > 0 ? 1.0 : 0);
var s = (timeseries && timeseries.series) || {};
var buckets = (timeseries && timeseries.labels && timeseries.labels.length) ? timeseries.labels.length : 24;
var TS = {
costPerHour: (s.cost_per_hour || []).map(Number),
tokensPerHour: (s.tokens_per_hour || []).map(Number),
cacheHitRate: repeatedSeries(buckets, hitRate || 0),
walletBalance: repeatedSeries(buckets, balance || 0),
requestLatency: (s.latency_p50_ms || []).map(Number),
sessionsPerHour: (s.sessions_per_hour || []).map(Number),
memoryEntries: repeatedSeries(buckets, 0),
cronSuccess: (s.cron_success_rate || []).map(Number),
breakerFailures: repeatedSeries(buckets, 0),
agentLoad: repeatedSeries(buckets, agentLoadVal),
};
if (TS.costPerHour.length === 0) TS.costPerHour = repeatedSeries(buckets, costPerHr);
if (TS.tokensPerHour.length === 0) TS.tokensPerHour = repeatedSeries(buckets, tokPerHr);
if (TS.requestLatency.length === 0) TS.requestLatency = repeatedSeries(buckets, 0);
if (TS.sessionsPerHour.length === 0) TS.sessionsPerHour = repeatedSeries(buckets, sessionCount);
if (TS.cronSuccess.length === 0) TS.cronSuccess = repeatedSeries(buckets, cronSuccessVal);
TS.errorRate = TS.cronSuccess.map(function(v) { return Math.max(0, 1 - v); });
pushFleetSnapshot(agents, agentActivity);
TS.agentLoadByAgent = {};
var agentCount = agents.length;
agents.forEach(function(a) {
var k = a.name || a.id;
var history = (_fleetHistory.byAgent[k] || []).slice();
if (history.length === 0) history = [agentActivity[k] || 0];
// Normalize: each agent contributes at most 1/agentCount to the
// stacked total so that 100% = all agents active simultaneously.
TS.agentLoadByAgent[k] = history.map(function(v) { return v / agentCount; });
});
TS.agentLoadLabels = _fleetHistory.labels.slice();
var costLast = seriesLast(TS.costPerHour), costPrev = seriesPrev(TS.costPerHour);
var tokLast = seriesLast(TS.tokensPerHour), tokPrev = seriesPrev(TS.tokensPerHour);
var hitLast = seriesLast(TS.cacheHitRate), hitPrev = seriesPrev(TS.cacheHitRate);
var balLast = seriesLast(TS.walletBalance), balPrev = seriesPrev(TS.walletBalance);
var latLast = seriesLast(TS.requestLatency), latPrev = seriesPrev(TS.requestLatency);
var memLast = seriesLast(TS.memoryEntries);
var cardCost = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Inference Cost (24h)</div><div class="cc-value">$' + totalCost.toFixed(2) + '</div><div class="cc-sub">$' + costLast.toFixed(4) + '/hr current</div></div><div class="cc-right">' + deltaHtml(costLast, costPrev) + '</div></div>' + renderSparkCanvas(TS.costPerHour, { color: '#c180ff' }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">Avg / hr</div><div class="cc-stat-value">$' + seriesAvg(TS.costPerHour).toFixed(4) + '</div></div><div class="cc-stat"><div class="cc-stat-label">Peak</div><div class="cc-stat-value">$' + seriesMax(TS.costPerHour).toFixed(4) + '</div></div><div class="cc-stat"><div class="cc-stat-label">Requests</div><div class="cc-stat-value">' + costs.length + '</div></div></div></div>';
var cardTokens = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Token Throughput</div><div class="cc-value">' + Math.round(tokLast).toLocaleString() + '</div><div class="cc-sub">tokens/hr current</div></div><div class="cc-right">' + deltaHtml(tokLast, tokPrev) + '</div></div>' + renderSparkCanvas(TS.tokensPerHour, { color: '#8b5cf6' }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">Avg / hr</div><div class="cc-stat-value">' + Math.round(seriesAvg(TS.tokensPerHour)).toLocaleString() + '</div></div><div class="cc-stat"><div class="cc-stat-label">Peak</div><div class="cc-stat-value">' + Math.round(seriesMax(TS.tokensPerHour)).toLocaleString() + '</div></div><div class="cc-stat"><div class="cc-stat-label">Total (24h)</div><div class="cc-stat-value">' + totalTokens.toLocaleString() + '</div></div></div></div>';
var cardCache = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Cache Hit Rate</div><div class="cc-value">' + (hitLast * 100).toFixed(1) + '%</div><div class="cc-sub">' + (seriesAvg(TS.cacheHitRate) * 100).toFixed(1) + '% avg over 24h</div></div><div class="cc-right">' + deltaHtml(hitLast, hitPrev) + '</div></div>' + renderSparkCanvas(TS.cacheHitRate, { color: '#22c55e' }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">Floor</div><div class="cc-stat-value">' + (Math.min.apply(null, TS.cacheHitRate) * 100).toFixed(1) + '%</div></div><div class="cc-stat"><div class="cc-stat-label">Ceiling</div><div class="cc-stat-value">' + (seriesMax(TS.cacheHitRate) * 100).toFixed(1) + '%</div></div></div></div>';
var cardWallet = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Wallet Balance</div><div class="cc-value">' + balLast.toFixed(3) + ' ' + esc(wallet.currency || 'SOL') + '</div><div class="cc-sub">managed wallet</div></div><div class="cc-right">' + deltaHtml(balLast, balPrev) + '</div></div>' + renderSparkCanvas(TS.walletBalance, { color: '#f59e0b' }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">24h Low</div><div class="cc-stat-value">' + Math.min.apply(null, TS.walletBalance).toFixed(3) + '</div></div><div class="cc-stat"><div class="cc-stat-label">24h High</div><div class="cc-stat-value">' + seriesMax(TS.walletBalance).toFixed(3) + '</div></div></div></div>';
var cardLatency = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Request Latency</div><div class="cc-value">' + Math.round(latLast) + 'ms</div><div class="cc-sub">p50 current</div></div><div class="cc-right">' + deltaHtml(latPrev, latLast) + '</div></div>' + renderSparkCanvas(TS.requestLatency, { color: '#06b6d4' }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">Avg</div><div class="cc-stat-value">' + Math.round(seriesAvg(TS.requestLatency)) + 'ms</div></div><div class="cc-stat"><div class="cc-stat-label">p99</div><div class="cc-stat-value">' + Math.round(seriesMax(TS.requestLatency)) + 'ms</div></div></div></div>';
var cardSessions = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Sessions</div><div class="cc-value">' + sessionCount + ' active</div></div><div class="cc-right"><span class="cc-delta flat">' + sessionCount + ' open</span></div></div>' + renderSparkCanvas(TS.sessionsPerHour, { color: '#ec4899' }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">Skills</div><div class="cc-stat-value">' + enabledSkills + '/' + skillCount + '</div></div><div class="cc-stat"><div class="cc-stat-label">Cron jobs</div><div class="cc-stat-value">' + jobCount + '</div></div></div></div>';
var healthStatus = String(health.status || 'unknown').toLowerCase();
var healthClass = healthStatus === 'ok' || healthStatus === 'healthy' ? 'success' : (healthStatus === 'warning' ? 'warning' : 'error');
var breakerState = String(breaker.note || 'closed').toLowerCase();
var breakerClass = breakerState.indexOf('open') >= 0 ? 'error' : (breakerState.indexOf('half') >= 0 ? 'warning' : 'success');
var cardSystem = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">System Health</div><div class="cc-value"><span class="badge ' + healthClass + '">' + esc(health.status || 'unknown') + '</span></div><div class="cc-sub">uptime ' + formatUptime(health.uptime_seconds) + '</div></div><div class="cc-right"><span class="cc-delta up">' + (seriesLast(TS.cronSuccess) * 100).toFixed(0) + '% cron</span></div></div>' + renderSparkCanvas(TS.cronSuccess, { color: '#22c55e' }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">Breaker</div><div class="cc-stat-value"><span class="badge ' + breakerClass + '" style="font-size:0.6875rem">' + esc(breaker.note || 'closed') + '</span></div></div><div class="cc-stat"><div class="cc-stat-label">Cron errors</div><div class="cc-stat-value">' + cronErrors + '</div></div></div></div>';
var errLast = seriesLast(TS.errorRate), errPrev = seriesPrev(TS.errorRate);
var errPeak = seriesMax(TS.errorRate);
var errAvg = seriesAvg(TS.errorRate);
var totalInferenceErrors = costs.filter(function(c) { return c.error; }).length;
var errColor = errLast > 0.05 ? '#ef4444' : '#22c55e';
var cardErrors = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Error Rate</div><div class="cc-value">' + (errLast * 100).toFixed(1) + '%</div><div class="cc-sub">' + (errAvg * 100).toFixed(1) + '% avg over 24h</div></div><div class="cc-right">' + deltaHtml(errPrev, errLast) + '</div></div>' + renderSparkCanvas(TS.errorRate, { color: errColor }) + '<div class="cc-footer"><div class="cc-stat"><div class="cc-stat-label">Peak</div><div class="cc-stat-value">' + (errPeak * 100).toFixed(1) + '%</div></div><div class="cc-stat"><div class="cc-stat-label">Inference errs</div><div class="cc-stat-value">' + totalInferenceErrors + '</div></div><div class="cc-stat"><div class="cc-stat-label">Cron errs</div><div class="cc-stat-value">' + cronErrors + '</div></div></div></div>';
var cardFleet = '';
if (agents.length > 0) {
var fleetColors = {};
var fleetLegend = agents.map(function(a) {
var k = a.name || a.id;
var isRunning = (a.state || '').toLowerCase() === 'running';
var lastVal = agentActivity[k] || 0;
var stateLabel = (a.activity || a.state || 'idle').toString().toLowerCase();
var color = a.color || '#c180ff';
fleetColors[k] = color;
return '<div class="fleet-legend-item">' +
'<div class="fleet-legend-swatch" style="background:' + color + '"></div>' +
'<span class="fleet-legend-name">' + esc(k) + '</span>' +
'<span class="fleet-legend-state ' + (isRunning ? 'running' : 'idle') + '">' + esc(stateLabel) + '</span>' +
'<span class="fleet-legend-pct">' + (lastVal * 100).toFixed(0) + '%</span>' +
'</div>';
}).join('');
var fleetKeys = agents.map(function(a) { return a.name || a.id; });
var fleetChart = renderStackedArea(
TS.agentLoadByAgent,
fleetKeys,
fleetColors,
{
height: 190,
labels: TS.agentLoadLabels.length > 1,
xLabels: TS.agentLoadLabels,
yAxis: true,
fixedMax: 1.0,
yFormat: function(v) { return (v * 100).toFixed(0) + '%'; }
}
);
cardFleet = '<div class="card composite-card"><div class="cc-header"><div class="cc-left"><div class="cc-label">Agent Fleet Activity</div><div class="cc-value">' + (seriesLast(TS.agentLoad) * 100).toFixed(0) + '% active load</div><div class="cc-sub">' + agentActiveNow + ' / ' + agents.length + ' agents active now</div></div><div class="cc-right">' + deltaHtml(seriesLast(TS.agentLoad), seriesPrev(TS.agentLoad)) + '</div></div>' + fleetChart + '<div class="fleet-legend-grid">' + fleetLegend + '</div></div>';
}
var attention = [];
if (failures.length > 0) attention.push(failures.length + ' data source(s) unavailable: ' + failures.map(function(f) { return f.label; }).join(', '));
if (cronErrors > 0) attention.push('Cron has ' + cronErrors + ' consecutive error(s)');
if (breakerClass !== 'success') attention.push('Circuit breaker is ' + (breaker.note || 'not closed'));
if (healthClass !== 'success') attention.push('System health reports "' + (health.status || 'unknown') + '"');
return {
cost: cardCost,
tokens: cardTokens,
cache: cardCache,
wallet: cardWallet,
latency: cardLatency,
sessions: cardSessions,
system: cardSystem,
errors: cardErrors,
fleet: cardFleet,
__attention: attention,
__failures: failures,
__meta: { updated_at: new Date().toLocaleTimeString() },
__channelStatuses: channelStatuses,
__throttle: throttleData
};
});
};