pi-mega-compact 0.11.2 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +9 -1
- package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +7 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
- package/dist/extensions/dashboard-server/index-reader.js +22 -0
- package/dist/extensions/dashboard-server/routes-cache.js +80 -0
- package/dist/extensions/dashboard-server/routes-cache.test.js +248 -0
- package/dist/extensions/dashboard-server/routes-maintenance.js +7 -7
- package/dist/extensions/dashboard-server/routes-repo.js +24 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +3 -1
- package/dist/extensions/mega-cache-replay.test.js +233 -24
- package/dist/extensions/mega-compact-s38.test.js +201 -6
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +23 -7
- package/dist/extensions/mega-events/context-handler.js +37 -19
- package/dist/extensions/mega-events/error-classifier.js +27 -16
- package/dist/extensions/mega-runtime/runtime-snapshot.js +14 -4
- package/dist/extensions/mega-runtime/snapshot.js +1 -1
- package/dist/extensions/mega-runtime/snapshot.test.js +127 -0
- package/dist/extensions/mega-runtime/widget.js +4 -4
- package/dist/extensions/mega-runtime/widget.test.js +103 -12
- package/dist/src/pricing.js +78 -0
- package/dist/src/store/sqlite/perf-samples.js +104 -0
- package/dist/src/store/sqlite/perf-samples.test.js +93 -1
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js → AchievementTiles-vEo2MtE2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js.map → AchievementTiles-vEo2MtE2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +2 -0
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-B8NqDt7I.js.map → AchievementsTab-DjxfYrDc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js → ConfigTab-Cm6ymbmx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js.map → ConfigTab-Cm6ymbmx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js → EventsTab-DXWl_YzI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js.map → EventsTab-DXWl_YzI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js → GameTab-BLDudcO5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js.map → GameTab-BLDudcO5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js → MetricsTab-Cq4u-rZF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js.map → MetricsTab-Cq4u-rZF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js → OverviewTab-D4kGvsiQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js.map → OverviewTab-D4kGvsiQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js → SessionsTab-DZC2-GCn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js.map → SessionsTab-DZC2-GCn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js → TopicsTab-01G71hv1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js.map → TopicsTab-01G71hv1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js → TurnsTab-DLRg0LZT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js.map → TurnsTab-DLRg0LZT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +1 -0
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js → index-BX0bcgLu.js} +4 -4
- package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js.map → index-BX0bcgLu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js → useSSE-BBYt5UPB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js.map → useSSE-BBYt5UPB.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/api/client.ts +6 -0
- package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +13 -1
- package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +141 -0
- package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +31 -3
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/maintenance.css +267 -0
- package/extensions/dashboard-client/src/styles/overview-extra.css +9 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +83 -29
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +301 -136
- package/extensions/dashboard-client/src/utils/format.ts +42 -7
- package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -8
- package/extensions/dashboard-server/api-contracts/index.ts +4 -0
- package/extensions/dashboard-server/api-contracts/multi-repo.ts +6 -0
- package/extensions/dashboard-server/api-contracts/provider-cache.ts +30 -0
- package/extensions/dashboard-server/api-contracts.test.ts +3 -2
- package/extensions/dashboard-server/index-reader.ts +23 -0
- package/extensions/dashboard-server/routes-cache.test.ts +302 -0
- package/extensions/dashboard-server/routes-cache.ts +123 -0
- package/extensions/dashboard-server/routes-maintenance.ts +18 -11
- package/extensions/dashboard-server/routes-repo.ts +31 -4
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +2 -0
- package/extensions/mega-cache-replay.test.ts +481 -166
- package/extensions/mega-compact-s38.test.ts +210 -6
- package/extensions/mega-config.ts +6 -0
- package/extensions/mega-events/agent-handlers.ts +24 -6
- package/extensions/mega-events/context-handler.ts +59 -24
- package/extensions/mega-events/error-classifier.ts +30 -13
- package/extensions/mega-runtime/runtime-snapshot.ts +31 -9
- package/extensions/mega-runtime/snapshot.test.ts +139 -0
- package/extensions/mega-runtime/snapshot.ts +6 -4
- package/extensions/mega-runtime/widget.test.ts +290 -105
- package/extensions/mega-runtime/widget.ts +29 -6
- package/package.json +1 -1
- package/src/pricing.ts +95 -0
- package/src/store/sqlite/perf-samples.test.ts +107 -3
- package/src/store/sqlite/perf-samples.ts +142 -6
- package/extensions/dashboard-client/dist/assets/AchievementsTab-B8NqDt7I.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-gn66lYD2.css +0 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dashboard-client/src/tabs/MaintenanceTab.tsx — Maintenance tab (S49B).
|
|
2
|
+
* dashboard-client/src/tabs/MaintenanceTab.tsx — Maintenance tab (v0.11.2 S49B).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import type React from "react";
|
|
@@ -12,82 +16,171 @@ import type {
|
|
|
12
16
|
SchemaHealthResponse,
|
|
13
17
|
MaintenanceAction,
|
|
14
18
|
MaintenanceActionResult,
|
|
15
|
-
TableStats,
|
|
16
|
-
DbFiles,
|
|
17
|
-
SchemaHealthRow,
|
|
18
19
|
} from "@contracts";
|
|
19
20
|
import { useApi } from "../hooks/useApi";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
fetchDbStats,
|
|
23
|
+
fetchSchemaHealth,
|
|
24
|
+
postMaintenanceAction,
|
|
25
|
+
} from "../api/client";
|
|
21
26
|
|
|
22
27
|
// ---------------------------------------------------------------------------
|
|
23
|
-
//
|
|
28
|
+
// Action config
|
|
24
29
|
// ---------------------------------------------------------------------------
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<div className="card-header">DB Stats</div>
|
|
34
|
-
<div className="card-body">
|
|
35
|
-
{loading && !data && <span className="text-muted">Loading…</span>}
|
|
36
|
-
{error && !data && <span className="text-error">{error.message}</span>}
|
|
37
|
-
{data && (
|
|
38
|
-
<>
|
|
39
|
-
<FileSizes files={data.storage.files} pageSize={data.storage.pageSize} pageCount={data.storage.pageCount} freelistPages={data.storage.freelistPages} />
|
|
40
|
-
<TableCounts tables={data.tables} />
|
|
41
|
-
</>
|
|
42
|
-
)}
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
31
|
+
interface ActionDef {
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
desc: string;
|
|
35
|
+
dangerous: boolean;
|
|
36
|
+
confirm?: string;
|
|
37
|
+
daysOld?: boolean;
|
|
46
38
|
}
|
|
47
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
|
+
|
|
48
95
|
function fmtBytes(b: number): string {
|
|
49
96
|
if (b < 1024) return `${b} B`;
|
|
50
97
|
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
|
|
51
98
|
return `${(b / (1024 * 1024)).toFixed(1)} MB`;
|
|
52
99
|
}
|
|
53
100
|
|
|
54
|
-
|
|
55
|
-
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// DB Stats card
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function FileSizesCard({
|
|
106
|
+
files,
|
|
107
|
+
pageSize,
|
|
108
|
+
pageCount,
|
|
109
|
+
freelistPages,
|
|
110
|
+
}: {
|
|
111
|
+
files: DbStatsResponse["storage"]["files"];
|
|
56
112
|
pageSize: number;
|
|
57
113
|
pageCount: number;
|
|
58
114
|
freelistPages: number;
|
|
59
|
-
}) {
|
|
115
|
+
}): React.ReactElement {
|
|
116
|
+
const total = files.dbBytes + files.walBytes + files.shmBytes;
|
|
60
117
|
return (
|
|
61
118
|
<div className="stat-section">
|
|
62
119
|
<div className="stat-label">File Sizes</div>
|
|
63
120
|
<table className="compact-table">
|
|
64
121
|
<tbody>
|
|
65
|
-
<tr
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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>
|
|
71
153
|
</tbody>
|
|
72
154
|
</table>
|
|
73
155
|
</div>
|
|
74
156
|
);
|
|
75
157
|
}
|
|
76
158
|
|
|
77
|
-
function
|
|
78
|
-
|
|
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);
|
|
79
165
|
return (
|
|
80
166
|
<div className="stat-section">
|
|
81
|
-
<div className="stat-label">
|
|
167
|
+
<div className="stat-label">
|
|
168
|
+
Table Row Counts ({totalRows.toLocaleString()} total)
|
|
169
|
+
</div>
|
|
82
170
|
<table className="compact-table">
|
|
83
171
|
<thead>
|
|
84
|
-
<tr
|
|
172
|
+
<tr>
|
|
173
|
+
<th>Table</th>
|
|
174
|
+
<th className="num">Rows</th>
|
|
175
|
+
</tr>
|
|
85
176
|
</thead>
|
|
86
177
|
<tbody>
|
|
87
178
|
{tables.map((t) => (
|
|
88
179
|
<tr key={t.table}>
|
|
89
180
|
<td>{t.table}</td>
|
|
90
|
-
<td className="num">
|
|
181
|
+
<td className="num">
|
|
182
|
+
{t.rowCount >= 0 ? t.rowCount.toLocaleString() : "—"}
|
|
183
|
+
</td>
|
|
91
184
|
</tr>
|
|
92
185
|
))}
|
|
93
186
|
</tbody>
|
|
@@ -96,39 +189,30 @@ function TableCounts({ tables }: { tables: TableStats[] }) {
|
|
|
96
189
|
);
|
|
97
190
|
}
|
|
98
191
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
data:
|
|
192
|
+
function DbStatsCard({
|
|
193
|
+
data,
|
|
194
|
+
loading,
|
|
195
|
+
error,
|
|
196
|
+
}: {
|
|
197
|
+
data: DbStatsResponse | null;
|
|
105
198
|
loading: boolean;
|
|
106
199
|
error: Error | null;
|
|
107
|
-
}) {
|
|
200
|
+
}): React.ReactElement {
|
|
108
201
|
return (
|
|
109
202
|
<div className="card">
|
|
110
|
-
<div className="card-header">
|
|
111
|
-
Schema Health
|
|
112
|
-
{data && (
|
|
113
|
-
<span className={`badge ${data.healthy ? "badge-ok" : "badge-bad"}`}>
|
|
114
|
-
{data.healthy ? "HEALTHY" : "DEGRADED"}
|
|
115
|
-
</span>
|
|
116
|
-
)}
|
|
117
|
-
</div>
|
|
203
|
+
<div className="card-header">DB Stats</div>
|
|
118
204
|
<div className="card-body">
|
|
119
205
|
{loading && !data && <span className="text-muted">Loading…</span>}
|
|
120
206
|
{error && !data && <span className="text-error">{error.message}</span>}
|
|
121
207
|
{data && (
|
|
122
208
|
<>
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
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} />
|
|
132
216
|
</>
|
|
133
217
|
)}
|
|
134
218
|
</div>
|
|
@@ -136,14 +220,29 @@ function SchemaHealthCard({ data, loading, error }: {
|
|
|
136
220
|
);
|
|
137
221
|
}
|
|
138
222
|
|
|
139
|
-
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Schema Health card
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
function ColumnAuditCard({
|
|
228
|
+
columns,
|
|
229
|
+
}: {
|
|
230
|
+
columns: SchemaHealthResponse["columns"];
|
|
231
|
+
}): React.ReactElement {
|
|
140
232
|
const missing = columns.filter((c) => !c.present);
|
|
141
233
|
return (
|
|
142
234
|
<div className="stat-section">
|
|
143
|
-
<div className="stat-label">
|
|
235
|
+
<div className="stat-label">
|
|
236
|
+
Column Audit ({columns.length} columns, {missing.length} missing)
|
|
237
|
+
</div>
|
|
144
238
|
<table className="compact-table">
|
|
145
239
|
<thead>
|
|
146
|
-
<tr
|
|
240
|
+
<tr>
|
|
241
|
+
<th>Table</th>
|
|
242
|
+
<th>Column</th>
|
|
243
|
+
<th>Decl</th>
|
|
244
|
+
<th>Status</th>
|
|
245
|
+
</tr>
|
|
147
246
|
</thead>
|
|
148
247
|
<tbody>
|
|
149
248
|
{columns.map((c) => (
|
|
@@ -151,7 +250,9 @@ function ColumnAudit({ columns }: { columns: SchemaHealthRow[] }) {
|
|
|
151
250
|
<td>{c.table}</td>
|
|
152
251
|
<td>{c.column}</td>
|
|
153
252
|
<td className="text-mono">{c.expectedDecl}</td>
|
|
154
|
-
<td className={c.present ? "text-ok" : "text-error"}>
|
|
253
|
+
<td className={c.present ? "text-ok" : "text-error"}>
|
|
254
|
+
{c.present ? "✓" : "MISSING"}
|
|
255
|
+
</td>
|
|
155
256
|
</tr>
|
|
156
257
|
))}
|
|
157
258
|
</tbody>
|
|
@@ -160,87 +261,138 @@ function ColumnAudit({ columns }: { columns: SchemaHealthRow[] }) {
|
|
|
160
261
|
);
|
|
161
262
|
}
|
|
162
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
|
+
|
|
163
322
|
// ---------------------------------------------------------------------------
|
|
164
|
-
//
|
|
323
|
+
// Actions card
|
|
165
324
|
// ---------------------------------------------------------------------------
|
|
166
325
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
label: string;
|
|
170
|
-
desc: string;
|
|
171
|
-
dangerous: boolean;
|
|
172
|
-
confirm?: string;
|
|
173
|
-
daysOld?: boolean;
|
|
174
|
-
}> = [
|
|
175
|
-
{ key: "integrity-check", label: "Integrity Check", desc: "Run PRAGMA integrity_check", dangerous: false },
|
|
176
|
-
{ key: "reconcile-dedup", label: "Reconcile Dedup Mirror", desc: "Fix orphan dedup rows + backfill refs", dangerous: false, confirm: "Reconcile dedup_mirror (safe read/write)?" },
|
|
177
|
-
{ key: "checkpoint", label: "WAL Checkpoint", desc: "Merge WAL into main DB (read-only)", dangerous: false },
|
|
178
|
-
{ key: "vacuum", label: "VACUUM", desc: "Rebuild DB reclaiming free pages", dangerous: true, confirm: "VACUUM rewrites the entire DB. Continue?" },
|
|
179
|
-
{ key: "reindex", label: "REINDEX", desc: "Rebuild all indexes", dangerous: true, confirm: "REINDEX rebuilds indexes. Continue?" },
|
|
180
|
-
{ key: "fts5-rebuild", label: "FTS5 Rebuild", desc: "Rebuild full-text search index", dangerous: true, confirm: "Rebuild FTS5 trigram index. Continue?" },
|
|
181
|
-
{ key: "prune", label: "Prune Old Rows", desc: "Delete transcripts + epochs older than N days", dangerous: true, daysOld: true, confirm: "Delete old raw_transcript rows. Continue?" },
|
|
182
|
-
];
|
|
183
|
-
|
|
184
|
-
interface ActionResult extends MaintenanceActionResult {
|
|
185
|
-
timestamp: number;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function ActionsPanel() {
|
|
189
|
-
const [results, setResults] = useState<ActionResult[]>([]);
|
|
326
|
+
function ActionsCard(): React.ReactElement {
|
|
327
|
+
const [results, setResults] = useState<MaintenanceActionResult[]>([]);
|
|
190
328
|
const [running, setRunning] = useState<string | null>(null);
|
|
191
329
|
const [days, setDays] = useState("30");
|
|
192
330
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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);
|
|
197
336
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
);
|
|
215
363
|
|
|
216
364
|
return (
|
|
217
365
|
<div className="card">
|
|
218
366
|
<div className="card-header">Actions</div>
|
|
219
367
|
<div className="card-body">
|
|
220
368
|
<div className="action-grid">
|
|
221
|
-
{ACTIONS.map((
|
|
222
|
-
const isRunning = running ===
|
|
223
|
-
const lastResult = results.find((r) => r.operation ===
|
|
369
|
+
{ACTIONS.map((def) => {
|
|
370
|
+
const isRunning = running === def.key;
|
|
371
|
+
const lastResult = results.find((r) => r.operation === def.key);
|
|
224
372
|
return (
|
|
225
|
-
<div key={
|
|
373
|
+
<div key={def.key} className="action-item">
|
|
226
374
|
<div className="action-info">
|
|
227
375
|
<div className="action-label">
|
|
228
|
-
{
|
|
229
|
-
{
|
|
376
|
+
{def.label}
|
|
377
|
+
{def.dangerous && (
|
|
378
|
+
<span className="badge badge-warn">DESTRUCTIVE</span>
|
|
379
|
+
)}
|
|
230
380
|
</div>
|
|
231
|
-
<div className="action-desc">{
|
|
381
|
+
<div className="action-desc">{def.desc}</div>
|
|
232
382
|
{lastResult && (
|
|
233
|
-
<div
|
|
383
|
+
<div
|
|
384
|
+
className={`action-result ${lastResult.success ? "text-ok" : "text-error"}`}
|
|
385
|
+
>
|
|
234
386
|
{lastResult.summary}
|
|
235
387
|
</div>
|
|
236
388
|
)}
|
|
237
389
|
</div>
|
|
238
390
|
<div className="action-controls">
|
|
239
|
-
{
|
|
391
|
+
{def.daysOld && (
|
|
240
392
|
<input
|
|
241
393
|
type="number"
|
|
242
|
-
min=
|
|
243
|
-
max=
|
|
394
|
+
min={1}
|
|
395
|
+
max={365}
|
|
244
396
|
value={days}
|
|
245
397
|
onChange={(e) => setDays(e.target.value)}
|
|
246
398
|
className="days-input"
|
|
@@ -248,11 +400,11 @@ function ActionsPanel() {
|
|
|
248
400
|
/>
|
|
249
401
|
)}
|
|
250
402
|
<button
|
|
251
|
-
className={`btn ${
|
|
252
|
-
disabled={
|
|
403
|
+
className={`btn ${def.dangerous ? "btn-danger" : "btn-primary"}`}
|
|
404
|
+
disabled={running !== null}
|
|
253
405
|
onClick={() => {
|
|
254
|
-
if (
|
|
255
|
-
|
|
406
|
+
if (def.confirm && !window.confirm(def.confirm)) return;
|
|
407
|
+
handleAction(def);
|
|
256
408
|
}}
|
|
257
409
|
>
|
|
258
410
|
{isRunning ? "Running…" : "Run"}
|
|
@@ -268,15 +420,24 @@ function ActionsPanel() {
|
|
|
268
420
|
}
|
|
269
421
|
|
|
270
422
|
// ---------------------------------------------------------------------------
|
|
271
|
-
//
|
|
423
|
+
// Main tab component
|
|
272
424
|
// ---------------------------------------------------------------------------
|
|
273
425
|
|
|
274
426
|
export default function MaintenanceTab(): React.ReactElement {
|
|
275
|
-
const {
|
|
427
|
+
const {
|
|
428
|
+
data: dbStats,
|
|
429
|
+
loading: statsLoading,
|
|
430
|
+
error: statsError,
|
|
431
|
+
} = useApi<DbStatsResponse>(
|
|
276
432
|
useCallback(() => fetchDbStats(), []),
|
|
277
433
|
{ pollInterval: 15000 },
|
|
278
434
|
);
|
|
279
|
-
|
|
435
|
+
|
|
436
|
+
const {
|
|
437
|
+
data: schemaHealth,
|
|
438
|
+
loading: healthLoading,
|
|
439
|
+
error: healthError,
|
|
440
|
+
} = useApi<SchemaHealthResponse>(
|
|
280
441
|
useCallback(() => fetchSchemaHealth(), []),
|
|
281
442
|
{ pollInterval: 10000 },
|
|
282
443
|
);
|
|
@@ -284,10 +445,14 @@ export default function MaintenanceTab(): React.ReactElement {
|
|
|
284
445
|
return (
|
|
285
446
|
<div className="maintenance-tab">
|
|
286
447
|
<div className="card-grid">
|
|
287
|
-
<SchemaHealthCard
|
|
288
|
-
|
|
448
|
+
<SchemaHealthCard
|
|
449
|
+
data={schemaHealth}
|
|
450
|
+
loading={healthLoading}
|
|
451
|
+
error={healthError}
|
|
452
|
+
/>
|
|
453
|
+
<DbStatsCard data={dbStats} loading={statsLoading} error={statsError} />
|
|
289
454
|
</div>
|
|
290
|
-
<
|
|
455
|
+
<ActionsCard />
|
|
291
456
|
</div>
|
|
292
457
|
);
|
|
293
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
|
+
}
|