groove-dev 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ const AMBER = '#e5c07b';
10
10
  const RED = '#e06c75';
11
11
  const PURPLE = '#c678dd';
12
12
  const BLUE = '#61afef';
13
- const COLORS = [ACCENT, AMBER, GREEN, PURPLE, RED, BLUE, '#d19a66', '#56b6c2'];
13
+ const COLORS = [ACCENT, AMBER, GREEN, PURPLE, RED, BLUE, '#d19a66'];
14
14
  const COST_PER_1K = { heavy: 0.045, medium: 0.009, light: 0.0024 };
15
15
 
16
16
  export default function CommandCenter() {
@@ -29,8 +29,6 @@ export default function CommandCenter() {
29
29
  const res = await fetch('/api/dashboard');
30
30
  const d = await res.json();
31
31
  setData(d);
32
-
33
- // Build telemetry in the Zustand store so it persists across tab switches
34
32
  useGrooveStore.setState((s) => {
35
33
  const telem = { ...s.dashTelemetry };
36
34
  const now = Date.now();
@@ -60,107 +58,112 @@ export default function CommandCenter() {
60
58
  const { tokens, routing, rotation, adaptive, journalist, uptime } = data;
61
59
  const agentBreakdown = data.agents.breakdown;
62
60
  const estDollarSaved = (tokens.savings.total / 1000) * COST_PER_1K.medium;
63
- // Show max context reached across ALL agents (historical peak, not just running)
64
- const maxCtx = agentBreakdown.length > 0
65
- ? Math.round(Math.max(...agentBreakdown.map((a) => a.contextUsage || 0)) * 100)
66
- : 0;
67
61
 
68
62
  return (
69
63
  <div style={s.root}>
70
64
 
71
- {/* ── HERO ROW Gauges + Money Shot ── */}
72
- <div style={s.heroRow}>
73
- <div style={s.heroGaugeGroup}>
74
- <GaugeChart value={tokens.savings.percentage || 0} max={100} label="EFFICIENCY" unit="%" color={GREEN} />
75
- <GaugeChart value={maxCtx} max={100} label="PEAK CONTEXT" unit="%" color={maxCtx > 80 ? RED : maxCtx > 60 ? AMBER : ACCENT} />
76
- </div>
77
- <div style={s.heroCenter}>
78
- <div style={s.heroDollar}>{estDollarSaved > 0 ? `$${estDollarSaved.toFixed(2)}` : '$0.00'}</div>
79
- <div style={s.heroCenterLabel}>ESTIMATED SAVINGS</div>
80
- <div style={s.heroSubStats}>
81
- <span>{fmtNum(tokens.totalTokens)} used</span>
82
- <span>{fmtNum(tokens.savings.total)} saved</span>
83
- </div>
84
- </div>
85
- <div style={s.heroGaugeGroup}>
86
- <GaugeChart value={data.agents.running} max={Math.max(data.agents.total, 1)} label="AGENTS" unit={`/${data.agents.total}`} color={ACCENT} />
87
- <GaugeChart value={rotation.totalRotations} max={Math.max(rotation.totalRotations, 10)} label="ROTATIONS" unit="" color={PURPLE} />
88
- </div>
65
+ {/* ROW 1Stat Cards */}
66
+ <div style={s.statRow}>
67
+ <StatCard
68
+ label="Total Tokens"
69
+ value={fmtNum(tokens.totalTokens)}
70
+ sub={`${data.agents.total} agent${data.agents.total !== 1 ? 's' : ''}`}
71
+ />
72
+ <StatCard
73
+ label="Estimated Savings"
74
+ value={estDollarSaved > 0 ? `$${estDollarSaved.toFixed(2)}` : '$0.00'}
75
+ sub={`${fmtNum(tokens.savings.total)} tokens saved`}
76
+ color={GREEN}
77
+ />
78
+ <StatCard
79
+ label="Efficiency"
80
+ value={`${tokens.savings.percentage || 0}%`}
81
+ sub="vs uncoordinated"
82
+ color={tokens.savings.percentage > 0 ? GREEN : undefined}
83
+ />
84
+ <StatCard
85
+ label="Rotations"
86
+ value={rotation.totalRotations}
87
+ sub={fmtUptime(uptime)}
88
+ />
89
89
  </div>
90
90
 
91
- {/* ── MAIN CHART Full-width live telemetry ── */}
92
- <div style={s.chartPanel}>
93
- <div style={s.chartHead}>
94
- <span>LIVE TELEMETRY</span>
95
- <span style={s.chartHeadRight}>
96
- {data.agents.breakdown.filter((a) => a.tokens > 0).map((a, i) => (
97
- <span key={a.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, marginLeft: 10 }}>
98
- <span style={{ width: 6, height: 6, borderRadius: '50%', background: COLORS[i % COLORS.length], display: 'inline-block' }} />
99
- <span style={{ fontSize: 9 }}>{a.name} {fmtNum(a.tokens)}</span>
100
- </span>
101
- ))}
102
- </span>
91
+ {/* ROW 2Area Chart + Donut */}
92
+ <div style={s.midRow}>
93
+ <div style={s.chartPanel}>
94
+ <div style={s.panelHead}>
95
+ <span>Token Usage</span>
96
+ <span style={s.panelHeadRight}>
97
+ {agentBreakdown.filter((a) => a.tokens > 0).map((a, i) => (
98
+ <span key={a.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, marginLeft: 12 }}>
99
+ <span style={{ width: 6, height: 2, background: COLORS[i % COLORS.length], display: 'inline-block', borderRadius: 1 }} />
100
+ <span style={{ fontSize: 9 }}>{a.name}</span>
101
+ </span>
102
+ ))}
103
+ </span>
104
+ </div>
105
+ <TelemetryChart tokenTimeline={dashTelemetry} agents={agents} />
106
+ </div>
107
+ <div style={s.donutPanel}>
108
+ <div style={s.panelHead}>Model Routing</div>
109
+ <DonutChart routing={routing} />
103
110
  </div>
104
- <TelemetryChart tokenTimeline={dashTelemetry} agents={agents} />
105
111
  </div>
106
112
 
107
- {/* ── BOTTOM ROW Three panels ── */}
113
+ {/* ROW 3Savings Bars + Agent Fleet */}
108
114
  <div style={s.bottomRow}>
109
-
110
- {/* AGENT FLEET */}
111
115
  <div style={s.panel}>
112
- <div style={s.panelHead}>AGENT FLEET</div>
116
+ <div style={s.panelHead}>Savings Breakdown</div>
113
117
  <div style={s.scrollInner}>
114
- {agentBreakdown.length === 0 ? (
115
- <div style={s.empty}>No agents spawned</div>
116
- ) : agentBreakdown.map((a, i) => (
117
- <AgentCard key={a.id} agent={a} total={tokens.totalTokens} color={COLORS[i % COLORS.length]} />
118
- ))}
119
- </div>
120
- </div>
121
-
122
- {/* SAVINGS + ROUTING + ADAPTIVE */}
123
- <div style={s.panel}>
124
- <div style={s.panelHead}>SAVINGS & ROUTING</div>
125
- <div style={s.scrollInner}>
126
- <SavingsBlock savings={tokens.savings} />
127
- <div style={s.divider} />
128
- <RoutingBlock routing={routing} />
118
+ <HorizBar label="Rotation" value={tokens.savings.fromRotation} max={tokens.savings.total || 1} color={ACCENT} />
119
+ <HorizBar label="Conflict Prevention" value={tokens.savings.fromConflictPrevention} max={tokens.savings.total || 1} color={AMBER} />
120
+ <HorizBar label="Cold-Start Skip" value={tokens.savings.fromColdStartSkip} max={tokens.savings.total || 1} color={GREEN} />
129
121
  <div style={s.divider} />
130
- <AdaptiveBlock adaptive={adaptive} />
122
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#5c6370', padding: '4px 0' }}>
123
+ <span>Without Groove</span>
124
+ <span style={{ color: RED }}>{fmtNum(tokens.savings.estimatedWithoutGroove)}</span>
125
+ </div>
126
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#5c6370', padding: '4px 0' }}>
127
+ <span>With Groove</span>
128
+ <span style={{ color: GREEN }}>{fmtNum(tokens.totalTokens)}</span>
129
+ </div>
130
+ {journalist.lastSummary && (
131
+ <>
132
+ <div style={s.divider} />
133
+ <div style={s.panelHead}>Journalist</div>
134
+ <div style={s.journSummary}>{journalist.lastSummary}</div>
135
+ </>
136
+ )}
131
137
  </div>
132
138
  </div>
133
-
134
- {/* JOURNALIST + ROTATION */}
135
139
  <div style={s.panel}>
136
- <div style={s.panelHead}>
137
- JOURNALIST
138
- <span style={{ ...s.liveBadge, background: journalist.running ? GREEN : 'var(--text-dim)' }}>
139
- {journalist.running ? 'LIVE' : 'IDLE'}
140
- </span>
141
- </div>
140
+ <div style={s.panelHead}>Agent Fleet</div>
142
141
  <div style={s.scrollInner}>
143
- <div style={s.journStats}>
144
- <span>{journalist.cycleCount || 0} cycles</span>
145
- <span>{journalist.intervalMs ? `${journalist.intervalMs / 1000}s interval` : '120s interval'}</span>
146
- </div>
147
- {journalist.lastSummary ? (
148
- <div style={s.journSummary}>{journalist.lastSummary}</div>
149
- ) : (
150
- <div style={s.journSummary}>Waiting for first synthesis cycle...</div>
151
- )}
152
- <div style={{ ...s.divider, margin: '8px 0' }} />
153
- <div style={s.miniHead}>ROTATION HISTORY</div>
154
- {rotation.history.length === 0 ? (
155
- <div style={s.empty}>No rotations yet</div>
156
- ) : rotation.history.slice().reverse().slice(0, 10).map((r, i) => (
157
- <div key={i} style={s.rotEntry}>
158
- <span style={s.rotDot} />
159
- <span style={s.rotName}>{r.agentName}</span>
160
- <span style={s.rotSaved}>{fmtNum(r.oldTokens)} saved</span>
161
- <span style={s.rotTime}>{timeAgo(r.timestamp)}</span>
162
- </div>
142
+ {agentBreakdown.length === 0 ? (
143
+ <div style={s.empty}>No agents spawned</div>
144
+ ) : agentBreakdown.map((a, i) => (
145
+ <HorizBar
146
+ key={a.id}
147
+ label={a.name}
148
+ value={a.tokens}
149
+ max={Math.max(...agentBreakdown.map((x) => x.tokens), 1)}
150
+ color={COLORS[i % COLORS.length]}
151
+ sub={`${a.role} · ${a.model || 'auto'}`}
152
+ />
163
153
  ))}
154
+ {rotation.history.length > 0 && (
155
+ <>
156
+ <div style={s.divider} />
157
+ <div style={s.panelHead}>Rotation History</div>
158
+ {rotation.history.slice().reverse().slice(0, 8).map((r, i) => (
159
+ <div key={i} style={s.rotEntry}>
160
+ <span style={s.rotName}>{r.agentName}</span>
161
+ <span style={{ color: GREEN, fontSize: 9 }}>{fmtNum(r.oldTokens)} saved</span>
162
+ <span style={s.rotTime}>{timeAgo(r.timestamp)}</span>
163
+ </div>
164
+ ))}
165
+ </>
166
+ )}
164
167
  </div>
165
168
  </div>
166
169
  </div>
@@ -168,163 +171,101 @@ export default function CommandCenter() {
168
171
  );
169
172
  }
170
173
 
171
- // ── GAUGE CHART — Semicircle arc gauge ──
172
- function GaugeChart({ value, max, label, unit, color }) {
173
- const pct = max > 0 ? Math.min(value / max, 1) : 0;
174
- const r = 32;
175
- const cx = 40;
176
- const cy = 38;
177
- const circumHalf = Math.PI * r;
178
- const dashLen = pct * circumHalf;
174
+ // ── STAT CARD ──
179
175
 
176
+ function StatCard({ label, value, sub, color }) {
180
177
  return (
181
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flex: 1 }}>
182
- <svg width="80" height="48" viewBox="0 0 80 48">
183
- {/* Track */}
184
- <path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
185
- fill="none" stroke="#2c313a" strokeWidth="4" strokeLinecap="round" />
186
- {/* Value arc */}
187
- <path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
188
- fill="none" stroke={color} strokeWidth="4" strokeLinecap="round"
189
- strokeDasharray={`${dashLen} ${circumHalf}`}
190
- style={{ transition: 'stroke-dasharray 0.5s ease' }} />
191
- {/* Value text */}
192
- <text x={cx} y={cy - 6} textAnchor="middle" fill="#e6e6e6"
193
- fontSize="14" fontWeight="700" fontFamily="JetBrains Mono, monospace">
194
- {typeof value === 'number' ? Math.round(value) : value}
195
- </text>
196
- <text x={cx} y={cy + 4} textAnchor="middle" fill="#5c6370"
197
- fontSize="7" fontFamily="JetBrains Mono, monospace">
198
- {unit}
199
- </text>
200
- </svg>
201
- <span style={{ fontSize: 7, fontWeight: 700, color: '#5c6370', textTransform: 'uppercase', letterSpacing: 1, marginTop: -2 }}>{label}</span>
178
+ <div style={s.statCard}>
179
+ <div style={{ fontSize: 22, fontWeight: 700, color: color || '#e6e6e6', lineHeight: 1 }}>{value}</div>
180
+ <div style={{ fontSize: 9, color: '#5c6370', marginTop: 6, textTransform: 'uppercase', letterSpacing: 1 }}>{label}</div>
181
+ {sub && <div style={{ fontSize: 9, color: '#3e4451', marginTop: 3 }}>{sub}</div>}
202
182
  </div>
203
183
  );
204
184
  }
205
185
 
206
- // ── AGENT CARD ──
207
- function AgentCard({ agent, total, color }) {
208
- const pct = total > 0 ? (agent.tokens / total) * 100 : 0;
209
- const alive = agent.status === 'running';
210
- const statusColor = alive ? GREEN : agent.status === 'completed' ? ACCENT : agent.status === 'crashed' ? RED : 'var(--text-dim)';
186
+ // ── HORIZONTAL BAR ──
211
187
 
188
+ function HorizBar({ label, value, max, color, sub }) {
189
+ const pct = max > 0 ? (value / max) * 100 : 0;
212
190
  return (
213
- <div style={s.agentCard}>
214
- <div style={s.agentCardRow}>
215
- <span style={{ ...s.dot, background: statusColor, ...(alive ? { animation: 'pulse 2s infinite' } : {}) }} />
216
- <span style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-bright)' }}>{agent.name}</span>
217
- <span style={{ fontSize: 9, color: 'var(--text-dim)' }}>{agent.role}</span>
218
- {agent.routingMode === 'auto' && <span style={s.tagAuto}>AUTO</span>}
219
- <span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-primary)', marginLeft: 'auto' }}>{fmtNum(agent.tokens)}</span>
191
+ <div style={{ padding: '6px 0' }}>
192
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
193
+ <span style={{ fontSize: 11, color: '#abb2bf' }}>{label}</span>
194
+ <span style={{ fontSize: 11, color: '#e6e6e6', fontWeight: 600 }}>{fmtNum(value)}</span>
220
195
  </div>
221
- <div style={s.agentBarRow}>
222
- <div style={s.agentBarTrack}>
223
- <div style={{ width: `${Math.max(pct, 0.5)}%`, height: '100%', background: color, borderRadius: 1 }} />
224
- </div>
225
- <span style={{ fontSize: 8, color: 'var(--text-dim)' }}>{agent.model || 'default'}</span>
226
- <CtxGauge value={agent.contextUsage} />
196
+ <div style={{ height: 4, background: '#1e222a', borderRadius: 2 }}>
197
+ <div style={{ width: `${Math.max(pct, value > 0 ? 1 : 0)}%`, height: '100%', background: color, borderRadius: 2, transition: 'width 0.3s' }} />
227
198
  </div>
199
+ {sub && <div style={{ fontSize: 8, color: '#3e4451', marginTop: 2 }}>{sub}</div>}
228
200
  </div>
229
201
  );
230
202
  }
231
203
 
232
- function CtxGauge({ value }) {
233
- const pct = Math.round((value || 0) * 100);
234
- const color = pct > 80 ? RED : pct > 60 ? AMBER : GREEN;
235
- return (
236
- <div style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
237
- <div style={{ width: 24, height: 3, background: '#2c313a', borderRadius: 1, overflow: 'hidden' }}>
238
- <div style={{ width: `${pct}%`, height: '100%', background: color, borderRadius: 1 }} />
239
- </div>
240
- <span style={{ fontSize: 8, color: 'var(--text-dim)', minWidth: 18, textAlign: 'right' }}>{pct}%</span>
241
- </div>
242
- );
243
- }
244
-
245
- // ── SAVINGS BLOCK ──
246
- function SavingsBlock({ savings }) {
247
- const items = [
248
- { label: 'Rotation', value: savings.fromRotation, color: ACCENT },
249
- { label: 'Conflicts', value: savings.fromConflictPrevention, color: AMBER },
250
- { label: 'Cold-start', value: savings.fromColdStartSkip, color: GREEN },
251
- ];
252
- const total = savings.total || 1;
253
-
254
- return (
255
- <div>
256
- <div style={s.miniHead}>TOKEN SAVINGS</div>
257
- <div style={s.stackedBar}>
258
- {items.map((it, i) => it.value > 0 && (
259
- <div key={i} style={{ width: `${(it.value / total) * 100}%`, height: '100%', background: it.color }} />
260
- ))}
261
- </div>
262
- {items.map((it, i) => (
263
- <div key={i} style={s.savRow}>
264
- <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
265
- <div style={{ width: 6, height: 6, background: it.color, borderRadius: 1, flexShrink: 0 }} />
266
- <span>{it.label}</span>
267
- </div>
268
- <span style={{ fontWeight: 600 }}>{fmtNum(it.value)}</span>
269
- </div>
270
- ))}
271
- </div>
272
- );
273
- }
204
+ // ── DONUT CHART ──
274
205
 
275
- // ── ROUTING BLOCK ──
276
- function RoutingBlock({ routing }) {
206
+ function DonutChart({ routing }) {
277
207
  const tiers = [
278
- { label: 'HEAVY', cost: '$0.045', color: RED, count: routing.byTier.heavy },
279
- { label: 'MEDIUM', cost: '$0.009', color: AMBER, count: routing.byTier.medium },
280
- { label: 'LIGHT', cost: '$0.002', color: GREEN, count: routing.byTier.light },
208
+ { label: 'Heavy', color: RED, count: routing.byTier.heavy, cost: '$0.045/1k' },
209
+ { label: 'Medium', color: AMBER, count: routing.byTier.medium, cost: '$0.009/1k' },
210
+ { label: 'Light', color: GREEN, count: routing.byTier.light, cost: '$0.002/1k' },
281
211
  ];
282
- const max = Math.max(...tiers.map((t) => t.count), 1);
212
+ const total = tiers.reduce((s, t) => s + t.count, 0) || 1;
213
+
214
+ const r = 50;
215
+ const cx = 60;
216
+ const cy = 60;
217
+ const circumference = 2 * Math.PI * r;
218
+ let offset = 0;
283
219
 
284
220
  return (
285
- <div>
286
- <div style={s.miniHead}>MODEL ROUTING</div>
287
- {tiers.map((t) => (
288
- <div key={t.label} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 0' }}>
289
- <span style={{ fontSize: 8, fontWeight: 700, color: t.color, minWidth: 42 }}>{t.label}</span>
290
- <div style={{ flex: 1, height: 4, background: '#2c313a', borderRadius: 1, overflow: 'hidden' }}>
291
- <div style={{ width: `${Math.max((t.count / max) * 100, t.count > 0 ? 3 : 0)}%`, height: '100%', background: t.color, borderRadius: 1 }} />
221
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, minHeight: 0 }}>
222
+ <svg width="120" height="120" viewBox="0 0 120 120">
223
+ <circle cx={cx} cy={cy} r={r} fill="none" stroke="#1e222a" strokeWidth="10" />
224
+ {tiers.map((t) => {
225
+ if (t.count === 0) return null;
226
+ const pct = t.count / total;
227
+ const dashLen = pct * circumference;
228
+ const el = (
229
+ <circle
230
+ key={t.label}
231
+ cx={cx} cy={cy} r={r}
232
+ fill="none" stroke={t.color} strokeWidth="10"
233
+ strokeDasharray={`${dashLen} ${circumference - dashLen}`}
234
+ strokeDashoffset={-offset}
235
+ strokeLinecap="butt"
236
+ transform={`rotate(-90 ${cx} ${cy})`}
237
+ style={{ transition: 'stroke-dasharray 0.5s' }}
238
+ />
239
+ );
240
+ offset += dashLen;
241
+ return el;
242
+ })}
243
+ <text x={cx} y={cy - 4} textAnchor="middle" fill="#e6e6e6" fontSize="16" fontWeight="700" fontFamily="JetBrains Mono, monospace">
244
+ {total > 1 ? total : routing.totalDecisions}
245
+ </text>
246
+ <text x={cx} y={cy + 10} textAnchor="middle" fill="#5c6370" fontSize="8" fontFamily="JetBrains Mono, monospace">
247
+ decisions
248
+ </text>
249
+ </svg>
250
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: '100%', padding: '0 8px' }}>
251
+ {tiers.map((t) => (
252
+ <div key={t.label} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 10 }}>
253
+ <span style={{ width: 8, height: 8, borderRadius: 2, background: t.color, flexShrink: 0 }} />
254
+ <span style={{ color: '#abb2bf', flex: 1 }}>{t.label}</span>
255
+ <span style={{ color: '#5c6370', fontSize: 9 }}>{t.cost}</span>
256
+ <span style={{ color: '#e6e6e6', fontWeight: 600, minWidth: 20, textAlign: 'right' }}>{t.count}</span>
292
257
  </div>
293
- <span style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-bright)', minWidth: 16, textAlign: 'right' }}>{t.count}</span>
294
- </div>
295
- ))}
296
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 8, color: 'var(--text-dim)', marginTop: 4 }}>
297
- <span>{routing.autoRoutedCount} auto</span>
298
- <span>{routing.totalDecisions} total</span>
258
+ ))}
259
+ </div>
260
+ <div style={{ fontSize: 9, color: '#3e4451' }}>
261
+ {routing.autoRoutedCount} auto-routed
299
262
  </div>
