pi-mega-compact 0.11.2 → 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.
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +9 -1
- package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +7 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
- package/dist/extensions/dashboard-server/index-reader.js +22 -0
- package/dist/extensions/dashboard-server/routes-cache.js +80 -0
- package/dist/extensions/dashboard-server/routes-cache.test.js +248 -0
- package/dist/extensions/dashboard-server/routes-maintenance.js +7 -7
- package/dist/extensions/dashboard-server/routes-repo.js +24 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +3 -1
- package/dist/extensions/mega-cache-replay.test.js +233 -24
- package/dist/extensions/mega-compact-s38.test.js +201 -6
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +23 -7
- package/dist/extensions/mega-events/context-handler.js +37 -19
- package/dist/extensions/mega-events/error-classifier.js +27 -16
- package/dist/extensions/mega-runtime/runtime-snapshot.js +14 -4
- package/dist/extensions/mega-runtime/snapshot.js +1 -1
- package/dist/extensions/mega-runtime/snapshot.test.js +127 -0
- package/dist/extensions/mega-runtime/widget.js +4 -4
- package/dist/extensions/mega-runtime/widget.test.js +103 -12
- package/dist/src/pricing.js +78 -0
- package/dist/src/store/sqlite/perf-samples.js +104 -0
- package/dist/src/store/sqlite/perf-samples.test.js +93 -1
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js → AchievementTiles-vEo2MtE2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js.map → AchievementTiles-vEo2MtE2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +2 -0
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B8NqDt7I.js.map → AchievementsTab-DjxfYrDc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js → ConfigTab-Cm6ymbmx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js.map → ConfigTab-Cm6ymbmx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js → EventsTab-DXWl_YzI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js.map → EventsTab-DXWl_YzI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js → GameTab-BLDudcO5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js.map → GameTab-BLDudcO5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js → MetricsTab-Cq4u-rZF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js.map → MetricsTab-Cq4u-rZF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js → OverviewTab-D4kGvsiQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js.map → OverviewTab-D4kGvsiQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js → SessionsTab-DZC2-GCn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js.map → SessionsTab-DZC2-GCn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js → TopicsTab-01G71hv1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js.map → TopicsTab-01G71hv1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js → TurnsTab-DLRg0LZT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js.map → TurnsTab-DLRg0LZT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +1 -0
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js → index-BX0bcgLu.js} +4 -4
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js.map → index-BX0bcgLu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js → useSSE-BBYt5UPB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js.map → useSSE-BBYt5UPB.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/api/client.ts +6 -0
- package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +13 -1
- package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +141 -0
- package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +31 -3
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/maintenance.css +267 -0
- package/extensions/dashboard-client/src/styles/overview-extra.css +9 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +83 -29
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +301 -136
- package/extensions/dashboard-client/src/utils/format.ts +42 -7
- package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -8
- package/extensions/dashboard-server/api-contracts/index.ts +4 -0
- package/extensions/dashboard-server/api-contracts/multi-repo.ts +6 -0
- package/extensions/dashboard-server/api-contracts/provider-cache.ts +30 -0
- package/extensions/dashboard-server/api-contracts.test.ts +3 -2
- package/extensions/dashboard-server/index-reader.ts +23 -0
- package/extensions/dashboard-server/routes-cache.test.ts +302 -0
- package/extensions/dashboard-server/routes-cache.ts +123 -0
- package/extensions/dashboard-server/routes-maintenance.ts +18 -11
- package/extensions/dashboard-server/routes-repo.ts +31 -4
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +2 -0
- package/extensions/mega-cache-replay.test.ts +481 -166
- package/extensions/mega-compact-s38.test.ts +210 -6
- package/extensions/mega-config.ts +6 -0
- package/extensions/mega-events/agent-handlers.ts +24 -6
- package/extensions/mega-events/context-handler.ts +59 -24
- package/extensions/mega-events/error-classifier.ts +30 -13
- package/extensions/mega-runtime/runtime-snapshot.ts +31 -9
- package/extensions/mega-runtime/snapshot.test.ts +139 -0
- package/extensions/mega-runtime/snapshot.ts +6 -4
- package/extensions/mega-runtime/widget.test.ts +290 -105
- package/extensions/mega-runtime/widget.ts +29 -6
- package/package.json +1 -1
- package/src/pricing.ts +95 -0
- package/src/store/sqlite/perf-samples.test.ts +107 -3
- package/src/store/sqlite/perf-samples.ts +142 -6
- package/extensions/dashboard-client/dist/assets/AchievementsTab-B8NqDt7I.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-gn66lYD2.css +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./index-
|
|
2
|
-
//# sourceMappingURL=useSSE-
|
|
1
|
+
import{r as e}from"./index-BX0bcgLu.js";function R(i={}){const{maxEvents:c=500,maxReconnects:l=5}=i,[f,v]=e.useState([]),[E,n]=e.useState("connecting"),[S,p]=e.useState(0),[m,d]=e.useState(null),o=e.useRef(0),s=e.useRef(null),r=e.useCallback(()=>{s.current&&s.current.close(),n("connecting");const t=new EventSource("/api/events");s.current=t,t.onopen=()=>{n("connected"),o.current=0},t.onmessage=g=>{try{const x=JSON.parse(g.data);v(u=>{const a=[...u,x];return a.length>c?a.slice(-c):a}),p(u=>u+1),d(Date.now())}catch{}},t.onerror=()=>{t.close(),o.current<l?(o.current+=1,n("connecting"),setTimeout(r,1e3)):n("disconnected")}},[c,l]);return e.useEffect(()=>(r(),()=>{s.current?.close()}),[r]),{events:f,status:E,eventCount:S,lastEventAt:m}}export{R as u};
|
|
2
|
+
//# sourceMappingURL=useSSE-BBYt5UPB.js.map
|
package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js.map → useSSE-BBYt5UPB.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSSE-
|
|
1
|
+
{"version":3,"file":"useSSE-BBYt5UPB.js","sources":["../../src/hooks/useSSE.ts"],"sourcesContent":["/**\n * dashboard-client/src/hooks/useSSE.ts — Server-Sent Events hook.\n *\n * Connects to /api/events, provides real-time event stream.\n * SPRINT-B1: basic connection. SPRINT-D1: reconnect with backoff.\n */\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport type { SseEvent } from '../../../dashboard-server/api-contracts';\n\nexport type SseStatus = 'connecting' | 'connected' | 'disconnected' | 'error';\n\nexport interface UseSSEResult {\n events: SseEvent[];\n status: SseStatus;\n eventCount: number;\n lastEventAt: number | null;\n}\n\nexport interface UseSSEOptions {\n /** Max events to keep in memory (ring buffer). */\n maxEvents?: number;\n /** Max reconnect attempts before giving up. */\n maxReconnects?: number;\n}\n\n// SPRINT-D1-REMAINING: exponential backoff (1s → 2s → 4s → max 30s).\n// SPRINT-D1-REMAINING: after 5 failures, set status='disconnected'.\n\nexport function useSSE(options: UseSSEOptions = {}): UseSSEResult {\n const { maxEvents = 500, maxReconnects = 5 } = options;\n const [events, setEvents] = useState<SseEvent[]>([]);\n const [status, setStatus] = useState<SseStatus>('connecting');\n const [eventCount, setEventCount] = useState(0);\n const [lastEventAt, setLastEventAt] = useState<number | null>(null);\n const reconnectCount = useRef(0);\n const sourceRef = useRef<EventSource | null>(null);\n\n const connect = useCallback(() => {\n if (sourceRef.current) {\n sourceRef.current.close();\n }\n setStatus('connecting');\n const source = new EventSource('/api/events');\n sourceRef.current = source;\n\n source.onopen = () => {\n setStatus('connected');\n reconnectCount.current = 0;\n };\n\n source.onmessage = (event) => {\n try {\n const parsed = JSON.parse(event.data) as SseEvent;\n setEvents(prev => {\n const next = [...prev, parsed];\n return next.length > maxEvents ? next.slice(-maxEvents) : next;\n });\n setEventCount(c => c + 1);\n setLastEventAt(Date.now());\n } catch {\n // non-fatal: skip malformed events\n }\n };\n\n source.onerror = () => {\n source.close();\n if (reconnectCount.current < maxReconnects) {\n reconnectCount.current += 1;\n setStatus('connecting');\n // SPRINT-D1: use exponential backoff instead of fixed delay\n setTimeout(connect, 1000);\n } else {\n setStatus('disconnected');\n }\n };\n }, [maxEvents, maxReconnects]);\n\n useEffect(() => {\n connect();\n return () => {\n sourceRef.current?.close();\n };\n }, [connect]);\n\n return { events, status, eventCount, lastEventAt };\n}\n"],"names":["useSSE","options","maxEvents","maxReconnects","events","setEvents","useState","status","setStatus","eventCount","setEventCount","lastEventAt","setLastEventAt","reconnectCount","useRef","sourceRef","connect","useCallback","source","event","parsed","prev","next","c","useEffect"],"mappings":"wCA6BO,SAASA,EAAOC,EAAyB,GAAkB,CAChE,KAAM,CAAE,UAAAC,EAAY,IAAK,cAAAC,EAAgB,GAAMF,EACzC,CAACG,EAAQC,CAAS,EAAIC,EAAAA,SAAqB,CAAA,CAAE,EAC7C,CAACC,EAAQC,CAAS,EAAIF,EAAAA,SAAoB,YAAY,EACtD,CAACG,EAAYC,CAAa,EAAIJ,EAAAA,SAAS,CAAC,EACxC,CAACK,EAAaC,CAAc,EAAIN,EAAAA,SAAwB,IAAI,EAC5DO,EAAiBC,EAAAA,OAAO,CAAC,EACzBC,EAAYD,EAAAA,OAA2B,IAAI,EAE3CE,EAAUC,EAAAA,YAAY,IAAM,CAC5BF,EAAU,SACZA,EAAU,QAAQ,MAAA,EAEpBP,EAAU,YAAY,EACtB,MAAMU,EAAS,IAAI,YAAY,aAAa,EAC5CH,EAAU,QAAUG,EAEpBA,EAAO,OAAS,IAAM,CACpBV,EAAU,WAAW,EACrBK,EAAe,QAAU,CAC3B,EAEAK,EAAO,UAAaC,GAAU,CAC5B,GAAI,CACF,MAAMC,EAAS,KAAK,MAAMD,EAAM,IAAI,EACpCd,EAAUgB,GAAQ,CAChB,MAAMC,EAAO,CAAC,GAAGD,EAAMD,CAAM,EAC7B,OAAOE,EAAK,OAASpB,EAAYoB,EAAK,MAAM,CAACpB,CAAS,EAAIoB,CAC5D,CAAC,EACDZ,EAAca,GAAKA,EAAI,CAAC,EACxBX,EAAe,KAAK,KAAK,CAC3B,MAAQ,CAER,CACF,EAEAM,EAAO,QAAU,IAAM,CACrBA,EAAO,MAAA,EACHL,EAAe,QAAUV,GAC3BU,EAAe,SAAW,EAC1BL,EAAU,YAAY,EAEtB,WAAWQ,EAAS,GAAI,GAExBR,EAAU,cAAc,CAE5B,CACF,EAAG,CAACN,EAAWC,CAAa,CAAC,EAE7BqB,OAAAA,EAAAA,UAAU,KACRR,EAAA,EACO,IAAM,CACXD,EAAU,SAAS,MAAA,CACrB,GACC,CAACC,CAAO,CAAC,EAEL,CAAE,OAAAZ,EAAQ,OAAAG,EAAQ,WAAAE,EAAY,YAAAE,CAAA,CACvC"}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>mega-compact dashboard</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BX0bcgLu.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BEdKlpAB.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
SchemaHealthResponse,
|
|
40
40
|
MaintenanceAction,
|
|
41
41
|
MaintenanceActionResult,
|
|
42
|
+
ProviderCacheResponse,
|
|
42
43
|
} from "@contracts";
|
|
43
44
|
|
|
44
45
|
/** Error thrown when a dashboard API response is not 2xx. */
|
|
@@ -190,6 +191,11 @@ export function fetchTopics(): Promise<TopicsResponse> {
|
|
|
190
191
|
return getJson<TopicsResponse>(ENDPOINTS.topics.path);
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
/** Lifetime provider prompt cache aggregates + $ savings estimate. */
|
|
195
|
+
export function fetchProviderCache(): Promise<ProviderCacheResponse> {
|
|
196
|
+
return getJson<ProviderCacheResponse>(ENDPOINTS.providerCache.path);
|
|
197
|
+
}
|
|
198
|
+
|
|
193
199
|
// ── S52: turn-by-turn memory tracking + recall + rewind ───────────────
|
|
194
200
|
|
|
195
201
|
export function fetchTurns(): Promise<TurnsResponse> {
|
|
@@ -30,6 +30,8 @@ const HEADERS = [
|
|
|
30
30
|
"State",
|
|
31
31
|
"Compactions (s/t)",
|
|
32
32
|
"Cache Hits (s/t)",
|
|
33
|
+
"Cache Hit %",
|
|
34
|
+
"Est. Saved",
|
|
33
35
|
"Compact s/t (s)",
|
|
34
36
|
"CacheHit s/t (s)",
|
|
35
37
|
] as const;
|
|
@@ -61,7 +63,7 @@ export function ActiveReposTable({
|
|
|
61
63
|
<tbody>
|
|
62
64
|
{servers.length === 0 && (
|
|
63
65
|
<tr>
|
|
64
|
-
<td colSpan={
|
|
66
|
+
<td colSpan={HEADERS.length} className="repo-empty">
|
|
65
67
|
No active repositories.
|
|
66
68
|
</td>
|
|
67
69
|
</tr>
|
|
@@ -97,6 +99,16 @@ export function ActiveReposTable({
|
|
|
97
99
|
<td className="num">
|
|
98
100
|
{ch.session} / {ch.total}
|
|
99
101
|
</td>
|
|
102
|
+
<td className="num">
|
|
103
|
+
{r.providerCache != null
|
|
104
|
+
? `${r.providerCache.avgHitPct.toFixed(1)}%`
|
|
105
|
+
: "—"}
|
|
106
|
+
</td>
|
|
107
|
+
<td className="num">
|
|
108
|
+
{r.providerCache?.estimatedSaved != null
|
|
109
|
+
? `$${r.providerCache.estimatedSaved.toFixed(2)}`
|
|
110
|
+
: "—"}
|
|
111
|
+
</td>
|
|
100
112
|
<td className="num">
|
|
101
113
|
{fmtSec(ts.compact.sessionSec)} /{" "}
|
|
102
114
|
{fmtSec(ts.compact.totalSec)}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/components/ProviderCacheCard.tsx — Provider prompt cache stats card.
|
|
3
|
+
*
|
|
4
|
+
* Displays lifetime provider prompt cache hit-rate aggregates + estimated
|
|
5
|
+
* dollar savings from the /api/provider-cache endpoint.
|
|
6
|
+
* Reuses the `ov-stat-row` / `StatRow` pattern from CacheHitsCard.tsx.
|
|
7
|
+
*
|
|
8
|
+
* B.2 spec: Cache Hit Rate with color classes, humanized token counts,
|
|
9
|
+
* priced $ rows with green/red net, model name, locale date + relative time.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type React from "react";
|
|
13
|
+
import {
|
|
14
|
+
fmtTokens,
|
|
15
|
+
fmtDollars,
|
|
16
|
+
fmtDate,
|
|
17
|
+
fmtRelativeTime,
|
|
18
|
+
} from "../utils/format";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Flattened props — destructured from ProviderCacheResponse in CacheTab.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface ProviderCacheCardProps {
|
|
25
|
+
/** Cache hit rate percentage (0–100). */
|
|
26
|
+
hitPct: number;
|
|
27
|
+
/** Number of turns with cache_hit_pct samples. */
|
|
28
|
+
turnCount: number;
|
|
29
|
+
/** Total cache-read tokens across all sampled turns. */
|
|
30
|
+
totalCacheRead: number;
|
|
31
|
+
/** Total cache-write tokens across all sampled turns. */
|
|
32
|
+
totalCacheWrite: number;
|
|
33
|
+
/** Total input tokens across all sampled turns. */
|
|
34
|
+
totalInput: number;
|
|
35
|
+
/** $ saved from cache reads (null when unpriced). */
|
|
36
|
+
cacheReadSaved: number | null;
|
|
37
|
+
/** $ spent on cache writes (null when unpriced). */
|
|
38
|
+
cacheWriteCost: number | null;
|
|
39
|
+
/** netSaved = cacheReadSaved - cacheWriteCost (null when unpriced). */
|
|
40
|
+
netSaved: number | null;
|
|
41
|
+
/** Model display name (modelName || modelId, or null). */
|
|
42
|
+
modelLabel: string | null;
|
|
43
|
+
/** ISO timestamp of first recorded turn. */
|
|
44
|
+
firstTurnAt: string | null;
|
|
45
|
+
/** ISO timestamp of latest recorded turn. */
|
|
46
|
+
latestTurnAt: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Helpers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** Hit-rate color: green ≥80, yellow ≥50, red <50. */
|
|
54
|
+
function hitPctClass(pct: number): string {
|
|
55
|
+
if (pct >= 80) return "ov-stat-value-green";
|
|
56
|
+
if (pct >= 50) return "ov-stat-value-yellow";
|
|
57
|
+
return "ov-stat-value-red";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Net-saved color: green positive, red negative. */
|
|
61
|
+
function netClass(v: number | null): string {
|
|
62
|
+
if (v == null) return "";
|
|
63
|
+
return v >= 0 ? "ov-stat-value-green" : "ov-stat-value-red";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function StatRow({
|
|
67
|
+
label,
|
|
68
|
+
value,
|
|
69
|
+
className,
|
|
70
|
+
title,
|
|
71
|
+
}: {
|
|
72
|
+
label: string;
|
|
73
|
+
value: string;
|
|
74
|
+
className?: string;
|
|
75
|
+
title?: string;
|
|
76
|
+
}): React.ReactElement {
|
|
77
|
+
return (
|
|
78
|
+
<div className="ov-stat-row">
|
|
79
|
+
<span className="ov-stat-label" title={title}>
|
|
80
|
+
{label}
|
|
81
|
+
</span>
|
|
82
|
+
<span className={`ov-stat-value${className ? ` ${className}` : ""}`}>
|
|
83
|
+
{value}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Card
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export function ProviderCacheCard(
|
|
94
|
+
props: ProviderCacheCardProps,
|
|
95
|
+
): React.ReactElement {
|
|
96
|
+
return (
|
|
97
|
+
<div className="card provider-cache-card">
|
|
98
|
+
{/* --- Hit Rate + Turns --- */}
|
|
99
|
+
<StatRow
|
|
100
|
+
label="Cache Hit Rate"
|
|
101
|
+
value={`${props.hitPct.toFixed(1)}%`}
|
|
102
|
+
className={hitPctClass(props.hitPct)}
|
|
103
|
+
/>
|
|
104
|
+
<StatRow label="Turns Recorded" value={String(props.turnCount)} />
|
|
105
|
+
|
|
106
|
+
{/* --- Token counts --- */}
|
|
107
|
+
<StatRow
|
|
108
|
+
label="Cache Read Tokens"
|
|
109
|
+
value={fmtTokens(props.totalCacheRead)}
|
|
110
|
+
/>
|
|
111
|
+
<StatRow
|
|
112
|
+
label="Cache Write Tokens"
|
|
113
|
+
value={fmtTokens(props.totalCacheWrite)}
|
|
114
|
+
/>
|
|
115
|
+
<StatRow label="Input Tokens" value={fmtTokens(props.totalInput)} />
|
|
116
|
+
|
|
117
|
+
{/* --- Dollar rows (null when unpriced) --- */}
|
|
118
|
+
<StatRow
|
|
119
|
+
label="$ Saved (reads)"
|
|
120
|
+
value={fmtDollars(props.cacheReadSaved)}
|
|
121
|
+
/>
|
|
122
|
+
<StatRow
|
|
123
|
+
label="Write Investment"
|
|
124
|
+
value={fmtDollars(props.cacheWriteCost)}
|
|
125
|
+
/>
|
|
126
|
+
<StatRow
|
|
127
|
+
label="Net Saved"
|
|
128
|
+
value={fmtDollars(props.netSaved)}
|
|
129
|
+
className={netClass(props.netSaved)}
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
{/* --- Model + timestamps --- */}
|
|
133
|
+
<StatRow label="Model" value={props.modelLabel ?? "\u2014"} />
|
|
134
|
+
<StatRow label="Tracked Since" value={fmtDate(props.firstTurnAt)} />
|
|
135
|
+
<StatRow
|
|
136
|
+
label="Last Updated"
|
|
137
|
+
value={fmtRelativeTime(props.latestTurnAt)}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -29,6 +29,8 @@ interface ModelGroup {
|
|
|
29
29
|
maxTokens: number[];
|
|
30
30
|
reasoning: boolean | null;
|
|
31
31
|
lastAt: number;
|
|
32
|
+
cacheRead: number;
|
|
33
|
+
cacheWrite: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** Collapse numeric samples: "—" | single | "lo–hi" (matches html.ts). */
|
|
@@ -71,6 +73,8 @@ function aggregate(repos: IndexesIndexRow[]): ModelGroup[] {
|
|
|
71
73
|
maxTokens: [],
|
|
72
74
|
reasoning: null,
|
|
73
75
|
lastAt: 0,
|
|
76
|
+
cacheRead: 0,
|
|
77
|
+
cacheWrite: 0,
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
const g = map[key];
|
|
@@ -87,6 +91,8 @@ function aggregate(repos: IndexesIndexRow[]): ModelGroup[] {
|
|
|
87
91
|
if (r.contextWindow) g.ctxWindows.push(r.contextWindow);
|
|
88
92
|
if (r.maxTokens) g.maxTokens.push(r.maxTokens);
|
|
89
93
|
if (r.reasoning != null) g.reasoning = r.reasoning;
|
|
94
|
+
if (r.providerCacheRead) g.cacheRead += r.providerCacheRead;
|
|
95
|
+
if (r.providerCacheWrite) g.cacheWrite += r.providerCacheWrite;
|
|
90
96
|
if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt)
|
|
91
97
|
g.lastAt = r.lastCompactedAt;
|
|
92
98
|
}
|
|
@@ -151,13 +157,19 @@ export function SavingsByModelTable({
|
|
|
151
157
|
Out $/tok
|
|
152
158
|
</th>
|
|
153
159
|
<th className="num">$ Saved</th>
|
|
160
|
+
<th className="num" title="Provider prompt-cache hit rate (reads / reads+writes+tokens_in)">
|
|
161
|
+
Cache Hit %
|
|
162
|
+
</th>
|
|
163
|
+
<th className="num" title="Estimated provider prompt-cache savings (read tokens * 0.9 * inputRate − write tokens * 0.25 * inputRate)">
|
|
164
|
+
Cache $ Saved
|
|
165
|
+
</th>
|
|
154
166
|
<th className="num">Last Used</th>
|
|
155
167
|
</tr>
|
|
156
168
|
</thead>
|
|
157
169
|
<tbody>
|
|
158
170
|
{groups.length === 0 && (
|
|
159
171
|
<tr>
|
|
160
|
-
<td colSpan={
|
|
172
|
+
<td colSpan={16} className="repo-empty">
|
|
161
173
|
No repositories registered yet.
|
|
162
174
|
</td>
|
|
163
175
|
</tr>
|
|
@@ -207,8 +219,24 @@ export function SavingsByModelTable({
|
|
|
207
219
|
<td className="num">
|
|
208
220
|
{collapseRate(g.outRates)}
|
|
209
221
|
</td>
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
<td className="num">{usd}</td>
|
|
223
|
+
<td className="num">
|
|
224
|
+
{g.cacheRead > 0
|
|
225
|
+
? `${(
|
|
226
|
+
(g.cacheRead * 100) /
|
|
227
|
+
(g.cacheRead + g.cacheWrite + g.tokensIn || 1)
|
|
228
|
+
).toFixed(1)}%`
|
|
229
|
+
: "—"}
|
|
230
|
+
</td>
|
|
231
|
+
<td className="num">
|
|
232
|
+
{g.cacheRead > 0
|
|
233
|
+
? `$${(
|
|
234
|
+
g.cacheRead * 0.9 * (g.inRates[0] || 0) -
|
|
235
|
+
g.cacheWrite * 0.25 * (g.inRates[0] || 0)
|
|
236
|
+
).toFixed(4)}`
|
|
237
|
+
: "—"}
|
|
238
|
+
</td>
|
|
239
|
+
<td className="num">{when}</td>
|
|
212
240
|
</tr>
|
|
213
241
|
);
|
|
214
242
|
})}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/styles/maintenance.css — Maintenance tab styles (S49B).
|
|
3
|
+
*
|
|
4
|
+
* Cards, stat tables, action grid, badges, and buttons.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* ─── Layout ──────────────────────────────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
.maintenance-tab {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: 12px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ─── Card structure (shared card base in base.css + overview-events.css) ── */
|
|
16
|
+
|
|
17
|
+
.card-header {
|
|
18
|
+
font-size: 13px;
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
letter-spacing: 0.5px;
|
|
22
|
+
color: var(--muted);
|
|
23
|
+
margin-bottom: 10px;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: 8px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.card-body {
|
|
30
|
+
font-size: 13px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ─── Stat sections ──────────────────────────────────────────────────────── */
|
|
34
|
+
|
|
35
|
+
.stat-section {
|
|
36
|
+
margin-bottom: 12px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.stat-section:last-child {
|
|
40
|
+
margin-bottom: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.stat-label {
|
|
44
|
+
font-size: 11px;
|
|
45
|
+
color: var(--muted);
|
|
46
|
+
text-transform: uppercase;
|
|
47
|
+
letter-spacing: 0.4px;
|
|
48
|
+
margin-bottom: 4px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.stat-row {
|
|
52
|
+
display: flex;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
align-items: baseline;
|
|
55
|
+
padding: 3px 0;
|
|
56
|
+
border-bottom: 1px solid var(--border);
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.num {
|
|
61
|
+
text-align: right;
|
|
62
|
+
font-family: monospace;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ─── Compact table ──────────────────────────────────────────────────────── */
|
|
67
|
+
|
|
68
|
+
.compact-table {
|
|
69
|
+
width: 100%;
|
|
70
|
+
border-collapse: collapse;
|
|
71
|
+
font-size: 13px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.compact-table th {
|
|
75
|
+
text-align: left;
|
|
76
|
+
padding: 4px 6px;
|
|
77
|
+
border-bottom: 1px solid var(--border);
|
|
78
|
+
font-size: 11px;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
text-transform: uppercase;
|
|
81
|
+
letter-spacing: 0.4px;
|
|
82
|
+
color: var(--muted);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.compact-table th.num {
|
|
86
|
+
text-align: right;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.compact-table td {
|
|
90
|
+
padding: 3px 6px;
|
|
91
|
+
border-bottom: 1px solid var(--border);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.compact-table td.num {
|
|
95
|
+
text-align: right;
|
|
96
|
+
font-family: monospace;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.compact-table tbody tr:last-child td {
|
|
101
|
+
border-bottom: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ─── Text colors ────────────────────────────────────────────────────────── */
|
|
105
|
+
|
|
106
|
+
.text-muted {
|
|
107
|
+
color: var(--muted);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.text-error {
|
|
111
|
+
color: var(--err);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.text-ok {
|
|
115
|
+
color: var(--ok);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.text-warn {
|
|
119
|
+
color: var(--warn);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.text-mono {
|
|
123
|
+
font-family: monospace;
|
|
124
|
+
font-size: 12px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ─── Badges ─────────────────────────────────────────────────────────────── */
|
|
128
|
+
|
|
129
|
+
.badge {
|
|
130
|
+
display: inline-block;
|
|
131
|
+
padding: 1px 8px;
|
|
132
|
+
font-size: 10px;
|
|
133
|
+
font-weight: 700;
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
text-transform: uppercase;
|
|
136
|
+
letter-spacing: 0.3px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.badge-ok {
|
|
140
|
+
background: color-mix(in srgb, var(--ok) 20%, transparent);
|
|
141
|
+
color: var(--ok);
|
|
142
|
+
border: 1px solid var(--ok);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.badge-bad {
|
|
146
|
+
background: color-mix(in srgb, var(--err) 20%, transparent);
|
|
147
|
+
color: var(--err);
|
|
148
|
+
border: 1px solid var(--err);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.badge-warn {
|
|
152
|
+
background: color-mix(in srgb, var(--warn) 20%, transparent);
|
|
153
|
+
color: var(--warn);
|
|
154
|
+
border: 1px solid var(--warn);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ─── Action grid ────────────────────────────────────────────────────────── */
|
|
158
|
+
|
|
159
|
+
.action-grid {
|
|
160
|
+
display: grid;
|
|
161
|
+
grid-template-columns: 1fr;
|
|
162
|
+
gap: 8px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.action-item {
|
|
166
|
+
display: flex;
|
|
167
|
+
justify-content: space-between;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 12px;
|
|
170
|
+
padding: 10px 12px;
|
|
171
|
+
background: var(--bg);
|
|
172
|
+
border: 1px solid var(--border);
|
|
173
|
+
border-radius: 6px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.action-info {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
gap: 2px;
|
|
180
|
+
flex: 1;
|
|
181
|
+
min-width: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.action-label {
|
|
185
|
+
font-size: 13px;
|
|
186
|
+
font-weight: 600;
|
|
187
|
+
color: var(--text);
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
gap: 6px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.action-desc {
|
|
194
|
+
font-size: 11px;
|
|
195
|
+
color: var(--muted);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.action-result {
|
|
199
|
+
font-size: 11px;
|
|
200
|
+
margin-top: 2px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.action-controls {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 8px;
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* ─── Buttons ────────────────────────────────────────────────────────────── */
|
|
211
|
+
|
|
212
|
+
.btn {
|
|
213
|
+
padding: 5px 14px;
|
|
214
|
+
font-size: 12px;
|
|
215
|
+
font-weight: 600;
|
|
216
|
+
border: none;
|
|
217
|
+
border-radius: 6px;
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
transition: opacity 0.15s;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.btn:disabled {
|
|
224
|
+
opacity: 0.5;
|
|
225
|
+
cursor: not-allowed;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.btn-primary {
|
|
229
|
+
background: var(--accent);
|
|
230
|
+
color: #0d1117;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.btn-danger {
|
|
234
|
+
background: var(--err);
|
|
235
|
+
color: #fff;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ─── Days input ─────────────────────────────────────────────────────────── */
|
|
239
|
+
|
|
240
|
+
.days-input {
|
|
241
|
+
width: 60px;
|
|
242
|
+
padding: 4px 6px;
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
background: var(--panel);
|
|
245
|
+
border: 1px solid var(--border);
|
|
246
|
+
border-radius: 4px;
|
|
247
|
+
color: var(--text);
|
|
248
|
+
text-align: center;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.days-input:disabled {
|
|
252
|
+
opacity: 0.5;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ─── Responsive ─────────────────────────────────────────────────────────── */
|
|
256
|
+
|
|
257
|
+
@media (max-width: 640px) {
|
|
258
|
+
.action-item {
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
align-items: stretch;
|
|
261
|
+
gap: 8px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.action-controls {
|
|
265
|
+
justify-content: flex-end;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -31,6 +31,15 @@
|
|
|
31
31
|
color: var(--text);
|
|
32
32
|
font-family: monospace;
|
|
33
33
|
}
|
|
34
|
+
.ov-stat-value-green {
|
|
35
|
+
color: var(--ok);
|
|
36
|
+
}
|
|
37
|
+
.ov-stat-value-yellow {
|
|
38
|
+
color: var(--warn);
|
|
39
|
+
}
|
|
40
|
+
.ov-stat-value-red {
|
|
41
|
+
color: var(--err);
|
|
42
|
+
}
|
|
34
43
|
.ov-value-ok {
|
|
35
44
|
color: var(--ok) !important;
|
|
36
45
|
}
|