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.
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +28 -1
- package/dist/extensions/dashboard-server/api-contracts/maintenance.js +7 -0
- 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 +293 -0
- package/dist/extensions/dashboard-server/routes-repo.js +24 -0
- package/dist/extensions/dashboard-server/routes.js +2 -0
- package/dist/extensions/dashboard-server/server.js +5 -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/maintenance.js +20 -1
- 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-DZp_xQOW.js → AchievementTiles-vEo2MtE2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-DZp_xQOW.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-BzFHpAHM.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-DQ4FG33J.js → ConfigTab-Cm6ymbmx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DQ4FG33J.js.map → ConfigTab-Cm6ymbmx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-D5HoEVYo.js → EventsTab-DXWl_YzI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-D5HoEVYo.js.map → EventsTab-DXWl_YzI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-Jzsc4yeY.js → GameTab-BLDudcO5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-Jzsc4yeY.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-D_XnsEYX.js → MetricsTab-Cq4u-rZF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-D_XnsEYX.js.map → MetricsTab-Cq4u-rZF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BMREvAth.js → OverviewTab-D4kGvsiQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BMREvAth.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-mz_vz8i2.js → SessionsTab-DZC2-GCn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-mz_vz8i2.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-DKuct4oD.js → TopicsTab-01G71hv1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DKuct4oD.js.map → TopicsTab-01G71hv1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-11MGLxY8.js → TurnsTab-DLRg0LZT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-11MGLxY8.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-CqLN7STl.js → index-BX0bcgLu.js} +10 -10
- package/extensions/dashboard-client/dist/assets/{index-CqLN7STl.js.map → index-BX0bcgLu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-CXrQSbVY.js → useSSE-BBYt5UPB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-CXrQSbVY.js.map → useSSE-BBYt5UPB.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +5 -1
- package/extensions/dashboard-client/src/api/client.ts +29 -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 +458 -0
- package/extensions/dashboard-client/src/utils/format.ts +42 -7
- package/extensions/dashboard-server/api-contracts/endpoints.ts +54 -1
- package/extensions/dashboard-server/api-contracts/index.ts +16 -0
- package/extensions/dashboard-server/api-contracts/maintenance.ts +84 -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/api-contracts.ts +9 -0
- 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 +352 -0
- package/extensions/dashboard-server/routes-repo.ts +31 -4
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +4 -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/maintenance.ts +20 -1
- 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-BzFHpAHM.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-DEQbTq2T.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-DEQbTq2T.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-DuyhOktG.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-DuyhOktG.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-B8AIjNTY.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-B8AIjNTY.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-gn66lYD2.css +0 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-cache.ts — Provider prompt-cache stats route handler.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/provider-cache — Lifetime provider prompt cache hit-rate aggregates
|
|
5
|
+
* plus dollar savings estimate (priced from latest model snapshot).
|
|
6
|
+
*
|
|
7
|
+
* Guardrails: PREVENT-PI-004 (loopback-only), PREVENT-001 (null-safe JSON parse),
|
|
8
|
+
* PREVENT-002 (parameterized SQL). Loopback-only localhost dashboard endpoint.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
13
|
+
|
|
14
|
+
import type { RouteContext } from "./routes-core.js";
|
|
15
|
+
import { computeCacheSavings } from "../../src/pricing.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// handleProviderCache — "/api/provider-cache"
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export function handleProviderCache(
|
|
22
|
+
req: IncomingMessage,
|
|
23
|
+
res: ServerResponse,
|
|
24
|
+
ctx: RouteContext,
|
|
25
|
+
): boolean {
|
|
26
|
+
if (!req.url?.startsWith("/api/provider-cache")) return false;
|
|
27
|
+
|
|
28
|
+
const { stateDir } = ctx;
|
|
29
|
+
|
|
30
|
+
// /api/provider-cache — GET returns lifetime provider prompt cache aggregates
|
|
31
|
+
// from perf_samples (cache_hit_pct rows with meta JSON) plus an optional
|
|
32
|
+
// dollar savings estimate priced from the latest model snapshot. The dashboard
|
|
33
|
+
// server is a detached child with no MegaRuntime ref, so it reads via a
|
|
34
|
+
// require()'d sqlite helper (same pattern as /api/perf). Non-GET → 405.
|
|
35
|
+
// PREVENT-PI-004: loopback.
|
|
36
|
+
const pfReq = createRequire(import.meta.url);
|
|
37
|
+
const { readProviderCacheLifetime, latestModelSnapshot } = pfReq(
|
|
38
|
+
"../../src/store/sqlite.js",
|
|
39
|
+
) as typeof import("../../src/store/sqlite.js");
|
|
40
|
+
if (req.method !== "GET") {
|
|
41
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
42
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
43
|
+
res.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const lifetime = readProviderCacheLifetime(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
48
|
+
|
|
49
|
+
// D3: compute dollar savings from the latest model snapshot. Null-safe:
|
|
50
|
+
// when no snapshot or zero input rate, savings stays null (UI shows "—").
|
|
51
|
+
let savings: {
|
|
52
|
+
cacheReadSaved: number;
|
|
53
|
+
cacheWriteCost: number;
|
|
54
|
+
netSaved: number;
|
|
55
|
+
model: string;
|
|
56
|
+
inputRate: number;
|
|
57
|
+
} | null = null;
|
|
58
|
+
try {
|
|
59
|
+
const snap = latestModelSnapshot(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
60
|
+
if (snap && snap.inputRate > 0) {
|
|
61
|
+
const s = computeCacheSavings(
|
|
62
|
+
lifetime.totalCacheRead,
|
|
63
|
+
lifetime.totalCacheWrite,
|
|
64
|
+
snap.inputRate,
|
|
65
|
+
);
|
|
66
|
+
savings = {
|
|
67
|
+
cacheReadSaved: s.cacheReadSaved,
|
|
68
|
+
cacheWriteCost: s.cacheWriteCost,
|
|
69
|
+
netSaved: s.netSaved,
|
|
70
|
+
model: snap.modelName ?? snap.modelId ?? "unknown",
|
|
71
|
+
inputRate: snap.inputRate,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
/* pricing unavailable — savings stays null */
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const body: {
|
|
79
|
+
cache: {
|
|
80
|
+
avgHitPct: number;
|
|
81
|
+
turnCount: number;
|
|
82
|
+
totalCacheRead: number;
|
|
83
|
+
totalCacheWrite: number;
|
|
84
|
+
totalInput: number;
|
|
85
|
+
firstTurnAt: string | null;
|
|
86
|
+
latestTurnAt: string | null;
|
|
87
|
+
};
|
|
88
|
+
savings: {
|
|
89
|
+
cacheReadSaved: number;
|
|
90
|
+
cacheWriteCost: number;
|
|
91
|
+
netSaved: number;
|
|
92
|
+
model: string;
|
|
93
|
+
inputRate: number;
|
|
94
|
+
} | null;
|
|
95
|
+
updatedAt: string;
|
|
96
|
+
} = {
|
|
97
|
+
cache: {
|
|
98
|
+
avgHitPct: lifetime.avgHitPct,
|
|
99
|
+
turnCount: lifetime.sampleCount,
|
|
100
|
+
totalCacheRead: lifetime.totalCacheRead,
|
|
101
|
+
totalCacheWrite: lifetime.totalCacheWrite,
|
|
102
|
+
totalInput: lifetime.totalInput,
|
|
103
|
+
firstTurnAt: lifetime.firstSampleAt,
|
|
104
|
+
latestTurnAt: lifetime.latestSampleAt,
|
|
105
|
+
},
|
|
106
|
+
savings,
|
|
107
|
+
updatedAt: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
110
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
111
|
+
res.end(JSON.stringify(body));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
|
|
114
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
115
|
+
res.end(
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
error: "provider_cache_unavailable",
|
|
118
|
+
detail: String(e),
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-maintenance.ts — S49B Maintenance tab API routes.
|
|
3
|
+
*
|
|
4
|
+
* Handlers for:
|
|
5
|
+
* GET /api/maintenance — DB stats (table row counts + storage)
|
|
6
|
+
* GET /api/maintenance/schema-health — SCHEMA_VERSION + integrity audit
|
|
7
|
+
* POST /api/maintenance/action — Trigger a maintenance action
|
|
8
|
+
*
|
|
9
|
+
* All pi-agnostic; lazy-requires maintenance primitives to avoid a top-level
|
|
10
|
+
* import of the store from the dashboard server process. PREVENT-PI-004: local
|
|
11
|
+
* SQLite only; zero network calls.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
|
+
|
|
17
|
+
import type { RouteContext } from "./routes-core.js";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Lazy imports (via createRequire)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const lazyMaintenance = (() => {
|
|
24
|
+
let mod: typeof import("../../src/store/sqlite/maintenance.js") | null = null;
|
|
25
|
+
return () => {
|
|
26
|
+
if (!mod)
|
|
27
|
+
mod = createRequire(import.meta.url)(
|
|
28
|
+
"../../src/store/sqlite/maintenance.js",
|
|
29
|
+
) as typeof import("../../src/store/sqlite/maintenance.js");
|
|
30
|
+
return mod;
|
|
31
|
+
};
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
const lazyStoreUtils = (() => {
|
|
35
|
+
let mod: typeof import("../../src/store/sqlite/utils.js") | null = null;
|
|
36
|
+
return () => {
|
|
37
|
+
if (!mod)
|
|
38
|
+
mod = createRequire(import.meta.url)(
|
|
39
|
+
"../../src/store/sqlite/utils.js",
|
|
40
|
+
) as typeof import("../../src/store/sqlite/utils.js");
|
|
41
|
+
return mod;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Types for JSON deserialisation
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
interface MaintenanceActionBody {
|
|
50
|
+
action: string;
|
|
51
|
+
daysOld?: number | string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// GET /api/maintenance and sub-paths
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export function handleMaintenance(
|
|
59
|
+
req: IncomingMessage,
|
|
60
|
+
res: ServerResponse,
|
|
61
|
+
ctx: RouteContext,
|
|
62
|
+
): boolean {
|
|
63
|
+
const url = req.url ?? "";
|
|
64
|
+
if (!url.startsWith("/api/maintenance")) return false;
|
|
65
|
+
|
|
66
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
67
|
+
|
|
68
|
+
// ── GET /api/maintenance/schema-health ────────────────────────────
|
|
69
|
+
if (method === "GET" && url === "/api/maintenance/schema-health") {
|
|
70
|
+
sendSchemaHealth(res, ctx);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── POST /api/maintenance/action ──────────────────────────────────
|
|
75
|
+
if (method === "POST" && url === "/api/maintenance/action") {
|
|
76
|
+
handleMaintenanceAction(req, res, ctx);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── GET /api/maintenance ──────────────────────────────────────────
|
|
81
|
+
if (method === "GET" && url === "/api/maintenance") {
|
|
82
|
+
sendDbStats(res, ctx);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Unknown sub-path ──────────────────────────────────────────────
|
|
87
|
+
sendJson(res, 404, { error: "not found" });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// GET /api/maintenance — DB stats
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function sendDbStats(res: ServerResponse, ctx: RouteContext): void {
|
|
96
|
+
try {
|
|
97
|
+
const { getDbStats } = lazyMaintenance();
|
|
98
|
+
const raw = getDbStats(ctx.stateDir);
|
|
99
|
+
|
|
100
|
+
const tables = Object.entries(raw.tableCounts).map(([table, rowCount]) => ({
|
|
101
|
+
table,
|
|
102
|
+
rowCount,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
sendJson(res, 200, {
|
|
106
|
+
type: "db-stats" as const,
|
|
107
|
+
tables,
|
|
108
|
+
storage: {
|
|
109
|
+
files: {
|
|
110
|
+
dbBytes: raw.dbBytes,
|
|
111
|
+
walBytes: raw.walBytes,
|
|
112
|
+
shmBytes: raw.shmBytes,
|
|
113
|
+
},
|
|
114
|
+
pageSize: raw.pageSize,
|
|
115
|
+
pageCount: raw.pageCount,
|
|
116
|
+
freelistPages: raw.freelistPages,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
sendJson(res, 500, { error: (err as Error).message });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// GET /api/maintenance/schema-health — schema version + integrity audit
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
function sendSchemaHealth(res: ServerResponse, ctx: RouteContext): void {
|
|
129
|
+
try {
|
|
130
|
+
const { integrityCheck } = lazyMaintenance();
|
|
131
|
+
const integrity = integrityCheck(ctx.stateDir);
|
|
132
|
+
|
|
133
|
+
const { openStore } = lazyStoreUtils();
|
|
134
|
+
const db = openStore(ctx.stateDir);
|
|
135
|
+
|
|
136
|
+
// SCHEMA_VERSION from meta table
|
|
137
|
+
const metaRow = db
|
|
138
|
+
.prepare("SELECT value FROM meta WHERE key = ?")
|
|
139
|
+
.get("schema_version") as { value: string } | undefined;
|
|
140
|
+
const schemaVersion = metaRow ? Number(metaRow.value) : 0;
|
|
141
|
+
|
|
142
|
+
// PRAGMA foreign_key_check
|
|
143
|
+
const fkRows = db.prepare("PRAGMA foreign_key_check").all() as Array<
|
|
144
|
+
Record<string, unknown>
|
|
145
|
+
>;
|
|
146
|
+
const fkCheck = fkRows.map((r) => JSON.stringify(r));
|
|
147
|
+
|
|
148
|
+
// Per-column audit: every ensureColumn migration column declared in
|
|
149
|
+
// schema.ts, checked via PRAGMA table_info (read-only).
|
|
150
|
+
const expectedColumns: Array<{
|
|
151
|
+
table: string;
|
|
152
|
+
column: string;
|
|
153
|
+
decl: string;
|
|
154
|
+
}> = [
|
|
155
|
+
{
|
|
156
|
+
table: "context_chunks",
|
|
157
|
+
column: "original_token_estimate",
|
|
158
|
+
decl: "INTEGER",
|
|
159
|
+
},
|
|
160
|
+
{ table: "raw_transcript", column: "content_ref", decl: "TEXT" },
|
|
161
|
+
{ table: "memories", column: "category", decl: "TEXT" },
|
|
162
|
+
{ table: "memories", column: "target", decl: "TEXT" },
|
|
163
|
+
{ table: "memories", column: "last_referenced", decl: "INTEGER" },
|
|
164
|
+
{ table: "memories", column: "source_turn", decl: "INTEGER" },
|
|
165
|
+
{ table: "raptor_nodes", column: "built_at", decl: "INTEGER" },
|
|
166
|
+
{ table: "raw_transcript", column: "turn_index", decl: "INTEGER" },
|
|
167
|
+
{ table: "session_state", column: "conversation_id", decl: "TEXT" },
|
|
168
|
+
{ table: "session_state", column: "last_turn_id", decl: "INTEGER" },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const columns = expectedColumns.map((ec) => {
|
|
172
|
+
const cols = db.prepare(`PRAGMA table_info(${ec.table})`).all() as Array<{
|
|
173
|
+
name: string;
|
|
174
|
+
}>;
|
|
175
|
+
const present = cols.some((c) => c.name === ec.column);
|
|
176
|
+
return {
|
|
177
|
+
table: ec.table,
|
|
178
|
+
column: ec.column,
|
|
179
|
+
present,
|
|
180
|
+
expectedDecl: ec.decl,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const healthy =
|
|
185
|
+
integrity.length === 1 &&
|
|
186
|
+
integrity[0] === "ok" &&
|
|
187
|
+
columns.every((c) => c.present);
|
|
188
|
+
|
|
189
|
+
sendJson(res, 200, {
|
|
190
|
+
type: "schema-health" as const,
|
|
191
|
+
schemaVersion,
|
|
192
|
+
integrity,
|
|
193
|
+
fkCheck,
|
|
194
|
+
columns,
|
|
195
|
+
healthy,
|
|
196
|
+
});
|
|
197
|
+
} catch (err) {
|
|
198
|
+
sendJson(res, 500, { error: (err as Error).message });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// POST /api/maintenance/action — Trigger a maintenance action
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
function handleMaintenanceAction(
|
|
207
|
+
req: IncomingMessage,
|
|
208
|
+
res: ServerResponse,
|
|
209
|
+
ctx: RouteContext,
|
|
210
|
+
): void {
|
|
211
|
+
let body = "";
|
|
212
|
+
req.setEncoding("utf8");
|
|
213
|
+
req.on("data", (chunk: string) => {
|
|
214
|
+
body += chunk;
|
|
215
|
+
if (body.length > 10_000) req.destroy();
|
|
216
|
+
});
|
|
217
|
+
req.on("end", () => {
|
|
218
|
+
let payload: MaintenanceActionBody;
|
|
219
|
+
try {
|
|
220
|
+
payload = JSON.parse(body) as MaintenanceActionBody;
|
|
221
|
+
if (!payload || !payload.action) {
|
|
222
|
+
sendJson(res, 400, { error: "missing 'action' field" });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
sendJson(res, 400, { error: "invalid JSON" });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const m = lazyMaintenance();
|
|
232
|
+
const u = lazyStoreUtils();
|
|
233
|
+
// All destructive actions: temp backup first
|
|
234
|
+
if (payload.action !== "integrity-check") {
|
|
235
|
+
// Non-blocking — backup failure is non-fatal
|
|
236
|
+
m.backupDb(ctx.stateDir);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
switch (payload.action) {
|
|
240
|
+
case "vacuum": {
|
|
241
|
+
const r = m.vacuumDb(ctx.stateDir);
|
|
242
|
+
sendJson(res, 200, {
|
|
243
|
+
operation: "vacuum",
|
|
244
|
+
success: true,
|
|
245
|
+
affected: r.affected,
|
|
246
|
+
reclaimedBytes: r.reclaimedBytes,
|
|
247
|
+
summary: r.summary,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
case "checkpoint": {
|
|
252
|
+
const r = m.checkpointWal(ctx.stateDir);
|
|
253
|
+
sendJson(res, 200, {
|
|
254
|
+
operation: "checkpoint",
|
|
255
|
+
success: true,
|
|
256
|
+
affected: r.affected,
|
|
257
|
+
reclaimedBytes: r.reclaimedBytes,
|
|
258
|
+
summary: r.summary,
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
case "reindex": {
|
|
263
|
+
const db = u.openStore(ctx.stateDir);
|
|
264
|
+
db.exec("REINDEX");
|
|
265
|
+
sendJson(res, 200, {
|
|
266
|
+
operation: "reindex",
|
|
267
|
+
success: true,
|
|
268
|
+
affected: 0,
|
|
269
|
+
reclaimedBytes: 0,
|
|
270
|
+
summary: "REINDEX completed",
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
case "fts5-rebuild": {
|
|
275
|
+
const db = u.openStore(ctx.stateDir);
|
|
276
|
+
db.exec(
|
|
277
|
+
"INSERT INTO context_chunks_trgm(context_chunks_trgm) VALUES('rebuild')",
|
|
278
|
+
);
|
|
279
|
+
sendJson(res, 200, {
|
|
280
|
+
operation: "fts5-rebuild",
|
|
281
|
+
success: true,
|
|
282
|
+
affected: 0,
|
|
283
|
+
reclaimedBytes: 0,
|
|
284
|
+
summary: "FTS5 rebuild triggered",
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
case "reconcile-dedup": {
|
|
289
|
+
const r = m.reconcileDedupMirror(ctx.stateDir);
|
|
290
|
+
const lines: string[] = [];
|
|
291
|
+
if (r.fixedRefCount)
|
|
292
|
+
lines.push(`${r.fixedRefCount} ref_counts corrected`);
|
|
293
|
+
if (r.orphansDeleted)
|
|
294
|
+
lines.push(`${r.orphansDeleted} orphans deleted`);
|
|
295
|
+
if (r.refsBackfilled)
|
|
296
|
+
lines.push(`${r.refsBackfilled} refs backfilled`);
|
|
297
|
+
sendJson(res, 200, {
|
|
298
|
+
operation: "reconcile-dedup",
|
|
299
|
+
success: true,
|
|
300
|
+
affected: r.fixedRefCount + r.orphansDeleted + r.refsBackfilled,
|
|
301
|
+
reclaimedBytes: 0,
|
|
302
|
+
summary: lines.length ? lines.join(", ") : "nothing to fix",
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
case "prune": {
|
|
307
|
+
const days = Number(payload.daysOld) || 30;
|
|
308
|
+
const r = m.pruneOldRows(ctx.stateDir, days);
|
|
309
|
+
sendJson(res, 200, {
|
|
310
|
+
operation: "prune",
|
|
311
|
+
success: true,
|
|
312
|
+
affected: r.affected,
|
|
313
|
+
reclaimedBytes: r.reclaimedBytes,
|
|
314
|
+
summary: r.summary,
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
case "integrity-check": {
|
|
319
|
+
const lines = m.integrityCheck(ctx.stateDir);
|
|
320
|
+
sendJson(res, 200, {
|
|
321
|
+
operation: "integrity-check",
|
|
322
|
+
success: lines.length === 1 && lines[0] === "ok",
|
|
323
|
+
affected: 0,
|
|
324
|
+
reclaimedBytes: 0,
|
|
325
|
+
summary: lines.join("; "),
|
|
326
|
+
detail: lines,
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
sendJson(res, 400, {
|
|
332
|
+
error: `unknown action: ${payload.action}`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
} catch (err) {
|
|
336
|
+
sendJson(res, 500, { error: (err as Error).message });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// JSON helper
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function sendJson(res: ServerResponse, status: number, data: unknown): void {
|
|
346
|
+
const body = JSON.stringify(data);
|
|
347
|
+
res.writeHead(status, {
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
"Content-Length": Buffer.byteLength(body),
|
|
350
|
+
});
|
|
351
|
+
res.end(body);
|
|
352
|
+
}
|
|
@@ -12,6 +12,9 @@ import { readIndex, getIndexDir } from "./index-reader.js";
|
|
|
12
12
|
import { ACTIVE_WINDOW_SEC } from "./types.js";
|
|
13
13
|
import type { RouteContext } from "./routes-core.js";
|
|
14
14
|
import type { LiveSnapshot } from "./types.js";
|
|
15
|
+
import { readProviderCacheForRepo } from "../../src/store/sqlite/perf-samples.js";
|
|
16
|
+
import { computeCacheSavings, lookupModelInputRate } from "../../src/pricing.js";
|
|
17
|
+
import { latestModelSnapshot } from "../../src/store/sqlite/model-snapshots.js";
|
|
15
18
|
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
// handleIndex — "/" | "/index.html" | "/api/snapshot" | "/api/version"
|
|
@@ -160,10 +163,34 @@ export function handleRepoIndex(
|
|
|
160
163
|
? snap.context.percent
|
|
161
164
|
: null;
|
|
162
165
|
out.state = (snap.session && snap.session.state) || null;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
out.cacheHits = snap.cacheHits ?? null;
|
|
167
|
+
out.compacts = snap.compacts ?? null;
|
|
168
|
+
out.timeSaved = snap.timeSaved ?? null;
|
|
169
|
+
out.updatedAt = snap.updatedAt ?? null;
|
|
170
|
+
// Provider cache per-repo lifetime (E.2)
|
|
171
|
+
try {
|
|
172
|
+
const pc = readProviderCacheForRepo(r.stateDir);
|
|
173
|
+
// Price from model snapshot if available
|
|
174
|
+
const modelSnap = latestModelSnapshot(r.stateDir);
|
|
175
|
+
const inputRate = modelSnap
|
|
176
|
+
? modelSnap.inputRate
|
|
177
|
+
: lookupModelInputRate(r.modelName ?? "") ?? 0;
|
|
178
|
+
const savings = computeCacheSavings(
|
|
179
|
+
pc.totalCacheRead,
|
|
180
|
+
pc.totalCacheWrite,
|
|
181
|
+
inputRate,
|
|
182
|
+
);
|
|
183
|
+
out.providerCache = {
|
|
184
|
+
avgHitPct: pc.avgHitPct,
|
|
185
|
+
cacheRead: pc.totalCacheRead,
|
|
186
|
+
cacheWrite: pc.totalCacheWrite,
|
|
187
|
+
totalInput: pc.totalInput,
|
|
188
|
+
sampleCount: pc.sampleCount,
|
|
189
|
+
estimatedSaved: savings.netSaved > 0 ? savings.netSaved : null,
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
/* best-effort — repo may not have perf_samples */
|
|
193
|
+
}
|
|
167
194
|
}
|
|
168
195
|
} catch {
|
|
169
196
|
/* best-effort */
|
|
@@ -18,3 +18,5 @@ export {
|
|
|
18
18
|
export { handleEvents, handleSessions } from "./routes-sessions.js";
|
|
19
19
|
export { handleTopics } from "./routes-topics.js";
|
|
20
20
|
export { handleTurns } from "./routes-turns.js";
|
|
21
|
+
export { handleMaintenance } from "./routes-maintenance.js";
|
|
22
|
+
export { handleProviderCache } from "./routes-cache.js";
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
handleSessions,
|
|
37
37
|
handleTopics,
|
|
38
38
|
handleTurns,
|
|
39
|
+
handleMaintenance,
|
|
40
|
+
handleProviderCache,
|
|
39
41
|
handleStatic,
|
|
40
42
|
} from "./routes.js";
|
|
41
43
|
|
|
@@ -223,6 +225,8 @@ export async function launchDashboardServer(
|
|
|
223
225
|
if (handleSessions(req, res, ctx)) return;
|
|
224
226
|
if (handleTopics(req, res, ctx)) return;
|
|
225
227
|
if (handleTurns(req, res, ctx)) return;
|
|
228
|
+
if (handleMaintenance(req, res, ctx)) return;
|
|
229
|
+
if (handleProviderCache(req, res, ctx)) return;
|
|
226
230
|
handleStatic(req, res, ctx);
|
|
227
231
|
});
|
|
228
232
|
|