300
263
  </div>
301
264
  );
302
265
  }
303
266
 
304
- // ── ADAPTIVE BLOCK ──
305
- function AdaptiveBlock({ adaptive }) {
306
- if (!adaptive || adaptive.length === 0) return null;
307
- return (
308
- <div>
309
- <div style={s.miniHead}>ADAPTIVE THRESHOLDS</div>
310
- {adaptive.map((p) => (
311
- <div key={p.key} style={{ padding: '4px 0' }}>
312
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, marginBottom: 2 }}>
313
- <span style={{ color: 'var(--text-bright)', fontWeight: 600 }}>{p.key}</span>
314
- <span style={{ color: p.converged ? GREEN : AMBER, fontSize: 8 }}>
315
- {p.converged ? 'CONVERGED' : `${p.adjustments} adj`}
316
- </span>
317
- </div>
318
- <div style={{ height: 5, background: '#2c313a', borderRadius: 2, overflow: 'hidden' }}>
319
- <div style={{ width: `${Math.round(p.threshold * 100)}%`, height: '100%', background: p.converged ? GREEN : ACCENT, borderRadius: 2 }} />
320
- </div>
321
- </div>
322
- ))}
323
- </div>
324
- );
325
- }
267
+ // ── TELEMETRY CHART ──
326
268
 
