Skip to content

Commit 79d5989

Browse files
vicarious11claude
andcommitted
feat: add activity breakdown + cost by project to web dashboard
New API: GET /api/activity — returns activity classification, one-shot rate, and cost by project (all deterministic, no LLM). Web Overview tab gains two new panels in the right sidebar: - Activity: bar chart with 8 categories + one-shot rate badge - Cost by Project: horizontal bars with dollar amounts Now consistent with TUI dashboard — same data, same panels. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 86b72d3 commit 79d5989

5 files changed

Lines changed: 134 additions & 1 deletion

File tree

src/agenttop/web/server.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,23 @@ def _build_session_index() -> dict[str, Any]:
141141
return index
142142

143143

144+
@app.get("/api/activity")
145+
def api_activity(days: int = 0) -> JSONResponse:
146+
"""Activity classification, one-shot rate, cost by project."""
147+
from agenttop.analysis.classifier import (
148+
classify_sessions,
149+
compute_cost_by_project,
150+
compute_oneshot_rate,
151+
)
152+
153+
sessions = _collect_all_sessions(days)
154+
return JSONResponse({
155+
"activities": classify_sessions(sessions),
156+
"oneshot_rate": compute_oneshot_rate(sessions),
157+
"cost_by_project": compute_cost_by_project(sessions)[:10],
158+
})
159+
160+
144161
@app.get("/api/sessions")
145162
def api_sessions(days: int = 7) -> JSONResponse:
146163
sessions = _collect_all_sessions(days)

