pi-mega-compact 0.14.0 → 0.15.0

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.
Files changed (91) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +6 -0
  2. package/dist/extensions/dashboard-server/routes-cache.js +17 -5
  3. package/dist/extensions/dashboard-server/routes-game.js +60 -0
  4. package/dist/extensions/dashboard-server/routes.js +1 -1
  5. package/dist/extensions/dashboard-server/server.js +3 -1
  6. package/dist/extensions/mega-runtime/capture-model.js +15 -2
  7. package/dist/extensions/mega-runtime/runtime-snapshot.js +26 -0
  8. package/dist/extensions/mega-runtime/runtime.js +1 -0
  9. package/extensions/dashboard-client/dist/assets/AreaChart-rJdaOUlj.js +2 -0
  10. package/extensions/dashboard-client/dist/assets/AreaChart-rJdaOUlj.js.map +1 -0
  11. package/extensions/dashboard-client/dist/assets/BarChart-DOfc8me9.js +2 -0
  12. package/extensions/dashboard-client/dist/assets/BarChart-DOfc8me9.js.map +1 -0
  13. package/extensions/dashboard-client/dist/assets/{CacheTab-CDyZqMO-.js → CacheTab-EhqkEglo.js} +2 -2
  14. package/extensions/dashboard-client/dist/assets/{CacheTab-CDyZqMO-.js.map → CacheTab-EhqkEglo.js.map} +1 -1
  15. package/extensions/dashboard-client/dist/assets/{EventsTab-ByPYMJVG.js → EventsTab-BUACwWXf.js} +2 -2
  16. package/extensions/dashboard-client/dist/assets/{EventsTab-ByPYMJVG.js.map → EventsTab-BUACwWXf.js.map} +1 -1
  17. package/extensions/dashboard-client/dist/assets/{HealthTab-CeHQR0py.js → HealthTab-CDEYVRBb.js} +2 -2
  18. package/extensions/dashboard-client/dist/assets/{HealthTab-CeHQR0py.js.map → HealthTab-CDEYVRBb.js.map} +1 -1
  19. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ceTD_c1A.js → MaintenanceTab-DtanIeCQ.js} +2 -2
  20. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ceTD_c1A.js.map → MaintenanceTab-DtanIeCQ.js.map} +1 -1
  21. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CRUdVR7u.js → MemoryMapTab-BGEhL7OG.js} +2 -2
  22. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CRUdVR7u.js.map → MemoryMapTab-BGEhL7OG.js.map} +1 -1
  23. package/extensions/dashboard-client/dist/assets/MetricsTab-Cktozn4s.js +2 -0
  24. package/extensions/dashboard-client/dist/assets/MetricsTab-Cktozn4s.js.map +1 -0
  25. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js +16 -0
  26. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js.map +1 -0
  27. package/extensions/dashboard-client/dist/assets/{ReposTab-DUrP7Efa.js → ReposTab-Bg72Ozdc.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{ReposTab-DUrP7Efa.js.map → ReposTab-Bg72Ozdc.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/SessionsTab-BcTK9Lg6.js +2 -0
  30. package/extensions/dashboard-client/dist/assets/{SessionsTab-Cg-IsijD.js.map → SessionsTab-BcTK9Lg6.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{SetupTab-ZE1daXjN.js → SetupTab-Bt-Ru1xp.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{SetupTab-ZE1daXjN.js.map → SetupTab-Bt-Ru1xp.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BkTgi3Af.js → TimeSavedCard-ic_LjJ79.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BkTgi3Af.js.map → TimeSavedCard-ic_LjJ79.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{TurnsTab-CZvudHQF.js → TurnsTab-D9AZTMgG.js} +4 -4
  36. package/extensions/dashboard-client/dist/assets/{TurnsTab-CZvudHQF.js.map → TurnsTab-D9AZTMgG.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/WikiTab-hRcJPi_m.js +2 -0
  38. package/extensions/dashboard-client/dist/assets/{WikiTab-BVAnGp8_.js.map → WikiTab-hRcJPi_m.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{button-CUrzhi2A.js → button-BdAtIbVu.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{button-CUrzhi2A.js.map → button-BdAtIbVu.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/{card-BsfojHfp.js → card-BFYc_QrK.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{card-BsfojHfp.js.map → card-BFYc_QrK.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/generateCategoricalChart-BwzyqytT.js +70 -0
  44. package/extensions/dashboard-client/dist/assets/generateCategoricalChart-BwzyqytT.js.map +1 -0
  45. package/extensions/dashboard-client/dist/assets/{index-OM3w-l2T.js → index-pmtEr3sb.js} +8 -8
  46. package/extensions/dashboard-client/dist/assets/index-pmtEr3sb.js.map +1 -0
  47. package/extensions/dashboard-client/dist/assets/index-za_gmEmQ.css +1 -0
  48. package/extensions/dashboard-client/dist/assets/{switch-qSz2Un3t.js → switch-DEyGlFwJ.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{switch-qSz2Un3t.js.map → switch-DEyGlFwJ.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{toggle-DyBKcLe-.js → toggle-pDMBQYQj.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{toggle-DyBKcLe-.js.map → toggle-pDMBQYQj.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{useSSE-DqVouqww.js → useSSE-BKaC4dao.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{useSSE-DqVouqww.js.map → useSSE-BKaC4dao.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/index.html +2 -2
  55. package/extensions/dashboard-client/package-lock.json +56 -0
  56. package/extensions/dashboard-client/package.json +3 -0
  57. package/extensions/dashboard-client/src/api/client.ts +11 -0
  58. package/extensions/dashboard-client/src/components/SessionContextGauges.tsx +14 -8
  59. package/extensions/dashboard-client/src/components/WidgetDetailModal.tsx +69 -0
  60. package/extensions/dashboard-client/src/components/charts/PerfBarChart.tsx +89 -0
  61. package/extensions/dashboard-client/src/components/charts/PerfLineChart.tsx +108 -0
  62. package/extensions/dashboard-client/src/components/layout/AppShell.tsx +1 -1
  63. package/extensions/dashboard-client/src/components/ui/SortableCard.tsx +75 -0
  64. package/extensions/dashboard-client/src/hooks/useCardPositions.ts +74 -0
  65. package/extensions/dashboard-client/src/styles/base.css +2 -4
  66. package/extensions/dashboard-client/src/styles/session-gauges.css +28 -15
  67. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +310 -79
  68. package/extensions/dashboard-client/src/types/card.ts +40 -0
  69. package/extensions/dashboard-client/tailwind.config.js +1 -1
  70. package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +10 -0
  71. package/extensions/dashboard-server/api-contracts/endpoints/types.ts +36 -0
  72. package/extensions/dashboard-server/api-contracts/endpoints.ts +3 -0
  73. package/extensions/dashboard-server/api-contracts/index.ts +3 -0
  74. package/extensions/dashboard-server/routes-cache.ts +17 -5
  75. package/extensions/dashboard-server/routes-game.ts +72 -0
  76. package/extensions/dashboard-server/routes.ts +1 -0
  77. package/extensions/dashboard-server/server.ts +2 -0
  78. package/extensions/mega-runtime/capture-model.ts +14 -2
  79. package/extensions/mega-runtime/runtime-snapshot.ts +40 -0
  80. package/extensions/mega-runtime/runtime.ts +1 -0
  81. package/package.json +1 -1
  82. package/extensions/dashboard-client/dist/assets/AreaChart-C68tGr2J.js +0 -70
  83. package/extensions/dashboard-client/dist/assets/AreaChart-C68tGr2J.js.map +0 -1
  84. package/extensions/dashboard-client/dist/assets/MetricsTab-DP7Kmdu9.js +0 -2
  85. package/extensions/dashboard-client/dist/assets/MetricsTab-DP7Kmdu9.js.map +0 -1
  86. package/extensions/dashboard-client/dist/assets/OverviewTab-4CGP0djv.js +0 -2
  87. package/extensions/dashboard-client/dist/assets/OverviewTab-4CGP0djv.js.map +0 -1
  88. package/extensions/dashboard-client/dist/assets/SessionsTab-Cg-IsijD.js +0 -2
  89. package/extensions/dashboard-client/dist/assets/WikiTab-BVAnGp8_.js +0 -2
  90. package/extensions/dashboard-client/dist/assets/index-CUA6EDRF.css +0 -1
  91. package/extensions/dashboard-client/dist/assets/index-OM3w-l2T.js.map +0 -1
@@ -11,20 +11,27 @@
11
11
  grid-column: 1 / -1;
12
12
  }
13
13
 
14
+ /* Full-width grid: each gauge gets plenty of room, wraps across the whole
15
+ * row. If there are more sessions than columns, the outer wrapper scrolls
16
+ * horizontally rather than cramping the gauges (see .session-gauges-scroll). */
14
17
  .session-gauges-grid {
15
18
  display: grid;
16
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
17
- gap: 14px;
19
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
20
+ gap: 16px;
21
+ }
22
+
23
+ .session-gauges-scroll {
24
+ overflow-x: auto;
18
25
  }
19
26
 
20
27
  .session-gauge {
21
28
  display: flex;
22
29
  flex-direction: column;
23
- gap: 6px;
24
- padding: 10px 12px;
30
+ gap: 8px;
31
+ padding: 14px 16px;
25
32
  background: var(--bg);
26
33
  border: 1px solid var(--border);
27
- border-radius: 6px;
34
+ border-radius: 8px;
28
35
  }
29
36
 
30
37
  .session-gauge-self {
@@ -37,17 +44,18 @@
37
44
  display: flex;
38
45
  justify-content: space-between;
39
46
  align-items: baseline;
40
- gap: 6px;
47
+ gap: 8px;
41
48
  }
42
49
 
50
+ /* Repo/session label — readable 14px. Not truncated by default (full repo
51
+ * names matter); the ellipsis is applied only as a last resort via a
52
+ * truncate+title pattern on the component when space is genuinely tight. */
43
53
  .session-gauge-label {
44
- font-size: 12px;
54
+ font-size: 14px;
45
55
  font-weight: 600;
56
+ line-height: 1.25;
46
57
  color: var(--text);
47
- overflow: hidden;
48
- text-overflow: ellipsis;
49
- white-space: nowrap;
50
- max-width: 70%;
58
+ min-width: 0;
51
59
  }
52
60
 
53
61
  .session-gauge-self .session-gauge-label {
@@ -55,23 +63,28 @@
55
63
  }
56
64
 
57
65
  .session-gauge-pct {
58
- font-size: 12px;
59
- font-weight: 600;
66
+ font-size: 20px;
67
+ font-weight: 700;
68
+ line-height: 1.1;
60
69
  color: var(--muted);
61
70
  white-space: nowrap;
71
+ flex-shrink: 0;
62
72
  }
63
73
 
64
74
  .session-gauges-grid .gauge-bar {
65
- height: 8px;
75
+ height: 12px;
66
76
  }
67
77
 
68
78
  .session-gauge-sub {
69
79
  margin: 0;
80
+ font-size: 14px;
81
+ font-weight: 500;
82
+ color: var(--text);
70
83
  }
71
84
 
72
85
  .session-gauge-meta {
73
86
  margin: 0;
74
- font-size: 11px;
87
+ font-size: 12px;
75
88
  color: var(--muted);
76
89
  overflow: hidden;
77
90
  text-overflow: ellipsis;
@@ -18,10 +18,10 @@
18
18
  */
19
19
 
20
20
  import type React from "react";
21
- import { useCallback, useEffect, useMemo } from "react";
21
+ import { useCallback, useEffect, useMemo, useState } from "react";
22
22
  import { useApi } from "../hooks/useApi";
23
23
  import { useSSE } from "../hooks/useSSE";
24
- import { fetchSessions } from "../api/client";
24
+ import { fetchSessions, fetchPerfSamples } from "../api/client";
25
25
  import type {
26
26
  SnapshotResponse,
27
27
  SseEvent,
@@ -44,12 +44,37 @@ import { LegendCard } from "../components/LegendCard";
44
44
  import { Badge } from "../components/ui/badge";
45
45
  import { NEW_UI } from "../config";
46
46
  import RagHealthCard from "./SetupTab/RagHealthCard";
47
+ import { WidgetDetailModal } from "../components/WidgetDetailModal";
48
+ import { PerfLineChart } from "../components/charts/PerfLineChart";
49
+ import { PerfBarChart } from "../components/charts/PerfBarChart";
50
+ import { DndContext, type DragEndEvent } from "@dnd-kit/core";
51
+ import { SortableContext } from "@dnd-kit/sortable";
52
+ import type { CardId } from "../types/card";
53
+ import { useCardPositions } from "../hooks/useCardPositions";
54
+ import { SortableCard } from "../components/ui/SortableCard";
47
55
 
48
56
  /** Type guard: narrows an SSE event to a session_sample event. */
49
57
  function isSessionSample(e: SseEvent): e is SseSessionSample {
50
58
  return e.type === "session_sample";
51
59
  }
52
60
 
61
+ /**
62
+ * Maps a widget id to its perf sample kind (null for snapshot-backed widgets,
63
+ * e.g. "model" which derives its drill-down from snapshot data in-component).
64
+ */
65
+ function perfKindFor(widget: string): string | null {
66
+ switch (widget) {
67
+ case "cache":
68
+ return "cache_hit_pct";
69
+ case "vector":
70
+ return "turn_latency_ms";
71
+ case "time":
72
+ return "disk_write_ms";
73
+ default:
74
+ return null;
75
+ }
76
+ }
77
+
53
78
  export interface OverviewTabProps {
54
79
  snapshot: SnapshotResponse | null;
55
80
  loading: boolean;
@@ -95,6 +120,60 @@ export default function OverviewTab({
95
120
  refetchSessions();
96
121
  }, [lastSampleTs, refetchSessions]);
97
122
 
123
+ // ── Widget drill-down state ─────────────────────────────────────────────
124
+ // which widget's modal is open (null = none)
125
+ const [openWidget, setOpenWidget] = useState<string | null>(null);
126
+ // perf samples fetched for the opened perf-backed widget
127
+ const [perfData, setPerfData] = useState<Array<{ ts: number; value: number }>>([]);
128
+ const [perfLoading, setPerfLoading] = useState(false);
129
+ const [perfError, setPerfError] = useState<Error | null>(null);
130
+
131
+ useEffect(() => {
132
+ setPerfData([]);
133
+ setPerfError(null);
134
+
135
+ const kind = openWidget ? perfKindFor(openWidget) : null;
136
+ if (kind === null) {
137
+ setPerfLoading(false);
138
+ return;
139
+ }
140
+
141
+ let cancelled = false;
142
+ setPerfLoading(true);
143
+ fetchPerfSamples(kind, 60)
144
+ .then((res) => {
145
+ if (!cancelled) {
146
+ setPerfData(res.samples.map((s) => ({ ts: s.ts, value: s.value })));
147
+ }
148
+ })
149
+ .catch((err) => {
150
+ if (!cancelled) {
151
+ setPerfError(
152
+ err instanceof Error ? err : new Error(String(err)),
153
+ );
154
+ }
155
+ })
156
+ .finally(() => {
157
+ if (!cancelled) setPerfLoading(false);
158
+ });
159
+ return () => {
160
+ cancelled = true;
161
+ };
162
+ }, [openWidget]);
163
+
164
+ // Drag-and-drop card reordering, persisted to localStorage.
165
+ const { order, moveCard } = useCardPositions();
166
+
167
+ const onDragEnd = useCallback(
168
+ (event: DragEndEvent) => {
169
+ const { active, over } = event;
170
+ if (over && active.id !== over.id) {
171
+ moveCard(active.id as CardId, over.id as CardId);
172
+ }
173
+ },
174
+ [moveCard],
175
+ );
176
+
98
177
  if (loading && !snapshot)
99
178
  return <div className="tab-stub">Loading snapshot…</div>;
100
179
  if (error && !snapshot)
@@ -113,6 +192,155 @@ export default function OverviewTab({
113
192
  const repo = d.repo;
114
193
  const model = d.model;
115
194
 
195
+ // Per-model cost breakdown for the Model & Cost drill-down, derived from
196
+ // snapshot data already in the component (no snapshot.byModel exists).
197
+ const modelBarData = useMemo(
198
+ () => [
199
+ { label: "Tokens freed", value: compression.repo.tokensFreed },
200
+ { label: "Input rate", value: model ? model.inputRate : 0 },
201
+ { label: "Output rate", value: model ? model.outputRate : 0 },
202
+ ],
203
+ [compression, model],
204
+ );
205
+
206
+ // Renders a single card (wrapped in SortableCard) by its CardId. Cards
207
+ // with a drill-down get an onClick; the rest are plain sortable items.
208
+ const renderCard = (cardId: CardId): React.ReactNode => {
209
+ switch (cardId) {
210
+ case "trigger":
211
+ return (
212
+ <SortableCard id={cardId} onClick={() => setOpenWidget("cache")}>
213
+ <TriggerStatus
214
+ armed={trigger.armed}
215
+ ready={trigger.ready}
216
+ currentTokens={trigger.currentTokens}
217
+ thresholdTokens={trigger.thresholdTokens}
218
+ fastGatePct={trigger.fastGatePct}
219
+ />
220
+ </SortableCard>
221
+ );
222
+ case "vector":
223
+ return (
224
+ <SortableCard id={cardId} onClick={() => setOpenWidget("vector")}>
225
+ <VectorStoreCard
226
+ checkpointCount={store.checkpointCount}
227
+ tokensIn={compression.session.tokensIn}
228
+ tokensOut={compression.session.tokensOut}
229
+ tokensFreed={compression.session.tokensFreed}
230
+ injectedCount={store.injectedCount}
231
+ dedupHitRate={store.dedupHitRate}
232
+ storageDedupRate={store.storageDedupRate}
233
+ dedupCollapsed={store.dedupCollapsed}
234
+ lastCheckpointId={session.lastCheckpointId}
235
+ compressionPct={compression.session.compressionPct}
236
+ dedupPct={compression.session.dedupPct}
237
+ />
238
+ </SortableCard>
239
+ );
240
+ case "repo-all":
241
+ return (
242
+ <SortableCard id={cardId}>
243
+ <RepoAllSessionsCard
244
+ checkpointCount={repo.checkpointCount}
245
+ tokensIn={compression.repo.tokensIn}
246
+ tokensOut={compression.repo.tokensOut}
247
+ tokensFreed={compression.repo.tokensFreed}
248
+ sessionCount={repo.sessionCount}
249
+ dedupCollapsed={repo.dedupCollapsed}
250
+ storageDedupRate={repo.storageDedupRate}
251
+ compressionPct={compression.repo.compressionPct}
252
+ dedupPct={compression.repo.dedupPct}
253
+ />
254
+ </SortableCard>
255
+ );
256
+ case "data-safety":
257
+ return (
258
+ <SortableCard id={cardId}>
259
+ <DataSafetyCard
260
+ regionsRetained={integrity.regionsRetained}
261
+ compressedOriginalBytes={integrity.compressedOriginalBytes}
262
+ duplicatesCollapsed={integrity.duplicatesCollapsed}
263
+ bytesPermanentlyDeleted={integrity.bytesPermanentlyDeleted}
264
+ />
265
+ </SortableCard>
266
+ );
267
+ case "config":
268
+ return (
269
+ <SortableCard id={cardId}>
270
+ <ConfigSummaryCard
271
+ tier={tier}
272
+ presetTier={d.presetTier}
273
+ pressure={d.pressure}
274
+ thresholdTokens={cfg.thresholdTokens}
275
+ tierPct={cfg.tierPct}
276
+ contextWindow={context.contextWindow}
277
+ fastGatePct={cfg.fastGatePct}
278
+ auto={cfg.auto}
279
+ anchorUserMessages={cfg.anchorUserMessages}
280
+ />
281
+ </SortableCard>
282
+ );
283
+ case "model":
284
+ return model ? (
285
+ <SortableCard id={cardId} onClick={() => setOpenWidget("model")}>
286
+ <CacheStatusPerModel
287
+ name={model.name}
288
+ provider={model.providerName || model.provider}
289
+ inputRate={model.inputRate}
290
+ outputRate={model.outputRate}
291
+ repoTokensFreed={compression.repo.tokensFreed}
292
+ contextWindow={context.contextWindow}
293
+ />
294
+ </SortableCard>
295
+ ) : null;
296
+ case "crew":
297
+ return (
298
+ <SortableCard id={cardId}>
299
+ <SessionInfo
300
+ activeAgents={crew.activeAgents}
301
+ currentTurn={crew.currentTurn}
302
+ />
303
+ </SortableCard>
304
+ );
305
+ case "cache-hits":
306
+ return (
307
+ <SortableCard id={cardId} onClick={() => setOpenWidget("cache")}>
308
+ <CacheHitsCard
309
+ cacheHitsSession={cacheHits.session}
310
+ cacheHitsTotal={cacheHits.total}
311
+ tokensSavedSession={cacheHits.sessionTokensSaved}
312
+ tokensSavedTotal={cacheHits.totalTokensSaved}
313
+ compactionsSession={compacts.session}
314
+ compactionsTotal={compacts.total}
315
+ />
316
+ </SortableCard>
317
+ );
318
+ case "time-saved":
319
+ return (
320
+ <SortableCard id={cardId} onClick={() => setOpenWidget("time")}>
321
+ <TimeSavedCard
322
+ compactSessionSec={timeSaved.compact.sessionSec}
323
+ compactTotalSec={timeSaved.compact.totalSec}
324
+ cacheHitSessionSec={timeSaved.cacheHit.sessionSec}
325
+ cacheHitTotalSec={timeSaved.cacheHit.totalSec}
326
+ />
327
+ </SortableCard>
328
+ );
329
+ case "rag-health":
330
+ return NEW_UI() ? (
331
+ <SortableCard id={cardId}>
332
+ <RagHealthCard />
333
+ </SortableCard>
334
+ ) : null;
335
+ case "legend":
336
+ return (
337
+ <SortableCard id={cardId}>
338
+ <LegendCard />
339
+ </SortableCard>
340
+ );
341
+ }
342
+ };
343
+
116
344
  return (
117
345
  <div className="flex flex-col gap-4">
118
346
  <div className="flex items-center justify-between">
@@ -121,7 +349,9 @@ export default function OverviewTab({
121
349
  updated {formatUpdatedAt(updatedAt)}
122
350
  </span>
123
351
  </div>
124
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
352
+ {/* Full-width live-context row: per-session gauges + per-repo stack
353
+ * sit above the grid so they get the whole width of the tab. */}
354
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
125
355
  <SessionContextGauges
126
356
  sessions={sessionsData}
127
357
  loading={sessionsLoading}
@@ -138,85 +368,86 @@ export default function OverviewTab({
138
368
  loading={sessionsLoading}
139
369
  error={sessionsErr}
140
370
  />
141
- <TriggerStatus
142
- armed={trigger.armed}
143
- ready={trigger.ready}
144
- currentTokens={trigger.currentTokens}
145
- thresholdTokens={trigger.thresholdTokens}
146
- fastGatePct={trigger.fastGatePct}
147
- />
148
- <VectorStoreCard
149
- checkpointCount={store.checkpointCount}
150
- tokensIn={compression.session.tokensIn}
151
- tokensOut={compression.session.tokensOut}
152
- tokensFreed={compression.session.tokensFreed}
153
- injectedCount={store.injectedCount}
154
- dedupHitRate={store.dedupHitRate}
155
- storageDedupRate={store.storageDedupRate}
156
- dedupCollapsed={store.dedupCollapsed}
157
- lastCheckpointId={session.lastCheckpointId}
158
- compressionPct={compression.session.compressionPct}
159
- dedupPct={compression.session.dedupPct}
160
- />
161
- <RepoAllSessionsCard
162
- checkpointCount={repo.checkpointCount}
163
- tokensIn={compression.repo.tokensIn}
164
- tokensOut={compression.repo.tokensOut}
165
- tokensFreed={compression.repo.tokensFreed}
166
- sessionCount={repo.sessionCount}
167
- dedupCollapsed={repo.dedupCollapsed}
168
- storageDedupRate={repo.storageDedupRate}
169
- compressionPct={compression.repo.compressionPct}
170
- dedupPct={compression.repo.dedupPct}
171
- />
172
- <DataSafetyCard
173
- regionsRetained={integrity.regionsRetained}
174
- compressedOriginalBytes={integrity.compressedOriginalBytes}
175
- duplicatesCollapsed={integrity.duplicatesCollapsed}
176
- bytesPermanentlyDeleted={integrity.bytesPermanentlyDeleted}
177
- />
178
- <ConfigSummaryCard
179
- tier={tier}
180
- presetTier={d.presetTier}
181
- pressure={d.pressure}
182
- thresholdTokens={cfg.thresholdTokens}
183
- tierPct={cfg.tierPct}
184
- contextWindow={context.contextWindow}
185
- fastGatePct={cfg.fastGatePct}
186
- auto={cfg.auto}
187
- anchorUserMessages={cfg.anchorUserMessages}
188
- />
189
- {model && (
190
- <CacheStatusPerModel
191
- name={model.name}
192
- provider={model.providerName || model.provider}
193
- inputRate={model.inputRate}
194
- outputRate={model.outputRate}
195
- repoTokensFreed={compression.repo.tokensFreed}
196
- contextWindow={context.contextWindow}
371
+ </div>
372
+ <DndContext onDragEnd={onDragEnd}>
373
+ <SortableContext items={order}>
374
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
375
+ {order
376
+ .map((cardId) => ({ cardId, node: renderCard(cardId) }))
377
+ .filter(({ node }) => node != null)
378
+ .map(({ cardId, node }) => (
379
+ <div key={cardId}>{node}</div>
380
+ ))}
381
+ </div>
382
+ </SortableContext>
383
+ </DndContext>
384
+
385
+ {/* ── Drill-down modals ───────────────────────────────────────── */}
386
+ <WidgetDetailModal
387
+ title="Cache Hits Trend"
388
+ open={openWidget === "cache"}
389
+ onClose={() => setOpenWidget(null)}
390
+ >
391
+ {perfLoading ? (
392
+ <p className="py-8 text-center text-sm text-muted-foreground">Loading…</p>
393
+ ) : perfError ? (
394
+ <p className="py-8 text-center text-sm text-red-400">{perfError.message}</p>
395
+ ) : (
396
+ <PerfLineChart
397
+ data={perfData}
398
+ label="Cache hit %"
399
+ color="#58a6ff"
197
400
  />
198
401
  )}
199
- <SessionInfo
200
- activeAgents={crew.activeAgents}
201
- currentTurn={crew.currentTurn}
202
- />
203
- <CacheHitsCard
204
- cacheHitsSession={cacheHits.session}
205
- cacheHitsTotal={cacheHits.total}
206
- tokensSavedSession={cacheHits.sessionTokensSaved}
207
- tokensSavedTotal={cacheHits.totalTokensSaved}
208
- compactionsSession={compacts.session}
209
- compactionsTotal={compacts.total}
210
- />
211
- <TimeSavedCard
212
- compactSessionSec={timeSaved.compact.sessionSec}
213
- compactTotalSec={timeSaved.compact.totalSec}
214
- cacheHitSessionSec={timeSaved.cacheHit.sessionSec}
215
- cacheHitTotalSec={timeSaved.cacheHit.totalSec}
402
+ </WidgetDetailModal>
403
+
404
+ <WidgetDetailModal
405
+ title="Turn Latency"
406
+ open={openWidget === "vector"}
407
+ onClose={() => setOpenWidget(null)}
408
+ >
409
+ {perfLoading ? (
410
+ <p className="py-8 text-center text-sm text-muted-foreground">Loading…</p>
411
+ ) : perfError ? (
412
+ <p className="py-8 text-center text-sm text-red-400">{perfError.message}</p>
413
+ ) : (
414
+ <PerfLineChart
415
+ data={perfData}
416
+ label="Turn latency (ms)"
417
+ color="#58a6ff"
418
+ />
419
+ )}
420
+ </WidgetDetailModal>
421
+
422
+ <WidgetDetailModal
423
+ title="Disk Write Time"
424
+ open={openWidget === "time"}
425
+ onClose={() => setOpenWidget(null)}
426
+ >
427
+ {perfLoading ? (
428
+ <p className="py-8 text-center text-sm text-muted-foreground">Loading…</p>
429
+ ) : perfError ? (
430
+ <p className="py-8 text-center text-sm text-red-400">{perfError.message}</p>
431
+ ) : (
432
+ <PerfLineChart
433
+ data={perfData}
434
+ label="Disk write (ms)"
435
+ color="#58a6ff"
436
+ />
437
+ )}
438
+ </WidgetDetailModal>
439
+
440
+ <WidgetDetailModal
441
+ title="Model &amp; Cost"
442
+ open={openWidget === "model"}
443
+ onClose={() => setOpenWidget(null)}
444
+ >
445
+ <PerfBarChart
446
+ data={modelBarData}
447
+ color="#3fb950"
448
+ valueFormatter={(v) => v.toLocaleString()}
216
449
  />
217
- {NEW_UI() && <RagHealthCard />}
218
- <LegendCard />
219
- </div>
450
+ </WidgetDetailModal>
220
451
  </div>
221
452
  );
222
453
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * dashboard-client/src/types/card.ts — Overview tab card identity + fixed order.
3
+ *
4
+ * Every card on the Overview grid maps to a stable CardId. The default order
5
+ * is the canonical render order; the user can reorder via drag-and-drop and
6
+ * their order is persisted by useCardPositions.
7
+ */
8
+
9
+ /** Identifies each card on the Overview grid. */
10
+ export type CardId =
11
+ | "trigger"
12
+ | "vector"
13
+ | "repo-all"
14
+ | "data-safety"
15
+ | "config"
16
+ | "model"
17
+ | "crew"
18
+ | "cache-hits"
19
+ | "time-saved"
20
+ | "rag-health"
21
+ | "legend";
22
+
23
+ /**
24
+ * Canonical default order of the Overview cards, matching the pre-dnd render
25
+ * order. Any persisted order must contain exactly these ids (no extras, no
26
+ * missing) to be accepted.
27
+ */
28
+ export const DEFAULT_CARD_ORDER: CardId[] = [
29
+ "trigger",
30
+ "vector",
31
+ "repo-all",
32
+ "data-safety",
33
+ "config",
34
+ "model",
35
+ "crew",
36
+ "cache-hits",
37
+ "time-saved",
38
+ "rag-health",
39
+ "legend",
40
+ ];
@@ -6,7 +6,7 @@ export default {
6
6
  container: {
7
7
  center: true,
8
8
  padding: "1.5rem",
9
- screens: { "2xl": "1600px" },
9
+ screens: { "2xl": "100%" },
10
10
  },
11
11
  extend: {
12
12
  colors: {
@@ -74,6 +74,8 @@ import type {
74
74
  ServersResponse,
75
75
  PerfQuery,
76
76
  PerfResponse,
77
+ PerfSamplesQuery,
78
+ PerfSamplesResponse,
77
79
  } from "./types.js";
78
80
 
79
81
  // ─── ENDPOINTS Registry ─────────────────────────────────────────────────────
@@ -184,6 +186,14 @@ export const ENDPOINTS = {
184
186
  description: "Rolling-window performance aggregates over perf_samples.",
185
187
  } as const satisfies EndpointDef<"GET", PerfQuery, PerfResponse>,
186
188
 
189
+ /** GET /api/perf/samples — Raw perf samples for a single kind (chart drill-down). */
190
+ perfSamples: {
191
+ method: "GET",
192
+ path: "/api/perf/samples",
193
+ description:
194
+ "Raw perf_samples for one kind (time-series drill-down across any perf metric).",
195
+ } as const satisfies EndpointDef<"GET", PerfSamplesQuery, PerfSamplesResponse>,
196
+
187
197
  /** GET /api/achievements — Achievement tiles with unlock state. */
188
198
  achievements: {
189
199
  method: "GET",
@@ -348,3 +348,39 @@ export interface PerfResponse {
348
348
  /** Diagnostic counters from the live snapshot, or null if unavailable. */
349
349
  readonly diag: PerfDiag | null;
350
350
  }
351
+
352
+ /**
353
+ * Query parameters for GET /api/perf/samples. `kind` selects which perf metric
354
+ * to return raw samples for; `minutes` controls the rolling window size.
355
+ */
356
+ export interface PerfSamplesQuery {
357
+ /** Perf metric kind (turn_latency_ms, tps, disk_write_ms, cache_hit_pct, ...). Required. */
358
+ readonly kind: string;
359
+ /** Rolling window size in minutes (default: 60). Optional. */
360
+ readonly minutes?: number;
361
+ }
362
+
363
+ /** A single raw perf sample for time-series charting. */
364
+ export interface PerfSamplePoint {
365
+ /** Epoch ms timestamp of the sample. */
366
+ readonly ts: number;
367
+ /** Sample value for the requested kind. */
368
+ readonly value: number;
369
+ /** Optional stringified metadata associated with the sample. */
370
+ readonly meta?: string;
371
+ }
372
+
373
+ /**
374
+ * Response for GET /api/perf/samples. Raw perf samples for a single kind,
375
+ * ascending by ts, for chart drill-down in the Perf tab.
376
+ */
377
+ export interface PerfSamplesResponse {
378
+ /** Array of raw samples ascending by ts. */
379
+ readonly samples: PerfSamplePoint[];
380
+ /** The requested perf kind. */
381
+ readonly kind: string;
382
+ /** The rolling window size in minutes used. */
383
+ readonly minutes: number;
384
+ /** Epoch ms timestamp when the response was generated. */
385
+ readonly updatedAt: number;
386
+ }
@@ -31,6 +31,9 @@ export type {
31
31
  PerfDiag,
32
32
  PerfQuery,
33
33
  PerfResponse,
34
+ PerfSamplesQuery,
35
+ PerfSamplePoint,
36
+ PerfSamplesResponse,
34
37
  GameScoreRow,
35
38
  GameScoresQuery,
36
39
  AchievementRow,
@@ -140,6 +140,9 @@ export type {
140
140
  PerfDiag,
141
141
  PerfQuery,
142
142
  PerfResponse,
143
+ PerfSamplesQuery,
144
+ PerfSamplePoint,
145
+ PerfSamplesResponse,
143
146
  GameScoreRow,
144
147
  GameScoresQuery,
145
148
  AchievementRow,