pi-mega-compact 0.14.1 → 0.15.1

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 (82) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +6 -0
  2. package/dist/extensions/dashboard-server/resolve-snapshot.js +20 -8
  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/extensions/dashboard-client/dist/assets/AreaChart-rJdaOUlj.js +2 -0
  7. package/extensions/dashboard-client/dist/assets/AreaChart-rJdaOUlj.js.map +1 -0
  8. package/extensions/dashboard-client/dist/assets/BarChart-DOfc8me9.js +2 -0
  9. package/extensions/dashboard-client/dist/assets/BarChart-DOfc8me9.js.map +1 -0
  10. package/extensions/dashboard-client/dist/assets/{CacheTab-VNFZSdRr.js → CacheTab-EhqkEglo.js} +2 -2
  11. package/extensions/dashboard-client/dist/assets/{CacheTab-VNFZSdRr.js.map → CacheTab-EhqkEglo.js.map} +1 -1
  12. package/extensions/dashboard-client/dist/assets/{EventsTab-Dr7RDBy2.js → EventsTab-BUACwWXf.js} +2 -2
  13. package/extensions/dashboard-client/dist/assets/{EventsTab-Dr7RDBy2.js.map → EventsTab-BUACwWXf.js.map} +1 -1
  14. package/extensions/dashboard-client/dist/assets/{HealthTab-B8VZ8gbb.js → HealthTab-CDEYVRBb.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{HealthTab-B8VZ8gbb.js.map → HealthTab-CDEYVRBb.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DMtrXRcn.js → MaintenanceTab-DtanIeCQ.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DMtrXRcn.js.map → MaintenanceTab-DtanIeCQ.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-PFrMdLOm.js → MemoryMapTab-BGEhL7OG.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-PFrMdLOm.js.map → MemoryMapTab-BGEhL7OG.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/MetricsTab-Cktozn4s.js +2 -0
  21. package/extensions/dashboard-client/dist/assets/MetricsTab-Cktozn4s.js.map +1 -0
  22. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js +16 -0
  23. package/extensions/dashboard-client/dist/assets/OverviewTab-08v6Ayuf.js.map +1 -0
  24. package/extensions/dashboard-client/dist/assets/{ReposTab-D6pEUtEJ.js → ReposTab-Bg72Ozdc.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{ReposTab-D6pEUtEJ.js.map → ReposTab-Bg72Ozdc.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/SessionsTab-BcTK9Lg6.js +2 -0
  27. package/extensions/dashboard-client/dist/assets/{SessionsTab-CcFzDJ7W.js.map → SessionsTab-BcTK9Lg6.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{SetupTab-YCt5NGB5.js → SetupTab-Bt-Ru1xp.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{SetupTab-YCt5NGB5.js.map → SetupTab-Bt-Ru1xp.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BsLNhOOj.js → TimeSavedCard-ic_LjJ79.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BsLNhOOj.js.map → TimeSavedCard-ic_LjJ79.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{TurnsTab-C1jpVZrH.js → TurnsTab-D9AZTMgG.js} +4 -4
  33. package/extensions/dashboard-client/dist/assets/{TurnsTab-C1jpVZrH.js.map → TurnsTab-D9AZTMgG.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/WikiTab-hRcJPi_m.js +2 -0
  35. package/extensions/dashboard-client/dist/assets/{WikiTab-RHoVt95g.js.map → WikiTab-hRcJPi_m.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{button-DSZI4aZ5.js → button-BdAtIbVu.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{button-DSZI4aZ5.js.map → button-BdAtIbVu.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{card-CsZzIyPW.js → card-BFYc_QrK.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{card-CsZzIyPW.js.map → card-BFYc_QrK.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/generateCategoricalChart-BwzyqytT.js +70 -0
  41. package/extensions/dashboard-client/dist/assets/generateCategoricalChart-BwzyqytT.js.map +1 -0
  42. package/extensions/dashboard-client/dist/assets/{index-B1_J_Y0b.js → index-pmtEr3sb.js} +8 -8
  43. package/extensions/dashboard-client/dist/assets/index-pmtEr3sb.js.map +1 -0
  44. package/extensions/dashboard-client/dist/assets/index-za_gmEmQ.css +1 -0
  45. package/extensions/dashboard-client/dist/assets/{switch-DOIA1Nxu.js → switch-DEyGlFwJ.js} +2 -2
  46. package/extensions/dashboard-client/dist/assets/{switch-DOIA1Nxu.js.map → switch-DEyGlFwJ.js.map} +1 -1
  47. package/extensions/dashboard-client/dist/assets/{toggle-CyS6PHCO.js → toggle-pDMBQYQj.js} +2 -2
  48. package/extensions/dashboard-client/dist/assets/{toggle-CyS6PHCO.js.map → toggle-pDMBQYQj.js.map} +1 -1
  49. package/extensions/dashboard-client/dist/assets/{useSSE-CY7UIUzm.js → useSSE-BKaC4dao.js} +2 -2
  50. package/extensions/dashboard-client/dist/assets/{useSSE-CY7UIUzm.js.map → useSSE-BKaC4dao.js.map} +1 -1
  51. package/extensions/dashboard-client/dist/index.html +2 -2
  52. package/extensions/dashboard-client/package-lock.json +56 -39
  53. package/extensions/dashboard-client/package.json +3 -0
  54. package/extensions/dashboard-client/src/api/client.ts +11 -0
  55. package/extensions/dashboard-client/src/components/SessionContextGauges.tsx +14 -8
  56. package/extensions/dashboard-client/src/components/WidgetDetailModal.tsx +69 -0
  57. package/extensions/dashboard-client/src/components/charts/PerfBarChart.tsx +89 -0
  58. package/extensions/dashboard-client/src/components/charts/PerfLineChart.tsx +108 -0
  59. package/extensions/dashboard-client/src/components/ui/SortableCard.tsx +75 -0
  60. package/extensions/dashboard-client/src/hooks/useCardPositions.ts +74 -0
  61. package/extensions/dashboard-client/src/styles/session-gauges.css +28 -15
  62. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +310 -79
  63. package/extensions/dashboard-client/src/types/card.ts +40 -0
  64. package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +10 -0
  65. package/extensions/dashboard-server/api-contracts/endpoints/types.ts +36 -0
  66. package/extensions/dashboard-server/api-contracts/endpoints.ts +3 -0
  67. package/extensions/dashboard-server/api-contracts/index.ts +3 -0
  68. package/extensions/dashboard-server/resolve-snapshot.ts +22 -10
  69. package/extensions/dashboard-server/routes-game.ts +72 -0
  70. package/extensions/dashboard-server/routes.ts +1 -0
  71. package/extensions/dashboard-server/server.ts +2 -0
  72. package/package.json +1 -1
  73. package/extensions/dashboard-client/dist/assets/AreaChart-BUUketyS.js +0 -70
  74. package/extensions/dashboard-client/dist/assets/AreaChart-BUUketyS.js.map +0 -1
  75. package/extensions/dashboard-client/dist/assets/MetricsTab-Dd1kXuvg.js +0 -2
  76. package/extensions/dashboard-client/dist/assets/MetricsTab-Dd1kXuvg.js.map +0 -1
  77. package/extensions/dashboard-client/dist/assets/OverviewTab-DOpObVA9.js +0 -2
  78. package/extensions/dashboard-client/dist/assets/OverviewTab-DOpObVA9.js.map +0 -1
  79. package/extensions/dashboard-client/dist/assets/SessionsTab-CcFzDJ7W.js +0 -2
  80. package/extensions/dashboard-client/dist/assets/WikiTab-RHoVt95g.js +0 -2
  81. package/extensions/dashboard-client/dist/assets/index-B1_J_Y0b.js.map +0 -1
  82. package/extensions/dashboard-client/dist/assets/index-C5TGTOd6.css +0 -1
@@ -0,0 +1,74 @@
1
+ /**
2
+ * dashboard-client/src/hooks/useCardPositions.ts — persisted, deterministic
3
+ * card order for the Overview tab.
4
+ *
5
+ * The order is read from localStorage on mount, validated against
6
+ * DEFAULT_CARD_ORDER (must contain exactly the same CardIds — no missing,
7
+ * no extras — and no duplicate ids), and falls back to the default if invalid.
8
+ * Pure deterministic: no Date.now() / Math.random() anywhere.
9
+ */
10
+ import { useCallback, useState } from "react";
11
+ import type { CardId } from "../types/card";
12
+ import { DEFAULT_CARD_ORDER } from "../types/card";
13
+
14
+ const STORAGE_KEY = "mega-compact-card-order";
15
+
16
+ /** Returns true when `ids` is a valid permutation of DEFAULT_CARD_ORDER. */
17
+ function isValidOrder(ids: unknown): ids is CardId[] {
18
+ if (!Array.isArray(ids)) return false;
19
+ if (ids.length !== DEFAULT_CARD_ORDER.length) return false;
20
+
21
+ const expected = new Set<CardId>(DEFAULT_CARD_ORDER);
22
+ const seen = new Set<CardId>();
23
+
24
+ for (const id of ids) {
25
+ // every entry must be a known CardId
26
+ if (typeof id !== "string" || !expected.has(id as CardId)) return false;
27
+ // no duplicates
28
+ if (seen.has(id as CardId)) return false;
29
+ seen.add(id as CardId);
30
+ }
31
+ return true;
32
+ }
33
+
34
+ /** Loads a persisted order from localStorage, validating it first. */
35
+ function loadOrder(): CardId[] {
36
+ try {
37
+ const raw = window.localStorage.getItem(STORAGE_KEY);
38
+ if (raw == null) return DEFAULT_CARD_ORDER;
39
+ const parsed: unknown = JSON.parse(raw);
40
+ return isValidOrder(parsed) ? parsed : DEFAULT_CARD_ORDER;
41
+ } catch {
42
+ return DEFAULT_CARD_ORDER;
43
+ }
44
+ }
45
+
46
+ export interface UseCardPositionsResult {
47
+ order: CardId[];
48
+ moveCard: (id: CardId, overId: CardId) => void;
49
+ }
50
+
51
+ export function useCardPositions(): UseCardPositionsResult {
52
+ const [order, setOrder] = useState<CardId[]>(loadOrder);
53
+
54
+ const moveCard = useCallback((id: CardId, overId: CardId) => {
55
+ setOrder((prev) => {
56
+ const from = prev.indexOf(id);
57
+ const to = prev.indexOf(overId);
58
+ if (from === -1 || to === -1 || from === to) return prev;
59
+
60
+ const next = [...prev];
61
+ next.splice(from, 1);
62
+ next.splice(to, 0, id);
63
+ try {
64
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
65
+ } catch {
66
+ // storage unavailable (private mode / quota) — ignore; order
67
+ // still holds for the session
68
+ }
69
+ return next;
70
+ });
71
+ }, []);
72
+
73
+ return { order, moveCard };
74
+ }
@@ -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
+ ];
@@ -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",