src/agenttop/web/static/css/theme.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,22 @@ h3 { font-size: var(--text-md); font-weight: 600; margin-bottom: var(--sp-3); co
353353
.cost-value { font-family: var(--font-mono); font-size: var(--text-xs); min-width: 50px; text-align: right; color: var(--text-secondary); }
354354
.cost-badge { }
355355

356+
/* ── Activity Panel ── */
357+
.activity-row { display: flex; align-items: center; gap: var(--sp-2); padding: 3px 0; }
358+
.activity-name { font-size: var(--text-xs); color: var(--text-secondary); min-width: 80px; text-transform: capitalize; }
359+
.activity-bar-track { flex: 1; height: 4px; background: var(--bg-elevated); border-radius: 2px; overflow: hidden; }
360+
.activity-bar-fill { height: 100%; border-radius: 2px; }
361+
.activity-pct { font-size: var(--text-xs); font-weight: 600; min-width: 30px; text-align: right; }
362+
.activity-count { font-size: 10px; font-family: var(--font-mono); color: var(--text-dim); min-width: 25px; text-align: right; }
363+
364+
/* ── Project Cost Panel ── */
365+
.pcost-row { display: flex; align-items: center; gap: var(--sp-2); padding: 3px 0; }
366+
.pcost-name { font-size: var(--text-xs); color: var(--text-secondary); min-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
367+
.pcost-bar-track { flex: 1; height: 4px; background: var(--bg-elevated); border-radius: 2px; overflow: hidden; }
368+
.pcost-bar-fill { height: 100%; border-radius: 2px; background: var(--accent); }
369+
.pcost-val { font-size: var(--text-xs); font-family: var(--font-mono); color: var(--warning); min-width: 45px; text-align: right; }
370+
.pcost-sess { font-size: 10px; color: var(--text-dim); min-width: 20px; text-align: right; }
371+
356372
/* ══════════════════════════════════════════════════════
357373
GRAPH SVG ELEMENTS
358374
══════════════════════════════════════════════════════ */

src/agenttop/web/static/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@
9292
<div id="cost-content" class="panel-body"></div>
9393
</div>
9494

95+
<div id="activity-panel" class="glass-panel mini-panel">
96+
<div class="panel-header">
97+
<div class="panel-title"><span class="panel-icon"></span> Activity</div>
98+
<div id="oneshot-badge" class="panel-badge"></div>
99+
</div>
100+
<div id="activity-content" class="panel-body"></div>
101+
</div>
102+
103+
<div id="project-cost-panel" class="glass-panel mini-panel">
104+
<div class="panel-header">
105+
<div class="panel-title"><span class="panel-icon"></span> Cost by Project</div>
106+
</div>
107+
<div id="project-cost-content" class="panel-body"></div>
108+
</div>
109+
95110
<div id="workflow-panel" class="glass-panel mini-panel">
96111
<div class="panel-header">
97112
<div class="panel-title"><span class="panel-icon"></span> Workflow</div>

src/agenttop/web/static/js/app.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,14 @@ const App = {
101101

102102
async refresh() {
103103
try {
104-
const [graphRes, statsRes, modelsRes, hoursRes, sessionsRes, budgetRes] = await Promise.all([
104+
const [graphRes, statsRes, modelsRes, hoursRes, sessionsRes, budgetRes, activityRes] = await Promise.all([
105105
fetch(`/api/graph?days=${App.days}`),
106106
fetch(`/api/stats?days=${App.days}`),
107107
fetch('/api/models'),
108108
fetch('/api/hours'),
109109
fetch(`/api/sessions?days=${App.days || 7}`),
110110
fetch(`/api/budget?days=${App.days}`),
111+
fetch(`/api/activity?days=${App.days}`),
111112
]);
112113

113114
App.data.graph = await graphRes.json();
@@ -116,6 +117,7 @@ const App = {
116117
App.data.hours = await hoursRes.json();
117118
App.data.sessions = await sessionsRes.json();
118119
App.data.budget = await budgetRes.json();
120+
App.data.activity = await activityRes.json();
119121

120122
// Render all panels
121123
Graph.render(App.data.graph);
@@ -124,6 +126,8 @@ const App = {
124126
Panels.renderModels(App.data.models);
125127
Panels.renderHourly(App.data.hours);
126128
Panels.renderCost(App.data.stats);
129+
Panels.renderActivity(App.data.activity);
130+
Panels.renderProjectCost(App.data.activity);
127131

128132
// Update session data and tab badges
129133
if (typeof SessionExplorer !== 'undefined') {

src/agenttop/web/static/js/panels.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,85 @@ const Panels = {
254254
}).join('')}
255255
`;
256256
},
257+
258+
/* ═══════════════════════════════════════════════════════
259+
ACTIVITY BREAKDOWN + ONE-SHOT RATE
260+
═══════════════════════════════════════════════════════ */
261+
262+
ACTIVITY_COLORS: {
263+
coding: '#34d399', debugging: '#f87171', testing: '#fbbf24',
264+
exploration: '#22d3ee', refactoring: '#c084fc', git_ops: '#60a5fa',
265+
planning: '#f97316', other: '#71717a',
266+
},
267+
268+
renderActivity(data) {
269+
const el = document.getElementById('activity-content');
270+
const badge = document.getElementById('oneshot-badge');
271+
if (!data || !data.activities) {
272+
el.innerHTML = '<div class="panel-empty">No activity data</div>';
273+
return;
274+
}
275+
276+
const acts = data.activities;
277+
const rate = data.oneshot_rate || 0;
278+
const total = Object.values(acts).reduce((s, v) => s + v, 0);
279+
280+
if (badge) {
281+
const rateColor = rate >= 80 ? 'var(--success)' : rate >= 60 ? 'var(--warning)' : 'var(--error)';
282+
badge.innerHTML = `<span style="color:${rateColor};font-weight:700">${rate.toFixed(0)}% one-shot</span>`;
283+
}
284+
285+
if (total === 0) {
286+
el.innerHTML = '<div class="panel-empty">No sessions</div>';
287+
return;
288+
}
289+
290+
const maxVal = Math.max(...Object.values(acts));
291+
292+
el.innerHTML = Object.entries(acts)
293+
.filter(([, v]) => v > 0)
294+
.map(([act, count]) => {
295+
const pct = (count / total * 100).toFixed(0);
296+
const color = Panels.ACTIVITY_COLORS[act] || '#71717a';
297+
const barW = Math.max((count / maxVal * 100), 2).toFixed(1);
298+
const name = act.replace('_', ' ');
299+
return `
300+
<div class="activity-row">
301+
<span class="activity-name">${name}</span>
302+
<div class="activity-bar-track">
303+
<div class="activity-bar-fill" style="width:${barW}%;background:${color}"></div>
304+
</div>
305+
<span class="activity-pct" style="color:${color}">${pct}%</span>
306+
<span class="activity-count">${count}</span>
307+
</div>`;
308+
}).join('');
309+
},
310+
311+
/* ═══════════════════════════════════════════════════════
312+
COST BY PROJECT
313+
═══════════════════════════════════════════════════════ */
314+
315+
renderProjectCost(data) {
316+
const el = document.getElementById('project-cost-content');
317+
if (!data || !data.cost_by_project || data.cost_by_project.length === 0) {
318+
el.innerHTML = '<div class="panel-empty">No project data</div>';
319+
return;
320+
}
321+
322+
const projects = data.cost_by_project;
323+
const maxCost = projects[0].cost || 1;
324+
325+
el.innerHTML = projects.map(p => {
326+
const barW = Math.max((p.cost / maxCost * 100), 2).toFixed(1);
327+
return `
328+
<div class="pcost-row">
329+
<span class="pcost-name">${p.project}</span>
330+
<div class="pcost-bar-track">
331+
<div class="pcost-bar-fill" style="width:${barW}%"></div>
332+
</div>
333+
<span class="pcost-val">${App.formatCost(p.cost)}</span>
334+
<span class="pcost-sess">${p.sessions}s</span>
335+
</div>`;
336+
}).join('');
337+
},
257338
};

0 commit comments

Comments
 (0)