diff options
Diffstat (limited to 'web/app.js')
| -rw-r--r-- | web/app.js | 195 |
1 files changed, 193 insertions, 2 deletions
@@ -1178,8 +1178,9 @@ function renderActiveTab(allTasks) { Promise.all([ fetchRecentExecutions(BASE_PATH, fetch), fetch(`${BASE_PATH}/api/agents/status?since=${encodeURIComponent(new Date(Date.now() - 24*60*60*1000).toISOString())}`).then(r => r.ok ? r.json() : { agents: [], events: [] }), + fetch(`${BASE_PATH}/api/stats?window=7d`).then(r => r.ok ? r.json() : { throughput: [], billing: [], failures: [] }), ]) - .then(([execs, agentData]) => renderStatsPanel(allTasks, execs, agentData)) + .then(([execs, agentData, dashStats]) => renderStatsPanel(allTasks, execs, agentData, dashStats)) .catch(() => {}); break; case 'drops': @@ -2435,7 +2436,7 @@ function formatDurationMs(ms) { return rm > 0 ? `${h}h ${rm}m` : `${h}h`; } -function renderStatsPanel(tasks, executions, agentData = { agents: [], events: [] }) { +function renderStatsPanel(tasks, executions, agentData = { agents: [], events: [] }, dashStats = { throughput: [], billing: [], failures: [] }) { const panel = document.querySelector('[data-panel="stats"]'); if (!panel) return; @@ -2599,6 +2600,196 @@ function renderStatsPanel(tasks, executions, agentData = { agents: [], events: [ panel.appendChild(execSection); + // ── Errors ──────────────────────────────────────────────────────────────── + const failures = dashStats.failures || []; + const errSection = document.createElement('div'); + errSection.className = 'stats-section'; + + const errHeading = document.createElement('h2'); + errHeading.textContent = 'Errors (Last 7d)'; + errSection.appendChild(errHeading); + + if (failures.length === 0) { + const none = document.createElement('p'); + none.className = 'task-meta'; + none.textContent = 'No failures in the last 7 days.'; + errSection.appendChild(none); + } else { + // Category summary bar + const cats = {}; + for (const f of failures) cats[f.category] = (cats[f.category] || 0) + 1; + const catOrder = ['quota', 'rate_limit', 'timeout', 'git', 'failed']; + const catLabels = { quota: 'Quota', rate_limit: 'Rate limit', timeout: 'Timeout', git: 'Git', failed: 'Failed' }; + const catColors = { quota: 'var(--state-budget-exceeded)', rate_limit: 'var(--state-failed)', timeout: 'var(--state-timed-out)', git: 'var(--state-cancelled)', failed: 'var(--state-failed)' }; + + const catRow = document.createElement('div'); + catRow.className = 'stats-kpis'; + const allCats = [...catOrder, ...Object.keys(cats).filter(c => !catOrder.includes(c))]; + for (const cat of allCats) { + if (!cats[cat]) continue; + const box = document.createElement('div'); + box.className = 'stats-kpi-box stats-err-cat'; + box.style.setProperty('--cat-color', catColors[cat] || 'var(--state-failed)'); + const val = document.createElement('span'); + val.className = 'stats-kpi-value'; + val.textContent = String(cats[cat]); + const lbl = document.createElement('span'); + lbl.className = 'stats-kpi-label'; + lbl.textContent = catLabels[cat] || cat; + box.appendChild(val); + box.appendChild(lbl); + catRow.appendChild(box); + } + errSection.appendChild(catRow); + + // Failure table + const errTable = document.createElement('table'); + errTable.className = 'stats-exec-table'; + errTable.style.marginTop = '0.75rem'; + errTable.innerHTML = '<thead><tr><th>Task</th><th>Category</th><th>Error</th><th>Time</th></tr></thead>'; + const errTbody = document.createElement('tbody'); + for (const f of failures.slice(0, 25)) { + const tr = document.createElement('tr'); + const ts = new Date(f.started_at).toLocaleString(); + const short = f.error_msg.length > 80 ? f.error_msg.slice(0, 80) + '…' : f.error_msg; + const catColor = catColors[f.category] || 'var(--state-failed)'; + tr.innerHTML = `<td class="stats-exec-name">${f.task_name}</td><td><span class="stats-err-badge" style="background:${catColor}">${catLabels[f.category] || f.category}</span></td><td class="stats-err-msg" title="${f.error_msg.replace(/"/g,'"')}">${short}</td><td style="white-space:nowrap">${ts}</td>`; + errTbody.appendChild(tr); + } + errTable.appendChild(errTbody); + errSection.appendChild(errTable); + } + + panel.appendChild(errSection); + + // ── Throughput ──────────────────────────────────────────────────────────── + const throughput = dashStats.throughput || []; + const tpSection = document.createElement('div'); + tpSection.className = 'stats-section'; + + const tpHeading = document.createElement('h2'); + tpHeading.textContent = 'Throughput (Last 7d)'; + tpSection.appendChild(tpHeading); + + if (throughput.length === 0) { + const none = document.createElement('p'); + none.className = 'task-meta'; + none.textContent = 'No execution data yet.'; + tpSection.appendChild(none); + } else { + const maxTotal = Math.max(...throughput.map(b => b.completed + b.failed + b.other), 1); + const chart = document.createElement('div'); + chart.className = 'stats-tp-chart'; + + for (const bucket of throughput) { + const total = bucket.completed + bucket.failed + bucket.other; + const col = document.createElement('div'); + col.className = 'stats-tp-col'; + const heightPct = (total / maxTotal) * 100; + const label = new Date(bucket.hour).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit' }); + col.title = `${label}\n✓ ${bucket.completed} ✗ ${bucket.failed} ○ ${bucket.other}`; + + if (total > 0) { + const bar = document.createElement('div'); + bar.className = 'stats-tp-bar'; + bar.style.height = `${heightPct.toFixed(1)}%`; + + const cPct = (bucket.completed / total) * 100; + const fPct = (bucket.failed / total) * 100; + const oPct = 100 - cPct - fPct; + + bar.style.background = `linear-gradient(to top, + var(--state-failed) 0% ${fPct.toFixed(1)}%, + var(--state-timed-out) ${fPct.toFixed(1)}% ${(fPct+oPct).toFixed(1)}%, + var(--state-completed) ${(fPct+oPct).toFixed(1)}% 100%)`; + + col.appendChild(bar); + } + + chart.appendChild(col); + } + tpSection.appendChild(chart); + + const tpLegend = document.createElement('div'); + tpLegend.className = 'stats-tp-legend'; + tpLegend.innerHTML = ` + <span class="stats-tp-legend-item"><span class="stats-tp-swatch" style="background:var(--state-completed)"></span>Completed</span> + <span class="stats-tp-legend-item"><span class="stats-tp-swatch" style="background:var(--state-failed)"></span>Failed</span> + <span class="stats-tp-legend-item"><span class="stats-tp-swatch" style="background:var(--state-timed-out)"></span>Other</span> + `; + tpSection.appendChild(tpLegend); + } + + panel.appendChild(tpSection); + + // ── Billing ─────────────────────────────────────────────────────────────── + const billing = dashStats.billing || []; + const billSection = document.createElement('div'); + billSection.className = 'stats-section'; + + const billHeading = document.createElement('h2'); + billHeading.textContent = 'Cost (Last 7d)'; + billSection.appendChild(billHeading); + + if (billing.length === 0) { + const none = document.createElement('p'); + none.className = 'task-meta'; + none.textContent = 'No cost data yet.'; + billSection.appendChild(none); + } else { + const totalCost = billing.reduce((s, d) => s + d.cost_usd, 0); + const totalRuns = billing.reduce((s, d) => s + d.runs, 0); + + const billKpis = document.createElement('div'); + billKpis.className = 'stats-kpis'; + for (const kpi of [ + { label: '7d Total', value: `$${totalCost.toFixed(2)}` }, + { label: 'Avg/Day', value: billing.length > 0 ? `$${(totalCost / billing.length).toFixed(2)}` : '—' }, + { label: 'Cost/Run', value: totalRuns > 0 ? `$${(totalCost / totalRuns).toFixed(3)}` : '—' }, + { label: 'Total Runs', value: String(totalRuns) }, + ]) { + const box = document.createElement('div'); + box.className = 'stats-kpi-box'; + const val = document.createElement('span'); + val.className = 'stats-kpi-value'; + val.textContent = kpi.value; + const lbl = document.createElement('span'); + lbl.className = 'stats-kpi-label'; + lbl.textContent = kpi.label; + box.appendChild(val); + box.appendChild(lbl); + billKpis.appendChild(box); + } + billSection.appendChild(billKpis); + + // Daily cost bar chart + const maxCost = Math.max(...billing.map(d => d.cost_usd), 0.001); + const billChart = document.createElement('div'); + billChart.className = 'stats-bill-chart'; + + for (const day of billing) { + const col = document.createElement('div'); + col.className = 'stats-bill-col'; + col.title = `${day.day}\n$${day.cost_usd.toFixed(3)} (${day.runs} runs)`; + + const bar = document.createElement('div'); + bar.className = 'stats-bill-bar'; + bar.style.height = `${((day.cost_usd / maxCost) * 100).toFixed(1)}%`; + + const dayLabel = document.createElement('span'); + dayLabel.className = 'stats-bill-day-label'; + const d = new Date(day.day + 'T12:00:00Z'); + dayLabel.textContent = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + + col.appendChild(bar); + col.appendChild(dayLabel); + billChart.appendChild(col); + } + billSection.appendChild(billChart); + } + + panel.appendChild(billSection); + // ── Agent Status ─────────────────────────────────────────────────────────── const agentSection = document.createElement('div'); agentSection.className = 'stats-section'; |
