pi-mega-compact 0.11.5 → 0.11.6
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/cache-stripes.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +13 -0
- package/dist/extensions/dashboard-server/api-contracts/memory-map.js +1 -0
- package/dist/extensions/dashboard-server/routes-cache.js +28 -0
- package/dist/extensions/dashboard-server/routes-game.js +3 -0
- package/dist/extensions/dashboard-server/routes-memory-map.js +50 -0
- package/dist/extensions/dashboard-server/routes-setup.js +105 -0
- package/dist/extensions/dashboard-server/routes.js +3 -1
- package/dist/extensions/dashboard-server/server.js +7 -1
- package/dist/extensions/mega-compact.js +5 -0
- package/dist/extensions/mega-config.js +5 -0
- package/dist/extensions/mega-events/context-handler.js +10 -2
- package/dist/extensions/mega-events/perf-handler.js +83 -1
- package/dist/extensions/mega-events/separated-prompt.js +248 -0
- package/dist/extensions/mega-runtime/env-loader.js +56 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +1 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +1 -0
- package/dist/extensions/mega-runtime/runtime.js +1 -0
- package/dist/extensions/mega-runtime/snapshot.js +1 -0
- package/dist/extensions/mega-runtime/widget.js +5 -0
- package/dist/src/cache-stripe-impl.js +288 -0
- package/dist/src/cache-stripe.js +11 -0
- package/dist/src/config/stopwords.js +24 -0
- package/dist/src/config.js +17 -0
- package/dist/src/memoryGraph.js +299 -0
- package/dist/src/monitoring.js +13 -0
- package/dist/src/queryExpansion.js +163 -0
- package/dist/src/queryReformulation/cache.js +123 -0
- package/dist/src/queryReformulation/rrf.js +29 -0
- package/dist/src/queryReformulation/tfidf.js +61 -0
- package/dist/src/queryReformulation/vagueness.js +33 -0
- package/dist/src/queryReformulation.js +225 -0
- package/dist/src/recallMetrics.js +258 -0
- package/dist/src/store/sqlite/cache-stripes.js +133 -0
- package/dist/src/store/sqlite/fts5-search.js +68 -0
- package/dist/src/store/sqlite/perf-samples.js +1 -0
- package/dist/src/store/sqlite/schema.js +60 -1
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/store/vectorIndex.js +29 -5
- package/dist/src/tiered-router/types.js +18 -0
- package/dist/src/tieredRouter.js +421 -0
- package/dist/src/vectorStore.js +8 -6
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js → AchievementTiles-DR9cLGDL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js.map → AchievementTiles-DR9cLGDL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js → AchievementsTab-svNUgJ1e.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js.map → AchievementsTab-svNUgJ1e.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js → ConfigTab-CmmWheuW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js.map → ConfigTab-CmmWheuW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js → EventsTab-CFE7jLsa.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js.map → EventsTab-CFE7jLsa.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/GameTab-CJAX_th_.js +2 -0
- package/extensions/dashboard-client/dist/assets/{GameTab-DehUX-KE.js.map → GameTab-CJAX_th_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js → MaintenanceTab-P4SUsvMf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js.map → MaintenanceTab-P4SUsvMf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js → MetricsTab-CHQscixy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js.map → MetricsTab-CHQscixy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js → OverviewTab-DBs4DJdd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js.map → OverviewTab-DBs4DJdd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js → ReposTab-DeFz76L5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js.map → ReposTab-DeFz76L5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js → SessionsTab-BWNsj1OB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js.map → SessionsTab-BWNsj1OB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js → TimeSavedCard-CiqCOQuL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js.map → TimeSavedCard-CiqCOQuL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js → TopicsTab-BWWQxltv.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js.map → TopicsTab-BWWQxltv.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js → TurnsTab-CVz4f4-o.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js.map → TurnsTab-CVz4f4-o.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-CUNn22iV.css +1 -0
- package/extensions/dashboard-client/dist/assets/{index-4gQ7auad.js → index-DrktNOoT.js} +11 -11
- package/extensions/dashboard-client/dist/assets/index-DrktNOoT.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js → useSSE-Bf5LOLSi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js.map → useSSE-Bf5LOLSi.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +24 -8
- package/extensions/dashboard-client/src/api/client.ts +15 -0
- package/extensions/dashboard-client/src/components/CacheHitRateTrendCard.tsx +100 -0
- package/extensions/dashboard-client/src/components/StripeDistributionCard.tsx +110 -0
- package/extensions/dashboard-client/src/components/TabBar.tsx +49 -4
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/base.css +30 -0
- package/extensions/dashboard-client/src/styles/cache.css +167 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +75 -13
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +440 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +65 -2
- package/extensions/dashboard-server/api-contracts/cache-stripes.ts +49 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -1
- package/extensions/dashboard-server/api-contracts/index.ts +16 -1
- package/extensions/dashboard-server/api-contracts/memory-map.ts +50 -0
- package/extensions/dashboard-server/api-contracts/setup.ts +22 -0
- package/extensions/dashboard-server/routes-cache.ts +36 -1
- package/extensions/dashboard-server/routes-game.ts +3 -0
- package/extensions/dashboard-server/routes-memory-map.ts +105 -0
- package/extensions/dashboard-server/routes-setup.ts +112 -0
- package/extensions/dashboard-server/routes.ts +3 -1
- package/extensions/dashboard-server/server.ts +6 -0
- package/extensions/mega-compact.ts +5 -0
- package/extensions/mega-config.ts +17 -0
- package/extensions/mega-events/context-handler.ts +10 -2
- package/extensions/mega-events/perf-handler.ts +111 -1
- package/extensions/mega-events/separated-prompt.ts +329 -0
- package/extensions/mega-runtime/env-loader.ts +55 -0
- package/extensions/mega-runtime/helpers.ts +2 -0
- package/extensions/mega-runtime/reset-runtime.ts +1 -0
- package/extensions/mega-runtime/runtime-snapshot.ts +1 -0
- package/extensions/mega-runtime/runtime.ts +1 -0
- package/extensions/mega-runtime/snapshot.ts +6 -0
- package/extensions/mega-runtime/widget-types.ts +3 -0
- package/extensions/mega-runtime/widget.ts +5 -0
- package/package.json +1 -1
- package/src/cache-stripe-impl.ts +385 -0
- package/src/cache-stripe.ts +18 -0
- package/src/config/stopwords.ts +25 -0
- package/src/config.ts +26 -0
- package/src/memoryGraph.ts +438 -0
- package/src/monitoring.ts +35 -0
- package/src/queryExpansion.ts +206 -0
- package/src/queryReformulation/cache.ts +169 -0
- package/src/queryReformulation/rrf.ts +51 -0
- package/src/queryReformulation/tfidf.ts +79 -0
- package/src/queryReformulation/vagueness.ts +38 -0
- package/src/queryReformulation.ts +355 -0
- package/src/recallMetrics.ts +388 -0
- package/src/store/sqlite/cache-stripes.ts +196 -0
- package/src/store/sqlite/fts5-search.ts +90 -0
- package/src/store/sqlite/perf-samples.ts +3 -1
- package/src/store/sqlite/schema.ts +60 -1
- package/src/store/sqlite.ts +1 -0
- package/src/store/vectorIndex.ts +38 -7
- package/src/tiered-router/types.ts +64 -0
- package/src/tieredRouter.ts +489 -0
- package/src/vectorStore.ts +9 -5
- package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/GameTab-DehUX-KE.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-4gQ7auad.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"./index-
|
|
2
|
-
//# sourceMappingURL=useSSE-
|
|
1
|
+
import{r as e}from"./index-DrktNOoT.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-Bf5LOLSi.js.map
|
package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js.map → useSSE-Bf5LOLSi.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSSE-
|
|
1
|
+
{"version":3,"file":"useSSE-Bf5LOLSi.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-DrktNOoT.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CUNn22iV.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -27,6 +27,7 @@ const SessionsTab = React.lazy(() => import("./tabs/SessionsTab"));
|
|
|
27
27
|
const TopicsTab = React.lazy(() => import("./tabs/TopicsTab"));
|
|
28
28
|
const TurnsTab = React.lazy(() => import("./tabs/TurnsTab"));
|
|
29
29
|
const MaintenanceTab = React.lazy(() => import("./tabs/MaintenanceTab"));
|
|
30
|
+
const MemoryMapTab = React.lazy(() => import("./tabs/MemoryMapTab"));
|
|
30
31
|
|
|
31
32
|
export type TabId =
|
|
32
33
|
| "overview"
|
|
@@ -41,24 +42,33 @@ export type TabId =
|
|
|
41
42
|
| "sessions"
|
|
42
43
|
| "topics"
|
|
43
44
|
| "turns"
|
|
44
|
-
| "maintenance"
|
|
45
|
+
| "maintenance"
|
|
46
|
+
| "memory-map";
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
type TabDef = { id: TabId; label: string };
|
|
49
|
+
|
|
50
|
+
const PRIMARY_TABS: TabDef[] = [
|
|
47
51
|
{ id: "overview", label: "Overview" },
|
|
52
|
+
{ id: "cache", label: "Cache" },
|
|
53
|
+
{ id: "sessions", label: "Sessions" },
|
|
54
|
+
{ id: "turns", label: "Turns" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const ADVANCED_TABS: TabDef[] = [
|
|
48
58
|
{ id: "repos", label: "Repos" },
|
|
49
59
|
{ id: "events", label: "Events" },
|
|
50
60
|
{ id: "config", label: "Config" },
|
|
51
61
|
{ id: "setup", label: "Setup" },
|
|
52
62
|
{ id: "metrics", label: "Metrics" },
|
|
53
|
-
{ id: "cache", label: "Cache" },
|
|
54
63
|
{ id: "game", label: "Game" },
|
|
55
64
|
{ id: "achievements", label: "Achievements" },
|
|
56
|
-
{ id: "sessions", label: "Sessions" },
|
|
57
65
|
{ id: "topics", label: "Topics" },
|
|
58
|
-
{ id: "turns", label: "Turns" },
|
|
59
66
|
{ id: "maintenance", label: "Maintenance" },
|
|
67
|
+
{ id: "memory-map", label: "Memory Map" },
|
|
60
68
|
];
|
|
61
69
|
|
|
70
|
+
const advancedTabIds = new Set<TabId>(ADVANCED_TABS.map((t) => t.id));
|
|
71
|
+
|
|
62
72
|
export default function App(): React.ReactElement {
|
|
63
73
|
const [activeTab, setActiveTab] = useState<TabId>("overview");
|
|
64
74
|
const {
|
|
@@ -86,7 +96,13 @@ export default function App(): React.ReactElement {
|
|
|
86
96
|
{version && <span className="version-pill">{version}</span>}
|
|
87
97
|
</h1>
|
|
88
98
|
</header>
|
|
89
|
-
<TabBar
|
|
99
|
+
<TabBar
|
|
100
|
+
primaryTabs={PRIMARY_TABS}
|
|
101
|
+
advancedTabs={ADVANCED_TABS}
|
|
102
|
+
advancedTabIds={advancedTabIds}
|
|
103
|
+
active={activeTab}
|
|
104
|
+
onTabChange={setActiveTab}
|
|
105
|
+
/>
|
|
90
106
|
<main className="dashboard-content">
|
|
91
107
|
<React.Suspense fallback={<LoadingSpinner />}>
|
|
92
108
|
{activeTab === "overview" && (
|
|
@@ -106,8 +122,8 @@ export default function App(): React.ReactElement {
|
|
|
106
122
|
{activeTab === "achievements" && <AchievementsTab />}
|
|
107
123
|
{activeTab === "sessions" && <SessionsTab />}
|
|
108
124
|
{activeTab === "topics" && <TopicsTab />}
|
|
109
|
-
{activeTab === "
|
|
110
|
-
|
|
125
|
+
{activeTab === "maintenance" && <MaintenanceTab />}
|
|
126
|
+
{activeTab === "memory-map" && <MemoryMapTab />}
|
|
111
127
|
</React.Suspense>
|
|
112
128
|
</main>
|
|
113
129
|
</div>
|
|
@@ -40,8 +40,11 @@ import type {
|
|
|
40
40
|
MaintenanceAction,
|
|
41
41
|
MaintenanceActionResult,
|
|
42
42
|
ProviderCacheResponse,
|
|
43
|
+
CacheStripesResponse,
|
|
43
44
|
SetupStatusResponse,
|
|
44
45
|
SetupDetectResponse,
|
|
46
|
+
SetupConfigureRequest,
|
|
47
|
+
SetupConfigureResponse,
|
|
45
48
|
} from "@contracts";
|
|
46
49
|
|
|
47
50
|
/** Error thrown when a dashboard API response is not 2xx. */
|
|
@@ -198,6 +201,11 @@ export function fetchProviderCache(): Promise<ProviderCacheResponse> {
|
|
|
198
201
|
return getJson<ProviderCacheResponse>(ENDPOINTS.providerCache.path);
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
/** Cache stripe distribution + health score (A3). */
|
|
205
|
+
export function fetchCacheStripes(): Promise<CacheStripesResponse> {
|
|
206
|
+
return getJson<CacheStripesResponse>(ENDPOINTS.cacheStripes.path);
|
|
207
|
+
}
|
|
208
|
+
|
|
201
209
|
// ── S52: turn-by-turn memory tracking + recall + rewind ───────────────
|
|
202
210
|
|
|
203
211
|
export function fetchTurns(): Promise<TurnsResponse> {
|
|
@@ -287,3 +295,10 @@ export function fetchSetupStatus(): Promise<SetupStatusResponse> {
|
|
|
287
295
|
export function fetchSetupDetect(): Promise<SetupDetectResponse> {
|
|
288
296
|
return getJson<SetupDetectResponse>(ENDPOINTS.setupDetect.path);
|
|
289
297
|
}
|
|
298
|
+
|
|
299
|
+
/** POST /api/setup-configure — write embedder config to .mega-compact.env. */
|
|
300
|
+
export function configureEmbedder(
|
|
301
|
+
body: SetupConfigureRequest,
|
|
302
|
+
): Promise<SetupConfigureResponse> {
|
|
303
|
+
return postJson<SetupConfigureResponse>(ENDPOINTS.setupConfigure.path, body);
|
|
304
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/components/CacheHitRateTrendCard.tsx -- Cache hit-rate
|
|
3
|
+
* time-series trend chart (A3, PLAN_V2 Phase 4).
|
|
4
|
+
*
|
|
5
|
+
* Renders a sparkline view of cache hit rate from the /api/perf endpoint using
|
|
6
|
+
* real cache_hit_pct samples with timestamps. Each bar represents one sample.
|
|
7
|
+
* Shows the latest, average, and sample count.
|
|
8
|
+
*
|
|
9
|
+
* Uses .ov-sparkline / .ov-sparkline-bar CSS classes from cache.css.
|
|
10
|
+
* No external charting dependencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type React from "react";
|
|
14
|
+
import type { PerfResponse, CacheHitSample } from "@contracts";
|
|
15
|
+
|
|
16
|
+
export interface CacheHitRateTrendCardProps {
|
|
17
|
+
perf: PerfResponse;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fmtPct(n: number): string {
|
|
21
|
+
return `${n.toFixed(1)}%`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Map a percentage (0–100) to a CSS color class:
|
|
26
|
+
* >= 80: green (healthy)
|
|
27
|
+
* >= 60: teal (fair)
|
|
28
|
+
* >= 40: yellow (degraded)
|
|
29
|
+
* >= 20: orange (poor)
|
|
30
|
+
* < 20: red (critical)
|
|
31
|
+
*/
|
|
32
|
+
function pctColorClass(pct: number): string {
|
|
33
|
+
if (pct >= 80) return "ov-bar-green";
|
|
34
|
+
if (pct >= 60) return "ov-bar-teal";
|
|
35
|
+
if (pct >= 40) return "ov-bar-yellow";
|
|
36
|
+
if (pct >= 20) return "ov-bar-orange";
|
|
37
|
+
return "ov-bar-red";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const CacheHitRateTrendCard: React.FC<CacheHitRateTrendCardProps> = ({
|
|
41
|
+
perf,
|
|
42
|
+
}) => {
|
|
43
|
+
const { cache_hit_pct: c } = perf;
|
|
44
|
+
const samples: CacheHitSample[] =
|
|
45
|
+
c.samples && c.samples.length > 0 ? c.samples : [];
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="ov-card cache-trend-card">
|
|
49
|
+
<h3 className="ov-card-title">Cache Hit-Rate Trend</h3>
|
|
50
|
+
|
|
51
|
+
{/* Metric summary */}
|
|
52
|
+
<div className="ov-perf-summary">
|
|
53
|
+
<div className="ov-perf-stat">
|
|
54
|
+
Avg: <strong>{fmtPct(c.avg)}</strong>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="ov-perf-stat">
|
|
57
|
+
Latest: <strong>{fmtPct(c.latest)}</strong>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="ov-perf-stat">
|
|
60
|
+
Samples: <strong>{c.n}</strong>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Sparkline bars from real samples */}
|
|
65
|
+
{samples.length > 0 ? (
|
|
66
|
+
<>
|
|
67
|
+
<div
|
|
68
|
+
className="ov-sparkline"
|
|
69
|
+
role="img"
|
|
70
|
+
aria-label={`Cache hit rate trend: ${c.n} samples, average ${fmtPct(c.avg)}, latest ${fmtPct(c.latest)}`}
|
|
71
|
+
>
|
|
72
|
+
{samples.map((s, i) => {
|
|
73
|
+
const barH = Math.max(2, Math.round(s.pct * 0.6));
|
|
74
|
+
const colorClass = pctColorClass(s.pct);
|
|
75
|
+
const isLatest = i === samples.length - 1;
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
key={i}
|
|
79
|
+
className={`ov-sparkline-bar ${colorClass}${isLatest ? " ov-bar-latest" : ""}`}
|
|
80
|
+
style={{ height: `${barH}px` }}
|
|
81
|
+
title={`${fmtPct(s.pct)} at ${new Date(s.ts).toLocaleTimeString()}`}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
<div className="ov-card-footer">
|
|
87
|
+
{samples.length} sample{samples.length !== 1 ? "s" : ""} over{" "}
|
|
88
|
+
{samples.length > 1
|
|
89
|
+
? `${Math.round(
|
|
90
|
+
(samples[samples.length - 1].ts - samples[0].ts) / 1000,
|
|
91
|
+
)}s`
|
|
92
|
+
: "latest turn"}
|
|
93
|
+
</div>
|
|
94
|
+
</>
|
|
95
|
+
) : (
|
|
96
|
+
<div className="ov-empty-state">No cache-hit samples yet</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/components/StripeDistributionCard.tsx — Cache stripe
|
|
3
|
+
* distribution visualization (A3, PLAN_V2 Phase 4).
|
|
4
|
+
*
|
|
5
|
+
* Renders a horizontal bar chart of stripe buckets (0–4) with stability
|
|
6
|
+
* indicators, plus a health-score summary box.
|
|
7
|
+
*
|
|
8
|
+
* Uses only CSS-based bar rendering — no external charting dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import type { CacheStripesResponse } from "@contracts";
|
|
13
|
+
import { fmtPctFromFraction } from "../utils/format";
|
|
14
|
+
|
|
15
|
+
const STRIPE_COLORS = [
|
|
16
|
+
"ov-bar-green", // 0 permanent
|
|
17
|
+
"ov-bar-blue", // 1 epoch
|
|
18
|
+
"ov-bar-teal", // 2 topic
|
|
19
|
+
"ov-bar-yellow", // 3 thread
|
|
20
|
+
"ov-bar-gray", // 4 volatile
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const STRIPE_LABELS = [
|
|
24
|
+
"Permanent",
|
|
25
|
+
"Epoch",
|
|
26
|
+
"Topic",
|
|
27
|
+
"Thread",
|
|
28
|
+
"Volatile",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const HEALTH_LABEL_COLOR: Record<string, string> = {
|
|
32
|
+
good: "ov-text-green",
|
|
33
|
+
fair: "ov-text-yellow",
|
|
34
|
+
degraded: "ov-text-orange",
|
|
35
|
+
poor: "ov-text-red",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface StripeDistributionCardProps {
|
|
39
|
+
data: CacheStripesResponse;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function StripeDistributionCard({
|
|
43
|
+
data,
|
|
44
|
+
}: StripeDistributionCardProps): React.ReactElement {
|
|
45
|
+
const maxCount = Math.max(...data.buckets.map((b) => b.count), 1);
|
|
46
|
+
const healthColor = HEALTH_LABEL_COLOR[data.health.label] ?? "ov-text-gray";
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="ov-card">
|
|
50
|
+
<h3 className="ov-card-title">Cache Stripe Distribution</h3>
|
|
51
|
+
|
|
52
|
+
{/* Health score summary */}
|
|
53
|
+
<div className="ov-stripe-health-summary">
|
|
54
|
+
<span className={`ov-stripe-health-badge ${healthColor}`}>
|
|
55
|
+
{data.health.label}
|
|
56
|
+
</span>
|
|
57
|
+
<span className="ov-stripe-health-score">
|
|
58
|
+
{fmtPctFromFraction(data.health.score)}
|
|
59
|
+
</span>
|
|
60
|
+
<span className="ov-stripe-churn">
|
|
61
|
+
Churn: {fmtPctFromFraction(data.health.churnRate)}
|
|
62
|
+
</span>
|
|
63
|
+
<span className="ov-stripe-total">
|
|
64
|
+
{data.totalChunks} total chunks
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Bar chart — one row per stripe */}
|
|
69
|
+
<div className="ov-stripe-bars">
|
|
70
|
+
{data.buckets.map((bucket) => {
|
|
71
|
+
const pct =
|
|
72
|
+
maxCount > 0
|
|
73
|
+
? Math.max(0, Math.min(100, (bucket.count / maxCount) * 100))
|
|
74
|
+
: 0;
|
|
75
|
+
const colorClass =
|
|
76
|
+
STRIPE_COLORS[bucket.stripe] ?? "ov-bar-gray";
|
|
77
|
+
const label =
|
|
78
|
+
bucket.label || (STRIPE_LABELS[bucket.stripe] ?? `Stripe ${bucket.stripe}`);
|
|
79
|
+
return (
|
|
80
|
+
<div key={bucket.stripe} className="ov-stripe-row">
|
|
81
|
+
<span className="ov-stripe-label" title={label}>
|
|
82
|
+
{label}
|
|
83
|
+
</span>
|
|
84
|
+
<div className="ov-bar-track">
|
|
85
|
+
<div
|
|
86
|
+
className={`ov-bar-fill ${colorClass}`}
|
|
87
|
+
style={{ width: `${pct}%` }}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<span className="ov-stripe-count">{bucket.count}</span>
|
|
91
|
+
<span
|
|
92
|
+
className="ov-stripe-stability"
|
|
93
|
+
title={`min ${fmtPctFromFraction(bucket.minStability)} / max ${fmtPctFromFraction(bucket.maxStability)}`}
|
|
94
|
+
>
|
|
95
|
+
{fmtPctFromFraction(bucket.avgStability)}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Epoch info */}
|
|
103
|
+
<div className="ov-stripe-footer">
|
|
104
|
+
Epoch: {data.epochId ?? "—"}
|
|
105
|
+
·
|
|
106
|
+
Dominant tier: {fmtPctFromFraction(data.health.dominantTier)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dashboard-client/src/components/TabBar.tsx — tab navigation.
|
|
2
|
+
* dashboard-client/src/components/TabBar.tsx — tab navigation with Advanced collapse.
|
|
3
|
+
*
|
|
4
|
+
* PRIMARY_TABS are always visible. The remaining tabs collapse behind an
|
|
5
|
+
* "Advanced" toggle button.
|
|
3
6
|
*/
|
|
4
7
|
|
|
8
|
+
import React, { useState } from "react";
|
|
5
9
|
import type { TabId } from "../App";
|
|
6
10
|
|
|
7
11
|
export interface TabBarProps {
|
|
8
|
-
|
|
12
|
+
primaryTabs: Array<{ id: TabId; label: string }>;
|
|
13
|
+
advancedTabs: Array<{ id: TabId; label: string }>;
|
|
14
|
+
advancedTabIds: Set<TabId>;
|
|
9
15
|
active: TabId;
|
|
10
16
|
onTabChange: (id: TabId) => void;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export function TabBar({
|
|
14
|
-
|
|
20
|
+
primaryTabs,
|
|
21
|
+
advancedTabs,
|
|
22
|
+
advancedTabIds,
|
|
15
23
|
active,
|
|
16
24
|
onTabChange,
|
|
17
25
|
}: TabBarProps): React.ReactElement {
|
|
26
|
+
const [advancedOpen, setAdvancedOpen] = useState(
|
|
27
|
+
() => advancedTabIds.has(active), // start open if an advanced tab is active
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const isAdvancedActive = advancedTabIds.has(active);
|
|
31
|
+
|
|
18
32
|
return (
|
|
19
33
|
<nav className="tab-bar" role="tablist">
|
|
20
|
-
{
|
|
34
|
+
{primaryTabs.map((tab) => (
|
|
21
35
|
<button
|
|
22
36
|
key={tab.id}
|
|
23
37
|
type="button"
|
|
@@ -29,6 +43,37 @@ export function TabBar({
|
|
|
29
43
|
{tab.label}
|
|
30
44
|
</button>
|
|
31
45
|
))}
|
|
46
|
+
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
className="advanced-toggle"
|
|
50
|
+
aria-expanded={advancedOpen}
|
|
51
|
+
onClick={() => setAdvancedOpen((o) => !o)}
|
|
52
|
+
>
|
|
53
|
+
Advanced {advancedOpen ? "▲" : "▼"}
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
{advancedOpen && (
|
|
57
|
+
<div className="advanced-tabs">
|
|
58
|
+
{advancedTabs.map((tab) => (
|
|
59
|
+
<button
|
|
60
|
+
key={tab.id}
|
|
61
|
+
type="button"
|
|
62
|
+
role="tab"
|
|
63
|
+
aria-selected={active === tab.id}
|
|
64
|
+
className={
|
|
65
|
+
active === tab.id ? "active" : ""
|
|
66
|
+
}
|
|
67
|
+
onClick={() => {
|
|
68
|
+
onTabChange(tab.id);
|
|
69
|
+
// Keep the panel open so users can browse.
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{tab.label}
|
|
73
|
+
</button>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
32
77
|
</nav>
|
|
33
78
|
);
|
|
34
79
|
}
|
|
@@ -93,6 +93,36 @@ body {
|
|
|
93
93
|
color: var(--text);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/* Advanced toggle — visually same as a tab button but toggles open/closed */
|
|
97
|
+
.tab-bar .advanced-toggle {
|
|
98
|
+
background: none;
|
|
99
|
+
border: none;
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
padding: 8px 14px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
font-size: clamp(12px, 0.85vw, 14px);
|
|
104
|
+
border-bottom: 2px solid transparent;
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
margin-left: 6px;
|
|
107
|
+
}
|
|
108
|
+
.tab-bar .advanced-toggle:hover {
|
|
109
|
+
color: var(--text);
|
|
110
|
+
}
|
|
111
|
+
.tab-bar .advanced-toggle[aria-expanded="true"] {
|
|
112
|
+
color: var(--text);
|
|
113
|
+
border-bottom-color: var(--border);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Wrapper for the advanced-tab row — appears underneath on a fresh line */
|
|
117
|
+
.tab-bar .advanced-tabs {
|
|
118
|
+
display: flex;
|
|
119
|
+
gap: 4px;
|
|
120
|
+
flex-basis: 100%;
|
|
121
|
+
border-top: 1px dashed var(--border);
|
|
122
|
+
padding-top: 6px;
|
|
123
|
+
margin-top: -1px;
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
.dashboard-content {
|
|
97
127
|
min-height: 400px;
|
|
98
128
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/styles/cache.css — Cache tab styles (B.3 + A3).
|
|
3
|
+
*
|
|
4
|
+
* Stripe distribution bars, sparkline trend chart, health badge, and
|
|
5
|
+
* per-cache-section layout. All colors use CSS variables from base.css.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* ─── Stripe Distribution Card ─────────────────────────────────────────── */
|
|
9
|
+
|
|
10
|
+
.ov-stripe-health-summary {
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: 12px;
|
|
13
|
+
align-items: center;
|
|
14
|
+
margin-bottom: 12px;
|
|
15
|
+
font-size: clamp(11px, 0.85vw, 14px);
|
|
16
|
+
flex-wrap: wrap;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ov-stripe-health-badge {
|
|
20
|
+
display: inline-block;
|
|
21
|
+
padding: 2px 10px;
|
|
22
|
+
border-radius: 10px;
|
|
23
|
+
background: var(--panel);
|
|
24
|
+
border: 1px solid var(--border);
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
text-transform: capitalize;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ov-text-green { color: var(--ok); }
|
|
30
|
+
.ov-text-yellow { color: var(--warn); }
|
|
31
|
+
.ov-text-orange { color: #d4761e; }
|
|
32
|
+
.ov-text-red { color: var(--err); }
|
|
33
|
+
.ov-text-gray { color: var(--muted); }
|
|
34
|
+
|
|
35
|
+
.ov-stripe-health-score,
|
|
36
|
+
.ov-stripe-churn,
|
|
37
|
+
.ov-stripe-total {
|
|
38
|
+
color: var(--muted);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Bar chart rows */
|
|
42
|
+
.ov-stripe-bars {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: 6px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ov-stripe-row {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: 80px 1fr 40px 48px;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
font-size: clamp(11px, 0.8vw, 13px);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.ov-stripe-label {
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
text-overflow: ellipsis;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
color: var(--text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ov-bar-track {
|
|
64
|
+
height: 18px;
|
|
65
|
+
background: var(--bg);
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
position: relative;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ov-bar-fill {
|
|
72
|
+
height: 100%;
|
|
73
|
+
border-radius: 4px;
|
|
74
|
+
transition: width 0.5s ease;
|
|
75
|
+
min-width: 2px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ov-bar-green { background: var(--ok); }
|
|
79
|
+
.ov-bar-blue { background: var(--accent); }
|
|
80
|
+
.ov-bar-teal { background: #3d9cb0; }
|
|
81
|
+
.ov-bar-yellow { background: var(--warn); }
|
|
82
|
+
.ov-bar-gray { background: var(--muted); }
|
|
83
|
+
.ov-bar-red { background: var(--err); }
|
|
84
|
+
.ov-bar-orange { background: #d4761e; }
|
|
85
|
+
|
|
86
|
+
.ov-bar-latest {
|
|
87
|
+
box-shadow: 0 0 6px rgba(255, 255, 255, 0.5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.ov-stripe-count {
|
|
91
|
+
text-align: right;
|
|
92
|
+
color: var(--text);
|
|
93
|
+
font-variant-numeric: tabular-nums;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.ov-stripe-stability {
|
|
97
|
+
color: var(--muted);
|
|
98
|
+
font-size: clamp(10px, 0.75vw, 12px);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ov-stripe-footer {
|
|
102
|
+
margin-top: 10px;
|
|
103
|
+
font-size: clamp(10px, 0.75vw, 12px);
|
|
104
|
+
color: var(--muted);
|
|
105
|
+
display: flex;
|
|
106
|
+
gap: 8px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ─── Hit-Rate Trend Card ──────────────────────────────────────────────── */
|
|
110
|
+
|
|
111
|
+
.ov-perf-summary {
|
|
112
|
+
display: flex;
|
|
113
|
+
gap: 14px;
|
|
114
|
+
align-items: center;
|
|
115
|
+
margin-bottom: 10px;
|
|
116
|
+
font-size: clamp(11px, 0.85vw, 14px);
|
|
117
|
+
flex-wrap: wrap;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ov-perf-stat strong {
|
|
121
|
+
font-variant-numeric: tabular-nums;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Sparkline bars */
|
|
125
|
+
.ov-sparkline {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: flex-end;
|
|
128
|
+
gap: 3px;
|
|
129
|
+
height: 60px;
|
|
130
|
+
padding: 4px 2px;
|
|
131
|
+
margin-bottom: 6px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ov-sparkline-bar {
|
|
135
|
+
flex: 1;
|
|
136
|
+
min-width: 4px;
|
|
137
|
+
border-radius: 2px 2px 0 0;
|
|
138
|
+
transition: height 0.4s ease;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.ov-empty-state {
|
|
142
|
+
font-size: clamp(11px, 0.8vw, 13px);
|
|
143
|
+
color: var(--muted);
|
|
144
|
+
padding: 16px 0;
|
|
145
|
+
text-align: center;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.ov-card-footer {
|
|
149
|
+
font-size: clamp(10px, 0.75vw, 12px);
|
|
150
|
+
color: var(--muted);
|
|
151
|
+
margin-top: 4px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ─── Overrides for card container ─────────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
.ov-card {
|
|
157
|
+
background: var(--panel);
|
|
158
|
+
border: 1px solid var(--border);
|
|
159
|
+
border-radius: 8px;
|
|
160
|
+
padding: clamp(8px, 1.5vw, 16px);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.ov-card-title {
|
|
164
|
+
margin: 0 0 10px;
|
|
165
|
+
font-size: clamp(13px, 0.9vw, 15px);
|
|
166
|
+
font-weight: 600;
|
|
167
|
+
}
|