pi-mega-compact 0.11.1 → 0.11.3

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 (109) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +28 -1
  2. package/dist/extensions/dashboard-server/api-contracts/maintenance.js +7 -0
  3. package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +7 -0
  4. package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
  5. package/dist/extensions/dashboard-server/index-reader.js +22 -0
  6. package/dist/extensions/dashboard-server/routes-cache.js +80 -0
  7. package/dist/extensions/dashboard-server/routes-cache.test.js +248 -0
  8. package/dist/extensions/dashboard-server/routes-maintenance.js +293 -0
  9. package/dist/extensions/dashboard-server/routes-repo.js +24 -0
  10. package/dist/extensions/dashboard-server/routes.js +2 -0
  11. package/dist/extensions/dashboard-server/server.js +5 -1
  12. package/dist/extensions/mega-cache-replay.test.js +233 -24
  13. package/dist/extensions/mega-compact-s38.test.js +201 -6
  14. package/dist/extensions/mega-config.js +3 -0
  15. package/dist/extensions/mega-events/agent-handlers.js +23 -7
  16. package/dist/extensions/mega-events/context-handler.js +37 -19
  17. package/dist/extensions/mega-events/error-classifier.js +27 -16
  18. package/dist/extensions/mega-runtime/runtime-snapshot.js +14 -4
  19. package/dist/extensions/mega-runtime/snapshot.js +1 -1
  20. package/dist/extensions/mega-runtime/snapshot.test.js +127 -0
  21. package/dist/extensions/mega-runtime/widget.js +4 -4
  22. package/dist/extensions/mega-runtime/widget.test.js +103 -12
  23. package/dist/src/pricing.js +78 -0
  24. package/dist/src/store/sqlite/maintenance.js +20 -1
  25. package/dist/src/store/sqlite/perf-samples.js +104 -0
  26. package/dist/src/store/sqlite/perf-samples.test.js +93 -1
  27. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DZp_xQOW.js → AchievementTiles-vEo2MtE2.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DZp_xQOW.js.map → AchievementTiles-vEo2MtE2.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +2 -0
  30. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BzFHpAHM.js.map → AchievementsTab-DjxfYrDc.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +2 -0
  32. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +1 -0
  33. package/extensions/dashboard-client/dist/assets/{ConfigTab-DQ4FG33J.js → ConfigTab-Cm6ymbmx.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{ConfigTab-DQ4FG33J.js.map → ConfigTab-Cm6ymbmx.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{EventsTab-D5HoEVYo.js → EventsTab-DXWl_YzI.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{EventsTab-D5HoEVYo.js.map → EventsTab-DXWl_YzI.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{GameTab-Jzsc4yeY.js → GameTab-BLDudcO5.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{GameTab-Jzsc4yeY.js.map → GameTab-BLDudcO5.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js +2 -0
  40. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js.map +1 -0
  41. package/extensions/dashboard-client/dist/assets/{MetricsTab-D_XnsEYX.js → MetricsTab-Cq4u-rZF.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{MetricsTab-D_XnsEYX.js.map → MetricsTab-Cq4u-rZF.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{OverviewTab-BMREvAth.js → OverviewTab-D4kGvsiQ.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{OverviewTab-BMREvAth.js.map → OverviewTab-D4kGvsiQ.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +2 -0
  46. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +1 -0
  47. package/extensions/dashboard-client/dist/assets/{SessionsTab-mz_vz8i2.js → SessionsTab-DZC2-GCn.js} +2 -2
  48. package/extensions/dashboard-client/dist/assets/{SessionsTab-mz_vz8i2.js.map → SessionsTab-DZC2-GCn.js.map} +1 -1
  49. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +2 -0
  50. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +1 -0
  51. package/extensions/dashboard-client/dist/assets/{TopicsTab-DKuct4oD.js → TopicsTab-01G71hv1.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{TopicsTab-DKuct4oD.js.map → TopicsTab-01G71hv1.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{TurnsTab-11MGLxY8.js → TurnsTab-DLRg0LZT.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{TurnsTab-11MGLxY8.js.map → TurnsTab-DLRg0LZT.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +1 -0
  56. package/extensions/dashboard-client/dist/assets/{index-CqLN7STl.js → index-BX0bcgLu.js} +10 -10
  57. package/extensions/dashboard-client/dist/assets/{index-CqLN7STl.js.map → index-BX0bcgLu.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{useSSE-CXrQSbVY.js → useSSE-BBYt5UPB.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{useSSE-CXrQSbVY.js.map → useSSE-BBYt5UPB.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/index.html +2 -2
  61. package/extensions/dashboard-client/src/App.tsx +5 -1
  62. package/extensions/dashboard-client/src/api/client.ts +29 -0
  63. package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +13 -1
  64. package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +141 -0
  65. package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +31 -3
  66. package/extensions/dashboard-client/src/main.tsx +1 -0
  67. package/extensions/dashboard-client/src/styles/maintenance.css +267 -0
  68. package/extensions/dashboard-client/src/styles/overview-extra.css +9 -0
  69. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +83 -29
  70. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +458 -0
  71. package/extensions/dashboard-client/src/utils/format.ts +42 -7
  72. package/extensions/dashboard-server/api-contracts/endpoints.ts +54 -1
  73. package/extensions/dashboard-server/api-contracts/index.ts +16 -0
  74. package/extensions/dashboard-server/api-contracts/maintenance.ts +84 -0
  75. package/extensions/dashboard-server/api-contracts/multi-repo.ts +6 -0
  76. package/extensions/dashboard-server/api-contracts/provider-cache.ts +30 -0
  77. package/extensions/dashboard-server/api-contracts.test.ts +3 -2
  78. package/extensions/dashboard-server/api-contracts.ts +9 -0
  79. package/extensions/dashboard-server/index-reader.ts +23 -0
  80. package/extensions/dashboard-server/routes-cache.test.ts +302 -0
  81. package/extensions/dashboard-server/routes-cache.ts +123 -0
  82. package/extensions/dashboard-server/routes-maintenance.ts +352 -0
  83. package/extensions/dashboard-server/routes-repo.ts +31 -4
  84. package/extensions/dashboard-server/routes.ts +2 -0
  85. package/extensions/dashboard-server/server.ts +4 -0
  86. package/extensions/mega-cache-replay.test.ts +481 -166
  87. package/extensions/mega-compact-s38.test.ts +210 -6
  88. package/extensions/mega-config.ts +6 -0
  89. package/extensions/mega-events/agent-handlers.ts +24 -6
  90. package/extensions/mega-events/context-handler.ts +59 -24
  91. package/extensions/mega-events/error-classifier.ts +30 -13
  92. package/extensions/mega-runtime/runtime-snapshot.ts +31 -9
  93. package/extensions/mega-runtime/snapshot.test.ts +139 -0
  94. package/extensions/mega-runtime/snapshot.ts +6 -4
  95. package/extensions/mega-runtime/widget.test.ts +290 -105
  96. package/extensions/mega-runtime/widget.ts +29 -6
  97. package/package.json +1 -1
  98. package/src/pricing.ts +95 -0
  99. package/src/store/sqlite/maintenance.ts +20 -1
  100. package/src/store/sqlite/perf-samples.test.ts +107 -3
  101. package/src/store/sqlite/perf-samples.ts +142 -6
  102. package/extensions/dashboard-client/dist/assets/AchievementsTab-BzFHpAHM.js +0 -2
  103. package/extensions/dashboard-client/dist/assets/CacheTab-DEQbTq2T.js +0 -2
  104. package/extensions/dashboard-client/dist/assets/CacheTab-DEQbTq2T.js.map +0 -1
  105. package/extensions/dashboard-client/dist/assets/ReposTab-DuyhOktG.js +0 -2
  106. package/extensions/dashboard-client/dist/assets/ReposTab-DuyhOktG.js.map +0 -1
  107. package/extensions/dashboard-client/dist/assets/TimeSavedCard-B8AIjNTY.js +0 -2
  108. package/extensions/dashboard-client/dist/assets/TimeSavedCard-B8AIjNTY.js.map +0 -1
  109. package/extensions/dashboard-client/dist/assets/index-gn66lYD2.css +0 -1
@@ -14,7 +14,7 @@
14
14
  // barrel (`extensions/mega-runtime.ts` does `export * from "./widget.js"`) and
15
15
  // every direct consumer keep resolving the same named exports.
16
16
  export { C, PULSE, DEFAULT_PANEL_BG, EFFECT_BASE, panelBgFor, themeAnsi, sgrReset, wrapLine, panelLine, panelBar, effectBorderSgr, effectBar, fmtTokens, ramp, sinceCompactStr, } from "./widget-ansi.js";
17
- import { C, PULSE, DEFAULT_PANEL_BG, panelBgFor, themeAnsi, sgrReset, wrapLine, panelLine, panelBar, effectBorderSgr, effectBar, fmtTokens, ramp, sinceCompactStr } from "./widget-ansi.js";
17
+ import { C, PULSE, DEFAULT_PANEL_BG, panelBgFor, themeAnsi, sgrReset, wrapLine, panelLine, panelBar, effectBorderSgr, effectBar, fmtTokens, ramp, sinceCompactStr, } from "./widget-ansi.js";
18
18
  // ── buildWidgetLines ───────────────────────────────────────────────────────
19
19
  // Kept as a free function (not a MegaRuntime method) so runtime.ts stays
20
20
  // focused on state management. It reads the WidgetData snapshot + the live
@@ -44,12 +44,12 @@ export function buildWidgetLines(wd, width, activeAgents) {
44
44
  // width guard + theme bg apply identically to the full panel. Level is shown
45
45
  // only when game mode is on (otherwise just the cache %).
46
46
  if (wd.tuiMode === "minimal") {
47
- const lvl = wd.gameMode ? wd.level ?? 1 : undefined;
47
+ const lvl = wd.gameMode ? (wd.level ?? 1) : undefined;
48
48
  const cachePct = wd.cachePct ?? 0;
49
49
  const cacheStr = `${Math.round(cachePct * 10) / 10}%`;
50
50
  const accent = themeAnsi(wd.theme, "accent");
51
51
  const mega = themeAnsi(wd.theme, "mega");
52
- const megaFlare = wd.gameMode && wd.megaCacheFlare && cachePct >= 100
52
+ const megaFlare = wd.gameMode && wd.megaCacheFlare && (wd.megaCacheFlarePct ?? 0) >= 100
53
53
  ? ` ${mega}MEGA CACHE${sgrReset(mega)}`
54
54
  : "";
55
55
  const body = lvl != null
@@ -71,7 +71,7 @@ export function buildWidgetLines(wd, width, activeAgents) {
71
71
  const lvlPrefix = wd.gameMode
72
72
  ? `${themeAnsi(wd.theme, "accent")}${wd.gameMode && wd.levelUpFlare ? "\x1b[5m" : ""}LVL ${wd.level ?? 1}${wd.gameMode && wd.levelUpFlare ? "\x1b[0m" : ""}${sgrReset(themeAnsi(wd.theme, "accent"))} `
73
73
  : "";
74
- const megaFlareSuffix = wd.gameMode && wd.megaCacheFlare && (wd.cachePct ?? 0) >= 100
74
+ const megaFlareSuffix = wd.gameMode && wd.megaCacheFlare && (wd.megaCacheFlarePct ?? 0) >= 100
75
75
  ? `${sep}${themeAnsi(wd.theme, "mega")}MEGA CACHE! (oops, you cached so hard the dedup caught fire)${sgrReset(themeAnsi(wd.theme, "mega"))}`
76
76
  : "";
77
77
  // Build one long content line — let terminal wrap it naturally
@@ -94,6 +94,8 @@ describe("buildWidgetLines (S31)", () => {
94
94
  level: 1,
95
95
  cachePct,
96
96
  megaCacheFlare: flare,
97
+ // C.3: flare gate reads megaCacheFlarePct, not cachePct
98
+ ...(flare ? { megaCacheFlarePct: cachePct } : {}),
97
99
  });
98
100
  const lines = buildWidgetLines(wd, WIDTH, 0);
99
101
  const body = contentLines(lines);
@@ -138,11 +140,21 @@ describe("buildWidgetLines (S31)", () => {
138
140
  });
139
141
  describe("buildWidgetLines ambient border effect (v0.8.3)", () => {
140
142
  const effBase = (overrides = {}) => baseWd({
141
- theme: DEFAULT_THEME, tuiMode: "full", gameMode: true, level: 1, cachePct: 42, ...overrides,
143
+ theme: DEFAULT_THEME,
144
+ tuiMode: "full",
145
+ gameMode: true,
146
+ level: 1,
147
+ cachePct: 42,
148
+ ...overrides,
142
149
  });
143
150
  const isBorder = (l) => l.includes("─");
144
151
  it("activeEffect (pulse, mid-window) -> border lines carry a 256-color fg SGR", () => {
145
- const ae = { type: "pulse", role: "accent", startedAt: Date.now() - 250, durationMs: 2000 };
152
+ const ae = {
153
+ type: "pulse",
154
+ role: "accent",
155
+ startedAt: Date.now() - 250,
156
+ durationMs: 2000,
157
+ };
146
158
  const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
147
159
  const borders = lines.filter(isBorder);
148
160
  assert.ok(borders.length >= 2, "has top + bottom borders");
@@ -158,7 +170,12 @@ describe("buildWidgetLines ambient border effect (v0.8.3)", () => {
158
170
  }
159
171
  });
160
172
  it("expired activeEffect -> plain borders (per-frame expiry enforced)", () => {
161
- const ae = { type: "pulse", role: "accent", startedAt: Date.now() - 5000, durationMs: 1000 };
173
+ const ae = {
174
+ type: "pulse",
175
+ role: "accent",
176
+ startedAt: Date.now() - 5000,
177
+ durationMs: 1000,
178
+ };
162
179
  const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
163
180
  const borders = lines.filter(isBorder);
164
181
  for (const b of borders) {
@@ -167,7 +184,12 @@ describe("buildWidgetLines ambient border effect (v0.8.3)", () => {
167
184
  });
168
185
  it("activeEffect border lines are width-safe (pulse, minimal + full)", () => {
169
186
  for (const tuiMode of ["minimal", "full"]) {
170
- const ae = { type: "pulse", role: "mega", startedAt: Date.now() - 100, durationMs: 2000 };
187
+ const ae = {
188
+ type: "pulse",
189
+ role: "mega",
190
+ startedAt: Date.now() - 100,
191
+ durationMs: 2000,
192
+ };
171
193
  const lines = buildWidgetLines(effBase({ activeEffect: ae, tuiMode }), 60, 0);
172
194
  for (const l of lines)
173
195
  assert.ok(visibleWidth(l) <= 60, `width safe (${tuiMode}): ${visibleWidth(l)}`);
@@ -175,12 +197,53 @@ describe("buildWidgetLines ambient border effect (v0.8.3)", () => {
175
197
  });
176
198
  it("flash effect mid-window border carries the full base index SGR", () => {
177
199
  // Force an 'on' phase of the 120ms alternate by starting just now.
178
- const ae = { type: "flash", role: "red", startedAt: Date.now(), durationMs: 1200 };
200
+ const ae = {
201
+ type: "flash",
202
+ role: "red",
203
+ startedAt: Date.now(),
204
+ durationMs: 1200,
205
+ };
179
206
  const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
180
207
  const borders = lines.filter(isBorder);
181
208
  assert.ok(borders.some((b) => b.includes("\x1b[38;5;203m")), `flash-on phase uses red base 203`);
182
209
  });
183
210
  });
211
+ describe("buildWidgetLines C.3 flare gate regression", () => {
212
+ const flareBase = (overrides = {}) => baseWd({
213
+ theme: DEFAULT_THEME,
214
+ tuiMode: "full",
215
+ gameMode: true,
216
+ level: 1,
217
+ cachePct: 42,
218
+ ...overrides,
219
+ });
220
+ it("flare fires when megaCacheFlarePct >= 100 even when cachePct < 100", () => {
221
+ // Regression: the old code read (wd.cachePct ?? 0) >= 100, so when
222
+ // cachePct was 56 (provider hit rate) the flare silently broke.
223
+ const lines = buildWidgetLines(flareBase({
224
+ cachePct: 56,
225
+ megaCacheFlare: true,
226
+ megaCacheFlarePct: 287,
227
+ }), WIDTH, 0);
228
+ assert.ok(lines.join("\n").includes("MEGA CACHE"), "flare fires from megaCacheFlarePct");
229
+ });
230
+ it("flare suppressed when megaCacheFlarePct < 100 even when cachePct >= 100", () => {
231
+ // Symmetric regression: high cachePct should NOT alone trigger flare.
232
+ const lines = buildWidgetLines(flareBase({
233
+ cachePct: 150,
234
+ megaCacheFlare: true,
235
+ megaCacheFlarePct: 42,
236
+ }), WIDTH, 0);
237
+ assert.ok(!lines.join("\n").includes("MEGA CACHE"), "no flare when megaCacheFlarePct below threshold");
238
+ });
239
+ it("flare suppressed when megaCacheFlarePct is missing (undefined)", () => {
240
+ const lines = buildWidgetLines(flareBase({
241
+ cachePct: 150,
242
+ megaCacheFlare: true,
243
+ }), WIDTH, 0);
244
+ assert.ok(!lines.join("\n").includes("MEGA CACHE"), "no flare without megaCacheFlarePct");
245
+ });
246
+ });
184
247
  describe("buildWidgetLines footer stability (P1 — no 250ms rotation)", () => {
185
248
  // P1: the L5 ticker branch used to re-pick the head text every 250ms via
186
249
  // `step = floor(Date.now()/250)`, flipping the footer line on a 250·N ms
@@ -188,7 +251,12 @@ describe("buildWidgetLines footer stability (P1 — no 250ms rotation)", () => {
188
251
  // the SAME WidgetData at t and t+500ms (well past one rotation slot) must
189
252
  // produce byte-identical footer lines. Proves the metronome is gone.
190
253
  const stabBase = (overrides = {}) => baseWd({
191
- theme: DEFAULT_THEME, tuiMode: "full", gameMode: false, level: 1, cachePct: 42, ...overrides,
254
+ theme: DEFAULT_THEME,
255
+ tuiMode: "full",
256
+ gameMode: false,
257
+ level: 1,
258
+ cachePct: 42,
259
+ ...overrides,
192
260
  });
193
261
  it("ticker footer is byte-identical across simulated frames 500ms apart", () => {
194
262
  const wd = stabBase({
@@ -220,7 +288,12 @@ describe("buildWidgetLines footer stability (P1 — no 250ms rotation)", () => {
220
288
  try {
221
289
  Date.now = () => 1_000_000;
222
290
  const before = buildWidgetLines(stabBase({ ticker: [{ text: "a", at: 1 }] }), WIDTH, 0);
223
- const after = buildWidgetLines(stabBase({ ticker: [{ text: "a", at: 1 }, { text: "b", at: 2 }] }), WIDTH, 0);
291
+ const after = buildWidgetLines(stabBase({
292
+ ticker: [
293
+ { text: "a", at: 1 },
294
+ { text: "b", at: 2 },
295
+ ],
296
+ }), WIDTH, 0);
224
297
  assert.notDeepEqual(before, after, "footer changes when ticker grows");
225
298
  }
226
299
  finally {
@@ -230,22 +303,40 @@ describe("buildWidgetLines footer stability (P1 — no 250ms rotation)", () => {
230
303
  });
231
304
  describe("buildWidgetLines achievement flare (S35)", () => {
232
305
  const achBase = (overrides = {}) => baseWd({
233
- theme: DEFAULT_THEME, tuiMode: "full", gameMode: true, level: 1, cachePct: 42, ...overrides,
306
+ theme: DEFAULT_THEME,
307
+ tuiMode: "full",
308
+ gameMode: true,
309
+ level: 1,
310
+ cachePct: 42,
311
+ ...overrides,
234
312
  });
235
313
  it("achievementFlare + titles -> renders the unlock toast line", () => {
236
- const lines = buildWidgetLines(achBase({ achievementFlare: true, achievementFlareTitles: ["First Compact"] }), WIDTH, 0);
314
+ const lines = buildWidgetLines(achBase({
315
+ achievementFlare: true,
316
+ achievementFlareTitles: ["First Compact"],
317
+ }), WIDTH, 0);
237
318
  assert.ok(lines.some((l) => l.includes("Achievement unlocked: First Compact")), "toast line present");
238
319
  });
239
320
  it("achievementFlare off -> no toast line", () => {
240
- const lines = buildWidgetLines(achBase({ achievementFlare: false, achievementFlareTitles: ["First Compact"] }), WIDTH, 0);
321
+ const lines = buildWidgetLines(achBase({
322
+ achievementFlare: false,
323
+ achievementFlareTitles: ["First Compact"],
324
+ }), WIDTH, 0);
241
325
  assert.ok(!lines.some((l) => l.includes("Achievement unlocked")), "no toast when flare off");
242
326
  });
243
327
  it("gameMode off -> no toast even if flare set", () => {
244
- const lines = buildWidgetLines(achBase({ gameMode: false, achievementFlare: true, achievementFlareTitles: ["X"] }), WIDTH, 0);
328
+ const lines = buildWidgetLines(achBase({
329
+ gameMode: false,
330
+ achievementFlare: true,
331
+ achievementFlareTitles: ["X"],
332
+ }), WIDTH, 0);
245
333
  assert.ok(!lines.some((l) => l.includes("Achievement unlocked")), "no toast when game off");
246
334
  });
247
335
  it("achievement toast is width-safe", () => {
248
- const lines = buildWidgetLines(achBase({ achievementFlare: true, achievementFlareTitles: ["First Compact", "Turn Veteran"] }), 60, 0);
336
+ const lines = buildWidgetLines(achBase({
337
+ achievementFlare: true,
338
+ achievementFlareTitles: ["First Compact", "Turn Veteran"],
339
+ }), 60, 0);
249
340
  for (const l of lines)
250
341
  assert.ok(visibleWidth(l) <= 60, "width safe");
251
342
  });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * pricing.ts — Reusable pricing constants and cache-savings computation.
3
+ *
4
+ * Single source of truth for provider prompt-cache pricing multipliers and
5
+ * known model input rates. Extracted from routes-cache.ts inline arithmetic.
6
+ *
7
+ * PREVENT-PI-004: compute-only, zero network. No SQL, no IO.
8
+ */
9
+ // ─── Cache Pricing Multipliers ──────────────────────────────────────────────
10
+ /**
11
+ * Fraction of input cost saved per cache-read token.
12
+ * Cache reads cost 10% of full input → you save 90% of the input cost.
13
+ */
14
+ export const CACHE_READ_MULTIPLIER = 0.9;
15
+ /**
16
+ * Fraction of input cost incurred as a premium per cache-write token.
17
+ * Cache writes cost 125% of full input → a 25% premium over a normal input token.
18
+ */
19
+ export const CACHE_WRITE_MULTIPLIER = 0.25;
20
+ // ─── Known Model Input Rates ─────────────────────────────────────────────────
21
+ // USD per input token. Exact-match first, then prefix-match.
22
+ export const MODEL_INPUT_RATES = {
23
+ "claude-sonnet-4": 3e-6, // $3/Mtok
24
+ "claude-3.5-sonnet": 3e-6,
25
+ "claude-3-opus": 15e-6,
26
+ "claude-3-haiku": 0.25e-6,
27
+ "claude-3.5-haiku": 0.8e-6,
28
+ "gpt-4o": 2.5e-6,
29
+ "gpt-4o-mini": 0.15e-6,
30
+ "gpt-4-turbo": 10e-6,
31
+ "gpt-3.5-turbo": 0.5e-6,
32
+ "gemini-2.5-pro": 1.25e-6,
33
+ "gemini-2.5-flash": 0.15e-6,
34
+ "gemini-2.0-flash": 0.1e-6,
35
+ "deepseek-chat": 0.14e-6,
36
+ "deepseek-reasoner": 0.55e-6,
37
+ "codestral": 0.3e-6,
38
+ "mistral-large": 2e-6,
39
+ "llama-3.1-405b": 1.33e-6,
40
+ "llama-3.1-70b": 0.8e-6,
41
+ "llama-3.1-8b": 0.05e-6,
42
+ };
43
+ /**
44
+ * Look up the input-token rate (USD) for a model string.
45
+ * Exact match first, then prefix match (e.g. "claude-sonnet-4-20250514" → 3e-6).
46
+ */
47
+ export function lookupModelInputRate(model) {
48
+ // exact match first
49
+ if (MODEL_INPUT_RATES[model] != null)
50
+ return MODEL_INPUT_RATES[model];
51
+ // prefix match
52
+ for (const [key, rate] of Object.entries(MODEL_INPUT_RATES)) {
53
+ if (model.startsWith(key))
54
+ return rate;
55
+ }
56
+ return undefined;
57
+ }
58
+ /**
59
+ * Compute dollar savings from provider prompt cache read/write token counts.
60
+ *
61
+ * @param totalCacheRead — total cache-read tokens over the lifetime.
62
+ * @param totalCacheWrite — total cache-write tokens over the lifetime.
63
+ * @param inputRate — the model's input-token rate in USD (e.g. 3e-6 for Sonnet).
64
+ * @returns `{ cacheReadSaved, cacheWriteCost, netSaved }` — or all zeros when
65
+ * `inputRate` is zero or negative.
66
+ */
67
+ export function computeCacheSavings(totalCacheRead, totalCacheWrite, inputRate) {
68
+ if (inputRate <= 0) {
69
+ return { cacheReadSaved: 0, cacheWriteCost: 0, netSaved: 0 };
70
+ }
71
+ const cacheReadSaved = totalCacheRead * inputRate * CACHE_READ_MULTIPLIER;
72
+ const cacheWriteCost = totalCacheWrite * inputRate * CACHE_WRITE_MULTIPLIER;
73
+ return {
74
+ cacheReadSaved,
75
+ cacheWriteCost,
76
+ netSaved: cacheReadSaved - cacheWriteCost,
77
+ };
78
+ }
@@ -4,8 +4,9 @@
4
4
  * All pi-agnostic, all parameterized (PREVENT-002), all local (PREVENT-PI-004).
5
5
  * Exposed via the /mega-db-* slash commands in extensions/mega-db-cmds.ts.
6
6
  */
7
- import { statSync } from "node:fs";
7
+ import { statSync, mkdirSync, existsSync } from "node:fs";
8
8
  import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
9
10
  import { getStateDir } from "../../store.js";
10
11
  import { openStore, withTx } from "./utils.js";
11
12
  const DB_TABLE_NAMES = [
@@ -70,6 +71,24 @@ export function getDbStats(stateDir = getStateDir()) {
70
71
  walFrames,
71
72
  };
72
73
  }
74
+ /**
75
+ * Lightweight temp backup of the main sqlite.db via VACUUM INTO.
76
+ *
77
+ * Creates a one-off backup in the OS tmpdir. Returns the backup path.
78
+ * VACUUM INTO produces a clean, defragmented copy — also serves as a
79
+ * pre-op safeguard for any destructive maintenance action.
80
+ */
81
+ export function backupDb(stateDir = getStateDir()) {
82
+ const backupDir = join(tmpdir(), "pi-mega-compact-backups");
83
+ if (!existsSync(backupDir))
84
+ mkdirSync(backupDir, { recursive: true });
85
+ const ts = Date.now();
86
+ const backupPath = join(backupDir, `sqlite-${ts}.db`);
87
+ const db = openStore(stateDir);
88
+ // guardrails-disable-next-line PREVENT-002: path built from tmpdir + timestamp, not user input
89
+ db.exec(`VACUUM INTO '${backupPath.replace(/'/g, "''")}'`);
90
+ return backupPath;
91
+ }
73
92
  /**
74
93
  * Prune raw_transcript + checkpoint_epochs rows older than `daysOld`.
75
94
  * Uses `message_timestamp` (raw_transcript) and `created_at` (epochs), both
@@ -79,3 +79,107 @@ export function readPerfSamples(stateDir = getStateDir(), sinceTs = 0, kind) {
79
79
  }
80
80
  return out;
81
81
  }
82
+ /**
83
+ * Read lifetime provider prompt cache aggregates from `perf_samples`.
84
+ *
85
+ * Aggregates `cache_creation_input_tokens` / `cache_read_input_tokens` from
86
+ * the `meta` JSON column of `cache_hit_pct` samples (`json_extract`,
87
+ * available in node:sqlite ≥22.13 / SQLite ≥3.38). Returns a zeroed
88
+ * `ProviderCacheLifetime` when no samples exist (never undefined).
89
+ */
90
+ export function readProviderCacheLifetime(stateDir = getStateDir()) {
91
+ const db = openStore(stateDir);
92
+ const rows = db
93
+ .prepare(`SELECT ts, meta FROM perf_samples
94
+ WHERE kind = ?
95
+ ORDER BY ts ASC`)
96
+ .all("cache_hit_pct");
97
+ if (rows.length === 0) {
98
+ return {
99
+ sampleCount: 0,
100
+ avgHitPct: 0,
101
+ totalCacheRead: 0,
102
+ totalCacheWrite: 0,
103
+ totalInput: 0,
104
+ firstSampleAt: null,
105
+ latestSampleAt: null,
106
+ };
107
+ }
108
+ let totalCacheRead = 0;
109
+ let totalCacheWrite = 0;
110
+ let totalInput = 0;
111
+ let sumHitPct = 0;
112
+ let hitPctCount = 0;
113
+ for (const r of rows) {
114
+ if (r.meta != null) {
115
+ let meta = null;
116
+ try {
117
+ meta = JSON.parse(r.meta);
118
+ }
119
+ catch {
120
+ meta = null;
121
+ }
122
+ if (meta != null) {
123
+ const cr = typeof meta.cacheRead === "number"
124
+ ? meta.cacheRead
125
+ : typeof meta.cache_read === "number"
126
+ ? meta.cache_read
127
+ : 0;
128
+ const cw = typeof meta.cacheWrite === "number"
129
+ ? meta.cacheWrite
130
+ : typeof meta.cache_write === "number"
131
+ ? meta.cache_write
132
+ : 0;
133
+ const inp = typeof meta.input === "number" ? meta.input : 0;
134
+ totalCacheRead += cr;
135
+ totalCacheWrite += cw;
136
+ totalInput += inp;
137
+ // Compute hit pct for this sample (same formula as perf-handler.ts)
138
+ const denom = cr + inp + cw;
139
+ if (denom > 0) {
140
+ sumHitPct += (cr / denom) * 100;
141
+ hitPctCount++;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return {
147
+ sampleCount: rows.length,
148
+ avgHitPct: hitPctCount > 0 ? sumHitPct / hitPctCount : 0,
149
+ totalCacheRead,
150
+ totalCacheWrite,
151
+ totalInput,
152
+ firstSampleAt: new Date(rows[0].ts).toISOString(),
153
+ latestSampleAt: new Date(rows[rows.length - 1].ts).toISOString(),
154
+ };
155
+ }
156
+ /**
157
+ * Read the most recent `cache_hit_pct` value from `perf_samples`.
158
+ *
159
+ * Returns 0 when no samples exist (never NaN/undefined). When multiple
160
+ * rows share the same timestamp the tie is broken by highest id (most
161
+ * recently inserted).
162
+ */
163
+ export function readLatestCacheHitPct(stateDir = getStateDir()) {
164
+ const db = openStore(stateDir);
165
+ const row = db
166
+ .prepare(`SELECT value FROM perf_samples
167
+ WHERE kind = ?
168
+ ORDER BY ts DESC, id DESC
169
+ LIMIT 1`)
170
+ .get("cache_hit_pct");
171
+ if (!row || typeof row.value !== "number" || !Number.isFinite(row.value))
172
+ return 0;
173
+ return row.value;
174
+ }
175
+ /**
176
+ * Read provider cache lifetime aggregates for a specific repo by state directory.
177
+ *
178
+ * This is a convenience alias over `readProviderCacheLifetime(stateDir)` —
179
+ * both open the same underlying global store database. Accepts an explicit
180
+ * `stateDir` rather than relying on the implicit default, making it suitable
181
+ * for per-repo dashboard handlers.
182
+ */
183
+ export function readProviderCacheForRepo(stateDir) {
184
+ return readProviderCacheLifetime(stateDir);
185
+ }
@@ -8,7 +8,7 @@ import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { mkdtempSync, rmSync } from "node:fs";
10
10
  import { closeStore } from "./utils.js";
11
- import { recordPerfSample, readPerfSamples, PERF_KINDS, } from "./perf-samples.js";
11
+ import { recordPerfSample, readPerfSamples, PERF_KINDS, readProviderCacheLifetime, readLatestCacheHitPct, } from "./perf-samples.js";
12
12
  describe("perf-samples (v0.8.8)", () => {
13
13
  let dir;
14
14
  before(() => {
@@ -52,3 +52,95 @@ describe("perf-samples (v0.8.8)", () => {
52
52
  assert.ok(PERF_KINDS.includes("cache_hit_pct"));
53
53
  });
54
54
  });
55
+ // ─── Provider cache lifetime aggregate tests (A.4) — each test uses its
56
+ // own clean stateDir so prior inserts never pollute later assertions. ───
57
+ describe("provider cache lifetime (A.4)", () => {
58
+ let dir2;
59
+ function freshDir(prefix) {
60
+ if (dir2) {
61
+ closeStore(dir2);
62
+ rmSync(dir2, { recursive: true, force: true });
63
+ }
64
+ dir2 = mkdtempSync(join(tmpdir(), prefix));
65
+ process.env.MEGACOMPACT_STATE_DIR = dir2;
66
+ }
67
+ after(() => {
68
+ if (dir2) {
69
+ closeStore(dir2);
70
+ delete process.env.MEGACOMPACT_STATE_DIR;
71
+ rmSync(dir2, { recursive: true, force: true });
72
+ }
73
+ });
74
+ it("empty table returns zeros/nulls", () => {
75
+ freshDir("mc-a4e-");
76
+ const r = readProviderCacheLifetime(dir2);
77
+ assert.equal(r.sampleCount, 0);
78
+ assert.equal(r.avgHitPct, 0);
79
+ assert.equal(r.totalCacheRead, 0);
80
+ assert.equal(r.totalCacheWrite, 0);
81
+ assert.equal(r.totalInput, 0);
82
+ assert.equal(r.firstSampleAt, null);
83
+ assert.equal(r.latestSampleAt, null);
84
+ });
85
+ it("rows with NULL meta are counted but contribute 0 tokens", () => {
86
+ freshDir("mc-a4n-");
87
+ recordPerfSample(dir2, "cache_hit_pct", 50, null);
88
+ recordPerfSample(dir2, "cache_hit_pct", 60, null);
89
+ const r = readProviderCacheLifetime(dir2);
90
+ assert.equal(r.sampleCount, 2);
91
+ assert.equal(r.totalCacheRead, 0);
92
+ assert.equal(r.totalCacheWrite, 0);
93
+ assert.equal(r.totalInput, 0);
94
+ });
95
+ it("partial meta — missing keys treated as 0", () => {
96
+ freshDir("mc-a4p-");
97
+ recordPerfSample(dir2, "cache_hit_pct", 42, { input: 1000 });
98
+ const r = readProviderCacheLifetime(dir2);
99
+ assert.equal(r.sampleCount, 1);
100
+ assert.equal(r.totalCacheRead, 0);
101
+ assert.equal(r.totalCacheWrite, 0);
102
+ assert.equal(r.totalInput, 1000);
103
+ });
104
+ it("full rows produce correct sums + avg", () => {
105
+ freshDir("mc-a4f-");
106
+ // cacheRead=1000, cacheWrite=500, input=100 → hit% = 1000/1600=62.5%
107
+ // cacheRead=500, cacheWrite=100, input=400 → hit% = 500/1000=50.0%
108
+ // cacheRead=100, cacheWrite=200, input=700 → hit% = 100/1000=10.0%
109
+ recordPerfSample(dir2, "cache_hit_pct", 55, {
110
+ cacheRead: 1000,
111
+ cacheWrite: 500,
112
+ input: 100,
113
+ });
114
+ recordPerfSample(dir2, "cache_hit_pct", 56, {
115
+ cacheRead: 500,
116
+ cacheWrite: 100,
117
+ input: 400,
118
+ });
119
+ recordPerfSample(dir2, "cache_hit_pct", 57, {
120
+ cacheRead: 100,
121
+ cacheWrite: 200,
122
+ input: 700,
123
+ });
124
+ const r = readProviderCacheLifetime(dir2);
125
+ assert.equal(r.sampleCount, 3);
126
+ assert.equal(r.totalCacheRead, 1600);
127
+ assert.equal(r.totalCacheWrite, 800);
128
+ assert.equal(r.totalInput, 1200);
129
+ // avg: (62.5 + 50.0 + 10.0) / 3 ≈ 40.833
130
+ assert.ok(Math.abs(r.avgHitPct - 40.833) < 0.1);
131
+ assert.ok(r.firstSampleAt != null);
132
+ assert.ok(r.latestSampleAt != null);
133
+ assert.ok(new Date(r.firstSampleAt).getTime() <=
134
+ new Date(r.latestSampleAt).getTime());
135
+ });
136
+ it("readLatestCacheHitPct: empty → 0", () => {
137
+ freshDir("mc-a4le-");
138
+ assert.equal(readLatestCacheHitPct(dir2), 0);
139
+ });
140
+ it("readLatestCacheHitPct: most recent by ts, tie → highest id", () => {
141
+ freshDir("mc-a4lm-");
142
+ recordPerfSample(dir2, "cache_hit_pct", 20);
143
+ recordPerfSample(dir2, "cache_hit_pct", 80);
144
+ assert.equal(readLatestCacheHitPct(dir2), 80);
145
+ });
146
+ });
@@ -1,2 +1,2 @@
1
- import{r as o,j as n}from"./index-CqLN7STl.js";function x(s){try{return new Date(s*1e3).toLocaleString()}catch{return"—"}}function f({achievements:s}){const i=o.useRef(0),[r,a]=o.useState(null),l=s.reduce((e,t)=>Math.max(e,t.unlocked_at??0),0),c=i.current;o.useEffect(()=>{l>i.current&&(i.current=l)},[l]),o.useEffect(()=>{if(c>0&&l>c){const e=s.filter(t=>t.unlocked_at!=null&&(t.unlocked_at??0)>c);if(e.length>0){const t=e.map(d=>`${d.icon??""} ${d.title}`.trim()).join(", ")+" unlocked!";a(t);const h=setTimeout(()=>a(null),4e3);return()=>clearTimeout(h)}}},[s,l,c]);const u=s.filter(e=>!(e.hidden===1&&e.unlocked_at==null));return n.jsxs("div",{className:"ach-section",children:[r!=null&&n.jsx("div",{className:"ach-toast show",children:r}),n.jsxs("div",{className:"ach-tiles",children:[u.length===0&&n.jsx("span",{className:"repo-none",children:"no achievements yet"}),u.map(e=>{if(e.unlocked_at!=null){const t=c>0&&(e.unlocked_at??0)>c;return n.jsxs("div",{className:`ach-tile unlocked${t?" just-unlocked":""}`,children:[e.icon??""," ",e.title,n.jsxs("span",{className:"ach-detail",children:["unlocked ",x(e.unlocked_at)]})]},e.id)}return n.jsxs("div",{className:"ach-tile locked",children:["??? ",e.title]},e.id)})]})]})}export{f as A};
2
- //# sourceMappingURL=AchievementTiles-DZp_xQOW.js.map
1
+ import{r as o,j as n}from"./index-BX0bcgLu.js";function x(s){try{return new Date(s*1e3).toLocaleString()}catch{return"—"}}function f({achievements:s}){const i=o.useRef(0),[r,a]=o.useState(null),l=s.reduce((e,t)=>Math.max(e,t.unlocked_at??0),0),c=i.current;o.useEffect(()=>{l>i.current&&(i.current=l)},[l]),o.useEffect(()=>{if(c>0&&l>c){const e=s.filter(t=>t.unlocked_at!=null&&(t.unlocked_at??0)>c);if(e.length>0){const t=e.map(d=>`${d.icon??""} ${d.title}`.trim()).join(", ")+" unlocked!";a(t);const h=setTimeout(()=>a(null),4e3);return()=>clearTimeout(h)}}},[s,l,c]);const u=s.filter(e=>!(e.hidden===1&&e.unlocked_at==null));return n.jsxs("div",{className:"ach-section",children:[r!=null&&n.jsx("div",{className:"ach-toast show",children:r}),n.jsxs("div",{className:"ach-tiles",children:[u.length===0&&n.jsx("span",{className:"repo-none",children:"no achievements yet"}),u.map(e=>{if(e.unlocked_at!=null){const t=c>0&&(e.unlocked_at??0)>c;return n.jsxs("div",{className:`ach-tile unlocked${t?" just-unlocked":""}`,children:[e.icon??""," ",e.title,n.jsxs("span",{className:"ach-detail",children:["unlocked ",x(e.unlocked_at)]})]},e.id)}return n.jsxs("div",{className:"ach-tile locked",children:["??? ",e.title]},e.id)})]})]})}export{f as A};
2
+ //# sourceMappingURL=AchievementTiles-vEo2MtE2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AchievementTiles-DZp_xQOW.js","sources":["../../src/components/AchievementTiles.tsx"],"sourcesContent":["/**\n * dashboard-client/src/components/AchievementTiles.tsx — shared achievement grid.\n *\n * Renders a grid of achievement tiles from fetchAchievements() data.\n * Tile states: unlocked (icon+title+date), visible-locked (??? teaser),\n * hidden+locked (not rendered). Just-unlocked tiles get a pulse animation.\n * Toast notification fires for newly-unlocked achievements.\n *\n * Used by both GameTab (sub-section) and AchievementsTab (standalone).\n */\n\nimport type React from \"react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\n\nexport interface AchievementTilesProps {\n\t/** Achievement rows from /api/achievements. */\n\tachievements: AchievementRow[];\n}\n\n/** Format unlock timestamp (seconds → locale string). */\nfunction fmtUnlockDate(ts: number): string {\n\ttry {\n\t\treturn new Date(ts * 1000).toLocaleString();\n\t} catch {\n\t\treturn \"\\u2014\";\n\t}\n}\n\nexport function AchievementTiles({\n\tachievements,\n}: AchievementTilesProps): React.ReactElement {\n\tconst lastMaxTsRef = useRef(0);\n\tconst [toastText, setToastText] = useState<string | null>(null);\n\n\tconst maxTs = achievements.reduce(\n\t\t(mx, a) => Math.max(mx, a.unlocked_at ?? 0),\n\t\t0,\n\t);\n\tconst prevMaxTs = lastMaxTsRef.current;\n\n\t/* Update ref after render so next poll sees previous baseline. */\n\tuseEffect(() => {\n\t\tif (maxTs > lastMaxTsRef.current) {\n\t\t\tlastMaxTsRef.current = maxTs;\n\t\t}\n\t}, [maxTs]);\n\n\t/* Toast for newly-unlocked achievements. */\n\tuseEffect(() => {\n\t\tif (prevMaxTs > 0 && maxTs > prevMaxTs) {\n\t\t\tconst newly = achievements.filter(\n\t\t\t\t(a) => a.unlocked_at != null && (a.unlocked_at ?? 0) > prevMaxTs,\n\t\t\t);\n\t\t\tif (newly.length > 0) {\n\t\t\t\tconst text =\n\t\t\t\t\tnewly\n\t\t\t\t\t\t.map((a) => `${a.icon ?? \"\"} ${a.title}`.trim())\n\t\t\t\t\t\t.join(\", \") + \" unlocked!\";\n\t\t\t\tsetToastText(text);\n\t\t\t\tconst t = setTimeout(() => setToastText(null), 4000);\n\t\t\t\treturn (): void => clearTimeout(t);\n\t\t\t}\n\t\t}\n\t}, [achievements, maxTs, prevMaxTs]);\n\n\t/* Visible tiles: skip hidden+locked (hidden === 1 && unlocked_at == null). */\n\tconst tiles = achievements.filter(\n\t\t(a) => !(a.hidden === 1 && a.unlocked_at == null),\n\t);\n\n\treturn (\n\t\t<div className=\"ach-section\">\n\t\t\t{toastText != null && (\n\t\t\t\t<div className=\"ach-toast show\">{toastText}</div>\n\t\t\t)}\n\t\t\t<div className=\"ach-tiles\">\n\t\t\t\t{tiles.length === 0 && (\n\t\t\t\t\t<span className=\"repo-none\">no achievements yet</span>\n\t\t\t\t)}\n\t\t\t\t{tiles.map((a) => {\n\t\t\t\t\tif (a.unlocked_at != null) {\n\t\t\t\t\t\tconst isNew = prevMaxTs > 0 && (a.unlocked_at ?? 0) > prevMaxTs;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\tclassName={`ach-tile unlocked${isNew ? \" just-unlocked\" : \"\"}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{a.icon ?? \"\"} {a.title}\n\t\t\t\t\t\t\t\t<span className=\"ach-detail\">\n\t\t\t\t\t\t\t\t\tunlocked {fmtUnlockDate(a.unlocked_at)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={a.id} className=\"ach-tile locked\">\n\t\t\t\t\t\t\t??? {a.title}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["fmtUnlockDate","ts","AchievementTiles","achievements","lastMaxTsRef","useRef","toastText","setToastText","useState","maxTs","mx","a","prevMaxTs","useEffect","newly","text","t","tiles","jsxs","jsx","isNew"],"mappings":"+CAqBA,SAASA,EAAcC,EAAoB,CAC1C,GAAI,CACH,OAAO,IAAI,KAAKA,EAAK,GAAI,EAAE,eAAA,CAC5B,MAAQ,CACP,MAAO,GACR,CACD,CAEO,SAASC,EAAiB,CAChC,aAAAC,CACD,EAA8C,CAC7C,MAAMC,EAAeC,EAAAA,OAAO,CAAC,EACvB,CAACC,EAAWC,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAExDC,EAAQN,EAAa,OAC1B,CAACO,EAAIC,IAAM,KAAK,IAAID,EAAIC,EAAE,aAAe,CAAC,EAC1C,CAAA,EAEKC,EAAYR,EAAa,QAG/BS,EAAAA,UAAU,IAAM,CACXJ,EAAQL,EAAa,UACxBA,EAAa,QAAUK,EAEzB,EAAG,CAACA,CAAK,CAAC,EAGVI,EAAAA,UAAU,IAAM,CACf,GAAID,EAAY,GAAKH,EAAQG,EAAW,CACvC,MAAME,EAAQX,EAAa,OACzBQ,GAAMA,EAAE,aAAe,OAASA,EAAE,aAAe,GAAKC,CAAA,EAExD,GAAIE,EAAM,OAAS,EAAG,CACrB,MAAMC,EACLD,EACE,IAAKH,GAAM,GAAGA,EAAE,MAAQ,EAAE,IAAIA,EAAE,KAAK,GAAG,KAAA,CAAM,EAC9C,KAAK,IAAI,EAAI,aAChBJ,EAAaQ,CAAI,EACjB,MAAMC,EAAI,WAAW,IAAMT,EAAa,IAAI,EAAG,GAAI,EACnD,MAAO,IAAY,aAAaS,CAAC,CAClC,CACD,CACD,EAAG,CAACb,EAAcM,EAAOG,CAAS,CAAC,EAGnC,MAAMK,EAAQd,EAAa,OACzBQ,GAAM,EAAEA,EAAE,SAAW,GAAKA,EAAE,aAAe,KAAA,EAG7C,OACCO,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAZ,GAAa,MACba,EAAAA,IAAC,MAAA,CAAI,UAAU,iBAAkB,SAAAb,EAAU,EAE5CY,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAD,EAAM,SAAW,GACjBE,MAAC,OAAA,CAAK,UAAU,YAAY,SAAA,sBAAmB,EAE/CF,EAAM,IAAKN,GAAM,CACjB,GAAIA,EAAE,aAAe,KAAM,CAC1B,MAAMS,EAAQR,EAAY,IAAMD,EAAE,aAAe,GAAKC,EACtD,OACCM,EAAAA,KAAC,MAAA,CAEA,UAAW,oBAAoBE,EAAQ,iBAAmB,EAAE,GAE3D,SAAA,CAAAT,EAAE,MAAQ,GAAG,IAAEA,EAAE,MAClBO,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAa,SAAA,CAAA,YAClBlB,EAAcW,EAAE,WAAW,CAAA,CAAA,CACtC,CAAA,CAAA,EANKA,EAAE,EAAA,CASV,CACA,OACCO,EAAAA,KAAC,MAAA,CAAe,UAAU,kBAAkB,SAAA,CAAA,OACtCP,EAAE,KAAA,CAAA,EADEA,EAAE,EAEZ,CAEF,CAAC,CAAA,CAAA,CACF,CAAA,EACD,CAEF"}
1
+ {"version":3,"file":"AchievementTiles-vEo2MtE2.js","sources":["../../src/components/AchievementTiles.tsx"],"sourcesContent":["/**\n * dashboard-client/src/components/AchievementTiles.tsx — shared achievement grid.\n *\n * Renders a grid of achievement tiles from fetchAchievements() data.\n * Tile states: unlocked (icon+title+date), visible-locked (??? teaser),\n * hidden+locked (not rendered). Just-unlocked tiles get a pulse animation.\n * Toast notification fires for newly-unlocked achievements.\n *\n * Used by both GameTab (sub-section) and AchievementsTab (standalone).\n */\n\nimport type React from \"react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\n\nexport interface AchievementTilesProps {\n\t/** Achievement rows from /api/achievements. */\n\tachievements: AchievementRow[];\n}\n\n/** Format unlock timestamp (seconds → locale string). */\nfunction fmtUnlockDate(ts: number): string {\n\ttry {\n\t\treturn new Date(ts * 1000).toLocaleString();\n\t} catch {\n\t\treturn \"\\u2014\";\n\t}\n}\n\nexport function AchievementTiles({\n\tachievements,\n}: AchievementTilesProps): React.ReactElement {\n\tconst lastMaxTsRef = useRef(0);\n\tconst [toastText, setToastText] = useState<string | null>(null);\n\n\tconst maxTs = achievements.reduce(\n\t\t(mx, a) => Math.max(mx, a.unlocked_at ?? 0),\n\t\t0,\n\t);\n\tconst prevMaxTs = lastMaxTsRef.current;\n\n\t/* Update ref after render so next poll sees previous baseline. */\n\tuseEffect(() => {\n\t\tif (maxTs > lastMaxTsRef.current) {\n\t\t\tlastMaxTsRef.current = maxTs;\n\t\t}\n\t}, [maxTs]);\n\n\t/* Toast for newly-unlocked achievements. */\n\tuseEffect(() => {\n\t\tif (prevMaxTs > 0 && maxTs > prevMaxTs) {\n\t\t\tconst newly = achievements.filter(\n\t\t\t\t(a) => a.unlocked_at != null && (a.unlocked_at ?? 0) > prevMaxTs,\n\t\t\t);\n\t\t\tif (newly.length > 0) {\n\t\t\t\tconst text =\n\t\t\t\t\tnewly\n\t\t\t\t\t\t.map((a) => `${a.icon ?? \"\"} ${a.title}`.trim())\n\t\t\t\t\t\t.join(\", \") + \" unlocked!\";\n\t\t\t\tsetToastText(text);\n\t\t\t\tconst t = setTimeout(() => setToastText(null), 4000);\n\t\t\t\treturn (): void => clearTimeout(t);\n\t\t\t}\n\t\t}\n\t}, [achievements, maxTs, prevMaxTs]);\n\n\t/* Visible tiles: skip hidden+locked (hidden === 1 && unlocked_at == null). */\n\tconst tiles = achievements.filter(\n\t\t(a) => !(a.hidden === 1 && a.unlocked_at == null),\n\t);\n\n\treturn (\n\t\t<div className=\"ach-section\">\n\t\t\t{toastText != null && (\n\t\t\t\t<div className=\"ach-toast show\">{toastText}</div>\n\t\t\t)}\n\t\t\t<div className=\"ach-tiles\">\n\t\t\t\t{tiles.length === 0 && (\n\t\t\t\t\t<span className=\"repo-none\">no achievements yet</span>\n\t\t\t\t)}\n\t\t\t\t{tiles.map((a) => {\n\t\t\t\t\tif (a.unlocked_at != null) {\n\t\t\t\t\t\tconst isNew = prevMaxTs > 0 && (a.unlocked_at ?? 0) > prevMaxTs;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\tclassName={`ach-tile unlocked${isNew ? \" just-unlocked\" : \"\"}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{a.icon ?? \"\"} {a.title}\n\t\t\t\t\t\t\t\t<span className=\"ach-detail\">\n\t\t\t\t\t\t\t\t\tunlocked {fmtUnlockDate(a.unlocked_at)}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div key={a.id} className=\"ach-tile locked\">\n\t\t\t\t\t\t\t??? {a.title}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"],"names":["fmtUnlockDate","ts","AchievementTiles","achievements","lastMaxTsRef","useRef","toastText","setToastText","useState","maxTs","mx","a","prevMaxTs","useEffect","newly","text","t","tiles","jsxs","jsx","isNew"],"mappings":"+CAqBA,SAASA,EAAcC,EAAoB,CAC1C,GAAI,CACH,OAAO,IAAI,KAAKA,EAAK,GAAI,EAAE,eAAA,CAC5B,MAAQ,CACP,MAAO,GACR,CACD,CAEO,SAASC,EAAiB,CAChC,aAAAC,CACD,EAA8C,CAC7C,MAAMC,EAAeC,EAAAA,OAAO,CAAC,EACvB,CAACC,EAAWC,CAAY,EAAIC,EAAAA,SAAwB,IAAI,EAExDC,EAAQN,EAAa,OAC1B,CAACO,EAAIC,IAAM,KAAK,IAAID,EAAIC,EAAE,aAAe,CAAC,EAC1C,CAAA,EAEKC,EAAYR,EAAa,QAG/BS,EAAAA,UAAU,IAAM,CACXJ,EAAQL,EAAa,UACxBA,EAAa,QAAUK,EAEzB,EAAG,CAACA,CAAK,CAAC,EAGVI,EAAAA,UAAU,IAAM,CACf,GAAID,EAAY,GAAKH,EAAQG,EAAW,CACvC,MAAME,EAAQX,EAAa,OACzBQ,GAAMA,EAAE,aAAe,OAASA,EAAE,aAAe,GAAKC,CAAA,EAExD,GAAIE,EAAM,OAAS,EAAG,CACrB,MAAMC,EACLD,EACE,IAAKH,GAAM,GAAGA,EAAE,MAAQ,EAAE,IAAIA,EAAE,KAAK,GAAG,KAAA,CAAM,EAC9C,KAAK,IAAI,EAAI,aAChBJ,EAAaQ,CAAI,EACjB,MAAMC,EAAI,WAAW,IAAMT,EAAa,IAAI,EAAG,GAAI,EACnD,MAAO,IAAY,aAAaS,CAAC,CAClC,CACD,CACD,EAAG,CAACb,EAAcM,EAAOG,CAAS,CAAC,EAGnC,MAAMK,EAAQd,EAAa,OACzBQ,GAAM,EAAEA,EAAE,SAAW,GAAKA,EAAE,aAAe,KAAA,EAG7C,OACCO,EAAAA,KAAC,MAAA,CAAI,UAAU,cACb,SAAA,CAAAZ,GAAa,MACba,EAAAA,IAAC,MAAA,CAAI,UAAU,iBAAkB,SAAAb,EAAU,EAE5CY,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAD,EAAM,SAAW,GACjBE,MAAC,OAAA,CAAK,UAAU,YAAY,SAAA,sBAAmB,EAE/CF,EAAM,IAAKN,GAAM,CACjB,GAAIA,EAAE,aAAe,KAAM,CAC1B,MAAMS,EAAQR,EAAY,IAAMD,EAAE,aAAe,GAAKC,EACtD,OACCM,EAAAA,KAAC,MAAA,CAEA,UAAW,oBAAoBE,EAAQ,iBAAmB,EAAE,GAE3D,SAAA,CAAAT,EAAE,MAAQ,GAAG,IAAEA,EAAE,MAClBO,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAa,SAAA,CAAA,YAClBlB,EAAcW,EAAE,WAAW,CAAA,CAAA,CACtC,CAAA,CAAA,EANKA,EAAE,EAAA,CASV,CACA,OACCO,EAAAA,KAAC,MAAA,CAAe,UAAU,kBAAkB,SAAA,CAAA,OACtCP,EAAE,KAAA,CAAA,EADEA,EAAE,EAEZ,CAEF,CAAC,CAAA,CAAA,CACF,CAAA,EACD,CAEF"}
@@ -0,0 +1,2 @@
1
+ import{r as t,i as m,j as s}from"./index-BX0bcgLu.js";import{A as v}from"./AchievementTiles-vEo2MtE2.js";function x(){const[r,o]=t.useState([]),[l,u]=t.useState(!0),[a,n]=t.useState(null),i=t.useCallback(async()=>{try{const e=await m();o(e),n(null)}catch(e){n(e instanceof Error?e:new Error(String(e)))}finally{u(!1)}},[]);return t.useEffect(()=>{let e=!0;const c=async()=>{e&&await i()};c();const h=setInterval(()=>void c(),15e3);return()=>{e=!1,clearInterval(h)}},[i]),l&&r.length===0?s.jsx("div",{className:"tab-stub",children:"Loading achievements…"}):a&&r.length===0?s.jsxs("div",{className:"tab-stub",children:["Error: ",a.message]}):s.jsxs("div",{className:"achievements-tab",children:[s.jsx("h2",{children:"Achievements"}),s.jsx(v,{achievements:r})]})}export{x as default};
2
+ //# sourceMappingURL=AchievementsTab-DjxfYrDc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AchievementsTab-BzFHpAHM.js","sources":["../../src/tabs/AchievementsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).\n *\n * Renders the shared AchievementTiles component standalone, plus a toast area.\n * Polls /api/achievements every 15s.\n */\n\nimport type React from \"react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\nimport { fetchAchievements } from \"../api/client\";\nimport { AchievementTiles } from \"../components/AchievementTiles\";\n\nexport default function AchievementsTab(): React.ReactElement {\n\tconst [achievements, setAchievements] = useState<AchievementRow[]>([]);\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst doFetch = useCallback(async (): Promise<void> => {\n\t\ttry {\n\t\t\tconst rows = await fetchAchievements();\n\t\t\tsetAchievements(rows);\n\t\t\tsetError(null);\n\t\t} catch (e) {\n\t\t\tsetError(e instanceof Error ? e : new Error(String(e)));\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet active = true;\n\t\tconst run = async (): Promise<void> => {\n\t\t\tif (active) await doFetch();\n\t\t};\n\t\tvoid run();\n\t\tconst timer = setInterval(() => void run(), 15000);\n\t\treturn () => {\n\t\t\tactive = false;\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [doFetch]);\n\n\tif (loading && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Loading achievements…</div>;\n\tif (error && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\n\treturn (\n\t\t<div className=\"achievements-tab\">\n\t\t\t<h2>Achievements</h2>\n\t\t\t<AchievementTiles achievements={achievements} />\n\t\t</div>\n\t);\n}\n"],"names":["AchievementsTab","achievements","setAchievements","useState","loading","setLoading","error","setError","doFetch","useCallback","rows","fetchAchievements","useEffect","active","run","timer","jsx","jsxs","AchievementTiles"],"mappings":"yGAaA,SAAwBA,GAAsC,CAC7D,KAAM,CAACC,EAAcC,CAAe,EAAIC,EAAAA,SAA2B,CAAA,CAAE,EAC/D,CAACC,EAASC,CAAU,EAAIF,EAAAA,SAAS,EAAI,EACrC,CAACG,EAAOC,CAAQ,EAAIJ,EAAAA,SAAuB,IAAI,EAE/CK,EAAUC,EAAAA,YAAY,SAA2B,CACtD,GAAI,CACH,MAAMC,EAAO,MAAMC,EAAA,EACnBT,EAAgBQ,CAAI,EACpBH,EAAS,IAAI,CACd,OAAS,EAAG,CACXA,EAAS,aAAa,MAAQ,EAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CACvD,QAAA,CACCF,EAAW,EAAK,CACjB,CACD,EAAG,CAAA,CAAE,EAeL,OAbAO,EAAAA,UAAU,IAAM,CACf,IAAIC,EAAS,GACb,MAAMC,EAAM,SAA2B,CAClCD,SAAcL,EAAA,CACnB,EACKM,EAAA,EACL,MAAMC,EAAQ,YAAY,IAAM,KAAKD,EAAA,EAAO,IAAK,EACjD,MAAO,IAAM,CACZD,EAAS,GACT,cAAcE,CAAK,CACpB,CACD,EAAG,CAACP,CAAO,CAAC,EAERJ,GAAWH,EAAa,SAAW,EAC/Be,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,wBAAqB,EACnDV,GAASL,EAAa,SAAW,EAC7BgB,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQX,EAAM,OAAA,EAAQ,EAGvDW,EAAAA,KAAC,MAAA,CAAI,UAAU,mBACd,SAAA,CAAAD,EAAAA,IAAC,MAAG,SAAA,cAAA,CAAY,EAChBA,MAACE,GAAiB,aAAAjB,CAAA,CAA4B,CAAA,EAC/C,CAEF"}
1
+ {"version":3,"file":"AchievementsTab-DjxfYrDc.js","sources":["../../src/tabs/AchievementsTab.tsx"],"sourcesContent":["/**\n * dashboard-client/src/tabs/AchievementsTab.tsx — Achievements tab (NEW).\n *\n * Renders the shared AchievementTiles component standalone, plus a toast area.\n * Polls /api/achievements every 15s.\n */\n\nimport type React from \"react\";\nimport { useState, useEffect, useCallback } from \"react\";\nimport type { AchievementRow } from \"@contracts\";\nimport { fetchAchievements } from \"../api/client\";\nimport { AchievementTiles } from \"../components/AchievementTiles\";\n\nexport default function AchievementsTab(): React.ReactElement {\n\tconst [achievements, setAchievements] = useState<AchievementRow[]>([]);\n\tconst [loading, setLoading] = useState(true);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst doFetch = useCallback(async (): Promise<void> => {\n\t\ttry {\n\t\t\tconst rows = await fetchAchievements();\n\t\t\tsetAchievements(rows);\n\t\t\tsetError(null);\n\t\t} catch (e) {\n\t\t\tsetError(e instanceof Error ? e : new Error(String(e)));\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\tlet active = true;\n\t\tconst run = async (): Promise<void> => {\n\t\t\tif (active) await doFetch();\n\t\t};\n\t\tvoid run();\n\t\tconst timer = setInterval(() => void run(), 15000);\n\t\treturn () => {\n\t\t\tactive = false;\n\t\t\tclearInterval(timer);\n\t\t};\n\t}, [doFetch]);\n\n\tif (loading && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Loading achievements…</div>;\n\tif (error && achievements.length === 0)\n\t\treturn <div className=\"tab-stub\">Error: {error.message}</div>;\n\n\treturn (\n\t\t<div className=\"achievements-tab\">\n\t\t\t<h2>Achievements</h2>\n\t\t\t<AchievementTiles achievements={achievements} />\n\t\t</div>\n\t);\n}\n"],"names":["AchievementsTab","achievements","setAchievements","useState","loading","setLoading","error","setError","doFetch","useCallback","rows","fetchAchievements","useEffect","active","run","timer","jsx","jsxs","AchievementTiles"],"mappings":"yGAaA,SAAwBA,GAAsC,CAC7D,KAAM,CAACC,EAAcC,CAAe,EAAIC,EAAAA,SAA2B,CAAA,CAAE,EAC/D,CAACC,EAASC,CAAU,EAAIF,EAAAA,SAAS,EAAI,EACrC,CAACG,EAAOC,CAAQ,EAAIJ,EAAAA,SAAuB,IAAI,EAE/CK,EAAUC,EAAAA,YAAY,SAA2B,CACtD,GAAI,CACH,MAAMC,EAAO,MAAMC,EAAA,EACnBT,EAAgBQ,CAAI,EACpBH,EAAS,IAAI,CACd,OAAS,EAAG,CACXA,EAAS,aAAa,MAAQ,EAAI,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,CACvD,QAAA,CACCF,EAAW,EAAK,CACjB,CACD,EAAG,CAAA,CAAE,EAeL,OAbAO,EAAAA,UAAU,IAAM,CACf,IAAIC,EAAS,GACb,MAAMC,EAAM,SAA2B,CAClCD,SAAcL,EAAA,CACnB,EACKM,EAAA,EACL,MAAMC,EAAQ,YAAY,IAAM,KAAKD,EAAA,EAAO,IAAK,EACjD,MAAO,IAAM,CACZD,EAAS,GACT,cAAcE,CAAK,CACpB,CACD,EAAG,CAACP,CAAO,CAAC,EAERJ,GAAWH,EAAa,SAAW,EAC/Be,EAAAA,IAAC,MAAA,CAAI,UAAU,WAAW,SAAA,wBAAqB,EACnDV,GAASL,EAAa,SAAW,EAC7BgB,EAAAA,KAAC,MAAA,CAAI,UAAU,WAAW,SAAA,CAAA,UAAQX,EAAM,OAAA,EAAQ,EAGvDW,EAAAA,KAAC,MAAA,CAAI,UAAU,mBACd,SAAA,CAAAD,EAAAA,IAAC,MAAG,SAAA,cAAA,CAAY,EAChBA,MAACE,GAAiB,aAAAjB,CAAA,CAA4B,CAAA,EAC/C,CAEF"}
@@ -0,0 +1,2 @@
1
+ import{j as e,u as h,r as v,g as S,c as x}from"./index-BX0bcgLu.js";import{b as r,c as i,d as C,e as b,C as j,T}from"./TimeSavedCard-UqljSyP-.js";function f(a){return a>=80?"ov-stat-value-green":a>=50?"ov-stat-value-yellow":"ov-stat-value-red"}function g(a){return a==null?"":a>=0?"ov-stat-value-green":"ov-stat-value-red"}function t({label:a,value:o,className:s,title:c}){return e.jsxs("div",{className:"ov-stat-row",children:[e.jsx("span",{className:"ov-stat-label",title:c,children:a}),e.jsx("span",{className:`ov-stat-value${s?` ${s}`:""}`,children:o})]})}function p(a){return e.jsxs("div",{className:"card provider-cache-card",children:[e.jsx(t,{label:"Cache Hit Rate",value:`${a.hitPct.toFixed(1)}%`,className:f(a.hitPct)}),e.jsx(t,{label:"Turns Recorded",value:String(a.turnCount)}),e.jsx(t,{label:"Cache Read Tokens",value:r(a.totalCacheRead)}),e.jsx(t,{label:"Cache Write Tokens",value:r(a.totalCacheWrite)}),e.jsx(t,{label:"Input Tokens",value:r(a.totalInput)}),e.jsx(t,{label:"$ Saved (reads)",value:i(a.cacheReadSaved)}),e.jsx(t,{label:"Write Investment",value:i(a.cacheWriteCost)}),e.jsx(t,{label:"Net Saved",value:i(a.netSaved),className:g(a.netSaved)}),e.jsx(t,{label:"Model",value:a.modelLabel??"—"}),e.jsx(t,{label:"Tracked Since",value:C(a.firstTurnAt)}),e.jsx(t,{label:"Last Updated",value:b(a.latestTurnAt)})]})}function k(){const{data:a,loading:o,error:s}=h(v.useCallback(()=>S(),[]),{pollInterval:5e3}),{data:c,loading:m,error:d}=h(v.useCallback(()=>x(),[]),{pollInterval:5e3});return e.jsxs("div",{className:"cache-tab",children:[e.jsx("h2",{children:"Provider Prompt Cache"}),o?e.jsx("p",{className:"tab-stub",children:"Loading…"}):s?e.jsxs("p",{className:"tab-stub",children:["Provider cache unavailable: ",s.message]}):a?e.jsx(p,{hitPct:a.cache.avgHitPct,turnCount:a.cache.turnCount,totalCacheRead:a.cache.totalCacheRead,totalCacheWrite:a.cache.totalCacheWrite,totalInput:a.cache.totalInput,cacheReadSaved:a.savings?.cacheReadSaved??null,cacheWriteCost:a.savings?.cacheWriteCost??null,netSaved:a.savings?.netSaved??null,modelLabel:a.savings?.model??null,firstTurnAt:a.cache.firstTurnAt,latestTurnAt:a.cache.latestTurnAt}):null,e.jsx("h2",{children:"Mega-Compact Dedup Cache"}),m?e.jsx("p",{className:"tab-stub",children:"Loading…"}):d?e.jsxs("p",{className:"tab-stub",children:["Snapshot unavailable: ",d.message]}):c&&(()=>{const{cacheHits:l,compacts:u,timeSaved:n}=c;return e.jsxs("div",{className:"card-grid overview-card-grid",children:[e.jsx(j,{cacheHitsSession:l.session,cacheHitsTotal:l.total,tokensSavedSession:l.sessionTokensSaved,tokensSavedTotal:l.totalTokensSaved,compactionsSession:u.session,compactionsTotal:u.total}),e.jsx(T,{compactSessionSec:n.compact.sessionSec,compactTotalSec:n.compact.totalSec,cacheHitSessionSec:n.cacheHit.sessionSec,cacheHitTotalSec:n.cacheHit.totalSec})]})})()]})}export{k as default};
2
+ //# sourceMappingURL=CacheTab-Dnpv94wq.js.map