327
- // ── TELEMETRY CHART — Full-width area chart with per-agent lines ──
328
269
  function TelemetryChart({ tokenTimeline, agents }) {
329
270
  const containerRef = useRef();
330
271
  const canvasRef = useRef();
@@ -352,85 +293,75 @@ function TelemetryChart({ tokenTimeline, agents }) {
352
293
  const chartH = h - padT - padB;
353
294
 
354
295
  // Grid
355
- ctx.strokeStyle = '#2c313a';
296
+ ctx.strokeStyle = '#1e222a';
356
297
  ctx.lineWidth = 0.5;
357
298
  for (let i = 0; i <= 4; i++) {
358
299
  const y = padT + (i / 4) * chartH;
359
300
  ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(w - padR, y); ctx.stroke();
360
301
  }
361
302
 
362
- // Gather ALL agent timelines — including completed agents (historical data)
363
303
  const agentIds = Object.keys(tokenTimeline);
364
-
365
304
  if (agentIds.length === 0) {
366
305
  ctx.fillStyle = '#3e4451';
367
- ctx.font = '11px monospace';
306
+ ctx.font = '10px monospace';
368
307
  ctx.textAlign = 'center';
369
308
  ctx.fillText('Waiting for agent telemetry...', w / 2, h / 2);
370
309
  return;
371
310
  }
372
311
 
373
- // Find global time range and max value
374
312
  let minT = Infinity, maxT = 0, maxV = 0;
375
313
  for (const id of agentIds) {
376
- const pts = tokenTimeline[id] || [];
377
- for (const p of pts) {
314
+ for (const p of tokenTimeline[id] || []) {
378
315
  if (p.t < minT) minT = p.t;
379
316
  if (p.t > maxT) maxT = p.t;
380
317
  if (p.v > maxV) maxV = p.v;
381
318
  }
382
319
  }
383
-
384
320
  if (maxT === minT) maxT = minT + 60000;
385
321
  if (maxV === 0) maxV = 100;
386
322
  const timeRange = maxT - minT;
387
323
 
388
- // Y-axis labels
324
+ // Y labels
389
325
  ctx.fillStyle = '#3e4451';
390
326
  ctx.font = '8px monospace';
391
327
  ctx.textAlign = 'right';
392
328
  for (let i = 0; i <= 4; i++) {
393
329
  const val = maxV * (1 - i / 4);
394
- const y = padT + (i / 4) * chartH;
395
- ctx.fillText(fmtNum(Math.round(val)), padL - 4, y + 3);
330
+ ctx.fillText(fmtNum(Math.round(val)), padL - 4, padT + (i / 4) * chartH + 3);
396
331
  }
397
332
 
398
- // X-axis time labels
333
+ // X labels
399
334
  ctx.textAlign = 'center';
400
- const timeLabels = 5;
401
- for (let i = 0; i <= timeLabels; i++) {
402
- const t = minT + (i / timeLabels) * timeRange;
403
- const x = padL + (i / timeLabels) * chartW;
335
+ for (let i = 0; i <= 4; i++) {
336
+ const t = minT + (i / 4) * timeRange;
404
337
  const d = new Date(t);
405
- ctx.fillText(`${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`, x, h - 4);
338
+ ctx.fillText(`${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`, padL + (i / 4) * chartW, h - 4);
406
339
  }
407
340
 
408
- // Draw each agent thin flat lines, subtle fill, no neon/glow
341
+ // Draw agent areas + lines
409
342
  agentIds.forEach((id, idx) => {
410
343
  const pts = tokenTimeline[id] || [];
411
344
  if (pts.length < 2) return;
412
345
 
413
346
  const color = COLORS[idx % COLORS.length];
414
-
415
- // Map points to canvas coords
416
347
  const coords = pts.map((p) => ({
417
348
  x: padL + ((p.t - minT) / timeRange) * chartW,
418
349
  y: padT + (1 - p.v / maxV) * chartH,
419
350
  }));
420
351
 
421
- // Subtle fill area
352
+ // Gradient fill
422
353
  ctx.beginPath();
423
354
  ctx.moveTo(coords[0].x, padT + chartH);
424
355
  for (const c of coords) ctx.lineTo(c.x, c.y);
425
356
  ctx.lineTo(coords[coords.length - 1].x, padT + chartH);
426
357
  ctx.closePath();
427
358
  const grad = ctx.createLinearGradient(0, padT, 0, padT + chartH);
428
- grad.addColorStop(0, color + '18');
359
+ grad.addColorStop(0, color + '20');
429
360
  grad.addColorStop(1, color + '03');
430
361
  ctx.fillStyle = grad;
431
362
  ctx.fill();
432
363
 
433
- // Thin flat line — 1px, no shadow/glow
364
+ // Line
434
365
  ctx.beginPath();
435
366
  for (let i = 0; i < coords.length; i++) {
436
367
  i === 0 ? ctx.moveTo(coords[i].x, coords[i].y) : ctx.lineTo(coords[i].x, coords[i].y);
@@ -439,7 +370,7 @@ function TelemetryChart({ tokenTimeline, agents }) {
439
370
  ctx.lineWidth = 1;
440
371
  ctx.stroke();
441
372
 
442
- // Small end marker — flat, no glow
373
+ // End dot
443
374
  const last = coords[coords.length - 1];
444
375
  ctx.beginPath();
445
376
  ctx.arc(last.x, last.y, 2, 0, Math.PI * 2);
@@ -463,6 +394,7 @@ function TelemetryChart({ tokenTimeline, agents }) {
463
394
  }
464
395
 
465
396
  // ── HELPERS ──
397
+
466
398
  function fmtNum(n) {
467
399
  if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
468
400
  if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
@@ -484,13 +416,14 @@ function timeAgo(ts) {
484
416
  }
485
417
 
486
418
  // ── STYLES ──
419
+
487
420
  const s = {
488
421
  root: {
489
422
  width: '100%', height: '100%',
490
423
  display: 'flex', flexDirection: 'column',
491
424
  overflow: 'hidden',
492
- padding: 12,
493
- gap: 10,
425
+ padding: 14,
426
+ gap: 12,
494
427
  background: 'var(--bg-base)',
495
428
  },
496
429
  loadingRoot: {
@@ -501,120 +434,66 @@ const s = {
501
434
  fontSize: 11, fontWeight: 700, color: 'var(--text-dim)',
502
435
  letterSpacing: 3, textTransform: 'uppercase',
503
436
  },
504
- loadingBar: { width: 120, height: 2, background: 'var(--bg-surface)', borderRadius: 1, overflow: 'hidden' },
437
+ loadingBar: { width: 120, height: 2, background: '#282c34', borderRadius: 1, overflow: 'hidden' },
505
438
  loadingFill: { width: '40%', height: '100%', background: ACCENT, animation: 'pulse 1.5s infinite' },
506
439
 
507
- // Hero row
508
- heroRow: {
509
- display: 'flex', alignItems: 'stretch', gap: 10,
510
- flexShrink: 0, height: 90,
511
- },
512
- heroGaugeGroup: {
513
- flex: 1, display: 'flex', gap: 4,
514
- background: 'var(--bg-surface)', border: '1px solid var(--border)',
515
- borderRadius: 8, padding: '6px 4px', alignItems: 'center',
516
- },
517
- heroCenter: {
518
- flex: 1.2, background: 'var(--bg-surface)', border: '1px solid var(--border)',
519
- borderRadius: 8,
520
- display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
521
- padding: '8px 16px',
522
- },
523
- heroDollar: {
524
- fontSize: 28, fontWeight: 800, color: GREEN, lineHeight: 1,
525
- },
526
- heroCenterLabel: {
527
- fontSize: 7, fontWeight: 700, color: 'var(--text-dim)',
528
- textTransform: 'uppercase', letterSpacing: 1.2, marginTop: 4,
440
+ // Stat cards row
441
+ statRow: {
442
+ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12,
443
+ flexShrink: 0,
529
444
  },
530
- heroSubStats: {
531
- display: 'flex', gap: 12, marginTop: 4,
532
- fontSize: 9, color: '#5c6370',
445
+ statCard: {
446
+ background: '#282c34', borderRadius: 12, padding: '16px 18px',
447
+ display: 'flex', flexDirection: 'column',
533
448
  },
534
449
 
535
- // Main chart
536
- chartPanel: {
450
+ // Mid row — chart + donut
451
+ midRow: {
452
+ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 12,
537
453
  flex: 2, minHeight: 0,
538
- background: 'var(--bg-surface)', border: '1px solid var(--border)',
539
- borderRadius: 8,
540
- padding: '8px 12px', display: 'flex', flexDirection: 'column',
541
454
  },
542
- chartHead: {
543
- fontSize: 9, fontWeight: 700, color: 'var(--text-dim)',
544
- textTransform: 'uppercase', letterSpacing: 1.5,
545
- paddingBottom: 6, flexShrink: 0,
546
- display: 'flex', justifyContent: 'space-between', alignItems: 'center',
455
+ chartPanel: {
456
+ background: '#282c34', borderRadius: 12, padding: '12px 14px',
457
+ display: 'flex', flexDirection: 'column', minHeight: 0,
458
+ },
459
+ donutPanel: {
460
+ background: '#282c34', borderRadius: 12, padding: '12px 14px',
461
+ display: 'flex', flexDirection: 'column', minHeight: 0,
547
462
  },
548
- chartHeadRight: { display: 'flex', alignItems: 'center', color: 'var(--text-dim)' },
549
463
 
550
- // Bottom row — three panels
464
+ // Bottom row
551
465
  bottomRow: {
466
+ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12,
552
467
  flex: 3, minHeight: 0,
553
- display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10,
554
468
  },
555
469
  panel: {
556
- minHeight: 0, overflow: 'hidden',
557
- background: 'var(--bg-surface)', border: '1px solid var(--border)',
558
- borderRadius: 8,
559
- padding: '8px 10px', display: 'flex', flexDirection: 'column',
470
+ background: '#282c34', borderRadius: 12, padding: '12px 14px',
471
+ display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden',
560
472
  },
561
- panelHead: {
562
- fontSize: 9, fontWeight: 700, color: 'var(--text-dim)',
563
- textTransform: 'uppercase', letterSpacing: 1.5,
564
- paddingBottom: 6, marginBottom: 6, flexShrink: 0,
565
- borderBottom: '1px solid var(--border)',
566
- display: 'flex', alignItems: 'center', gap: 8,
567
- },
568
- scrollInner: { flex: 1, minHeight: 0, overflowY: 'auto' },
569
- empty: { color: 'var(--text-dim)', fontSize: 10, textAlign: 'center', padding: 16, opacity: 0.6 },
570
473
 
571
474
  // Shared
572
- divider: { height: 1, background: 'var(--border)', margin: '6px 0', flexShrink: 0 },
573
- miniHead: {
574
- fontSize: 8, fontWeight: 700, color: 'var(--text-dim)',
575
- textTransform: 'uppercase', letterSpacing: 1, marginBottom: 4,
576
- },
577
- dot: { width: 6, height: 6, borderRadius: '50%', flexShrink: 0 },
578
- liveBadge: {
579
- fontSize: 7, fontWeight: 700, color: '#1a1d23',
580
- padding: '1px 5px', borderRadius: 2, letterSpacing: 0.5, marginLeft: 'auto',
581
- },
582
-
583
- // Agent cards
584
- agentCard: { padding: '5px 0', borderBottom: '1px solid var(--bg-base)' },
585
- agentCardRow: { display: 'flex', alignItems: 'center', gap: 6 },
586
- agentBarRow: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 3 },
587
- agentBarTrack: { flex: 1, height: 2, background: '#2c313a', borderRadius: 1, overflow: 'hidden' },
588
- tagAuto: {
589
- fontSize: 7, fontWeight: 700, color: ACCENT,
590
- border: `1px solid ${ACCENT}`, padding: '0 3px', lineHeight: '11px', letterSpacing: 0.5,
591
- },
592
-
593
- // Savings
594
- stackedBar: { height: 8, background: '#2c313a', borderRadius: 2, overflow: 'hidden', display: 'flex', marginBottom: 4 },
595
- savRow: {
475
+ panelHead: {
476
+ fontSize: 10, fontWeight: 600, color: '#5c6370',
477
+ textTransform: 'uppercase', letterSpacing: 1,
478
+ marginBottom: 8, flexShrink: 0,
596
479
  display: 'flex', justifyContent: 'space-between', alignItems: 'center',
597
- padding: '2px 0', fontSize: 10, color: 'var(--text-primary)',
598
480
  },
481
+ panelHeadRight: { display: 'flex', alignItems: 'center', color: '#3e4451' },
482
+ scrollInner: { flex: 1, minHeight: 0, overflowY: 'auto' },
483
+ empty: { color: '#3e4451', fontSize: 10, textAlign: 'center', padding: 20 },
484
+ divider: { height: 1, background: '#1e222a', margin: '8px 0', flexShrink: 0 },
599
485
 
600
486
  // Journalist
601
- journStats: {
602
- display: 'flex', justifyContent: 'space-between',
603
- fontSize: 9, color: 'var(--text-dim)', marginBottom: 6,
604
- },
605
487
  journSummary: {
606
- fontSize: 10, color: 'var(--text-primary)', lineHeight: 1.6,
607
- padding: '6px 8px', background: 'var(--bg-base)', border: '1px solid var(--border)',
608
- overflowY: 'auto', whiteSpace: 'pre-wrap', minHeight: 60, maxHeight: 200, flex: 1,
488
+ fontSize: 10, color: '#8b929e', lineHeight: 1.5,
489
+ padding: '4px 0', whiteSpace: 'pre-wrap', maxHeight: 80, overflowY: 'auto',
609
490
  },
610
491
 
611
492
  // Rotation
612
493
  rotEntry: {
613
- display: 'flex', alignItems: 'center', gap: 6,
614
- padding: '3px 0', fontSize: 10,
494
+ display: 'flex', alignItems: 'center', gap: 8,
495
+ padding: '4px 0', fontSize: 10,
615
496
  },
616
- rotDot: { width: 5, height: 5, borderRadius: '50%', background: ACCENT, flexShrink: 0 },
617
- rotName: { color: 'var(--text-bright)', fontWeight: 600, flex: 1 },
618
- rotSaved: { color: GREEN, fontSize: 9, fontWeight: 600 },
619
- rotTime: { color: 'var(--text-dim)', fontSize: 9, flexShrink: 0 },
497
+ rotName: { color: '#abb2bf', flex: 1 },
498
+ rotTime: { color: '#3e4451', fontSize: 9 },
620
499
  };