diff --git a/cmd/web-ui/main.go b/cmd/web-ui/main.go
index a968aa9..817d810 100644
--- a/cmd/web-ui/main.go
+++ b/cmd/web-ui/main.go
@@ -99,6 +99,15 @@ func main() {
staticFS, _ := fs.Sub(staticFiles, "static")
fileServer := http.FileServer(http.FS(staticFS))
+ mux.HandleFunc("/favicon.svg", func(w http.ResponseWriter, r *http.Request) {
+ r.URL.Path = "/favicon.svg"
+ fileServer.ServeHTTP(w, r)
+ })
+
+ mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "/favicon.svg", http.StatusMovedPermanently)
+ })
+
mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/static")
fileServer.ServeHTTP(w, r)
diff --git a/cmd/web-ui/static/app.js b/cmd/web-ui/static/app.js
index ce81f07..839101b 100644
--- a/cmd/web-ui/static/app.js
+++ b/cmd/web-ui/static/app.js
@@ -47,12 +47,62 @@
});
}
+ // Cmd+K hint button
+ document.getElementById('cmd-k-hint')?.addEventListener('click', openCommandPalette);
+
+ // Global error tracking — persists across page navigations
+ subscribeWS(function globalErrorTracker(msg) {
+ if (msg.type !== 'message') return;
+ if (getEnvelopeType(msg.data) === 'error') {
+ incrementErrorBadge();
+ }
+ });
+
// Keyboard Shortcuts
+ let _pendingGoto = false;
document.addEventListener('keydown', (e) => {
- // '/' to focus search
- if (e.key === '/' && document.activeElement !== searchInput && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
+ // Ignore when typing in inputs
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
+ if (e.key === 'Escape') document.activeElement.blur();
+ return;
+ }
+
+ // Cmd+K or Ctrl+K — command palette
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
- searchInput.focus();
+ if (paletteOpen) closeCommandPalette();
+ else openCommandPalette();
+ return;
+ }
+
+ // '/' to focus search
+ if (e.key === '/' && !paletteOpen) {
+ e.preventDefault();
+ const searchInput = document.getElementById('global-search');
+ if (searchInput) searchInput.focus();
+ return;
+ }
+
+ // Escape closes palette
+ if (e.key === 'Escape' && paletteOpen) {
+ closeCommandPalette();
+ return;
+ }
+
+ // 'g' prefix for goto shortcuts
+ if (e.key === 'g' && !_pendingGoto) {
+ _pendingGoto = true;
+ setTimeout(() => { _pendingGoto = false; }, 800);
+ return;
+ }
+
+ if (_pendingGoto) {
+ _pendingGoto = false;
+ if (e.key === 'd') navigate('/');
+ else if (e.key === 's') navigate('/sessions');
+ else if (e.key === 'a') navigate('/agents');
+ else if (e.key === 'i') navigate('/infrastructure');
+ return;
}
});
});
@@ -104,8 +154,173 @@
`;
}
+
+ // ── Command Palette ─────────────────────────────────────
+ let paletteOpen = false;
+ let paletteSelectedIndex = 0;
+
+ function getCommandPaletteItems(query) {
+ const items = [
+ { label: 'Dashboard', path: '/', icon: '◉', shortcut: 'g d' },
+ { label: 'Sessions', path: '/sessions', icon: '▶', shortcut: 'g s' },
+ { label: 'Agents', path: '/agents', icon: '◎', shortcut: 'g a' },
+ { label: 'Infrastructure', path: '/infrastructure', icon: '⚡', shortcut: 'g i' },
+ { label: 'Toggle Theme', action: 'theme', icon: '◐' },
+ ];
+
+ // Add agent items dynamically
+ if (agentsState && agentsState.agents) {
+ for (const [key, agent] of Object.entries(agentsState.agents)) {
+ items.push({
+ label: 'Agent: ' + (agent.name || key),
+ path: '/agents',
+ action: 'select-agent',
+ agentKey: key,
+ icon: isAgentOnline(agent) ? '●' : '○',
+ });
+ }
+ }
+
+ if (!query) return items;
+ const q = query.toLowerCase();
+ return items.filter(item => item.label.toLowerCase().includes(q));
+ }
+
+ function openCommandPalette() {
+ if (paletteOpen) return;
+ paletteOpen = true;
+ paletteSelectedIndex = 0;
+
+ const backdrop = document.createElement('div');
+ backdrop.className = 'cmd-palette-backdrop';
+ backdrop.id = 'cmd-palette-backdrop';
+ backdrop.innerHTML = `
+
+ `;
+
+ document.body.appendChild(backdrop);
+ const input = document.getElementById('cmd-palette-input');
+ input.focus();
+ renderPaletteItems('');
+
+ input.addEventListener('input', () => {
+ paletteSelectedIndex = 0;
+ renderPaletteItems(input.value);
+ });
+
+ input.addEventListener('keydown', (e) => {
+ const items = document.querySelectorAll('.cmd-palette-item');
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ paletteSelectedIndex = Math.min(paletteSelectedIndex + 1, items.length - 1);
+ updatePaletteSelection();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ paletteSelectedIndex = Math.max(paletteSelectedIndex - 1, 0);
+ updatePaletteSelection();
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ const selected = items[paletteSelectedIndex];
+ if (selected) selected.click();
+ } else if (e.key === 'Escape') {
+ closeCommandPalette();
+ }
+ });
+
+ backdrop.addEventListener('click', (e) => {
+ if (e.target === backdrop) closeCommandPalette();
+ });
+ }
+
+ function closeCommandPalette() {
+ paletteOpen = false;
+ const backdrop = document.getElementById('cmd-palette-backdrop');
+ if (backdrop) backdrop.remove();
+ }
+
+ function renderPaletteItems(query) {
+ const container = document.getElementById('cmd-palette-results');
+ if (!container) return;
+ const items = getCommandPaletteItems(query);
+
+ // If query looks like an ID (8+ hex chars), add a search option
+ if (query.length >= 8) {
+ items.unshift({ label: 'Search for ID: ' + query, action: 'search', query, icon: '🔍' });
+ }
+
+ container.innerHTML = items.map((item, i) => `
+
+
${item.icon}
+
${escapeHTML(item.label)}
+ ${item.shortcut ? `
${item.shortcut}` : ''}
+
+ `).join('');
+
+ container.querySelectorAll('.cmd-palette-item').forEach((el, i) => {
+ el.addEventListener('click', () => executePaletteItem(items[i]));
+ el.addEventListener('mouseenter', () => {
+ paletteSelectedIndex = i;
+ updatePaletteSelection();
+ });
+ });
+ }
+
+ function updatePaletteSelection() {
+ document.querySelectorAll('.cmd-palette-item').forEach((el, i) => {
+ el.classList.toggle('selected', i === paletteSelectedIndex);
+ if (i === paletteSelectedIndex) el.scrollIntoView({ block: 'nearest' });
+ });
+ }
+
+ function executePaletteItem(item) {
+ closeCommandPalette();
+ if (item.action === 'theme') {
+ cycleTheme();
+ } else if (item.action === 'search') {
+ handleGlobalSearch(item.query);
+ } else if (item.action === 'select-agent') {
+ navigate('/agents');
+ setTimeout(() => selectAgent(item.agentKey, 'live'), 100);
+ } else if (item.path) {
+ navigate(item.path);
+ }
+ }
+
// ─────────────────────────────────────────────────────────
+ // ── Error Badge ─────────────────────────────────────────
+ let _unseenErrors = 0;
+
+ function incrementErrorBadge() {
+ if (window.location.pathname === '/') return; // On dashboard, don't badge
+ _unseenErrors++;
+ const badge = document.getElementById('nav-error-badge');
+ if (badge) {
+ badge.textContent = _unseenErrors > 99 ? '99+' : String(_unseenErrors);
+ badge.classList.add('visible');
+ }
+ }
+
+ function clearErrorBadge() {
+ _unseenErrors = 0;
+ const badge = document.getElementById('nav-error-badge');
+ if (badge) {
+ badge.classList.remove('visible');
+ badge.textContent = '';
+ }
+ }
+
const app = document.getElementById('app');
let ws = null;
@@ -116,9 +331,12 @@
let sessionsState = { sessions: [], cursor: null, activeSessionByBackend: {} };
let sessionsUnsubscribe = null;
+ // ── Session Filter Pills ────────────────────────────────
+ let sessionFilterMode = 'all';
let openclawState = { instances: {} };
let openclawUnsubscribe = null;
let infraUnsubscribe = null;
+ let _infraTimerInterval = null;
let swarmState = { services: {} }; // keyed by service name
let agentsState = createAgentsState();
let agentsUnsubscribe = null;
@@ -212,6 +430,10 @@
infraUnsubscribe();
infraUnsubscribe = null;
}
+ if (_infraTimerInterval) {
+ clearInterval(_infraTimerInterval);
+ _infraTimerInterval = null;
+ }
if (agentsUnsubscribe) {
agentsUnsubscribe();
agentsUnsubscribe = null;
@@ -254,23 +476,33 @@
cleanupLiveViews();
renderBreadcrumbs();
- const path = window.location.pathname;
- if (path === '/') {
- renderDashboard();
- } else if (path === '/sessions') {
- renderSessions();
- } else if (path.startsWith('/agents')) {
- renderAgents();
- } else if (path.startsWith('/infrastructure')) {
- renderInfrastructure();
- } else if (path.startsWith('/sessions/')) {
- renderSession(path.split('/sessions/')[1]);
- } else if (path.startsWith('/runs/')) {
- renderRun(path.split('/runs/')[1]);
- } else {
- app.innerHTML = '';
- }
- updateActiveNav();
+ app.classList.add('transitioning');
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ const path = window.location.pathname;
+ if (path === '/') {
+ renderDashboard();
+ } else if (path === '/sessions') {
+ renderSessions();
+ } else if (path.startsWith('/agents')) {
+ renderAgents();
+ } else if (path.startsWith('/infrastructure')) {
+ renderInfrastructure();
+ } else if (path.startsWith('/sessions/')) {
+ renderSession(path.split('/sessions/')[1]);
+ } else if (path.startsWith('/runs/')) {
+ renderRun(path.split('/runs/')[1]);
+ } else {
+ app.innerHTML = '';
+ }
+ updateActiveNav();
+
+ // Fade back in
+ requestAnimationFrame(() => {
+ app.classList.remove('transitioning');
+ });
+ }, 80);
+ });
}
function renderBreadcrumbs() {
@@ -365,6 +597,58 @@
.replace(/'/g, ''');
}
+ function animateCounter(elementId, newValue) {
+ const elem = document.getElementById(elementId);
+ if (!elem) return;
+ const oldText = elem.textContent;
+ const newText = String(newValue);
+ if (oldText === newText) return;
+ elem.textContent = newText;
+ elem.classList.remove('bumped');
+ void elem.offsetWidth; // force reflow
+ elem.classList.add('bumped');
+ }
+
+ // ── Dashboard Sparklines ────────────────────────────────
+ function buildSparklineSVG(values, color) {
+ if (!values || values.length < 2) return '';
+ const max = Math.max(...values, 1);
+ const w = 100;
+ const h = 30;
+ const points = values.map((v, i) => {
+ const x = (i / (values.length - 1)) * w;
+ const y = h - (v / max) * h;
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
+ });
+ const polyline = points.join(' ');
+ // Area fill: close the path along the bottom
+ const areaPath = `M0,${h} L${points.map(p => p).join(' L')} L${w},${h} Z`;
+ return ``;
+ }
+
+ function renderDashSparklines() {
+ const ts = dashboardState.timeseries;
+ if (!ts || !ts.series || ts.series.length < 2) return;
+ const cards = document.querySelectorAll('.summary-card');
+ if (cards.length < 4) return;
+
+ const runsData = ts.series.map(b => b.runs || 0);
+ const toolsData = ts.series.map(b => b.tools || 0);
+ const errorsData = ts.series.map(b => b.errors || 0);
+ const totalData = ts.series.map((b, i) => runsData[i] + toolsData[i] + errorsData[i]);
+
+ // Remove existing sparklines
+ cards.forEach(c => { const s = c.querySelector('.summary-card-sparkline'); if (s) s.remove(); });
+
+ cards[0].insertAdjacentHTML('beforeend', buildSparklineSVG(totalData, 'var(--accent)'));
+ cards[1].insertAdjacentHTML('beforeend', buildSparklineSVG(runsData, 'var(--success)'));
+ cards[2].insertAdjacentHTML('beforeend', buildSparklineSVG(toolsData, 'var(--purple)'));
+ cards[3].insertAdjacentHTML('beforeend', buildSparklineSVG(errorsData, 'var(--error)'));
+ }
+
function relativeTime(ts) {
if (!ts) {
return '-';
@@ -411,6 +695,41 @@
).join('');
}
+ // ── Content-Aware Skeletons ─────────────────────────────
+ function dashboardSkeleton() {
+ return `
+ ${Array(4).fill('
').join('')}
+
+ `;
+ }
+
+ function sessionsSkeleton() {
+ // Returns rows suitable for insertion into a
+ return Array(8).fill(0).map((_, i) => {
+ const widths = [['55%','25%'], ['65%','20%'], ['45%','30%'], ['70%','15%'], ['50%','22%'], ['60%','18%'], ['42%','28%'], ['68%','12%']];
+ const [w1, w2] = widths[i % widths.length];
+ return `
+ |
+ |
+ |
+ |
+ |
+
`;
+ }).join('');
+ }
+
+ function agentsSkeleton() {
+ return `
+ ${Array(4).fill('
').join('')}
+
`;
+ }
+
+ function infrastructureSkeleton() {
+ return `
+ ${Array(6).fill('
').join('')}
+
`;
+ }
+
function extractEnvelope(record) {
if (record && typeof record === 'object' && record.payload && record.payload.event && record.payload.schema) {
return record.payload;
@@ -536,11 +855,51 @@
function refreshSessionsTable() {
const tbody = document.getElementById('sessions-body');
if (!tbody) return;
- const groups = groupSessionsByDate(sessionsState.sessions);
+
+ // Update pill counts based on full unfiltered sessions list
+ const all = sessionsState.sessions;
+ const activeCount = all.filter(s => isSessionActive(s)).length;
+ const endedCount = all.filter(s => !isSessionActive(s)).length;
+ const erroredCount = all.filter(s => (s._errorCount || 0) > 0).length;
+ const pillDefs = [
+ { filter: 'all', count: all.length },
+ { filter: 'active', count: activeCount },
+ { filter: 'ended', count: endedCount },
+ { filter: 'errored', count: erroredCount },
+ ];
+ pillDefs.forEach(({ filter, count }) => {
+ const btn = document.querySelector(`#session-pills [data-filter="${filter}"]`);
+ if (!btn) return;
+ let countEl = btn.querySelector('.pill-count');
+ if (!countEl) {
+ countEl = document.createElement('span');
+ countEl.className = 'pill-count';
+ btn.appendChild(countEl);
+ }
+ countEl.textContent = count;
+ });
+
+ // Apply filter
+ let filtered = sessionsState.sessions;
+ if (sessionFilterMode === 'active') {
+ filtered = filtered.filter(s => isSessionActive(s));
+ } else if (sessionFilterMode === 'ended') {
+ filtered = filtered.filter(s => !isSessionActive(s));
+ } else if (sessionFilterMode === 'errored') {
+ filtered = filtered.filter(s => (s._errorCount || 0) > 0);
+ }
+
+ const groups = groupSessionsByDate(filtered);
if (groups.length === 0) {
tbody.innerHTML = '| No sessions found |
';
return;
}
+ const allFiltered = groups.flatMap(g => g.items);
+ const maxDuration = Math.max(...allFiltered.map(s => {
+ const start = new Date(s.started_at).getTime();
+ const end = s.ended_at ? new Date(s.ended_at).getTime() : Date.now();
+ return end - start;
+ }), 1);
tbody.innerHTML = groups.map(group => {
const rows = group.items.map(s => {
const fw = s.framework || 'unknown';
@@ -550,13 +909,19 @@
const dotTitle = dotState === 'active'
? 'Currently active session'
: (active ? 'Open session' : 'Session ended');
+ const rowClass = active ? 'clickable active-session' : 'clickable';
+ const start = new Date(s.started_at).getTime();
+ const end = s.ended_at ? new Date(s.ended_at).getTime() : Date.now();
+ const duration = end - start;
+ const barWidth = Math.max(4, (duration / maxDuration) * 80);
+ const durationBar = ``;
return `
-
+
| ${escapeHTML(s.session_id.substring(0, 12))}…${renderCopyButton(s.session_id)} |
${escapeHTML(fw)} |
${escapeHTML(s.host || '-')} |
${s.run_count} |
- ${escapeHTML(relativeTime(s.started_at))} |
+ ${escapeHTML(relativeTime(s.started_at))}${durationBar} |
`;
}).join('');
return `| ${escapeHTML(group.label)} |
${rows}`;
@@ -567,6 +932,9 @@
}
async function renderSessions() {
+ // Reset filter mode on each page visit
+ sessionFilterMode = 'all';
+
app.innerHTML = `
+
+
+
+
+
+
@@ -592,12 +966,22 @@
| Time |
- ${skeletonRows(8, 5)}
+ ${sessionsSkeleton()}
`;
+ // Wire up filter pill click handlers
+ document.querySelectorAll('#session-pills .filter-pill').forEach(btn => {
+ btn.addEventListener('click', () => {
+ document.querySelectorAll('#session-pills .filter-pill').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ sessionFilterMode = btn.dataset.filter;
+ refreshSessionsTable();
+ });
+ });
+
api('/v1/stats/summary').then(data => {
const sel = document.getElementById('filter-framework');
if (!sel || !data.by_framework) return;
@@ -668,7 +1052,12 @@
const row = tbody.querySelector(`[data-session="${s.session_id}"]`);
if (row) {
const td = row.cells[4];
- if (td) td.textContent = relativeTime(s.started_at);
+ if (td) {
+ // Update only the text node, preserving the duration bar span
+ const bar = td.querySelector('.session-duration-bar');
+ td.textContent = relativeTime(s.started_at);
+ if (bar) td.appendChild(bar);
+ }
}
});
}
@@ -722,6 +1111,13 @@
}
}
+ if (eventType === 'error' && sessionId) {
+ const session = sessionsState.sessions.find(s => s.session_id === sessionId);
+ if (session) {
+ session._errorCount = (session._errorCount || 0) + 1;
+ }
+ }
+
refreshSessionsTable();
}
@@ -836,7 +1232,7 @@
const correlation = getEnvelopeCorrelation(msg.data);
if (correlation?.session_id !== sessionID) return;
const eventType = getEnvelopeType(msg.data);
- if (!['run.start', 'run.end', 'session.end', 'error'].includes(eventType)) return;
+ if (!['run.start', 'run.end', 'span.start', 'span.end', 'session.end', 'error'].includes(eventType)) return;
clearTimeout(_sessionReloadTimer);
_sessionReloadTimer = setTimeout(() => loadSessionData(sessionID), 300);
}
@@ -1241,7 +1637,7 @@
}
async function renderInfrastructure() {
- app.innerHTML = 'Loading...
';
+ app.innerHTML = `${infrastructureSkeleton()}
`;
infraUnsubscribe = subscribeWS(handleInfraWS);
@@ -1364,6 +1760,14 @@
}
`;
+
+ // Start freshness timer — update "Updated X ago" text every 10s
+ if (_infraTimerInterval) clearInterval(_infraTimerInterval);
+ _infraTimerInterval = setInterval(() => {
+ document.querySelectorAll('.freshness-timer[data-ts]').forEach(el => {
+ el.textContent = 'Updated ' + relativeTime(el.dataset.ts);
+ });
+ }, 10000);
}
function renderVMCard(name) {
@@ -1382,7 +1786,7 @@
${host.state === 'running' ? 'Running' : 'Stopped'}
- Updated ${escapeHTML(relativeTime(getEnvelopeTS(evt)))}
+ Updated ${escapeHTML(relativeTime(getEnvelopeTS(evt)))}
| Host | ${escapeHTML(inst.host || '-')} |
| Domain | ${escapeHTML(inst.domain || '-')} |
@@ -1434,10 +1838,11 @@
}
function serviceCardHeader(svc) {
+ const uptimeBadge = getUptimeBadge(svc.uptime_sec);
return `
-
+ ${agentsSkeleton()}
`;
bindAgentViewToggle();
@@ -2038,6 +2452,31 @@
}
}
+ // ── Agent Lane Sparklines ───────────────────────────────
+ function buildAgentActivityBars(agent, bucketCount) {
+ const events = agent.events || [];
+ if (events.length === 0) return '';
+ const count = bucketCount || 20;
+ const now = Date.now();
+ const windowMS = 3600000; // 1 hour
+ const bucketMS = windowMS / count;
+ const buckets = new Array(count).fill(0);
+
+ for (const evt of events) {
+ const ts = new Date(getEnvelopeTS(evt)).getTime();
+ const age = now - ts;
+ if (age > windowMS || age < 0) continue;
+ const idx = Math.min(count - 1, Math.floor((windowMS - age) / bucketMS));
+ buckets[idx]++;
+ }
+
+ const max = Math.max(...buckets, 1);
+ return `${buckets.map(b => {
+ const pct = (b / max * 100).toFixed(0);
+ return `
`;
+ }).join('')}
`;
+ }
+
function renderAgentLanes() {
const contentEl = document.getElementById('agents-content');
if (!contentEl) return;
@@ -2108,6 +2547,7 @@
${escapeHTML(agent.name || key)}
${escapeHTML(agent.framework || 'unknown')}${agent.host && agent.host !== agent.name ? ' · ' + escapeHTML(agent.host) : ''}
+ ${buildAgentActivityBars(agent)}
${statusText}
@@ -2790,6 +3230,7 @@
}
async function renderDashboard() {
+ clearErrorBadge();
dashboardState = {
summary: null,
timeseries: null,
@@ -2970,7 +3411,7 @@
const cachedSummary = tryParseJSON(localStorage.getItem('agentmon:dash:summary'));
const cachedTS = tryParseJSON(localStorage.getItem('agentmon:dash:ts:' + dashboardState.window));
if (cachedSummary) { dashboardState.summary = cachedSummary; renderSummaryCards(); }
- if (cachedTS) { dashboardState.timeseries = cachedTS; renderTimeseriesChart(); renderRightPanel(); }
+ if (cachedTS) { dashboardState.timeseries = cachedTS; renderTimeseriesChart(); renderDashSparklines(); renderRightPanel(); }
try {
const [summaryData, tsData, recentData, snapshots, swarmSnaps, topToolsData, topModelsData] = await Promise.all([
@@ -2995,6 +3436,7 @@
localStorage.setItem('agentmon:dash:ts:' + dashboardState.window, JSON.stringify(tsData));
renderSummaryCards();
renderTimeseriesChart();
+ renderDashSparklines();
renderRightPanel();
// Seed tool counts from the dedicated top-tools endpoint
@@ -3149,15 +3591,10 @@
const s = dashboardState.summary;
if (!s) return;
- const el = (id, val) => {
- const e = document.getElementById(id);
- if (e) e.textContent = String(val);
- };
-
- el('dash-active', s.active_sessions);
- el('dash-runs', s.runs_today);
- el('dash-tools', s.tool_calls_today);
- el('dash-errors', s.errors_today);
+ animateCounter('dash-active', s.active_sessions);
+ animateCounter('dash-runs', s.runs_today);
+ animateCounter('dash-tools', s.tool_calls_today);
+ animateCounter('dash-errors', s.errors_today);
// Sub-line: framework breakdown for active sessions
const fws = Object.keys(s.by_framework || {});
@@ -3172,15 +3609,15 @@
}
// Metrics strip
- el('dash-tokens-today', formatTokenCount(s.tokens_today || 0));
- el('dash-cost-today', s.cost_today ? formatCost(s.cost_today) : '$0.0000');
- el('dash-avg-duration', s.avg_duration_ms ? formatDuration(s.avg_duration_ms) : '-');
+ animateCounter('dash-tokens-today', formatTokenCount(s.tokens_today || 0));
+ animateCounter('dash-cost-today', s.cost_today ? formatCost(s.cost_today) : '$0.0000');
+ animateCounter('dash-avg-duration', s.avg_duration_ms ? formatDuration(s.avg_duration_ms) : '-');
const errorRateEl = document.getElementById('dash-error-rate');
if (errorRateEl) {
const totalOps = (s.runs_today || 0) + (s.tool_calls_today || 0);
const rate = totalOps > 0 ? ((s.errors_today || 0) / totalOps * 100) : 0;
- errorRateEl.textContent = rate.toFixed(1) + '%';
+ animateCounter('dash-error-rate', rate.toFixed(1) + '%');
errorRateEl.classList.toggle('alert', rate > 5);
}
}
@@ -3194,12 +3631,13 @@
}
dashboardState.chartCursorIndex = null;
const cachedWin = tryParseJSON(localStorage.getItem('agentmon:dash:ts:' + dashboardState.window));
- if (cachedWin) { dashboardState.timeseries = cachedWin; renderTimeseriesChart(); renderRightPanel(); }
+ if (cachedWin) { dashboardState.timeseries = cachedWin; renderTimeseriesChart(); renderDashSparklines(); renderRightPanel(); }
const data = await api('/v1/stats/timeseries?window=' + dashboardState.window);
if (!isCurrentPath('/')) return;
dashboardState.timeseries = data;
localStorage.setItem('agentmon:dash:ts:' + dashboardState.window, JSON.stringify(data));
renderTimeseriesChart();
+ renderDashSparklines();
renderRightPanel();
} catch (e) {
console.error('Failed to load timeseries:', e);
@@ -3500,6 +3938,7 @@
dashboardState.chartCursorIndex = ts.series.length - 1;
renderTimeseriesChart();
+ renderDashSparklines();
}
function renderRightPanel() {
diff --git a/cmd/web-ui/static/favicon.svg b/cmd/web-ui/static/favicon.svg
new file mode 100644
index 0000000..8ad1da5
--- /dev/null
+++ b/cmd/web-ui/static/favicon.svg
@@ -0,0 +1,15 @@
+
diff --git a/cmd/web-ui/static/index.html b/cmd/web-ui/static/index.html
index fe45396..c144bb6 100644
--- a/cmd/web-ui/static/index.html
+++ b/cmd/web-ui/static/index.html
@@ -4,6 +4,7 @@
agentmon
+
@@ -16,11 +17,14 @@
-
+