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
|
@@ -1,50 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dashboard-client/src/tabs/CacheTab.tsx — Cache tab (
|
|
2
|
+
* dashboard-client/src/tabs/CacheTab.tsx — Cache tab (B.3).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two sections, each with its own header:
|
|
5
|
+
* 1. Provider Prompt Cache → ProviderCacheCard (/api/provider-cache)
|
|
6
|
+
* 2. Mega-Compact Dedup Cache → CacheHitsCard + TimeSavedCard (/api/snapshot)
|
|
7
|
+
*
|
|
8
|
+
* Both fetch at 5s polling (same interval). Loading / error shown per-section
|
|
9
|
+
* only when its own fetch fails; the other section keeps rendering.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import type React from "react";
|
|
9
13
|
import { useCallback } from "react";
|
|
10
|
-
import type { SnapshotResponse } from "@contracts";
|
|
14
|
+
import type { SnapshotResponse, ProviderCacheResponse } from "@contracts";
|
|
11
15
|
import { useApi } from "../hooks/useApi";
|
|
12
|
-
import { fetchSnapshot } from "../api/client";
|
|
16
|
+
import { fetchSnapshot, fetchProviderCache } from "../api/client";
|
|
13
17
|
import { CacheHitsCard } from "../components/CacheHitsCard";
|
|
14
18
|
import { TimeSavedCard } from "../components/TimeSavedCard";
|
|
19
|
+
import { ProviderCacheCard } from "../components/ProviderCacheCard";
|
|
15
20
|
|
|
16
21
|
export default function CacheTab(): React.ReactElement {
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
/* --- Provider prompt cache (5s poll) --- */
|
|
23
|
+
const {
|
|
24
|
+
data: providerCache,
|
|
25
|
+
loading: pcLoading,
|
|
26
|
+
error: pcError,
|
|
27
|
+
} = useApi<ProviderCacheResponse>(
|
|
28
|
+
useCallback(() => fetchProviderCache(), []),
|
|
19
29
|
{ pollInterval: 5000 },
|
|
20
30
|
);
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
/* --- Dedup cache snapshot (5s poll) --- */
|
|
33
|
+
const {
|
|
34
|
+
data: snapshot,
|
|
35
|
+
loading: snapLoading,
|
|
36
|
+
error: snapError,
|
|
37
|
+
} = useApi<SnapshotResponse>(
|
|
38
|
+
useCallback(() => fetchSnapshot(), []),
|
|
39
|
+
{ pollInterval: 5000 },
|
|
40
|
+
);
|
|
29
41
|
|
|
30
42
|
return (
|
|
31
43
|
<div className="cache-tab">
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
{/* ==============================================================
|
|
45
|
+
Section 1 — Provider Prompt Cache
|
|
46
|
+
============================================================== */}
|
|
47
|
+
<h2>Provider Prompt Cache</h2>
|
|
48
|
+
{pcLoading ? (
|
|
49
|
+
<p className="tab-stub">Loading…</p>
|
|
50
|
+
) : pcError ? (
|
|
51
|
+
<p className="tab-stub">
|
|
52
|
+
Provider cache unavailable: {pcError.message}
|
|
53
|
+
</p>
|
|
54
|
+
) : providerCache ? (
|
|
55
|
+
<ProviderCacheCard
|
|
56
|
+
hitPct={providerCache.cache.avgHitPct}
|
|
57
|
+
turnCount={providerCache.cache.turnCount}
|
|
58
|
+
totalCacheRead={providerCache.cache.totalCacheRead}
|
|
59
|
+
totalCacheWrite={providerCache.cache.totalCacheWrite}
|
|
60
|
+
totalInput={providerCache.cache.totalInput}
|
|
61
|
+
cacheReadSaved={providerCache.savings?.cacheReadSaved ?? null}
|
|
62
|
+
cacheWriteCost={providerCache.savings?.cacheWriteCost ?? null}
|
|
63
|
+
netSaved={providerCache.savings?.netSaved ?? null}
|
|
64
|
+
modelLabel={providerCache.savings?.model ?? null}
|
|
65
|
+
firstTurnAt={providerCache.cache.firstTurnAt}
|
|
66
|
+
latestTurnAt={providerCache.cache.latestTurnAt}
|
|
40
67
|
/>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
) : null}
|
|
69
|
+
|
|
70
|
+
{/* ==============================================================
|
|
71
|
+
Section 2 — Mega-Compact Dedup Cache
|
|
72
|
+
============================================================== */}
|
|
73
|
+
<h2>Mega-Compact Dedup Cache</h2>
|
|
74
|
+
{snapLoading ? (
|
|
75
|
+
<p className="tab-stub">Loading…</p>
|
|
76
|
+
) : snapError ? (
|
|
77
|
+
<p className="tab-stub">Snapshot unavailable: {snapError.message}</p>
|
|
78
|
+
) : (
|
|
79
|
+
snapshot &&
|
|
80
|
+
(() => {
|
|
81
|
+
const { cacheHits, compacts, timeSaved } = snapshot;
|
|
82
|
+
return (
|
|
83
|
+
<div className="card-grid overview-card-grid">
|
|
84
|
+
<CacheHitsCard
|
|
85
|
+
cacheHitsSession={cacheHits.session}
|
|
86
|
+
cacheHitsTotal={cacheHits.total}
|
|
87
|
+
tokensSavedSession={cacheHits.sessionTokensSaved}
|
|
88
|
+
tokensSavedTotal={cacheHits.totalTokensSaved}
|
|
89
|
+
compactionsSession={compacts.session}
|
|
90
|
+
compactionsTotal={compacts.total}
|
|
91
|
+
/>
|
|
92
|
+
<TimeSavedCard
|
|
93
|
+
compactSessionSec={timeSaved.compact.sessionSec}
|
|
94
|
+
compactTotalSec={timeSaved.compact.totalSec}
|
|
95
|
+
cacheHitSessionSec={timeSaved.cacheHit.sessionSec}
|
|
96
|
+
cacheHitTotalSec={timeSaved.cacheHit.totalSec}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
})()
|
|
101
|
+
)}
|
|
48
102
|
</div>
|
|
49
103
|
);
|
|
50
104
|
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/tabs/MaintenanceTab.tsx — Maintenance tab (v0.11.2 S49B).
|
|
3
|
+
*
|
|
4
|
+
* Three cards:
|
|
5
|
+
* 1. Schema Health (schema version + integrity + column audit)
|
|
6
|
+
* 2. DB Stats (file sizes + table row counts)
|
|
7
|
+
* 3. Actions (one-time maintenance operations with results history)
|
|
8
|
+
*
|
|
9
|
+
* All three fetch independently; failures in one do not block the others.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type React from "react";
|
|
13
|
+
import { useState, useCallback } from "react";
|
|
14
|
+
import type {
|
|
15
|
+
DbStatsResponse,
|
|
16
|
+
SchemaHealthResponse,
|
|
17
|
+
MaintenanceAction,
|
|
18
|
+
MaintenanceActionResult,
|
|
19
|
+
} from "@contracts";
|
|
20
|
+
import { useApi } from "../hooks/useApi";
|
|
21
|
+
import {
|
|
22
|
+
fetchDbStats,
|
|
23
|
+
fetchSchemaHealth,
|
|
24
|
+
postMaintenanceAction,
|
|
25
|
+
} from "../api/client";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Action config
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
interface ActionDef {
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
desc: string;
|
|
35
|
+
dangerous: boolean;
|
|
36
|
+
confirm?: string;
|
|
37
|
+
daysOld?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ACTIONS: ActionDef[] = [
|
|
41
|
+
{
|
|
42
|
+
key: "integrity-check",
|
|
43
|
+
label: "Integrity Check",
|
|
44
|
+
desc: "Run PRAGMA integrity_check",
|
|
45
|
+
dangerous: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "reconcile-dedup",
|
|
49
|
+
label: "Reconcile Dedup Mirror",
|
|
50
|
+
desc: "Fix orphan dedup rows + backfill refs",
|
|
51
|
+
dangerous: false,
|
|
52
|
+
confirm: "Reconcile dedup_mirror (safe read/write)?",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "checkpoint",
|
|
56
|
+
label: "WAL Checkpoint",
|
|
57
|
+
desc: "Merge WAL into main DB (read-only)",
|
|
58
|
+
dangerous: false,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "vacuum",
|
|
62
|
+
label: "VACUUM",
|
|
63
|
+
desc: "Rebuild DB reclaiming free pages",
|
|
64
|
+
dangerous: true,
|
|
65
|
+
confirm: "VACUUM rewrites the entire DB. Continue?",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "reindex",
|
|
69
|
+
label: "REINDEX",
|
|
70
|
+
desc: "Rebuild all indexes",
|
|
71
|
+
dangerous: true,
|
|
72
|
+
confirm: "REINDEX rebuilds indexes. Continue?",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "fts5-rebuild",
|
|
76
|
+
label: "FTS5 Rebuild",
|
|
77
|
+
desc: "Rebuild full-text search index",
|
|
78
|
+
dangerous: true,
|
|
79
|
+
confirm: "Rebuild FTS5 trigram index. Continue?",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "prune",
|
|
83
|
+
label: "Prune Old Rows",
|
|
84
|
+
desc: "Delete transcripts + epochs older than N days",
|
|
85
|
+
dangerous: true,
|
|
86
|
+
daysOld: true,
|
|
87
|
+
confirm: "Delete old raw_transcript rows. Continue?",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Format helpers (local — matches html.ts fmtBytes)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function fmtBytes(b: number): string {
|
|
96
|
+
if (b < 1024) return `${b} B`;
|
|
97
|
+
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
|
|
98
|
+
return `${(b / (1024 * 1024)).toFixed(1)} MB`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// DB Stats card
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function FileSizesCard({
|
|
106
|
+
files,
|
|
107
|
+
pageSize,
|
|
108
|
+
pageCount,
|
|
109
|
+
freelistPages,
|
|
110
|
+
}: {
|
|
111
|
+
files: DbStatsResponse["storage"]["files"];
|
|
112
|
+
pageSize: number;
|
|
113
|
+
pageCount: number;
|
|
114
|
+
freelistPages: number;
|
|
115
|
+
}): React.ReactElement {
|
|
116
|
+
const total = files.dbBytes + files.walBytes + files.shmBytes;
|
|
117
|
+
return (
|
|
118
|
+
<div className="stat-section">
|
|
119
|
+
<div className="stat-label">File Sizes</div>
|
|
120
|
+
<table className="compact-table">
|
|
121
|
+
<tbody>
|
|
122
|
+
<tr>
|
|
123
|
+
<td>sqlite.db</td>
|
|
124
|
+
<td className="num">{fmtBytes(files.dbBytes)}</td>
|
|
125
|
+
</tr>
|
|
126
|
+
{files.walBytes > 0 && (
|
|
127
|
+
<tr>
|
|
128
|
+
<td>.wal</td>
|
|
129
|
+
<td className="num">{fmtBytes(files.walBytes)}</td>
|
|
130
|
+
</tr>
|
|
131
|
+
)}
|
|
132
|
+
{files.shmBytes > 0 && (
|
|
133
|
+
<tr>
|
|
134
|
+
<td>.shm</td>
|
|
135
|
+
<td className="num">{fmtBytes(files.shmBytes)}</td>
|
|
136
|
+
</tr>
|
|
137
|
+
)}
|
|
138
|
+
<tr>
|
|
139
|
+
<td>Total</td>
|
|
140
|
+
<td className="num">{fmtBytes(total)}</td>
|
|
141
|
+
</tr>
|
|
142
|
+
<tr>
|
|
143
|
+
<td>Page size</td>
|
|
144
|
+
<td className="num">{fmtBytes(pageSize)}</td>
|
|
145
|
+
</tr>
|
|
146
|
+
<tr>
|
|
147
|
+
<td>Pages</td>
|
|
148
|
+
<td className="num">
|
|
149
|
+
{pageCount.toLocaleString()} ({freelistPages.toLocaleString()}{" "}
|
|
150
|
+
free)
|
|
151
|
+
</td>
|
|
152
|
+
</tr>
|
|
153
|
+
</tbody>
|
|
154
|
+
</table>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function TableRowCountsCard({
|
|
160
|
+
tables,
|
|
161
|
+
}: {
|
|
162
|
+
tables: DbStatsResponse["tables"];
|
|
163
|
+
}): React.ReactElement {
|
|
164
|
+
const totalRows = tables.reduce((s, t) => s + Math.max(0, t.rowCount), 0);
|
|
165
|
+
return (
|
|
166
|
+
<div className="stat-section">
|
|
167
|
+
<div className="stat-label">
|
|
168
|
+
Table Row Counts ({totalRows.toLocaleString()} total)
|
|
169
|
+
</div>
|
|
170
|
+
<table className="compact-table">
|
|
171
|
+
<thead>
|
|
172
|
+
<tr>
|
|
173
|
+
<th>Table</th>
|
|
174
|
+
<th className="num">Rows</th>
|
|
175
|
+
</tr>
|
|
176
|
+
</thead>
|
|
177
|
+
<tbody>
|
|
178
|
+
{tables.map((t) => (
|
|
179
|
+
<tr key={t.table}>
|
|
180
|
+
<td>{t.table}</td>
|
|
181
|
+
<td className="num">
|
|
182
|
+
{t.rowCount >= 0 ? t.rowCount.toLocaleString() : "—"}
|
|
183
|
+
</td>
|
|
184
|
+
</tr>
|
|
185
|
+
))}
|
|
186
|
+
</tbody>
|
|
187
|
+
</table>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function DbStatsCard({
|
|
193
|
+
data,
|
|
194
|
+
loading,
|
|
195
|
+
error,
|
|
196
|
+
}: {
|
|
197
|
+
data: DbStatsResponse | null;
|
|
198
|
+
loading: boolean;
|
|
199
|
+
error: Error | null;
|
|
200
|
+
}): React.ReactElement {
|
|
201
|
+
return (
|
|
202
|
+
<div className="card">
|
|
203
|
+
<div className="card-header">DB Stats</div>
|
|
204
|
+
<div className="card-body">
|
|
205
|
+
{loading && !data && <span className="text-muted">Loading…</span>}
|
|
206
|
+
{error && !data && <span className="text-error">{error.message}</span>}
|
|
207
|
+
{data && (
|
|
208
|
+
<>
|
|
209
|
+
<FileSizesCard
|
|
210
|
+
files={data.storage.files}
|
|
211
|
+
pageSize={data.storage.pageSize}
|
|
212
|
+
pageCount={data.storage.pageCount}
|
|
213
|
+
freelistPages={data.storage.freelistPages}
|
|
214
|
+
/>
|
|
215
|
+
<TableRowCountsCard tables={data.tables} />
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Schema Health card
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
function ColumnAuditCard({
|
|
228
|
+
columns,
|
|
229
|
+
}: {
|
|
230
|
+
columns: SchemaHealthResponse["columns"];
|
|
231
|
+
}): React.ReactElement {
|
|
232
|
+
const missing = columns.filter((c) => !c.present);
|
|
233
|
+
return (
|
|
234
|
+
<div className="stat-section">
|
|
235
|
+
<div className="stat-label">
|
|
236
|
+
Column Audit ({columns.length} columns, {missing.length} missing)
|
|
237
|
+
</div>
|
|
238
|
+
<table className="compact-table">
|
|
239
|
+
<thead>
|
|
240
|
+
<tr>
|
|
241
|
+
<th>Table</th>
|
|
242
|
+
<th>Column</th>
|
|
243
|
+
<th>Decl</th>
|
|
244
|
+
<th>Status</th>
|
|
245
|
+
</tr>
|
|
246
|
+
</thead>
|
|
247
|
+
<tbody>
|
|
248
|
+
{columns.map((c) => (
|
|
249
|
+
<tr key={`${c.table}.${c.column}`}>
|
|
250
|
+
<td>{c.table}</td>
|
|
251
|
+
<td>{c.column}</td>
|
|
252
|
+
<td className="text-mono">{c.expectedDecl}</td>
|
|
253
|
+
<td className={c.present ? "text-ok" : "text-error"}>
|
|
254
|
+
{c.present ? "✓" : "MISSING"}
|
|
255
|
+
</td>
|
|
256
|
+
</tr>
|
|
257
|
+
))}
|
|
258
|
+
</tbody>
|
|
259
|
+
</table>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function SchemaHealthCard({
|
|
265
|
+
data,
|
|
266
|
+
loading,
|
|
267
|
+
error,
|
|
268
|
+
}: {
|
|
269
|
+
data: SchemaHealthResponse | null;
|
|
270
|
+
loading: boolean;
|
|
271
|
+
error: Error | null;
|
|
272
|
+
}): React.ReactElement {
|
|
273
|
+
return (
|
|
274
|
+
<div className="card">
|
|
275
|
+
<div className="card-header">
|
|
276
|
+
Schema Health
|
|
277
|
+
{data && (
|
|
278
|
+
<span className={`badge ${data.healthy ? "badge-ok" : "badge-bad"}`}>
|
|
279
|
+
{data.healthy ? "HEALTHY" : "DEGRADED"}
|
|
280
|
+
</span>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
<div className="card-body">
|
|
284
|
+
{loading && !data && <span className="text-muted">Loading…</span>}
|
|
285
|
+
{error && !data && <span className="text-error">{error.message}</span>}
|
|
286
|
+
{data && (
|
|
287
|
+
<>
|
|
288
|
+
<div className="stat-row">
|
|
289
|
+
<span className="stat-label">Schema version</span>
|
|
290
|
+
<span className="num">{data.schemaVersion}</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div className="stat-row">
|
|
293
|
+
<span className="stat-label">Integrity</span>
|
|
294
|
+
<span
|
|
295
|
+
className={
|
|
296
|
+
data.integrity[0] === "ok" ? "text-ok" : "text-error"
|
|
297
|
+
}
|
|
298
|
+
>
|
|
299
|
+
{data.integrity.join("; ")}
|
|
300
|
+
</span>
|
|
301
|
+
</div>
|
|
302
|
+
<ColumnAuditCard columns={data.columns} />
|
|
303
|
+
{data.fkCheck.length > 0 && (
|
|
304
|
+
<div className="stat-section">
|
|
305
|
+
<div className="stat-label text-warn">
|
|
306
|
+
FK Check ({data.fkCheck.length})
|
|
307
|
+
</div>
|
|
308
|
+
{data.fkCheck.map((line, i) => (
|
|
309
|
+
<div key={i} className="text-warn text-mono">
|
|
310
|
+
{line}
|
|
311
|
+
</div>
|
|
312
|
+
))}
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Actions card
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
function ActionsCard(): React.ReactElement {
|
|
327
|
+
const [results, setResults] = useState<MaintenanceActionResult[]>([]);
|
|
328
|
+
const [running, setRunning] = useState<string | null>(null);
|
|
329
|
+
const [days, setDays] = useState("30");
|
|
330
|
+
|
|
331
|
+
const handleAction = useCallback(
|
|
332
|
+
async (def: ActionDef) => {
|
|
333
|
+
const action: MaintenanceAction = def.daysOld
|
|
334
|
+
? { action: "prune", daysOld: Number(days) }
|
|
335
|
+
: ({ action: def.key } as MaintenanceAction);
|
|
336
|
+
|
|
337
|
+
setRunning(def.key);
|
|
338
|
+
try {
|
|
339
|
+
const res = await postMaintenanceAction(action);
|
|
340
|
+
setResults((prev) =>
|
|
341
|
+
[{ ...res, timestamp: Date.now() }, ...prev].slice(0, 20),
|
|
342
|
+
);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
setResults((prev) =>
|
|
345
|
+
[
|
|
346
|
+
{
|
|
347
|
+
operation: def.key,
|
|
348
|
+
success: false,
|
|
349
|
+
affected: 0,
|
|
350
|
+
reclaimedBytes: 0,
|
|
351
|
+
summary: err instanceof Error ? err.message : String(err),
|
|
352
|
+
timestamp: Date.now(),
|
|
353
|
+
},
|
|
354
|
+
...prev,
|
|
355
|
+
].slice(0, 20),
|
|
356
|
+
);
|
|
357
|
+
} finally {
|
|
358
|
+
setRunning(null);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
[days],
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<div className="card">
|
|
366
|
+
<div className="card-header">Actions</div>
|
|
367
|
+
<div className="card-body">
|
|
368
|
+
<div className="action-grid">
|
|
369
|
+
{ACTIONS.map((def) => {
|
|
370
|
+
const isRunning = running === def.key;
|
|
371
|
+
const lastResult = results.find((r) => r.operation === def.key);
|
|
372
|
+
return (
|
|
373
|
+
<div key={def.key} className="action-item">
|
|
374
|
+
<div className="action-info">
|
|
375
|
+
<div className="action-label">
|
|
376
|
+
{def.label}
|
|
377
|
+
{def.dangerous && (
|
|
378
|
+
<span className="badge badge-warn">DESTRUCTIVE</span>
|
|
379
|
+
)}
|
|
380
|
+
</div>
|
|
381
|
+
<div className="action-desc">{def.desc}</div>
|
|
382
|
+
{lastResult && (
|
|
383
|
+
<div
|
|
384
|
+
className={`action-result ${lastResult.success ? "text-ok" : "text-error"}`}
|
|
385
|
+
>
|
|
386
|
+
{lastResult.summary}
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
<div className="action-controls">
|
|
391
|
+
{def.daysOld && (
|
|
392
|
+
<input
|
|
393
|
+
type="number"
|
|
394
|
+
min={1}
|
|
395
|
+
max={365}
|
|
396
|
+
value={days}
|
|
397
|
+
onChange={(e) => setDays(e.target.value)}
|
|
398
|
+
className="days-input"
|
|
399
|
+
disabled={isRunning}
|
|
400
|
+
/>
|
|
401
|
+
)}
|
|
402
|
+
<button
|
|
403
|
+
className={`btn ${def.dangerous ? "btn-danger" : "btn-primary"}`}
|
|
404
|
+
disabled={running !== null}
|
|
405
|
+
onClick={() => {
|
|
406
|
+
if (def.confirm && !window.confirm(def.confirm)) return;
|
|
407
|
+
handleAction(def);
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
{isRunning ? "Running…" : "Run"}
|
|
411
|
+
</button>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
})}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Main tab component
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
export default function MaintenanceTab(): React.ReactElement {
|
|
427
|
+
const {
|
|
428
|
+
data: dbStats,
|
|
429
|
+
loading: statsLoading,
|
|
430
|
+
error: statsError,
|
|
431
|
+
} = useApi<DbStatsResponse>(
|
|
432
|
+
useCallback(() => fetchDbStats(), []),
|
|
433
|
+
{ pollInterval: 15000 },
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const {
|
|
437
|
+
data: schemaHealth,
|
|
438
|
+
loading: healthLoading,
|
|
439
|
+
error: healthError,
|
|
440
|
+
} = useApi<SchemaHealthResponse>(
|
|
441
|
+
useCallback(() => fetchSchemaHealth(), []),
|
|
442
|
+
{ pollInterval: 10000 },
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div className="maintenance-tab">
|
|
447
|
+
<div className="card-grid">
|
|
448
|
+
<SchemaHealthCard
|
|
449
|
+
data={schemaHealth}
|
|
450
|
+
loading={healthLoading}
|
|
451
|
+
error={healthError}
|
|
452
|
+
/>
|
|
453
|
+
<DbStatsCard data={dbStats} loading={statsLoading} error={statsError} />
|
|
454
|
+
</div>
|
|
455
|
+
<ActionsCard />
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
@@ -26,9 +26,7 @@ export function fmtSec(s: number | null | undefined): string {
|
|
|
26
26
|
* Display a 0–1 fraction as a percentage string.
|
|
27
27
|
* Uses one decimal place below 10%, rounded above (matches html.ts pct logic).
|
|
28
28
|
*/
|
|
29
|
-
export function fmtPctFromFraction(
|
|
30
|
-
v: number | null | undefined,
|
|
31
|
-
): string {
|
|
29
|
+
export function fmtPctFromFraction(v: number | null | undefined): string {
|
|
32
30
|
const n = v ?? 0;
|
|
33
31
|
const pct = n * 100;
|
|
34
32
|
if (pct >= 10) return `${Math.round(pct)}%`;
|
|
@@ -48,10 +46,7 @@ export function fmtMs(v: number | null | undefined): string {
|
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
/** Format a number with fixed decimals or em-dash for null (matches fmtNum). */
|
|
51
|
-
export function fmtNum(
|
|
52
|
-
v: number | null | undefined,
|
|
53
|
-
dec: number,
|
|
54
|
-
): string {
|
|
49
|
+
export function fmtNum(v: number | null | undefined, dec: number): string {
|
|
55
50
|
if (v == null || typeof v !== "number") return "\u2014";
|
|
56
51
|
return v.toFixed(dec);
|
|
57
52
|
}
|
|
@@ -83,3 +78,43 @@ export function fmtDate(ts: string | number | null | undefined): string {
|
|
|
83
78
|
return "\u2014";
|
|
84
79
|
}
|
|
85
80
|
}
|
|
81
|
+
|
|
82
|
+
/** Format tokens as human-readable (1.2M / 340K / 890 / —). */
|
|
83
|
+
export function fmtTokens(v: number | null | undefined): string {
|
|
84
|
+
if (v == null) return "\u2014";
|
|
85
|
+
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
|
86
|
+
if (v >= 1_000) return `${Math.round(v / 1000)}K`;
|
|
87
|
+
return v.toLocaleString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Format USD savings (always 4 decimals, null → em-dash). */
|
|
91
|
+
export function fmtDollars(v: number | null | undefined): string {
|
|
92
|
+
if (v == null) return "\u2014";
|
|
93
|
+
return `$${v.toFixed(4)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Format an ISO timestamp as relative time ("2m ago", "3h ago", "just now").
|
|
98
|
+
* Falls back to locale date for times >24h or unparseable input.
|
|
99
|
+
*/
|
|
100
|
+
export function fmtRelativeTime(
|
|
101
|
+
ts: string | number | null | undefined,
|
|
102
|
+
): string {
|
|
103
|
+
if (!ts) return "\u2014";
|
|
104
|
+
let date: Date;
|
|
105
|
+
try {
|
|
106
|
+
date = new Date(ts);
|
|
107
|
+
if (isNaN(date.getTime())) return "\u2014";
|
|
108
|
+
} catch {
|
|
109
|
+
return "\u2014";
|
|
110
|
+
}
|
|
111
|
+
const diffMs = Date.now() - date.getTime();
|
|
112
|
+
if (diffMs < 0) return "just now";
|
|
113
|
+
const sec = Math.floor(diffMs / 1000);
|
|
114
|
+
if (sec < 60) return "just now";
|
|
115
|
+
const min = Math.floor(sec / 60);
|
|
116
|
+
if (min < 60) return `${min}m ago`;
|
|
117
|
+
const hrs = Math.floor(min / 60);
|
|
118
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
119
|
+
return date.toLocaleDateString();
|
|
120
|
+
}
|