pi-mega-compact 0.8.8 → 0.8.11
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/core.js +7 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +104 -0
- package/dist/extensions/dashboard-server/api-contracts/game.js +9 -0
- package/dist/extensions/dashboard-server/api-contracts/index.js +9 -0
- package/dist/extensions/dashboard-server/api-contracts/infrastructure.js +10 -0
- package/dist/extensions/dashboard-server/api-contracts/multi-repo.js +9 -0
- package/dist/extensions/dashboard-server/api-contracts/snapshot.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +869 -0
- package/dist/extensions/dashboard-server/auth.js +18 -0
- package/dist/extensions/dashboard-server/helpers.js +37 -0
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
- package/dist/extensions/dashboard-server/html/body-open.js +23 -0
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
- package/dist/extensions/dashboard-server/html/head-open.js +16 -0
- package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
- package/dist/extensions/dashboard-server/html/script.js +259 -0
- package/dist/extensions/dashboard-server/html/styles.js +103 -0
- package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
- package/dist/extensions/dashboard-server/html-template.js +41 -0
- package/dist/extensions/dashboard-server/server.js +191 -39
- package/dist/extensions/dashboard-server/tailscale.js +8 -0
- package/dist/extensions/dashboard-server/types.js +4 -0
- package/dist/src/store/sqlite/connection.js +35 -0
- package/dist/src/store/sqlite/index-store.js +167 -0
- package/dist/src/store/sqlite/memory.js +54 -0
- package/dist/src/store/sqlite/minhash-lsh.js +47 -0
- package/dist/src/store/sqlite/sessions.js +39 -0
- package/dist/src/store/sqlite/transaction.js +19 -0
- package/dist/src/vectorStore/add.js +260 -0
- package/dist/src/vectorStore/dedup.js +52 -0
- package/dist/src/vectorStore/index.js +10 -0
- package/dist/src/vectorStore/queries.js +83 -0
- package/dist/src/vectorStore/search.js +95 -0
- package/dist/src/vectorStore/session.js +19 -0
- package/dist/src/vectorStore/store.js +105 -0
- package/dist/src/vectorStore/types.js +6 -0
- package/dist/src/vectorStore/utils.js +23 -0
- package/extensions/dashboard-client/package-lock.json +1771 -0
- package/extensions/dashboard-client/package.json +27 -0
- package/extensions/dashboard-client/src/App.tsx +82 -0
- package/extensions/dashboard-client/src/api/client.ts +145 -0
- package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +44 -0
- package/extensions/dashboard-client/src/components/CompressionCard.tsx +61 -0
- package/extensions/dashboard-client/src/components/ContextGauge.tsx +59 -0
- package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +49 -0
- package/extensions/dashboard-client/src/components/ErrorBoundary.tsx +53 -0
- package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +16 -0
- package/extensions/dashboard-client/src/components/EventStream.tsx +152 -0
- package/extensions/dashboard-client/src/components/LoadingSpinner.tsx +7 -0
- package/extensions/dashboard-client/src/components/MemoryStatusCard.tsx +24 -0
- package/extensions/dashboard-client/src/components/ModelBadge.tsx +41 -0
- package/extensions/dashboard-client/src/components/PerfChart.tsx +160 -0
- package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +126 -0
- package/extensions/dashboard-client/src/components/RepoTable.tsx +150 -0
- package/extensions/dashboard-client/src/components/SessionInfo.tsx +65 -0
- package/extensions/dashboard-client/src/components/SummaryTiles.tsx +52 -0
- package/extensions/dashboard-client/src/components/TabBar.tsx +34 -0
- package/extensions/dashboard-client/src/components/TriggerStatus.tsx +62 -0
- package/extensions/dashboard-client/src/hooks/useApi.ts +77 -0
- package/extensions/dashboard-client/src/hooks/useSSE.ts +87 -0
- package/extensions/dashboard-client/src/index.html +12 -0
- package/extensions/dashboard-client/src/main.tsx +23 -0
- package/extensions/dashboard-client/src/styles/base.css +121 -0
- package/extensions/dashboard-client/src/styles/overview-events.css +318 -0
- package/extensions/dashboard-client/src/styles/repos-metrics.css +307 -0
- package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +16 -0
- package/extensions/dashboard-client/src/tabs/EventsTab.tsx +37 -0
- package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +49 -0
- package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +80 -0
- package/extensions/dashboard-client/src/tabs/ReposTab.tsx +59 -0
- package/extensions/dashboard-client/tsconfig.json +28 -0
- package/extensions/dashboard-client/vite.config.ts +38 -0
- package/extensions/dashboard-server/api-contracts/core.ts +302 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +476 -0
- package/extensions/dashboard-server/api-contracts/game.ts +145 -0
- package/extensions/dashboard-server/api-contracts/index.ts +159 -0
- package/extensions/dashboard-server/api-contracts/infrastructure.ts +465 -0
- package/extensions/dashboard-server/api-contracts/multi-repo.ts +328 -0
- package/extensions/dashboard-server/api-contracts/snapshot.ts +386 -0
- package/extensions/dashboard-server/api-contracts.test.ts +1050 -0
- package/extensions/dashboard-server/api-contracts.ts +96 -0
- package/extensions/dashboard-server/auth.ts +20 -0
- package/extensions/dashboard-server/server.ts +862 -582
- package/extensions/dashboard-server/tailscale.ts +7 -0
- package/extensions/dashboard-server/types.ts +23 -114
- package/package.json +2 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline CSS for the dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from the original monolithic `html-template.ts`.
|
|
5
|
+
* Kept as a single cohesive stylesheet — splitting CSS into per-tab files
|
|
6
|
+
* would fragment the design system (shared tokens, e.g. `.card`, `.stat-grid`,
|
|
7
|
+
* `.meter-*` are reused across all tabs).
|
|
8
|
+
*/
|
|
9
|
+
export function styles() {
|
|
10
|
+
return `<style>
|
|
11
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
12
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }
|
|
13
|
+
h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: #f0f6fc; }
|
|
14
|
+
h1 .tier { background: #1f6feb; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
|
|
15
|
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
|
|
16
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
17
|
+
.card.safe { border-color: #238636; }
|
|
18
|
+
.card.safe h2 { color: #3fb950; }
|
|
19
|
+
.safe-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; line-height: 1.5; }
|
|
20
|
+
.value.ok { color: #3fb950; }
|
|
21
|
+
.label {
|
|
22
|
+
cursor: help;
|
|
23
|
+
border-bottom: 1px dotted #484f58;
|
|
24
|
+
}
|
|
25
|
+
.card.legend { grid-column: 1 / -1; }
|
|
26
|
+
.legend-list { margin: 0; padding-left: 18px; color: #c9d1d9; }
|
|
27
|
+
.legend-list li { margin-bottom: 8px; line-height: 1.5; }
|
|
28
|
+
.legend-list b { color: #f0f6fc; }
|
|
29
|
+
.legend-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; font-style: italic; }
|
|
30
|
+
.card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
|
|
31
|
+
.meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
|
|
32
|
+
.meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
|
|
33
|
+
.meter-green { background: #238636; }
|
|
34
|
+
.meter-yellow { background: #d29922; }
|
|
35
|
+
.meter-red { background: #f85149; }
|
|
36
|
+
.meter-label { font-size: 24px; font-weight: 700; color: #f0f6fc; }
|
|
37
|
+
.meter-sub { font-size: 12px; color: #8b949e; }
|
|
38
|
+
.status-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 14px; }
|
|
39
|
+
.status-row .bullet { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
40
|
+
.bullet-on { background: #3fb950; box-shadow: 0 0 6px #3fb95088; }
|
|
41
|
+
.bullet-off { background: #484f58; }
|
|
42
|
+
.bullet-na { background: #d29922; }
|
|
43
|
+
.state-text { font-size: 13px; color: #8b949e; margin-top: 8px; font-family: monospace; }
|
|
44
|
+
.stat-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
|
|
45
|
+
.stat-grid .label { color: #8b949e; }
|
|
46
|
+
.stat-grid .value { color: #f0f6fc; font-weight: 600; font-family: monospace; }
|
|
47
|
+
.conf-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 14px; }
|
|
48
|
+
.conf-grid .label { color: #8b949e; }
|
|
49
|
+
.conf-grid .value { color: #f0f6fc; font-family: monospace; }
|
|
50
|
+
.events { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
51
|
+
.events h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
|
|
52
|
+
.events-wrap { max-height: 240px; overflow-y: auto; font-family: monospace; font-size: 12px; }
|
|
53
|
+
.ev { padding: 3px 0; border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: baseline; }
|
|
54
|
+
.ev:last-child { border-bottom: none; }
|
|
55
|
+
.ev-type { font-weight: 700; min-width: 70px; text-align: right; }
|
|
56
|
+
.ev-type-compact { color: #3fb950; }
|
|
57
|
+
.ev-type-recall { color: #a371f7; }
|
|
58
|
+
.ev-time { color: #484f58; font-size: 10px; min-width: 80px; }
|
|
59
|
+
.ev-detail { color: #8b949e; flex: 1; }
|
|
60
|
+
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
61
|
+
.empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
|
|
62
|
+
.offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
|
|
63
|
+
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
64
|
+
.tab { background: #161b22; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s ease; }
|
|
65
|
+
.tab:hover { color: #c9d1d9; border-color: #484f58; }
|
|
66
|
+
.tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
|
|
67
|
+
.tab.future { opacity: .7; }
|
|
68
|
+
.tab .soon { font-size: 9px; font-weight: 700; background: #6e40c9; color: #fff; padding: 1px 6px; border-radius: 8px; margin-left: 6px; text-transform: uppercase; letter-spacing: .5px; }
|
|
69
|
+
.tab-panel { display: none; }
|
|
70
|
+
.tab-panel.active { display: block; }
|
|
71
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
|
72
|
+
.summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
|
|
73
|
+
.summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
|
|
74
|
+
.summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
|
|
75
|
+
table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
|
|
76
|
+
table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
|
|
77
|
+
table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
|
|
78
|
+
table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
|
|
79
|
+
table.repos tr:last-child td { border-bottom: none; }
|
|
80
|
+
table.repos tr:hover td { background: #1c2128; }
|
|
81
|
+
.repo-model { color: #a371f7; }
|
|
82
|
+
.repo-none { color: #484f58; font-style: italic; }
|
|
83
|
+
.updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
|
|
84
|
+
.model-pill { background: #6e40c9; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
|
|
85
|
+
.card.cost h2 { color: #a371f7; }
|
|
86
|
+
.cost-usd { font-size: 22px; font-weight: 700; color: #3fb950; }
|
|
87
|
+
.cost-sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
|
|
88
|
+
.repo-link { cursor: pointer; }
|
|
89
|
+
.repo-link:hover td { color: #58a6ff; }
|
|
90
|
+
.repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
|
|
91
|
+
.repo-detail.open { display: flex; }
|
|
92
|
+
.repo-detail-box { background: #161b22; border: 1px solid #30363d; border-radius: 10px; padding: 24px; width: 560px; max-width: 92vw; max-height: 86vh; overflow-y: auto; }
|
|
93
|
+
.repo-detail-box h2 { font-size: 14px; color: #f0f6fc; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
|
|
94
|
+
.repo-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
|
|
95
|
+
.repo-close:hover { color: #f0f6fc; }
|
|
96
|
+
.repo-path { font-size: 11px; color: #484f58; word-break: break-all; margin: -8px 0 12px; }
|
|
97
|
+
.placeholder { text-align: center; padding: 48px 16px; color: #8b949e; }
|
|
98
|
+
.placeholder .em { font-size: 40px; margin-bottom: 12px; }
|
|
99
|
+
.placeholder h2 { font-size: 16px; color: #f0f6fc; margin-bottom: 8px; }
|
|
100
|
+
.placeholder p { font-size: 13px; line-height: 1.6; max-width: 420px; margin: 0 auto; }
|
|
101
|
+
</style>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Summary" tab panel — aggregate counts across all registered repos.
|
|
3
|
+
*
|
|
4
|
+
* Populated by the index poller in `script.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export function summaryTab() {
|
|
7
|
+
return `<!-- Summary (aggregate across all repos) -->
|
|
8
|
+
<div class="tab-panel" id="panel-summary">
|
|
9
|
+
<div class="summary-grid">
|
|
10
|
+
<div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
|
|
11
|
+
<div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
|
|
12
|
+
<div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
|
|
13
|
+
<div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="updated" id="sm-updated"></div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML template for the dashboard server — composer.
|
|
3
|
+
*
|
|
4
|
+
* Originally a single 518-line monolithic file returning one large HTML page
|
|
5
|
+
* template. Split into per-concern fragments under `./html/` (each tab is its
|
|
6
|
+
* own file; plus shared styles, header chrome, the repo-detail modal, and the
|
|
7
|
+
* client script). This file composes them back into the full document.
|
|
8
|
+
*
|
|
9
|
+
* `tierName` is threaded into the heading badge and the Configuration card.
|
|
10
|
+
*
|
|
11
|
+
* Fragment layout:
|
|
12
|
+
* headOpen() — <!DOCTYPE html>…<title>
|
|
13
|
+
* styles() — <style>…</style> (shared CSS, design-system level)
|
|
14
|
+
* bodyOpen(tier) — </head><body>…<h1>…</nav> (incl. High Score future tab)
|
|
15
|
+
* currentRepoTab(tier) — panel-current (cards + event stream + in-panel table)
|
|
16
|
+
* allReposTab() — panel-all (machine-wide repo table)
|
|
17
|
+
* summaryTab() — panel-summary (aggregate cards)
|
|
18
|
+
* highScoreTab() — panel-highscore (FUTURE placeholder, next milestone)
|
|
19
|
+
* repoDetailModal() — per-repo overlay (position-independent)
|
|
20
|
+
* script() — <script>…</script></body></html>
|
|
21
|
+
*/
|
|
22
|
+
import { headOpen } from "./html/head-open.js";
|
|
23
|
+
import { styles } from "./html/styles.js";
|
|
24
|
+
import { bodyOpen } from "./html/body-open.js";
|
|
25
|
+
import { currentRepoTab } from "./html/current-repo-tab.js";
|
|
26
|
+
import { allReposTab } from "./html/all-repos-tab.js";
|
|
27
|
+
import { summaryTab } from "./html/summary-tab.js";
|
|
28
|
+
import { highScoreTab } from "./html/high-score-tab.js";
|
|
29
|
+
import { repoDetailModal } from "./html/repo-detail-modal.js";
|
|
30
|
+
import { script } from "./html/script.js";
|
|
31
|
+
export function dashboardHtml(tierName) {
|
|
32
|
+
return (headOpen() +
|
|
33
|
+
styles() +
|
|
34
|
+
bodyOpen(tierName) +
|
|
35
|
+
currentRepoTab(tierName) +
|
|
36
|
+
repoDetailModal() +
|
|
37
|
+
allReposTab() +
|
|
38
|
+
summaryTab() +
|
|
39
|
+
highScoreTab() +
|
|
40
|
+
script());
|
|
41
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dashboard-server/server.ts — HTTP server creation + launch + CLI entry point.
|
|
3
3
|
*/
|
|
4
|
-
import { createServer } from "node:http";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
4
|
+
import { createServer, } from "node:http";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync, } from "node:fs";
|
|
6
6
|
import { join, dirname } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
@@ -16,12 +16,14 @@ export async function launchDashboardServer(stateDir) {
|
|
|
16
16
|
// detect a stale server (started by an older build) and replace it on
|
|
17
17
|
// upgrade instead of reuse it.
|
|
18
18
|
let SERVER_VERSION = "0.0.0";
|
|
19
|
+
// `here` is hoisted out of the version-detection try block so the
|
|
20
|
+
// dashboard-client dist path (Sprint B1) can reuse it without recompute.
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
22
|
try {
|
|
20
23
|
// Since v0.7.9 (8821ef3) dashboard-server.js lives at
|
|
21
24
|
// <pkg>/dist/extensions/dashboard-server/, so package.json is THREE levels
|
|
22
25
|
// up. Keep the two- and one-level-up candidates as fallbacks for flatter
|
|
23
26
|
// dev-checkout layouts. Guard each candidate so a missing file is skipped.
|
|
24
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
27
|
const candidates = [
|
|
26
28
|
join(here, "..", "..", "..", "package.json"),
|
|
27
29
|
join(here, "..", "..", "package.json"),
|
|
@@ -38,16 +40,68 @@ export async function launchDashboardServer(stateDir) {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
|
-
catch {
|
|
43
|
+
catch {
|
|
44
|
+
/* non-fatal */
|
|
45
|
+
}
|
|
42
46
|
// Lazy-loaded via require so the dashboard stays cheap to boot and we don't
|
|
43
47
|
// need a top-level await in the handler.
|
|
44
48
|
const driftReq = createRequire(import.meta.url);
|
|
45
|
-
const detectCrossRepoDrift = (idxDir) => driftReq("../../src/driftDetection.js")
|
|
46
|
-
.detectCrossRepoDrift(idxDir);
|
|
49
|
+
const detectCrossRepoDrift = (idxDir) => driftReq("../../src/driftDetection.js").detectCrossRepoDrift(idxDir);
|
|
47
50
|
const portFile = join(stateDir, "port.pid");
|
|
48
51
|
const snapshotPath = join(stateDir, "dashboard.json");
|
|
49
52
|
const eventsPath = join(stateDir, "events.log");
|
|
50
53
|
setLogPath(join(stateDir, "dashboard.log"));
|
|
54
|
+
// ── React client build (Sprint B1) ────────────────────────────────────
|
|
55
|
+
// If the Vite-built dashboard-client bundle is present, serve it as the
|
|
56
|
+
// dashboard UI (SPA fallback for all non-/api/* routes). If absent, fall
|
|
57
|
+
// back to the legacy inline html.ts template. Candidate paths cover both
|
|
58
|
+
// the dist/ build layout and a flat dev checkout (mirrors the package.json
|
|
59
|
+
// candidate pattern above).
|
|
60
|
+
const clientDistCandidates = [
|
|
61
|
+
join(here, "..", "dashboard-client", "dist"), // dist/extensions/dashboard-client/dist
|
|
62
|
+
join(here, "..", "..", "dashboard-client", "dist"), // dist/dashboard-client/dist (flat)
|
|
63
|
+
join(here, "..", "..", "..", "extensions", "dashboard-client", "dist"), // repo-root extensions/dashboard-client/dist (dist build)
|
|
64
|
+
join(here, "..", "dashboard-client", "dist"), // dev: extensions/dashboard-server/../dashboard-client/dist
|
|
65
|
+
];
|
|
66
|
+
const clientDist = clientDistCandidates.find((p) => existsSync(join(p, "index.html"))) ??
|
|
67
|
+
clientDistCandidates[0];
|
|
68
|
+
const clientIndexHtml = join(clientDist, "index.html");
|
|
69
|
+
const hasClientBuild = existsSync(clientIndexHtml);
|
|
70
|
+
if (hasClientBuild)
|
|
71
|
+
log("client build present", { clientDist });
|
|
72
|
+
// guardrails-allow PREVENT-PI-004: read-only static file serving from the local dashboard-client/dist bundle (loopback-only UI).
|
|
73
|
+
const serveClientAsset = (reqPath, res) => {
|
|
74
|
+
if (!hasClientBuild)
|
|
75
|
+
return false;
|
|
76
|
+
// Normalize: strip query, prevent path traversal, map "/" to index.html.
|
|
77
|
+
const clean = reqPath.split("?")[0];
|
|
78
|
+
if (clean.includes(".."))
|
|
79
|
+
return false;
|
|
80
|
+
const rel = clean === "/" || clean === "" ? "index.html" : clean.replace(/^\//, "");
|
|
81
|
+
const file = join(clientDist, rel);
|
|
82
|
+
if (!file.startsWith(clientDist) || !existsSync(file)) {
|
|
83
|
+
// SPA fallback: unknown non-asset routes serve index.html (client-side routing).
|
|
84
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
85
|
+
res.end(readFileSync(clientIndexHtml));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const ext = rel.slice(rel.lastIndexOf(".") + 1);
|
|
89
|
+
const types = {
|
|
90
|
+
html: "text/html; charset=utf-8",
|
|
91
|
+
js: "text/javascript",
|
|
92
|
+
css: "text/css",
|
|
93
|
+
json: "application/json",
|
|
94
|
+
svg: "image/svg+xml",
|
|
95
|
+
png: "image/png",
|
|
96
|
+
ico: "image/x-icon",
|
|
97
|
+
map: "application/json",
|
|
98
|
+
};
|
|
99
|
+
res.writeHead(200, {
|
|
100
|
+
"Content-Type": types[ext] ?? "application/octet-stream",
|
|
101
|
+
});
|
|
102
|
+
res.end(readFileSync(file));
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
51
105
|
log("launch invoked", { stateDir });
|
|
52
106
|
// ── Existing server? ───────────────────────────────────────────────────────
|
|
53
107
|
// A stale port.pid pointing at a dead/competing process is the classic cause
|
|
@@ -70,7 +124,9 @@ export async function launchDashboardServer(stateDir) {
|
|
|
70
124
|
log("reusing live server from port.pid", { port: info.port });
|
|
71
125
|
return { port: info.port, url: `http://localhost:${info.port}` }; // guardrails-allow PREVENT-PI-004: localhost dashboard URL (loopback-only)
|
|
72
126
|
}
|
|
73
|
-
log("port.pid present but no live server — treating as stale", {
|
|
127
|
+
log("port.pid present but no live server — treating as stale", {
|
|
128
|
+
port: info.port,
|
|
129
|
+
});
|
|
74
130
|
}
|
|
75
131
|
}
|
|
76
132
|
catch {
|
|
@@ -81,7 +137,9 @@ export async function launchDashboardServer(stateDir) {
|
|
|
81
137
|
try {
|
|
82
138
|
unlinkSync(portFile);
|
|
83
139
|
}
|
|
84
|
-
catch {
|
|
140
|
+
catch {
|
|
141
|
+
/* ignore */
|
|
142
|
+
}
|
|
85
143
|
}
|
|
86
144
|
// ── New server ────────────────────────────────────────────────────────────
|
|
87
145
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -112,7 +170,9 @@ export async function launchDashboardServer(stateDir) {
|
|
|
112
170
|
const prevCp = cur.checkpointCount;
|
|
113
171
|
const prevBytes = cur.compressedOriginalBytes;
|
|
114
172
|
const comp = snap.compression?.repo;
|
|
115
|
-
const liveSaved = comp
|
|
173
|
+
const liveSaved = comp
|
|
174
|
+
? comp.tokensFreed
|
|
175
|
+
: (snap.repo.tokensSaved ?? prevSaved);
|
|
116
176
|
const liveCp = snap.repo.checkpointCount ?? prevCp;
|
|
117
177
|
const liveBytes = snap.integrity?.compressedOriginalBytes ?? prevBytes;
|
|
118
178
|
cur.tokensSaved = liveSaved;
|
|
@@ -126,9 +186,14 @@ export async function launchDashboardServer(stateDir) {
|
|
|
126
186
|
idx.updatedAt = snap.updatedAt ?? idx.updatedAt;
|
|
127
187
|
}
|
|
128
188
|
const server = createServer((req, res) => {
|
|
129
|
-
// guardrails-allow PREVENT-PI-004: optional, user-triggered /dashboard localhost server (loopback-only) — CORS
|
|
130
|
-
// CORS for local access
|
|
131
|
-
|
|
189
|
+
// guardrails-allow PREVENT-PI-004: optional, user-triggered /dashboard localhost server (loopback-only) — CORS restricted to same-origin localhost browsers.
|
|
190
|
+
// CORS for local access — restricted to loopback origins (the dashboard server only binds to localhost).
|
|
191
|
+
const origin = req.headers.origin;
|
|
192
|
+
if (typeof origin === "string" &&
|
|
193
|
+
/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
|
|
194
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
195
|
+
res.setHeader("Vary", "Origin");
|
|
196
|
+
}
|
|
132
197
|
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, OPTIONS");
|
|
133
198
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
134
199
|
if (req.method === "OPTIONS") {
|
|
@@ -137,6 +202,10 @@ export async function launchDashboardServer(stateDir) {
|
|
|
137
202
|
return;
|
|
138
203
|
}
|
|
139
204
|
if (req.url === "/" || req.url === "/index.html") {
|
|
205
|
+
// Sprint B1: prefer the React client build when present; fall back to the
|
|
206
|
+
// legacy inline html.ts template when the client dist is absent.
|
|
207
|
+
if (serveClientAsset("/", res))
|
|
208
|
+
return;
|
|
140
209
|
const tier = readSnapshot(snapshotPath).tier;
|
|
141
210
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
142
211
|
res.end(dashboardHtml(tier));
|
|
@@ -184,7 +253,11 @@ export async function launchDashboardServer(stateDir) {
|
|
|
184
253
|
}
|
|
185
254
|
}
|
|
186
255
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
187
|
-
res.end(JSON.stringify({
|
|
256
|
+
res.end(JSON.stringify({
|
|
257
|
+
updatedAt: idx?.updatedAt ?? null,
|
|
258
|
+
repos,
|
|
259
|
+
count: repos.length,
|
|
260
|
+
}));
|
|
188
261
|
return;
|
|
189
262
|
}
|
|
190
263
|
// /api/summary — header tiles without the full repo list (keeps payload
|
|
@@ -219,14 +292,26 @@ export async function launchDashboardServer(stateDir) {
|
|
|
219
292
|
try {
|
|
220
293
|
const idx = readIndex();
|
|
221
294
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
222
|
-
const servers = (idx?.repos ?? [])
|
|
223
|
-
|
|
295
|
+
const servers = (idx?.repos ?? [])
|
|
296
|
+
.filter((r) => (r.lastSeen ?? 0) >= nowSec - ACTIVE_WINDOW_SEC)
|
|
297
|
+
.map((r) => {
|
|
298
|
+
const out = {
|
|
299
|
+
repoRoot: r.repoRoot,
|
|
300
|
+
displayName: r.displayName,
|
|
301
|
+
model: r.modelName,
|
|
302
|
+
provider: r.providerName,
|
|
303
|
+
lastSeen: r.lastSeen,
|
|
304
|
+
lastCompactedAt: r.lastCompactedAt,
|
|
305
|
+
};
|
|
224
306
|
try {
|
|
225
307
|
const p = join(r.stateDir, "dashboard.json");
|
|
226
308
|
if (existsSync(p)) {
|
|
227
309
|
const snap = JSON.parse(readFileSync(p, "utf-8"));
|
|
228
310
|
out.tier = snap.tier ?? null;
|
|
229
|
-
out.contextPct =
|
|
311
|
+
out.contextPct =
|
|
312
|
+
snap.context && snap.context.percent != null
|
|
313
|
+
? snap.context.percent
|
|
314
|
+
: null;
|
|
230
315
|
out.state = (snap.session && snap.session.state) || null;
|
|
231
316
|
out.cacheHits = snap.cacheHits ?? null;
|
|
232
317
|
out.compacts = snap.compacts ?? null;
|
|
@@ -234,9 +319,12 @@ export async function launchDashboardServer(stateDir) {
|
|
|
234
319
|
out.updatedAt = snap.updatedAt ?? null;
|
|
235
320
|
}
|
|
236
321
|
}
|
|
237
|
-
catch {
|
|
322
|
+
catch {
|
|
323
|
+
/* best-effort */
|
|
324
|
+
}
|
|
238
325
|
return out;
|
|
239
|
-
})
|
|
326
|
+
})
|
|
327
|
+
.sort((a, b) => b.lastSeen - a.lastSeen);
|
|
240
328
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
241
329
|
res.end(JSON.stringify({ updatedAt: new Date().toISOString(), servers }));
|
|
242
330
|
}
|
|
@@ -250,7 +338,7 @@ export async function launchDashboardServer(stateDir) {
|
|
|
250
338
|
res.writeHead(200, {
|
|
251
339
|
"Content-Type": "text/event-stream",
|
|
252
340
|
"Cache-Control": "no-cache",
|
|
253
|
-
|
|
341
|
+
Connection: "keep-alive",
|
|
254
342
|
});
|
|
255
343
|
// Drain existing events so the client starts with history
|
|
256
344
|
const { data: existing, offset: initialOffset } = readFrom(eventsPath, 0);
|
|
@@ -282,7 +370,9 @@ export async function launchDashboardServer(stateDir) {
|
|
|
282
370
|
try {
|
|
283
371
|
watcher = watch(eventsPath, onWatch);
|
|
284
372
|
}
|
|
285
|
-
catch {
|
|
373
|
+
catch {
|
|
374
|
+
/* give up */
|
|
375
|
+
}
|
|
286
376
|
}
|
|
287
377
|
if (existsSync(eventsPath)) {
|
|
288
378
|
startFileWatch();
|
|
@@ -325,7 +415,10 @@ export async function launchDashboardServer(stateDir) {
|
|
|
325
415
|
}
|
|
326
416
|
catch (e) {
|
|
327
417
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
328
|
-
res.end(JSON.stringify({
|
|
418
|
+
res.end(JSON.stringify({
|
|
419
|
+
error: "game_state_unavailable",
|
|
420
|
+
detail: String(e),
|
|
421
|
+
}));
|
|
329
422
|
}
|
|
330
423
|
return;
|
|
331
424
|
}
|
|
@@ -335,6 +428,7 @@ export async function launchDashboardServer(stateDir) {
|
|
|
335
428
|
let body = "";
|
|
336
429
|
let tooBig = false;
|
|
337
430
|
req.on("data", (chunk) => {
|
|
431
|
+
// guardrails-allow PREVENT-PI-004: loopback dashboard request body (local)
|
|
338
432
|
if (body.length > 65536) {
|
|
339
433
|
tooBig = true;
|
|
340
434
|
return;
|
|
@@ -359,7 +453,9 @@ export async function launchDashboardServer(stateDir) {
|
|
|
359
453
|
// Reject valid-but-non-object JSON (null/[]/42) — dereferencing
|
|
360
454
|
// patch.game_mode_on would throw an unhandled TypeError inside this
|
|
361
455
|
// 'end' listener and crash the detached server (audit P1: loopback DoS).
|
|
362
|
-
if (typeof patch !== "object" ||
|
|
456
|
+
if (typeof patch !== "object" ||
|
|
457
|
+
patch === null ||
|
|
458
|
+
Array.isArray(patch)) {
|
|
363
459
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
364
460
|
res.end(JSON.stringify({ error: "invalid_patch_object" }));
|
|
365
461
|
return;
|
|
@@ -380,7 +476,8 @@ export async function launchDashboardServer(stateDir) {
|
|
|
380
476
|
clean.theme = patch.theme;
|
|
381
477
|
}
|
|
382
478
|
if (patch.tui_display_mode != null) {
|
|
383
|
-
if (patch.tui_display_mode !== "full" &&
|
|
479
|
+
if (patch.tui_display_mode !== "full" &&
|
|
480
|
+
patch.tui_display_mode !== "minimal")
|
|
384
481
|
bad = true;
|
|
385
482
|
else
|
|
386
483
|
clean.tui_display_mode = patch.tui_display_mode;
|
|
@@ -397,7 +494,10 @@ export async function launchDashboardServer(stateDir) {
|
|
|
397
494
|
}
|
|
398
495
|
catch (e) {
|
|
399
496
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
400
|
-
res.end(JSON.stringify({
|
|
497
|
+
res.end(JSON.stringify({
|
|
498
|
+
error: "game_state_write_failed",
|
|
499
|
+
detail: String(e),
|
|
500
|
+
}));
|
|
401
501
|
}
|
|
402
502
|
});
|
|
403
503
|
return;
|
|
@@ -440,7 +540,10 @@ export async function launchDashboardServer(stateDir) {
|
|
|
440
540
|
}
|
|
441
541
|
catch (e) {
|
|
442
542
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
443
|
-
res.end(JSON.stringify({
|
|
543
|
+
res.end(JSON.stringify({
|
|
544
|
+
error: "game_scores_unavailable",
|
|
545
|
+
detail: String(e),
|
|
546
|
+
}));
|
|
444
547
|
}
|
|
445
548
|
return;
|
|
446
549
|
}
|
|
@@ -506,22 +609,53 @@ export async function launchDashboardServer(stateDir) {
|
|
|
506
609
|
if (parsed && typeof parsed === "object" && parsed.diag)
|
|
507
610
|
diag = parsed.diag;
|
|
508
611
|
}
|
|
509
|
-
catch {
|
|
612
|
+
catch {
|
|
613
|
+
/* dashboard.json not written yet */
|
|
614
|
+
}
|
|
510
615
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
511
616
|
res.end(JSON.stringify({
|
|
512
617
|
updatedAt: new Date().toISOString(),
|
|
513
618
|
windowMinutes: minutes,
|
|
514
619
|
sampleCount: rows.length,
|
|
515
|
-
turn_latency_ms: {
|
|
516
|
-
|
|
620
|
+
turn_latency_ms: {
|
|
621
|
+
p50: pct(get("turn_latency_ms"), 50),
|
|
622
|
+
p95: pct(get("turn_latency_ms"), 95),
|
|
623
|
+
n: get("turn_latency_ms").length,
|
|
624
|
+
},
|
|
625
|
+
provider_latency_ms: {
|
|
626
|
+
p50: pct(get("provider_latency_ms"), 50),
|
|
627
|
+
p95: pct(get("provider_latency_ms"), 95),
|
|
628
|
+
n: get("provider_latency_ms").length,
|
|
629
|
+
},
|
|
517
630
|
tps: { avg: avg(get("tps")), n: get("tps").length },
|
|
518
|
-
cache_hit_pct: {
|
|
519
|
-
|
|
520
|
-
|
|
631
|
+
cache_hit_pct: {
|
|
632
|
+
avg: avg(get("cache_hit_pct")),
|
|
633
|
+
latest: latest(get("cache_hit_pct")),
|
|
634
|
+
n: get("cache_hit_pct").length,
|
|
635
|
+
},
|
|
636
|
+
db_recompute_ms: {
|
|
637
|
+
p50: pct(get("db_recompute_ms"), 50),
|
|
638
|
+
p95: pct(get("db_recompute_ms"), 95),
|
|
639
|
+
n: get("db_recompute_ms").length,
|
|
640
|
+
},
|
|
641
|
+
disk_write_ms: {
|
|
642
|
+
p50: pct(get("disk_write_ms"), 50),
|
|
643
|
+
p95: pct(get("disk_write_ms"), 95),
|
|
644
|
+
n: get("disk_write_ms").length,
|
|
645
|
+
},
|
|
521
646
|
rss_mb: { latest: latest(get("rss_mb")), n: get("rss_mb").length },
|
|
522
|
-
heap_mb: {
|
|
523
|
-
|
|
524
|
-
|
|
647
|
+
heap_mb: {
|
|
648
|
+
latest: latest(get("heap_mb")),
|
|
649
|
+
n: get("heap_mb").length,
|
|
650
|
+
},
|
|
651
|
+
cpu_user_ms: {
|
|
652
|
+
latest: latest(get("cpu_user_ms")),
|
|
653
|
+
n: get("cpu_user_ms").length,
|
|
654
|
+
},
|
|
655
|
+
cpu_sys_ms: {
|
|
656
|
+
latest: latest(get("cpu_sys_ms")),
|
|
657
|
+
n: get("cpu_sys_ms").length,
|
|
658
|
+
},
|
|
525
659
|
diag,
|
|
526
660
|
}));
|
|
527
661
|
}
|
|
@@ -550,11 +684,21 @@ export async function launchDashboardServer(stateDir) {
|
|
|
550
684
|
}
|
|
551
685
|
catch (e) {
|
|
552
686
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
553
|
-
res.end(JSON.stringify({
|
|
687
|
+
res.end(JSON.stringify({
|
|
688
|
+
error: "achievements_unavailable",
|
|
689
|
+
detail: String(e),
|
|
690
|
+
}));
|
|
554
691
|
}
|
|
555
692
|
return;
|
|
556
693
|
}
|
|
557
|
-
// Fallback — serve the dashboard
|
|
694
|
+
// Fallback — serve the React client build (SPA route) or legacy dashboard.
|
|
695
|
+
// Non-/api/* GETs hit here: serve client assets if built, else inline HTML.
|
|
696
|
+
if (req.method === "GET" &&
|
|
697
|
+
req.url &&
|
|
698
|
+
!req.url.startsWith("/api/") &&
|
|
699
|
+
serveClientAsset(req.url, res)) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
558
702
|
const tier = readSnapshot(snapshotPath).tier;
|
|
559
703
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
560
704
|
res.end(dashboardHtml(tier));
|
|
@@ -592,7 +736,11 @@ export async function launchDashboardServer(stateDir) {
|
|
|
592
736
|
const v4Handler = server.listeners("request")[0];
|
|
593
737
|
if (v4Handler) {
|
|
594
738
|
v6 = createServer((r, s) => v4Handler.call(server, r, s));
|
|
595
|
-
v6.on("error", (e) => log("ipv6 loopback bind skipped", {
|
|
739
|
+
v6.on("error", (e) => log("ipv6 loopback bind skipped", {
|
|
740
|
+
port,
|
|
741
|
+
code: e.code,
|
|
742
|
+
message: e.message,
|
|
743
|
+
}));
|
|
596
744
|
v6.listen(port, "::1", () => log("ipv6 loopback bound", { port })); // guardrails-allow PREVENT-PI-004: IPv6 loopback (::1) mirror of the localhost dashboard server
|
|
597
745
|
}
|
|
598
746
|
// Write port.pid
|
|
@@ -607,12 +755,16 @@ export async function launchDashboardServer(stateDir) {
|
|
|
607
755
|
try {
|
|
608
756
|
unlinkSync(portFile);
|
|
609
757
|
}
|
|
610
|
-
catch {
|
|
758
|
+
catch {
|
|
759
|
+
/* already gone */
|
|
760
|
+
}
|
|
611
761
|
server.close();
|
|
612
762
|
try {
|
|
613
763
|
v6?.close();
|
|
614
764
|
}
|
|
615
|
-
catch {
|
|
765
|
+
catch {
|
|
766
|
+
/* not bound */
|
|
767
|
+
}
|
|
616
768
|
process.exit(0);
|
|
617
769
|
};
|
|
618
770
|
process.on("SIGTERM", cleanup);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function setupTailscaleServe(port = 3000, httpsPort = 443) {
|
|
2
|
+
// Tailscale serve: exposes localhost dashboard securely.
|
|
3
|
+
// Only activates when TAILSCALE_ENABLED=1 is set.
|
|
4
|
+
if (process.env.TAILSCALE_ENABLED !== "1")
|
|
5
|
+
return false;
|
|
6
|
+
console.log(`[tailscale] Serve enabled on port ${port} (https:${httpsPort})`);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dashboard-server/types.ts — shared types for the dashboard server.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports shared shapes from `api-contracts/` where they overlap with
|
|
5
|
+
* the legacy local types, while preserving 100% backward compatibility for
|
|
6
|
+
* all existing consumers (server.ts, index-reader.ts, snapshot.ts).
|
|
3
7
|
*/
|
|
4
8
|
// Active-window cutoff (seconds) for the /api/servers endpoint.
|
|
5
9
|
export const ACTIVE_WINDOW_SEC = 1800;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-repo SQLite connection management (open / reuse / close).
|
|
3
|
+
*
|
|
4
|
+
* In-process cache so the same stateDir reuses one connection (and so a fresh
|
|
5
|
+
* VectorStore over the same dir shares the open DB). Cross-process durability
|
|
6
|
+
* comes from reopening the same file path — proven by the integration test.
|
|
7
|
+
*/
|
|
8
|
+
import { DatabaseSync } from "node:sqlite";
|
|
9
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getStateDir } from "../../store.js";
|
|
12
|
+
import { initSchema } from "./schema.js";
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
/** Open (or reuse) the SQLite store for a state dir. */
|
|
15
|
+
export function openStore(stateDir = getStateDir()) {
|
|
16
|
+
const existing = cache.get(stateDir);
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
if (!existsSync(stateDir))
|
|
20
|
+
mkdirSync(stateDir, { recursive: true });
|
|
21
|
+
const db = new DatabaseSync(join(stateDir, "sqlite.db"));
|
|
22
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
23
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
24
|
+
initSchema(db);
|
|
25
|
+
cache.set(stateDir, db);
|
|
26
|
+
return db;
|
|
27
|
+
}
|
|
28
|
+
/** Close and evict a cached connection (test teardown only). */
|
|
29
|
+
export function closeStore(stateDir) {
|
|
30
|
+
const db = cache.get(stateDir);
|
|
31
|
+
if (db) {
|
|
32
|
+
db.close();
|
|
33
|
+
cache.delete(stateDir);
|
|
34
|
+
}
|
|
35
|
+
}
|