From c44e7fe72e0ddf3556c3a0f0ccefee81cc01b357 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 22 May 2026 12:21:48 -0700 Subject: [PATCH] refactor(web-ui): extract shared component primitives Introduce components.js with barTrack, barRow, barRankList, metricPill, metricStrip, and chartHeader helpers. Migrate dashboard.js and usage.js to use these primitives, replacing 13 families of duplicated CSS (stat-list, fw-bar, token-bar, metric-pill, chart-insight, chart-header, usage-chart-total, etc.) with a unified .am-* namespace. Net: -256 lines. Co-Authored-By: Claude Opus 4.7 --- cmd/web-ui/static/modules/components.js | 102 ++++ cmd/web-ui/static/modules/pages/dashboard.js | 208 +++----- cmd/web-ui/static/modules/pages/usage.js | 92 +--- cmd/web-ui/static/style.css | 520 +++++++------------ 4 files changed, 384 insertions(+), 538 deletions(-) create mode 100644 cmd/web-ui/static/modules/components.js diff --git a/cmd/web-ui/static/modules/components.js b/cmd/web-ui/static/modules/components.js new file mode 100644 index 0000000..7eae4d1 --- /dev/null +++ b/cmd/web-ui/static/modules/components.js @@ -0,0 +1,102 @@ +// ── components.js — shared UI primitives ───────────────── +// +// One-stop helpers for the repeating bar / pill / chart-header +// patterns across pages. Renderers return HTML strings — callers +// inject via innerHTML and wire events on the resulting DOM. + +import { escapeHTML } from './utils.js'; + +// ── Bar primitives ─────────────────────────────────────── + +// barTrack — just the percentage bar (no label/count head). +// value: numeric value to display +// max: ceiling for percentage; 0/falsy means 0% +// fwClass: optional framework slug (openclaw, claude-code, hermes, …) +// — applied as fw- on the fill for color +// size: 'xs' | 'sm' | 'md' | 'lg' (default 'md') +// modifier: extra class on the fill (e.g. 'model', 'input', 'output') +export function barTrack({ value = 0, max = 0, fwClass = '', size = 'md', modifier = '' } = {}) { + const pct = max > 0 ? Math.min(100, (value / max) * 100).toFixed(1) : '0'; + const fillClasses = ['am-bar-fill']; + if (fwClass) fillClasses.push('fw-' + fwClass); + if (modifier) fillClasses.push(modifier); + return `
`; +} + +// barRow — head (name + count) plus track. +// countDisplay overrides the rendered count text (e.g. "238" vs "238 events"). +export function barRow({ name = '', count = 0, countDisplay, max = 0, fwClass = '', size = 'sm', modifier = '' } = {}) { + const display = countDisplay != null ? countDisplay : count; + return ` +
+
+ ${escapeHTML(String(name))} + ${escapeHTML(String(display))} +
+ ${barTrack({ value: count, max, fwClass, size, modifier })} +
`; +} + +// barRankList — ranked list of barRows wrapped in
    . +// items: array of arbitrary objects +// mapItem: fn(item) → { name, count, countDisplay?, fwClass?, modifier? } +// maxOverride: ceiling override; default = max of items' counts +// size: bar height (default 'xs' for ranked lists) +// emptyText: shown when items is empty +export function barRankList(items, { mapItem, maxOverride, size = 'xs', emptyText = 'No data' } = {}) { + if (!items || items.length === 0) { + return `

    ${escapeHTML(emptyText)}

    `; + } + const mapped = items.map(item => (mapItem ? mapItem(item) : { name: item.name, count: item.count })); + const max = maxOverride != null ? maxOverride : Math.max(1, ...mapped.map(m => Number(m.count) || 0)); + const rows = mapped.map(m => `
  • ${barRow({ ...m, max, size })}
  • `).join(''); + return `
      ${rows}
    `; +} + +// ── Pill primitives ────────────────────────────────────── + +// metricPill — small label + bold value, optional meta line. +// value: pre-escaped HTML when valueHTML=true, else escaped as text +// valueId: id attribute (for animateCounter targets) +// variant: 'insight' | 'total' | 'range' | '' (default — standalone metric pill) +// alert: adds .alert to the pill (turns value red) +export function metricPill({ label, value = '-', valueId = '', valueHTML = false, meta = '', variant = '', alert = false } = {}) { + const classes = ['am-pill']; + if (variant) classes.push('am-pill--' + variant); + if (alert) classes.push('alert'); + const idAttr = valueId ? ` id="${escapeHTML(valueId)}"` : ''; + const rendered = valueHTML ? value : escapeHTML(String(value)); + const metaLine = meta ? `${escapeHTML(String(meta))}` : ''; + return ` +
    + ${escapeHTML(String(label))} + ${rendered} + ${metaLine} +
    `; +} + +// metricStrip — flex/grid container around a list of pill specs. +// variant: '' | 'insights' (4-up grid) | 'totals' (tight inline pills) +export function metricStrip(pills, { variant = '', className = '' } = {}) { + const classes = ['am-pill-strip']; + if (variant) classes.push('am-pill-strip--' + variant); + if (className) classes.push(className); + return `
    ${pills.map(p => metricPill(p)).join('')}
    `; +} + +// ── Chart header ───────────────────────────────────────── + +// chartHeader — title + optional subtitle on the left, raw controls HTML on the right. +// controls is passed through unchanged (legend/buttons/tabs vary by chart). +export function chartHeader({ title = '', subtitle = '', controls = '' } = {}) { + const subtitleHTML = subtitle ? `${escapeHTML(subtitle)}` : ''; + const controlsHTML = controls ? `
    ${controls}
    ` : ''; + return ` +
    +
    + ${escapeHTML(title)} + ${subtitleHTML} +
    + ${controlsHTML} +
    `; +} diff --git a/cmd/web-ui/static/modules/pages/dashboard.js b/cmd/web-ui/static/modules/pages/dashboard.js index 591c0fe..54be3ca 100644 --- a/cmd/web-ui/static/modules/pages/dashboard.js +++ b/cmd/web-ui/static/modules/pages/dashboard.js @@ -39,6 +39,13 @@ import { import { clearErrorBadge } from '../palette.js'; import { app, navigate, isRouteCurrent } from '../router.js'; import { api } from '../api.js'; +import { + barRankList, + barRow, + metricPill, + metricStrip, + chartHeader, +} from '../components.js'; // uPlot is loaded as a global IIFE; access via window.uPlot /* global uPlot */ @@ -182,7 +189,8 @@ function renderSummaryCards() { const totalOps = (s.runs_today || 0) + (s.tool_calls_today || 0); const rate = totalOps > 0 ? ((s.errors_today || 0) / totalOps * 100) : 0; animateCounter('dash-error-rate', rate.toFixed(1) + '%'); - errorRateEl.classList.toggle('alert', rate > 5); + const pill = errorRateEl.closest('.am-pill'); + if (pill) pill.classList.toggle('alert', rate > 5); } if (document.getElementById('dash-cost-per-run')) { @@ -287,12 +295,12 @@ function renderDashboardChartInsights() { } const peakBucket = dashboardState.timeseries.series[stats.peakIndex]; - container.innerHTML = ` -
    window total${escapeHTML(formatCount(stats.totalEvents))}
    -
    peak bucket${escapeHTML(formatCount(stats.peakTotal))}${escapeHTML(formatBucketLabel(peakBucket.ts))}
    -
    mix${escapeHTML(formatCount(stats.totalRuns))}r / ${escapeHTML(formatCount(stats.totalTools))}t / ${escapeHTML(formatCount(stats.totalErrors))}e
    -
    bucket${escapeHTML(dashboardState.timeseries.bucket || '-')}${escapeHTML(String(stats.bucketCount))} points
    - `; + container.innerHTML = metricStrip([ + { label: 'window total', value: formatCount(stats.totalEvents), variant: 'insight' }, + { label: 'peak bucket', value: formatCount(stats.peakTotal), meta: formatBucketLabel(peakBucket.ts), variant: 'insight' }, + { label: 'mix', value: `${formatCount(stats.totalRuns)}r / ${formatCount(stats.totalTools)}t / ${formatCount(stats.totalErrors)}e`, variant: 'insight' }, + { label: 'bucket', value: dashboardState.timeseries.bucket || '-', meta: stats.bucketCount + ' points', variant: 'insight' }, + ], { variant: 'insights' }); } function renderDashboardChartHover(idx) { @@ -589,23 +597,11 @@ function renderTokenPanel() {
    ${escapeHTML(formatTokenCount(totalTokens))}
    -
    - Input -
    -
    -
    - ${escapeHTML(formatTokenCount(inputTokens))} -
    -
    - Output -
    -
    -
    - ${escapeHTML(formatTokenCount(outputTokens))} -
    + ${barRow({ name: 'Input', count: inputTokens, countDisplay: formatTokenCount(inputTokens), max: maxIO, modifier: 'input', size: 'md' })} + ${barRow({ name: 'Output', count: outputTokens, countDisplay: formatTokenCount(outputTokens), max: maxIO, modifier: 'output', size: 'md' })}
    - Est. cost today + Est. cost today ${escapeHTML(totalCost ? formatCost(totalCost) : '$0.0000')}
    @@ -636,18 +632,9 @@ function renderLatencyPanel() { container.innerHTML = `
    -
    - Min - ${escapeHTML(formatDuration(min))} -
    -
    - Avg - ${escapeHTML(formatDuration(avg))} -
    -
    - Max - ${escapeHTML(formatDuration(max))} -
    + ${metricPill({ label: 'Min', value: formatDuration(min), variant: 'range' })} + ${metricPill({ label: 'Avg', value: formatDuration(avg), variant: 'range' })} + ${metricPill({ label: 'Max', value: formatDuration(max), variant: 'range' })}
    ${durSeries.map((v, i) => { @@ -657,7 +644,7 @@ function renderLatencyPanel() { return `
    `; }).join('')}
    -
    Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})
    +
    Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})
    `; } @@ -680,21 +667,17 @@ function renderFrameworkBars() { const maxTotal = Math.max(...entries.map(([, s]) => s.runs + s.tools + s.errors)); - container.innerHTML = '
    ' + entries.map(([name, stats]) => { + container.innerHTML = '
    ' + entries.map(([name, stats]) => { const total = stats.runs + stats.tools + stats.errors; - const pct = maxTotal > 0 ? (total / maxTotal * 100) : 0; const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-'); - return ` -
    -
    - ${escapeHTML(name)} - ${total} events -
    -
    -
    -
    -
    - `; + return barRow({ + name, + count: total, + countDisplay: total + ' events', + max: maxTotal, + fwClass: cssClass, + size: 'lg', + }); }).join('') + '
    '; } @@ -763,61 +746,21 @@ function renderDashFeed() { function renderDashTopTools() { const list = document.getElementById('dash-top-tools'); if (!list) return; - const topTools = Object.entries(dashboardState.toolCounts) .sort((a, b) => b[1] - a[1]) - .slice(0, 10); - - if (topTools.length === 0) { - list.innerHTML = '
  • No tool data yet
  • '; - return; - } - - const maxCount = topTools[0]?.[1] || 1; - list.innerHTML = topTools.map(([name, count]) => { - const pct = (count / maxCount * 100).toFixed(1); - return ` -
  • -
    - ${escapeHTML(name)} - ${count} -
    -
    -
    -
    -
  • - `; - }).join(''); + .slice(0, 10) + .map(([name, count]) => ({ name, count })); + list.innerHTML = barRankList(topTools, { emptyText: 'No tool data yet' }); } function renderDashTopModels() { const list = document.getElementById('dash-top-models'); if (!list) return; - const topModels = Object.entries(dashboardState.modelCounts) .sort((a, b) => b[1] - a[1]) - .slice(0, 10); - - if (topModels.length === 0) { - list.innerHTML = '
  • No model data yet
  • '; - return; - } - - const maxCount = topModels[0]?.[1] || 1; - list.innerHTML = topModels.map(([name, count]) => { - const pct = (count / maxCount * 100).toFixed(1); - return ` -
  • -
    - ${escapeHTML(name)} - ${count} -
    -
    -
    -
    -
  • - `; - }).join(''); + .slice(0, 10) + .map(([name, count]) => ({ name, count, modifier: 'model' })); + list.innerHTML = barRankList(topModels, { emptyText: 'No model data yet' }); } // ── Exports ────────────────────────────────────────────── @@ -863,38 +806,21 @@ export async function renderDashboard(routeToken) {
     
    -
    -
    - Tokens today - - -
    -
    - Cost today - - -
    -
    - Avg run duration - - -
    -
    - Error rate - - -
    -
    - Cost / run - - -
    -
    +
    ${metricStrip([ + { label: 'Tokens today', valueId: 'dash-tokens-today' }, + { label: 'Cost today', valueId: 'dash-cost-today' }, + { label: 'Avg run duration', valueId: 'dash-avg-duration' }, + { label: 'Error rate', valueId: 'dash-error-rate' }, + { label: 'Cost / run', valueId: 'dash-cost-per-run' }, + ])}
    Infrastructure
    -
    -
    - Event Rate - Runs, tool spans, and errors over time -
    -
    + ${chartHeader({ + title: 'Event Rate', + subtitle: 'Runs, tool spans, and errors over time', + controls: `
    total runs @@ -910,21 +836,21 @@ export async function renderDashboard(routeToken) { -
    -
    -
    -
    +
    `, + })} +
    -
    -
    - - - -
    -
    + ${chartHeader({ + controls: ` +
    + + + +
    `, + })}

    Loading...

    @@ -932,28 +858,20 @@ export async function renderDashboard(routeToken) {
    -
    - Recent Activity -
    + ${chartHeader({ title: 'Recent Activity' })}

    Loading...

    -
    - Top Usage -
    + ${chartHeader({ title: 'Top Usage' })}
    Tools
    -
      -
    • Loading...
    • -
    +

    Loading...

    Models
    -
      -
    • Loading...
    • -
    +

    Loading...

    diff --git a/cmd/web-ui/static/modules/pages/usage.js b/cmd/web-ui/static/modules/pages/usage.js index ceb6bee..5ff0e7c 100644 --- a/cmd/web-ui/static/modules/pages/usage.js +++ b/cmd/web-ui/static/modules/pages/usage.js @@ -1,6 +1,7 @@ import { app, isRouteCurrent } from '../router.js'; import { api } from '../api.js'; import { escapeHTML, formatTokenCount, formatCost } from '../utils.js'; +import { barTrack, barRankList, metricStrip, chartHeader } from '../components.js'; /* global uPlot */ @@ -104,7 +105,6 @@ function renderFrameworkBreakdown(byFw) { el.innerHTML = entries.map(([name, stats]) => { const total = stats.runs + stats.tools + stats.errors; - const pct = (total / maxTotal * 100).toFixed(1); const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-'); const active = (stats.active_sessions || 0) > 0; return ` @@ -119,9 +119,7 @@ function renderFrameworkBreakdown(byFw) { tools${stats.tools || 0} ${(stats.errors || 0) > 0 ? `err${stats.errors}` : ''} -
    -
    -
    + ${barTrack({ value: total, max: maxTotal, fwClass: cssClass, size: 'sm' })} `; }).join(''); } @@ -194,36 +192,22 @@ export async function renderUsage(routeToken) {
    -
    - 7-Day Trend -
    - - - -
    -
    -
    - - runs - ${t.runs} - - - tools - ${t.tools} - - - errors - ${t.errors} - - - tokens - ${formatTokenCount(t.tokens)} - - - cost - ${formatCost(t.cost)} - -
    + ${chartHeader({ + title: '7-Day Trend', + controls: ` +
    + + + +
    `, + })} + ${metricStrip([ + { label: 'runs', value: String(t.runs), variant: 'total' }, + { label: 'tools', value: String(t.tools), variant: 'total' }, + { label: 'errors', value: String(t.errors), variant: 'total', alert: t.errors > 0 }, + { label: 'tokens', value: formatTokenCount(t.tokens), variant: 'total' }, + { label: 'cost', value: formatCost(t.cost), variant: 'total' }, + ])}
    @@ -238,40 +222,20 @@ export async function renderUsage(routeToken) {
    Top Models ${models.length}
    - ${models.length === 0 ? '

    No model data

    ' : ` -
      - ${models.map(m => { - const pct = (m.count / maxModel * 100).toFixed(1); - return `
    • -
      - ${escapeHTML(m.name)} - ${m.count} -
      -
      -
      -
      -
    • `; - }).join('')} -
    `} + ${barRankList(models, { + mapItem: m => ({ name: m.name, count: m.count, modifier: 'model' }), + maxOverride: maxModel, + emptyText: 'No model data', + })}
    Top Tools ${tools.length}
    - ${tools.length === 0 ? '

    No tool data

    ' : ` -
      - ${tools.map(t => { - const pct = (t.count / maxTool * 100).toFixed(1); - return `
    • -
      - ${escapeHTML(t.name)} - ${t.count} -
      -
      -
      -
      -
    • `; - }).join('')} -
    `} + ${barRankList(tools, { + mapItem: x => ({ name: x.name, count: x.count }), + maxOverride: maxTool, + emptyText: 'No tool data', + })}
    `; diff --git a/cmd/web-ui/static/style.css b/cmd/web-ui/static/style.css index 41f08a1..d7d1bbe 100644 --- a/cmd/web-ui/static/style.css +++ b/cmd/web-ui/static/style.css @@ -1416,56 +1416,214 @@ tr.expandable:hover .expand-icon::before { margin-top: 0.1rem; } -.stat-list { - list-style: none; -} +/* ============================================================ + Shared primitives (am-*): bars, pills, chart headers. + See modules/components.js for the renderers. + ============================================================ */ -.stat-list li { +/* ── Bars ─────────────────────────────────────────────── */ +.am-bar-list { + list-style: none; display: flex; flex-direction: column; +} + +.am-bar-list li { padding: 0.35rem 0; border-bottom: 1px solid var(--border-soft); font-size: 0.8rem; } -.stat-list li:last-child { - border-bottom: none; +.am-bar-list li:last-child { border-bottom: none; } + +.am-bar-row { + display: flex; + flex-direction: column; + gap: 0.3rem; } -.stat-list-name { - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--text); -} - -.stat-list-count { - font-family: var(--font-mono); - font-size: 0.72rem; - color: var(--text-dim); - background: var(--surface-2); - padding: 0.1rem 0.4rem; - border-radius: 4px; -} - -.stat-list-header { +.am-bar-row-head { display: flex; justify-content: space-between; align-items: center; } -.stat-list-bar-track { - height: 3px; - background: var(--surface-2); - border-radius: 2px; - margin-top: 0.3rem; - overflow: hidden; +.am-bar-row-name { + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--text); } -.stat-list-bar-fill { +.am-bar-row-count { + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--text-dim); +} + +.am-bar-track { + background: var(--surface-2); + border-radius: 3px; + overflow: hidden; + height: 6px; +} + +.am-bar-track--xs { height: 3px; border-radius: 2px; } +.am-bar-track--sm { height: 4px; border-radius: 2px; } +.am-bar-track--md { height: 6px; border-radius: 3px; } +.am-bar-track--lg { height: 8px; border-radius: 4px; } + +.am-bar-fill { height: 100%; background: var(--accent); - border-radius: 2px; - transition: width 0.3s ease; + transition: width 0.4s ease; +} + +/* Framework / category color modifiers (applied to .am-bar-fill) */ +.am-bar-fill.fw-openclaw { background: var(--accent); } +.am-bar-fill.fw-claude-code { background: var(--success); } +.am-bar-fill.fw-opencode { background: var(--purple); } +.am-bar-fill.fw-hermes { background: var(--warning); } +.am-bar-fill.fw-codex { background: #60a5fa; } +.am-bar-fill.fw-gemini { background: #f97316; } +.am-bar-fill.fw-copilot { background: #2dd4bf; } +.am-bar-fill.fw-unknown { background: var(--text-dim); } +.am-bar-fill.model { background: var(--success); } +.am-bar-fill.input { background: var(--accent); } +.am-bar-fill.output { background: var(--purple); } + +/* ── Pills ────────────────────────────────────────────── */ +.am-pill-strip { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.am-pill-strip--insights { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; +} + +@media (max-width: 900px) { + .am-pill-strip--insights { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 560px) { + .am-pill-strip--insights { grid-template-columns: 1fr; } +} + +.am-pill { + display: flex; + flex-direction: column; + gap: 0.15rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.5rem 0.85rem; + min-width: 130px; + flex: 1; +} + +.am-pill-label { + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--text-dim); + font-family: var(--font-mono); +} + +.am-pill-value { + font-size: 1.05rem; + font-weight: 600; + font-family: var(--font-mono); + color: var(--text-bright); +} + +.am-pill-meta { + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--text-dim); + letter-spacing: 0.03em; + text-transform: uppercase; +} + +.am-pill.alert .am-pill-value { color: var(--error); } + +/* Variant: chart insight (taller, gradient background) */ +.am-pill--insight { + min-height: 62px; + border-color: var(--border-soft); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)); + padding: 0.75rem 0.85rem; + gap: 0.25rem; +} +.am-pill--insight .am-pill-value { font-size: 0.95rem; } + +/* Variant: tight inline total (e.g. usage chart totals) */ +.am-pill--total { + flex-direction: row; + align-items: baseline; + gap: 0.3rem; + background: var(--surface-2); + border-color: var(--border-soft); + border-radius: var(--radius); + padding: 0.22rem 0.55rem; + min-width: auto; + flex: 0 0 auto; +} +.am-pill--total .am-pill-label { font-size: 0.6rem; } +.am-pill--total .am-pill-value { font-size: 0.8rem; font-weight: normal; } + +/* Variant: latency-range item (centered, borderless) */ +.am-pill--range { + flex: 0 0 auto; + min-width: auto; + align-items: center; + gap: 0.1rem; + background: transparent; + border: none; + padding: 0; +} +.am-pill--range .am-pill-label { font-size: 0.68rem; } +.am-pill--range .am-pill-value { font-size: 1rem; font-weight: 600; color: var(--text); } + +/* ── Chart headers ────────────────────────────────────── */ +.am-chart-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + gap: 1rem; + flex-wrap: wrap; +} + +.am-chart-title-group { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.am-chart-title { + font-family: var(--font-display); + font-size: 0.88rem; + font-weight: 700; + color: var(--text-bright); + letter-spacing: 0.01em; +} + +.am-chart-subtitle { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--text-dim); + letter-spacing: 0.02em; +} + +.am-chart-controls { + display: flex; + align-items: center; + gap: 0.85rem; + flex-wrap: wrap; + justify-content: flex-end; } .event-icon { @@ -1592,7 +1750,7 @@ tr.expandable:hover .expand-icon::before { 100% { transform: scale(1); } } -.metric-pill-value.bumped { +.am-pill-value.bumped { animation: counterBump 400ms ease; } @@ -1635,44 +1793,6 @@ tr.expandable:hover .expand-icon::before { min-height: 280px; } -.chart-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; - gap: 1rem; - flex-wrap: wrap; -} - -.chart-title { - font-family: var(--font-display); - font-size: 0.88rem; - font-weight: 700; - color: var(--text-bright); - letter-spacing: 0.01em; -} - -.chart-title-group { - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -.chart-subtitle { - font-family: var(--font-mono); - font-size: 0.7rem; - color: var(--text-dim); - letter-spacing: 0.02em; -} - -.chart-header-controls { - display: flex; - align-items: center; - gap: 0.85rem; - flex-wrap: wrap; - justify-content: flex-end; -} - .window-selector { display: flex; gap: 0; @@ -1746,50 +1866,6 @@ tr.expandable:hover .expand-icon::before { background: rgba(52, 211, 153, 0.12); } -.chart-insights { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 0.75rem; - margin-bottom: 0.9rem; -} - -@media (max-width: 900px) { - .chart-insights { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 560px) { - .chart-insights { - grid-template-columns: 1fr; - } -} - -.chart-insight-pill { - min-height: 62px; - border: 1px solid var(--border-soft); - border-radius: var(--radius); - background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)); - padding: 0.75rem 0.85rem; - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.chart-insight-label, -.chart-insight-meta { - font-family: var(--font-mono); - font-size: 0.68rem; - color: var(--text-dim); - letter-spacing: 0.03em; - text-transform: uppercase; -} - -.chart-insight-pill strong { - font-size: 0.95rem; - color: var(--text-bright); -} - .chart-container { width: 100%; min-height: 200px; @@ -1869,59 +1945,6 @@ tr.expandable:hover .expand-icon::before { .chart-hover-metric.errors strong { color: #f87171; } .chart-hover-metric.delta strong { color: var(--accent); } -.fw-bars { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-top: 0.25rem; -} - -.fw-bar-row { - display: flex; - flex-direction: column; - gap: 0.3rem; -} - -.fw-bar-label { - display: flex; - justify-content: space-between; - align-items: center; -} - -.fw-bar-name { - font-family: var(--font-mono); - font-size: 0.78rem; - color: var(--text); -} - -.fw-bar-count { - font-family: var(--font-mono); - font-size: 0.72rem; - color: var(--text-dim); -} - -.fw-bar-track { - height: 8px; - background: var(--surface-2); - border-radius: 4px; - overflow: hidden; -} - -.fw-bar-fill { - height: 100%; - border-radius: 4px; - transition: width 0.4s ease; -} - -.fw-bar-fill.openclaw { background: var(--accent); } -.fw-bar-fill.claude-code { background: var(--success); } -.fw-bar-fill.opencode { background: var(--purple); } -.fw-bar-fill.hermes { background: var(--warning); } -.fw-bar-fill.codex { background: #60a5fa; } -.fw-bar-fill.gemini { background: #f97316; } -.fw-bar-fill.copilot { background: #2dd4bf; } -.fw-bar-fill.unknown { background: var(--text-dim); } - .bottom-panels { display: grid; grid-template-columns: 1fr 320px; @@ -2017,10 +2040,6 @@ tr.expandable:hover .expand-icon::before { box-shadow: 0 0 0 1px rgba(248, 250, 252, 0.2); } -.stat-list-bar-fill.model { - background: var(--success); -} - /* ── Framework dots ───────────────────────────────────────── */ .fw-dot { display: inline-block; @@ -3122,44 +3141,6 @@ tr.clickable.active-session td:first-child { margin: 0; } -/* ── Metrics strip ────────────────────────────────────────── */ -.metrics-strip { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1.25rem; -} - -.metric-pill { - display: flex; - flex-direction: column; - gap: 0.15rem; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 0.5rem 0.85rem; - min-width: 130px; - flex: 1; -} - -.metric-pill-label { - font-size: 0.68rem; - text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--text-dim); -} - -.metric-pill-value { - font-size: 1.05rem; - font-weight: 600; - font-family: var(--font-mono); - color: var(--text); -} - -.metric-pill-alert.alert { - color: var(--error); -} - /* ── Right panel tabs ─────────────────────────────────────── */ .right-panel-tabs { display: flex; @@ -3229,47 +3210,6 @@ tr.clickable.active-session td:first-child { gap: 0.5rem; } -.token-bar-row { - display: grid; - grid-template-columns: 3.5rem 1fr 3rem; - align-items: center; - gap: 0.5rem; -} - -.token-bar-label { - font-size: 0.72rem; - color: var(--text-dim); - text-align: right; -} - -.token-bar-track { - height: 6px; - background: var(--border); - border-radius: 3px; - overflow: hidden; -} - -.token-bar-fill { - height: 100%; - border-radius: 3px; - transition: width 0.3s ease; -} - -.token-bar-fill.input { - background: var(--accent); -} - -.token-bar-fill.output { - background: var(--purple, #a78bfa); -} - -.token-bar-count { - font-size: 0.72rem; - font-family: 'Fira Code', monospace; - color: var(--text-muted, var(--text-dim)); - text-align: right; -} - .token-cost-display { display: flex; justify-content: space-between; @@ -3298,27 +3238,6 @@ tr.clickable.active-session td:first-child { justify-content: space-between; } -.latency-range-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.1rem; -} - -.latency-range-label { - font-size: 0.68rem; - text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--text-dim); -} - -.latency-range-val { - font-size: 1rem; - font-weight: 600; - font-family: 'Fira Code', monospace; - color: var(--text); -} - .latency-mini-bars { display: flex; align-items: flex-end; @@ -3640,7 +3559,7 @@ tr.clickable.active-session td:first-child { display: none; } - .chart-header-controls { + .am-chart-controls { justify-content: flex-start; gap: 0.5rem; } @@ -3944,15 +3863,6 @@ tr.clickable:focus-visible { .usage-chart-panel { flex: 2 1 400px; } .usage-fw-panel { flex: 1 1 240px; } -.usage-chart-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.75rem; - flex-wrap: wrap; - gap: 0.5rem; -} - .usage-chart-tabs { display: flex; border: 1px solid var(--border); @@ -3976,33 +3886,6 @@ tr.clickable:focus-visible { .usage-chart-tab:hover { color: var(--text-bright); background: var(--surface-2); } .usage-chart-tab.active { color: var(--accent); background: var(--accent-dim); } -.usage-chart-totals { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin-bottom: 0.75rem; -} - -.usage-chart-total-pill { - display: inline-flex; - align-items: baseline; - gap: 0.3rem; - background: var(--surface-2); - border: 1px solid var(--border-soft); - border-radius: var(--radius); - padding: 0.22rem 0.55rem; - font-family: var(--font-mono); - font-size: 0.8rem; -} -.usage-chart-total-label { - font-size: 0.6rem; - text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--text-dim); -} -.usage-chart-total-pill strong { color: var(--text-bright); } -.usage-total-errors { color: var(--error); } - /* ── Usage Page: Framework Breakdown ─────────────────────── */ .usage-fw-row { display: flex; @@ -4056,27 +3939,6 @@ tr.clickable:focus-visible { color: var(--text-dim); } -.usage-fw-bar-track { - height: 4px; - background: var(--surface-2); - border-radius: 2px; - overflow: hidden; -} - -.usage-fw-bar-fill { - height: 100%; - border-radius: 2px; - transition: width 0.4s ease; - background: var(--text-dim); -} -.usage-fw-bar-fill.openclaw { background: var(--accent); } -.usage-fw-bar-fill.claude-code { background: var(--success); } -.usage-fw-bar-fill.opencode { background: var(--purple); } -.usage-fw-bar-fill.hermes { background: var(--warning); } -.usage-fw-bar-fill.codex { background: #60a5fa; } -.usage-fw-bar-fill.gemini { background: #f97316; } -.usage-fw-bar-fill.copilot { background: #2dd4bf; } - /* ── Run Detail: Prompt Preview ──────────────────────────── */ .prompt-preview-section { background: var(--surface);