@tekmidian/pai 0.66.6 → 0.66.8
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/{aibroker-client-WCZi_tn-.mjs → aibroker-client-BfnVwGZl.mjs} +3 -3
- package/dist/{aibroker-client-WCZi_tn-.mjs.map → aibroker-client-BfnVwGZl.mjs.map} +1 -1
- package/dist/{aibroker-client-Ck3p3k_-.mjs → aibroker-client-C-O3Fflh.mjs} +1 -1
- package/dist/{auto-route-BnizyALK.mjs → auto-route-D8TBDNqb.mjs} +3 -3
- package/dist/{auto-route-BnizyALK.mjs.map → auto-route-D8TBDNqb.mjs.map} +1 -1
- package/dist/auto-route-DCxTKz19.mjs +7 -0
- package/dist/{chain-2_uoiEdD.mjs → chain-BL4khGgy.mjs} +4 -3
- package/dist/{chain-2_uoiEdD.mjs.map → chain-BL4khGgy.mjs.map} +1 -1
- package/dist/cli/index.mjs +20 -16
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +19 -15
- package/dist/{config-Ddc4DIPk.mjs → config-DjZELvoA.mjs} +12 -3
- package/dist/config-DjZELvoA.mjs.map +1 -0
- package/dist/{run-env-CQRnxNht.mjs → config-Dpe2JlbY.mjs} +5 -114
- package/dist/config-Dpe2JlbY.mjs.map +1 -0
- package/dist/{config-CnsDMETf.mjs → config-yomayAwe.mjs} +1 -1
- package/dist/{context-handover-cache-SFkWucuT.mjs → context-handover-cache-DFqxNh-H.mjs} +27 -13
- package/dist/context-handover-cache-DFqxNh-H.mjs.map +1 -0
- package/dist/daemon/index.mjs +20 -15
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-vLtT3yaI.mjs → daemon-CsR1S_Qr.mjs} +762 -24
- package/dist/daemon-CsR1S_Qr.mjs.map +1 -0
- package/dist/daemon-WrncnGua.mjs +23 -0
- package/dist/daemon-mcp/index.mjs +15 -12
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/embedding-gate-DqbKvn56.mjs +378 -0
- package/dist/embedding-gate-DqbKvn56.mjs.map +1 -0
- package/dist/embedding-gate-SJMCPwxG.mjs +5 -0
- package/dist/{embeddings-DMVzpvRZ.mjs → embeddings-BUjZMD9F.mjs} +16 -2
- package/dist/{embeddings-DMVzpvRZ.mjs.map → embeddings-BUjZMD9F.mjs.map} +1 -1
- package/dist/embeddings-DrJ-ewfi.mjs +3 -0
- package/dist/{env-DiolswKQ.mjs → env-CPCNHz3i.mjs} +1 -1
- package/dist/{env-DiolswKQ.mjs.map → env-CPCNHz3i.mjs.map} +1 -1
- package/dist/factory-DjtXElOI.mjs +9 -0
- package/dist/{factory-CXFHwvcv.mjs → factory-yElzJc6f.mjs} +31 -9
- package/dist/factory-yElzJc6f.mjs.map +1 -0
- package/dist/{fallback-DNzd3jJc.mjs → fallback-2yWDDYiT.mjs} +5 -4
- package/dist/{fallback-DNzd3jJc.mjs.map → fallback-2yWDDYiT.mjs.map} +1 -1
- package/dist/{helpers-CZsi_49C.mjs → helpers-B9LHRJ8R.mjs} +1 -1
- package/dist/{helpers-CZsi_49C.mjs.map → helpers-B9LHRJ8R.mjs.map} +1 -1
- package/dist/hooks/block-sleep-poll.mjs +7 -2
- package/dist/hooks/block-sleep-poll.mjs.map +3 -3
- package/dist/hooks/context-compression-hook.mjs +2 -0
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-core-context.mjs +5 -5
- package/dist/hooks/load-core-context.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +2 -0
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/post-compact-inject.mjs +2 -0
- package/dist/hooks/post-compact-inject.mjs.map +2 -2
- package/dist/hooks/route-agents-to-worker.mjs +2 -0
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs +2 -0
- package/dist/hooks/security-validator.mjs.map +2 -2
- package/dist/hooks/whisper-rules.mjs +2 -0
- package/dist/hooks/whisper-rules.mjs.map +2 -2
- package/dist/hooks/worker-guard.mjs +82 -5
- package/dist/hooks/worker-guard.mjs.map +4 -4
- package/dist/hooks/worker-proxy.mjs +2 -0
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +7 -2
- package/dist/hooks/worker-status-line.mjs.map +3 -3
- package/dist/hooks/worker-supervision.mjs +2 -0
- package/dist/hooks/worker-supervision.mjs.map +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +7 -4
- package/dist/{ipc-client-DkxRay-o.mjs → ipc-client-DVZ_EGnu.mjs} +2 -2
- package/dist/{ipc-client-DkxRay-o.mjs.map → ipc-client-DVZ_EGnu.mjs.map} +1 -1
- package/dist/main-resolver-DjQsexge.mjs +8 -0
- package/dist/{main-resolver-SfGGg1DB.mjs → main-resolver-Dxvn4Mv8.mjs} +7 -6
- package/dist/{main-resolver-SfGGg1DB.mjs.map → main-resolver-Dxvn4Mv8.mjs.map} +1 -1
- package/dist/{pai-marker-DXVpFsYz.mjs → pai-marker-1B7iC7oq.mjs} +121 -3
- package/dist/pai-marker-1B7iC7oq.mjs.map +1 -0
- package/dist/{planner-C-gzg7ye.mjs → planner-CspP59ya.mjs} +13 -10
- package/dist/{planner-C-gzg7ye.mjs.map → planner-CspP59ya.mjs.map} +1 -1
- package/dist/{postgres-BxmyQHoA.mjs → postgres-CCAGq1qp.mjs} +86 -5
- package/dist/postgres-CCAGq1qp.mjs.map +1 -0
- package/dist/postgres-CzkEPfAm.mjs +7 -0
- package/dist/{program-CfG1T_ko.mjs → program-cWqZI0_L.mjs} +231 -125
- package/dist/program-cWqZI0_L.mjs.map +1 -0
- package/dist/{query-feedback-BPdapXKd.mjs → query-feedback-GPeVGvIa.mjs} +2 -2
- package/dist/{query-feedback-BPdapXKd.mjs.map → query-feedback-GPeVGvIa.mjs.map} +1 -1
- package/dist/{reranker-3lnggwgq.mjs → reranker-BTiOia8Y.mjs} +1 -1
- package/dist/{reranker-3lnggwgq.mjs.map → reranker-BTiOia8Y.mjs.map} +1 -1
- package/dist/{reranker-CZ2mP4cf.mjs → reranker-YwDT2n5e.mjs} +1 -1
- package/dist/{router-BaTbc9VX.mjs → router-Bk_ySIZT.mjs} +2 -2
- package/dist/{router-BaTbc9VX.mjs.map → router-Bk_ySIZT.mjs.map} +1 -1
- package/dist/router-DyiZNI6o.mjs +3 -0
- package/dist/{run-ChlIbDJ0.mjs → run-BZNdpUGQ.mjs} +226 -1797
- package/dist/run-BZNdpUGQ.mjs.map +1 -0
- package/dist/run-env-Cd0Son-Y.mjs +115 -0
- package/dist/run-env-Cd0Son-Y.mjs.map +1 -0
- package/dist/{pai-home-UncxWxlX.mjs → runtime-paths-DkZoxofH.mjs} +50 -2
- package/dist/runtime-paths-DkZoxofH.mjs.map +1 -0
- package/dist/{search-CNAGTiJP.mjs → search-cPDj-9fO.mjs} +2 -2
- package/dist/{search-CNAGTiJP.mjs.map → search-cPDj-9fO.mjs.map} +1 -1
- package/dist/{sources-CM2g-CLT.mjs → sources-CvvzNZad.mjs} +1 -1
- package/dist/{sources-CM2g-CLT.mjs.map → sources-CvvzNZad.mjs.map} +1 -1
- package/dist/{stop-words-DtxaTWU_.mjs → stop-words-LtkS-Pej.mjs} +1 -1
- package/dist/{stop-words-DtxaTWU_.mjs.map → stop-words-LtkS-Pej.mjs.map} +1 -1
- package/dist/worktree-CwMRTa0H.mjs +1578 -0
- package/dist/worktree-CwMRTa0H.mjs.map +1 -0
- package/dist/{zettelkasten-BRIaHzFm.mjs → zettelkasten-DSIe6wpF.mjs} +6 -5
- package/dist/zettelkasten-DSIe6wpF.mjs.map +1 -0
- package/dist/{zettelkasten-G0-ZTybr.mjs → zettelkasten-DtkAvA18.mjs} +5 -3
- package/docker/docker-compose.yml +4 -0
- package/docs/commands/README.md +6 -0
- package/docs/commands/memory.md +52 -0
- package/docs/commands/worker.md +22 -0
- package/docs/install-linux.md +1 -0
- package/docs/memory.md +69 -0
- package/package.json +1 -1
- package/src/hooks/ts/lib/sleep-poll-gate.test.ts +2 -2
- package/src/hooks/ts/lib/sleep-poll-gate.ts +4 -4
- package/src/hooks/ts/lib/worker-guard-longwait.test.ts +48 -0
- package/src/hooks/ts/lib/worker-guard.ts +50 -6
- package/src/hooks/ts/pre-tool-use/block-sleep-poll.test.ts +1 -1
- package/src/hooks/ts/session-start/load-core-context.ts +11 -8
- package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +34 -2
- package/dist/auto-route-D6fW1Q8z.mjs +0 -3
- package/dist/config-Ddc4DIPk.mjs.map +0 -1
- package/dist/context-handover-cache-SFkWucuT.mjs.map +0 -1
- package/dist/daemon-BHQoEjt0.mjs +0 -18
- package/dist/daemon-vLtT3yaI.mjs.map +0 -1
- package/dist/embeddings-D0WOQpYR.mjs +0 -3
- package/dist/factory-CXFHwvcv.mjs.map +0 -1
- package/dist/factory-CnKsU6zX.mjs +0 -8
- package/dist/main-resolver-DrHtx8iW.mjs +0 -7
- package/dist/pai-home-UncxWxlX.mjs.map +0 -1
- package/dist/pai-marker-DXVpFsYz.mjs.map +0 -1
- package/dist/postgres-B2BEBT-J.mjs +0 -5
- package/dist/postgres-BxmyQHoA.mjs.map +0 -1
- package/dist/program-CfG1T_ko.mjs.map +0 -1
- package/dist/router-BK-hFeQ6.mjs +0 -3
- package/dist/run-ChlIbDJ0.mjs.map +0 -1
- package/dist/run-env-CQRnxNht.mjs.map +0 -1
- package/dist/runtime-paths-Bw3Jk8qJ.mjs +0 -52
- package/dist/runtime-paths-Bw3Jk8qJ.mjs.map +0 -1
- package/dist/zettelkasten-BRIaHzFm.mjs.map +0 -1
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { n as
|
|
6
|
-
import {
|
|
7
|
-
import { i as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { i as
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
1
|
+
import { S as DEFAULT_NOTIFICATION_CONFIG, d as readMainConfigRaw, f as writeMainConfigRaw } from "./config-DjZELvoA.mjs";
|
|
2
|
+
import { u as paiHomePath } from "./runtime-paths-DkZoxofH.mjs";
|
|
3
|
+
import { T as readWorkersSection } from "./config-Dpe2JlbY.mjs";
|
|
4
|
+
import { E as isChatPane, O as loadStatus, U as eventsPath, W as ledgerPath, _ as appendLedger, j as nowStamp, k as loadStatuses, q as workersLogDir, x as alive } from "./worktree-CwMRTa0H.mjs";
|
|
5
|
+
import { c as yieldToEventLoop, f as fileContentHash, l as buildOutline, n as chunkId, u as chunkMarkdown } from "./helpers-B9LHRJ8R.mjs";
|
|
6
|
+
import { n as TITLE_STOP_WORDS, t as STOP_WORDS } from "./stop-words-LtkS-Pej.mjs";
|
|
7
|
+
import { i as populateSlugs, t as applyRecencyBoost } from "./search-cPDj-9fO.mjs";
|
|
8
|
+
import { n as rerankResults } from "./reranker-BTiOia8Y.mjs";
|
|
9
|
+
import { c as resolveFromModule, n as embedChunksWithBackend } from "./postgres-CCAGq1qp.mjs";
|
|
10
|
+
import { d as getBackendOutage, n as createRegistryBackend, r as createStorageBackend } from "./factory-yElzJc6f.mjs";
|
|
11
|
+
import { a as requireQueryEmbedding, g as mismatchReason, h as bindingMatches, l as getConfiguredBackend, n as embedQuery } from "./embedding-gate-DqbKvn56.mjs";
|
|
12
|
+
import { i as deserializeEmbedding, n as configureEmbeddingModel, r as cosineSimilarity$1 } from "./embeddings-BUjZMD9F.mjs";
|
|
13
|
+
import { t as routeNotification } from "./router-Bk_ySIZT.mjs";
|
|
14
|
+
import { $ as shutdownRequested, A as indexInProgress, B as setEmbedInProgress, C as handleSessionSummary, D as daemonConfig, E as cacheKeepaliveTimer, F as notificationConfig, G as setLastIndexTime, H as setIndexInProgress, I as passFailures, It as formatDetectionJson, J as setRegistryBackend, K as setLastVaultIndexTime, L as registryBackend, M as lastEmbedTime, N as lastIndexTime, O as embedInProgress, P as lastVaultIndexTime, Pt as detectProject, Q as setVaultIndexInProgress, R as setCacheKeepaliveTimer, S as getGitContext, T as spawnSummarizer, Tt as performScan, U as setIndexSchedulerTimer, V as setEmbedSchedulerTimer, W as setLastEmbedTime, X as setStartTime, Y as setShutdownRequested, Z as setStorageBackend, _ as markCompleted, _t as buildWakeupContext, at as updateTodoContinue, b as contentToText$2, c as removeDaemonPidfile, d as cleanup, dt as addWorkToSessionNote, et as startTime, f as dequeue, ft as appendCheckpoint, g as loadQueue, gt as findNotesDir, h as hasPendingOrProcessingOfType, ht as archiveSessionFilesToSessionsDir, i as runSessionKeepaliveTick, it as sessionIdFromTranscript, j as indexSchedulerTimer, k as embedSchedulerTimer, m as getStats, mt as getCurrentNotePath, n as writeContextHandoverCache, nt as vaultIndexInProgress, p as enqueue, pt as finalizeSessionNote, q as setNotificationConfig, t as readContextHandoverCache, tt as storageBackend, u as writeDaemonPidfile, v as markFailed, x as findLatestJsonl, xt as runPassChild, yt as PassSpawnError, z as setDaemonConfig } from "./context-handover-cache-DFqxNh-H.mjs";
|
|
15
|
+
import { C as itermForClaudeSession, n as runWorker } from "./run-BZNdpUGQ.mjs";
|
|
16
|
+
import { r as saveQueryResult } from "./query-feedback-GPeVGvIa.mjs";
|
|
17
|
+
import { i as sendToSession } from "./aibroker-client-BfnVwGZl.mjs";
|
|
18
|
+
import { n as formatAutoRouteJson, r as detectTopicShift, t as autoRoute } from "./auto-route-D8TBDNqb.mjs";
|
|
19
|
+
import { a as zettelThemes, c as zettelExplore, i as zettelHealth, n as zettelGodNotes, o as zettelConverse, r as zettelSuggest, s as zettelSurprise, t as zettelCommunities } from "./zettelkasten-DSIe6wpF.mjs";
|
|
17
20
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
18
21
|
import { setPriority } from "node:os";
|
|
19
22
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
23
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
20
24
|
import { connect, createServer } from "node:net";
|
|
21
|
-
import {
|
|
25
|
+
import { createServer as createServer$1 } from "node:http";
|
|
26
|
+
import { promisify } from "node:util";
|
|
27
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
22
28
|
|
|
23
29
|
//#region src/notifications/config.ts
|
|
24
30
|
/**
|
|
@@ -2152,10 +2158,15 @@ async function toolMemorySearch(registry, federation, params, searchDefaults) {
|
|
|
2152
2158
|
maxResults: params.limit ?? searchDefaults?.defaultLimit ?? 5
|
|
2153
2159
|
};
|
|
2154
2160
|
let results;
|
|
2161
|
+
let embedNote = "";
|
|
2155
2162
|
if (mode === "keyword") results = await federation.searchKeyword(params.query, searchOpts);
|
|
2156
2163
|
else if (mode === "semantic" || mode === "hybrid") {
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2164
|
+
const q = await embedQuery(federation, params.query);
|
|
2165
|
+
const queryEmbedding = q.vec;
|
|
2166
|
+
if (!queryEmbedding) {
|
|
2167
|
+
embedNote = `\nNote: ${q.note}`;
|
|
2168
|
+
results = await federation.searchKeyword(params.query, searchOpts);
|
|
2169
|
+
} else if (mode === "semantic") results = await federation.searchSemantic(queryEmbedding, searchOpts);
|
|
2159
2170
|
else {
|
|
2160
2171
|
const [kwResults, semResults] = await Promise.all([federation.searchKeyword(params.query, {
|
|
2161
2172
|
...searchOpts,
|
|
@@ -2192,7 +2203,7 @@ async function toolMemorySearch(registry, federation, params, searchDefaults) {
|
|
|
2192
2203
|
const withSlugs = await populateSlugs(results, registry);
|
|
2193
2204
|
if (withSlugs.length === 0) return { content: [{
|
|
2194
2205
|
type: "text",
|
|
2195
|
-
text: `No results found for query: "${params.query}" (mode: ${mode})`
|
|
2206
|
+
text: `No results found for query: "${params.query}" (mode: ${mode})${embedNote}`
|
|
2196
2207
|
}] };
|
|
2197
2208
|
const rerankLabel = shouldRerank ? " +rerank" : "";
|
|
2198
2209
|
const useCompact = params.format === "compact";
|
|
@@ -2216,7 +2227,7 @@ async function toolMemorySearch(registry, federation, params, searchDefaults) {
|
|
|
2216
2227
|
} catch {}
|
|
2217
2228
|
return { content: [{
|
|
2218
2229
|
type: "text",
|
|
2219
|
-
text: `Found ${withSlugs.length} result(s) for "${params.query}" (mode: ${mode}${rerankLabel})
|
|
2230
|
+
text: `Found ${withSlugs.length} result(s) for "${params.query}" (mode: ${mode}${rerankLabel}):${embedNote}\n\n${formatted}`
|
|
2220
2231
|
}] };
|
|
2221
2232
|
} catch (e) {
|
|
2222
2233
|
return {
|
|
@@ -3419,10 +3430,12 @@ async function bfsExpand(federation, entityNames, hops, projectId) {
|
|
|
3419
3430
|
*/
|
|
3420
3431
|
async function rerankTriples(triples, queryVec, maxTriples) {
|
|
3421
3432
|
if (triples.length === 0) return [];
|
|
3433
|
+
const embedder = getConfiguredBackend();
|
|
3422
3434
|
return (await Promise.all(triples.map(async (triple) => {
|
|
3423
3435
|
const text = `${triple.subject} ${triple.predicate} ${triple.object}`;
|
|
3424
3436
|
try {
|
|
3425
|
-
const
|
|
3437
|
+
const [vec] = await embedder.embed([text]);
|
|
3438
|
+
const score = cosineSimilarity$1(queryVec, vec);
|
|
3426
3439
|
return {
|
|
3427
3440
|
...triple,
|
|
3428
3441
|
relevanceScore: score
|
|
@@ -3511,7 +3524,7 @@ async function toolMemoryKgSearch(federation, params) {
|
|
|
3511
3524
|
}],
|
|
3512
3525
|
isError: true
|
|
3513
3526
|
};
|
|
3514
|
-
const result = await graphCompletionSearch(federation, await
|
|
3527
|
+
const result = await graphCompletionSearch(federation, await requireQueryEmbedding(federation, params.query), {
|
|
3515
3528
|
seedCount: params.wide_k ?? 50,
|
|
3516
3529
|
hops: params.neighborhood_depth ?? 1,
|
|
3517
3530
|
maxTriples: params.top_k ?? 20,
|
|
@@ -4361,6 +4374,7 @@ async function handleRequest(request, socket) {
|
|
|
4361
4374
|
lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,
|
|
4362
4375
|
vaultPath: daemonConfig.vaultPath ?? null,
|
|
4363
4376
|
workQueue: getStats(),
|
|
4377
|
+
dashboard: daemonConfig.dashboard.enabled ? `http://${daemonConfig.dashboard.bind}:${daemonConfig.dashboard.port}/` : null,
|
|
4364
4378
|
passFailures: Object.fromEntries([...passFailures].map(([k, v]) => [k, {
|
|
4365
4379
|
...v,
|
|
4366
4380
|
at: new Date(v.at).toISOString()
|
|
@@ -4819,6 +4833,725 @@ async function handleRequest(request, socket) {
|
|
|
4819
4833
|
socket.end();
|
|
4820
4834
|
}
|
|
4821
4835
|
|
|
4836
|
+
//#endregion
|
|
4837
|
+
//#region src/daemon/dashboard-page.ts
|
|
4838
|
+
/** The dashboard page: one self-contained file (inline SVG + CSS), updated in place with textContent/attributes only. */
|
|
4839
|
+
const DASHBOARD_HTML = `<!doctype html>
|
|
4840
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
4841
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4842
|
+
<title>PAI memory</title>
|
|
4843
|
+
<style>
|
|
4844
|
+
:root { color-scheme: light dark; --green:#2e9e5b; --amber:#d99a1c; --red:#d64545; --grey:#8a8f98;
|
|
4845
|
+
--bg:#f2f3f5; --card:#fff; --fg:#1b1d21; --mute:#6b7280; --track:rgba(128,128,128,.25); --new:#2e9e5b; --old:#d99a1c; --miss:#8a8f98; }
|
|
4846
|
+
@media (prefers-color-scheme: dark) { :root { --bg:#111317; --card:#1b1e24; --fg:#e8eaed; --mute:#9aa0a8; } }
|
|
4847
|
+
* { box-sizing: border-box; }
|
|
4848
|
+
body { font: 15px/1.4 -apple-system, system-ui, sans-serif; margin: 0; padding: 12px; background: var(--bg); color: var(--fg); }
|
|
4849
|
+
main { max-width: 1000px; margin: 0 auto; }
|
|
4850
|
+
header { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
4851
|
+
h1 { font-size: 20px; margin: 0; }
|
|
4852
|
+
.pill { color: #fff; font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: .05em; padding: 4px 12px; border-radius: 999px; background: var(--grey); }
|
|
4853
|
+
.pill.green { background: var(--green); } .pill.amber { background: var(--amber); } .pill.red { background: var(--red); }
|
|
4854
|
+
#reason { color: var(--mute); flex: 1 1 200px; }
|
|
4855
|
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
4856
|
+
.wide { grid-column: 1 / -1; }
|
|
4857
|
+
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
|
|
4858
|
+
.card { background: var(--card); border-radius: 12px; padding: 12px 14px; border-top: 4px solid var(--grey); box-shadow: 0 1px 3px rgba(0,0,0,.12); min-width: 0; }
|
|
4859
|
+
.card.green { border-color: var(--green); } .card.amber { border-color: var(--amber); } .card.red { border-color: var(--red); }
|
|
4860
|
+
h2 { font-size: 12px; margin: 0 0 8px; text-transform: uppercase; letter-spacing: .06em; color: var(--mute); }
|
|
4861
|
+
.k { color: var(--mute); } .bad { color: var(--red); font-weight: 600; }
|
|
4862
|
+
.big { font-size: 34px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
|
4863
|
+
.unit { font-size: 14px; font-weight: 400; color: var(--mute); }
|
|
4864
|
+
.job { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
|
|
4865
|
+
.ring { width: 150px; height: 150px; flex: none; }
|
|
4866
|
+
.ring .bg { stroke: var(--track); } .ring .fg { stroke: var(--green); transition: stroke-dashoffset .6s; }
|
|
4867
|
+
.ring text { fill: var(--fg); text-anchor: middle; }
|
|
4868
|
+
.stats { flex: 1 1 200px; min-width: 0; display: grid; gap: 6px; }
|
|
4869
|
+
svg { display: block; }
|
|
4870
|
+
.spark { width: 100%; height: 56px; }
|
|
4871
|
+
.spark polyline { fill: none; stroke: var(--green); stroke-width: 2; vector-effect: non-scaling-stroke; }
|
|
4872
|
+
.spark polygon { fill: var(--green); opacity: .15; }
|
|
4873
|
+
.stack { width: 100%; height: 22px; border-radius: 6px; }
|
|
4874
|
+
.legend { display: grid; gap: 4px; margin: 8px 0; }
|
|
4875
|
+
.legend div { display: flex; align-items: center; gap: 8px; }
|
|
4876
|
+
.legend i { width: 12px; height: 12px; border-radius: 3px; flex: none; }
|
|
4877
|
+
.legend b { margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
4878
|
+
.dot { width: 12px; height: 12px; border-radius: 50%; background: var(--grey); display: inline-block; flex: none; }
|
|
4879
|
+
.dot.green { background: var(--green); } .dot.amber { background: var(--amber); } .dot.red { background: var(--red); }
|
|
4880
|
+
.line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
4881
|
+
.gauge { width: 100%; height: 14px; border-radius: 7px; }
|
|
4882
|
+
.pass { padding: 6px 0; border-top: 1px solid var(--track); }
|
|
4883
|
+
.pass:first-of-type { border-top: 0; }
|
|
4884
|
+
.pass .tl { width: 100%; height: 16px; }
|
|
4885
|
+
.pass .tl line { stroke: var(--track); stroke-width: 3; stroke-linecap: round; }
|
|
4886
|
+
.times { display: flex; justify-content: space-between; gap: 8px; color: var(--mute); font-size: 13px; flex-wrap: wrap; }
|
|
4887
|
+
.err { color: var(--red); font-size: 13px; word-break: break-word; }
|
|
4888
|
+
.kv { display: flex; flex-wrap: wrap; gap: 4px 18px; }
|
|
4889
|
+
.kv span span { color: var(--mute); margin-right: 4px; }
|
|
4890
|
+
#foot { color: var(--mute); font-size: 12px; margin-top: 12px; }
|
|
4891
|
+
[hidden] { display: none !important; }
|
|
4892
|
+
</style></head><body><main>
|
|
4893
|
+
<header><h1>PAI memory</h1><span class="pill" id="pill">…</span><span id="reason">loading…</span></header>
|
|
4894
|
+
<div class="grid">
|
|
4895
|
+
<section class="card wide" id="jobs-empty"><h2>Re-embed job</h2><span class="k">no job running</span></section>
|
|
4896
|
+
<div class="wide" id="jobs" style="display:contents"></div>
|
|
4897
|
+
|
|
4898
|
+
<section class="card" id="index"><h2>Index</h2>
|
|
4899
|
+
<div class="line"><span class="big" id="ix-cov">?</span><span class="unit" id="ix-chunks"></span></div>
|
|
4900
|
+
<svg class="stack" id="ix-stack" viewBox="0 0 100 10" preserveAspectRatio="none" role="img" aria-label="vector distribution">
|
|
4901
|
+
<rect id="sg-new" x="0" y="0" width="0" height="10" fill="var(--new)"/>
|
|
4902
|
+
<rect id="sg-old" x="0" y="0" width="0" height="10" fill="var(--old)"/>
|
|
4903
|
+
<rect id="sg-miss" x="0" y="0" width="0" height="10" fill="var(--miss)"/>
|
|
4904
|
+
</svg>
|
|
4905
|
+
<div class="legend">
|
|
4906
|
+
<div id="lg-new"><i style="background:var(--new)"></i><span>new backend</span><b id="n-new">0</b></div>
|
|
4907
|
+
<div id="lg-old"><i style="background:var(--old)"></i><span id="t-old">embedded</span><b id="n-old">0</b></div>
|
|
4908
|
+
<div><i style="background:var(--miss)"></i><span>missing</span><b id="n-miss">0</b></div>
|
|
4909
|
+
</div>
|
|
4910
|
+
<div><span class="k">files</span> <span id="ix-files">?</span></div>
|
|
4911
|
+
<div><span class="k">bound to</span> <span id="ix-bound">?</span>, <span class="k">configured</span> <span id="ix-conf">?</span></div>
|
|
4912
|
+
<div class="bad" id="ix-hint" hidden></div>
|
|
4913
|
+
</section>
|
|
4914
|
+
|
|
4915
|
+
<section class="card" id="backend"><h2>Backend</h2>
|
|
4916
|
+
<div class="line"><span class="dot" id="be-dot"></span><b id="be-id">?</b><span id="be-state" class="k"></span></div>
|
|
4917
|
+
<div class="k" id="be-reason"></div>
|
|
4918
|
+
<div style="margin-top:10px"><span class="k">loaded model</span> <b id="be-model">none</b></div>
|
|
4919
|
+
<div class="line" style="margin-top:8px"><span class="big" id="gpu-pct">?</span><span class="unit">GPU</span></div>
|
|
4920
|
+
<svg class="gauge" viewBox="0 0 100 10" preserveAspectRatio="none" role="img" aria-label="GPU share">
|
|
4921
|
+
<rect x="0" y="0" width="100" height="10" fill="var(--track)"/>
|
|
4922
|
+
<rect id="gpu-bar" x="0" y="0" width="0" height="10" fill="var(--green)"/>
|
|
4923
|
+
</svg>
|
|
4924
|
+
</section>
|
|
4925
|
+
|
|
4926
|
+
<section class="card" id="passes"><h2>Passes</h2><div id="pass-list"></div></section>
|
|
4927
|
+
|
|
4928
|
+
<section class="card" id="daemon"><h2>Daemon</h2>
|
|
4929
|
+
<div class="kv">
|
|
4930
|
+
<span><span>pid</span><b id="d-pid">?</b></span><span><span>uptime</span><b id="d-up">?</b></span>
|
|
4931
|
+
<span><span>version</span><b id="d-ver">?</b></span><span><span>storage</span><b id="d-sto">?</b></span>
|
|
4932
|
+
</div>
|
|
4933
|
+
</section>
|
|
4934
|
+
</div>
|
|
4935
|
+
<div id="foot"></div>
|
|
4936
|
+
|
|
4937
|
+
<template id="job-tpl"><section class="card">
|
|
4938
|
+
<h2></h2>
|
|
4939
|
+
<div class="job">
|
|
4940
|
+
<svg class="ring" viewBox="0 0 120 120" role="img" aria-label="progress">
|
|
4941
|
+
<circle class="bg" cx="60" cy="60" r="52" fill="none" stroke-width="12"/>
|
|
4942
|
+
<circle class="fg" cx="60" cy="60" r="52" fill="none" stroke-width="12" stroke-linecap="round" transform="rotate(-90 60 60)" stroke-dasharray="326.73" stroke-dashoffset="326.73"/>
|
|
4943
|
+
<text class="pct" x="60" y="68" font-size="24" font-weight="700">0%</text>
|
|
4944
|
+
</svg>
|
|
4945
|
+
<div class="stats">
|
|
4946
|
+
<div><span class="big rate">?</span> <span class="unit">chunks/s</span></div>
|
|
4947
|
+
<div><span class="k">done</span> <b class="done">?</b></div>
|
|
4948
|
+
<div><span class="k">ETA</span> <b class="eta">?</b> <span class="k fin"></span></div>
|
|
4949
|
+
<div class="k phase"></div>
|
|
4950
|
+
<div class="bad stall" hidden>STALLED</div>
|
|
4951
|
+
</div>
|
|
4952
|
+
</div>
|
|
4953
|
+
<div class="k" style="margin-top:8px;font-size:12px">rate, last hour</div>
|
|
4954
|
+
<svg class="spark" viewBox="0 0 240 56" preserveAspectRatio="none" role="img" aria-label="rate sparkline">
|
|
4955
|
+
<polygon points=""/><polyline points=""/>
|
|
4956
|
+
</svg>
|
|
4957
|
+
</section></template>
|
|
4958
|
+
|
|
4959
|
+
<script>
|
|
4960
|
+
const $ = (id) => document.getElementById(id);
|
|
4961
|
+
const n = (x) => (x === null || x === undefined ? "?" : Number(x).toLocaleString());
|
|
4962
|
+
const when = (t) => (t ? new Date(t).toLocaleString([], { dateStyle: "short", timeStyle: "short" }) : "never");
|
|
4963
|
+
const clock = (t) => new Date(t).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
4964
|
+
function dur(secs) {
|
|
4965
|
+
if (secs === null || secs === undefined) return "?";
|
|
4966
|
+
const h = Math.floor(secs / 3600), m = Math.floor((secs % 3600) / 60);
|
|
4967
|
+
return h >= 24 ? Math.floor(h / 24) + "d " + (h % 24) + "h" : h > 0 ? h + "h " + m + "m" : m + "m";
|
|
4968
|
+
}
|
|
4969
|
+
const set = (id, text) => { const e = $(id); if (e.textContent !== text) e.textContent = text; };
|
|
4970
|
+
const lvl = (e, base, level) => { e.className = base + " " + (level || ""); };
|
|
4971
|
+
const CIRC = 326.73;
|
|
4972
|
+
|
|
4973
|
+
function sparkPoints(history) {
|
|
4974
|
+
const r = [];
|
|
4975
|
+
for (let i = 1; i < history.length; i++) {
|
|
4976
|
+
const dt = (history[i].t - history[i - 1].t) / 1000;
|
|
4977
|
+
r.push(dt > 0 ? Math.max(0, (history[i].done - history[i - 1].done) / dt) : 0);
|
|
4978
|
+
}
|
|
4979
|
+
if (r.length < 2) return ["", ""];
|
|
4980
|
+
const max = Math.max(...r, 0.001);
|
|
4981
|
+
const pts = r.map((v, i) => (i / (r.length - 1) * 240).toFixed(1) + "," + (52 - (v / max) * 48).toFixed(1));
|
|
4982
|
+
return [pts.join(" "), "0,56 " + pts.join(" ") + " 240,56"];
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
const jobEls = new Map();
|
|
4986
|
+
function renderJobs(s) {
|
|
4987
|
+
const names = new Set(s.jobs.map((j) => j.name));
|
|
4988
|
+
for (const [name, e] of jobEls) if (!names.has(name)) { e.remove(); jobEls.delete(name); }
|
|
4989
|
+
$("jobs-empty").hidden = s.jobs.length > 0;
|
|
4990
|
+
for (const j of s.jobs) {
|
|
4991
|
+
let e = jobEls.get(j.name);
|
|
4992
|
+
if (!e) { e = $("job-tpl").content.firstElementChild.cloneNode(true); $("jobs").append(e); jobEls.set(j.name, e); }
|
|
4993
|
+
const q = (c) => e.querySelector(c);
|
|
4994
|
+
const pct = j.total > 0 ? Math.min(100, (j.done / j.total) * 100) : 0;
|
|
4995
|
+
e.className = "card wide " + j.level;
|
|
4996
|
+
q("h2").textContent = "Re-embed job " + j.name;
|
|
4997
|
+
q(".fg").setAttribute("stroke-dashoffset", String(CIRC * (1 - pct / 100)));
|
|
4998
|
+
q(".pct").textContent = pct.toFixed(1) + "%";
|
|
4999
|
+
q(".rate").textContent = j.rate === null ? "?" : j.rate.toFixed(1);
|
|
5000
|
+
q(".done").textContent = n(j.done) + " / " + n(j.total);
|
|
5001
|
+
q(".eta").textContent = j.etaSecs === null ? "?" : dur(j.etaSecs);
|
|
5002
|
+
q(".fin").textContent = j.etaSecs === null ? "" : "done about " + clock(s.generatedAt + j.etaSecs * 1000);
|
|
5003
|
+
q(".phase").textContent = "phase " + j.phase + ", last progress " + when(j.lastProgressAt);
|
|
5004
|
+
q(".stall").hidden = !j.stalled;
|
|
5005
|
+
const [line, area] = sparkPoints(j.history || []);
|
|
5006
|
+
q("polyline").setAttribute("points", line);
|
|
5007
|
+
q("polygon").setAttribute("points", area);
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
function renderIndex(s) {
|
|
5012
|
+
const i = s.index, g = i.segments;
|
|
5013
|
+
$("index").className = "card " + i.level;
|
|
5014
|
+
set("ix-cov", i.coverage === null ? "?" : (i.coverage * 100).toFixed(1) + "%");
|
|
5015
|
+
set("ix-chunks", "embedded of " + n(i.chunks) + (i.countsExact ? "" : " (est.)") + " chunks");
|
|
5016
|
+
set("ix-files", n(i.files));
|
|
5017
|
+
const total = g ? g.fresh + g.old + g.missing : 0;
|
|
5018
|
+
const w = (v) => (total > 0 ? (v / total) * 100 : 0);
|
|
5019
|
+
const a = w(g ? g.fresh : 0), b = w(g ? g.old : 0), c = w(g ? g.missing : 0);
|
|
5020
|
+
const setRect = (id, x, width) => { $(id).setAttribute("x", String(x)); $(id).setAttribute("width", String(width)); };
|
|
5021
|
+
setRect("sg-new", 0, a); setRect("sg-old", a, b); setRect("sg-miss", a + b, c);
|
|
5022
|
+
const reembed = !!g && s.jobs.some((j) => j.phase !== "done") && g.fresh > 0;
|
|
5023
|
+
$("lg-new").hidden = !reembed;
|
|
5024
|
+
set("t-old", reembed ? "old backend, still to replace" : "embedded");
|
|
5025
|
+
set("n-new", n(g && g.fresh)); set("n-old", n(g && g.old)); set("n-miss", n(g && g.missing));
|
|
5026
|
+
const bound = i.binding ? i.binding.backend + (i.binding.model ? " / " + i.binding.model : "") : "none";
|
|
5027
|
+
set("ix-bound", bound);
|
|
5028
|
+
set("ix-conf", i.configured.backend + " / " + i.configured.model);
|
|
5029
|
+
const msg = i.error ? "binding error: " + i.error : i.hint || "";
|
|
5030
|
+
$("ix-hint").hidden = !msg; set("ix-hint", msg);
|
|
5031
|
+
$("ix-hint").style.color = i.level === "amber" ? "var(--amber)" : "";
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
function renderBackend(s) {
|
|
5035
|
+
const b = s.backend, m = b.loaded && b.loaded[0];
|
|
5036
|
+
$("backend").className = "card " + b.level;
|
|
5037
|
+
lvl($("be-dot"), "dot", b.available === null ? "" : b.available ? "green" : "red");
|
|
5038
|
+
set("be-id", b.id);
|
|
5039
|
+
set("be-state", b.available === null ? "unknown" : b.available ? "available" : "unavailable");
|
|
5040
|
+
set("be-reason", b.reason || "");
|
|
5041
|
+
set("be-model", m ? m.name : b.model + " (not loaded)");
|
|
5042
|
+
const share = m && m.gpuShare !== null ? m.gpuShare : null;
|
|
5043
|
+
set("gpu-pct", share === null ? "?" : (share * 100).toFixed(0) + "%");
|
|
5044
|
+
$("gpu-bar").setAttribute("width", String(share === null ? 0 : Math.min(100, share * 100)));
|
|
5045
|
+
$("gpu-bar").setAttribute("fill", share !== null && share < 1 ? "var(--amber)" : "var(--green)");
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
const passEls = new Map();
|
|
5049
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
5050
|
+
function passRow(name) {
|
|
5051
|
+
const e = document.createElement("div"); e.className = "pass";
|
|
5052
|
+
e.innerHTML = '<div class="line"><span class="dot"></span><b></b><span class="k st"></span></div>' +
|
|
5053
|
+
'<svg class="tl" viewBox="0 0 100 16" preserveAspectRatio="none"><line x1="4" y1="8" x2="96" y2="8"/></svg>' +
|
|
5054
|
+
'<div class="times"><span class="last"></span><span class="next"></span></div><div class="err" hidden></div>';
|
|
5055
|
+
e.querySelector("b").textContent = name;
|
|
5056
|
+
const svg = e.querySelector("svg");
|
|
5057
|
+
const mk = (cls, cx) => { const c = document.createElementNS(NS, "ellipse"); c.setAttribute("class", cls); c.setAttribute("cx", cx); c.setAttribute("cy", "8"); c.setAttribute("rx", "2.2"); c.setAttribute("ry", "5"); svg.append(c); return c; };
|
|
5058
|
+
mk("m-last", "4"); mk("m-next", "96");
|
|
5059
|
+
return e;
|
|
5060
|
+
}
|
|
5061
|
+
function renderPasses(s) {
|
|
5062
|
+
$("passes").className = "card " + (s.passes.some((p) => p.level === "red") ? "red" : "green");
|
|
5063
|
+
for (const p of s.passes) {
|
|
5064
|
+
let e = passEls.get(p.name);
|
|
5065
|
+
if (!e) { e = passRow(p.name); $("pass-list").append(e); passEls.set(p.name, e); }
|
|
5066
|
+
const q = (c) => e.querySelector(c);
|
|
5067
|
+
const state = p.failure ? "red" : p.running ? "amber" : "green";
|
|
5068
|
+
lvl(q(".dot"), "dot", state);
|
|
5069
|
+
q(".st").textContent = p.failure ? "failed" : p.running ? "running" : "ok";
|
|
5070
|
+
q(".last").textContent = "last " + (p.lastEnd ? when(p.lastEnd) : "none since start");
|
|
5071
|
+
q(".next").textContent = p.nextAt ? "next " + when(p.nextAt) : "";
|
|
5072
|
+
const col = "var(--" + state + ")";
|
|
5073
|
+
q(".m-last").setAttribute("fill", col);
|
|
5074
|
+
q(".m-next").setAttribute("fill", "none"); q(".m-next").setAttribute("stroke", "var(--grey)"); q(".m-next").setAttribute("stroke-width", "1.5");
|
|
5075
|
+
q(".m-next").style.display = p.nextAt ? "" : "none";
|
|
5076
|
+
const err = q(".err");
|
|
5077
|
+
err.hidden = !p.failure;
|
|
5078
|
+
if (p.failure) err.textContent = p.failure.error + " (" + (p.failure.gaveUp ? "gave up until next scheduled run" : "retry " + when(p.failure.retryAt)) + ")";
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
|
|
5082
|
+
function render(s) {
|
|
5083
|
+
lvl($("pill"), "pill", s.level);
|
|
5084
|
+
set("pill", s.level === "green" ? "healthy" : s.level === "amber" ? "attention" : "problem");
|
|
5085
|
+
set("reason", s.reason);
|
|
5086
|
+
renderJobs(s); renderIndex(s); renderBackend(s); renderPasses(s);
|
|
5087
|
+
set("d-pid", String(s.daemon.pid)); set("d-up", dur(s.daemon.uptimeSecs));
|
|
5088
|
+
set("d-ver", s.daemon.version); set("d-sto", s.daemon.storage);
|
|
5089
|
+
set("foot", "updated " + new Date(s.generatedAt).toLocaleTimeString() + " · refreshes every 10 s");
|
|
5090
|
+
}
|
|
5091
|
+
async function load() {
|
|
5092
|
+
try {
|
|
5093
|
+
const r = await fetch("/api/status", { cache: "no-store" });
|
|
5094
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
5095
|
+
render(await r.json());
|
|
5096
|
+
} catch (e) {
|
|
5097
|
+
lvl($("pill"), "pill", "red"); set("pill", "unreachable");
|
|
5098
|
+
set("reason", "daemon unreachable: " + e.message);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
load();
|
|
5102
|
+
setInterval(load, 10000);
|
|
5103
|
+
<\/script></main></body></html>
|
|
5104
|
+
`;
|
|
5105
|
+
|
|
5106
|
+
//#endregion
|
|
5107
|
+
//#region src/daemon/dashboard.ts
|
|
5108
|
+
/**
|
|
5109
|
+
* dashboard.ts — read-only memory status dashboard served by the daemon.
|
|
5110
|
+
*
|
|
5111
|
+
* Embedding and index passes run unattended for hours; a crashed embed pass
|
|
5112
|
+
* went unnoticed for 7 h. This puts what is running and whether it is healthy
|
|
5113
|
+
* on one page (and /api/status) reachable from a phone on the tailnet.
|
|
5114
|
+
*
|
|
5115
|
+
* Nothing here computes anything per request: a background tick (30 s) samples
|
|
5116
|
+
* the job table, binding and backend health, and real row counts are refreshed
|
|
5117
|
+
* at most every 5 min. Requests only assemble the cached values.
|
|
5118
|
+
*/
|
|
5119
|
+
const TICK_MS = 3e4;
|
|
5120
|
+
const COUNTS_TTL_MS = 5 * 6e4;
|
|
5121
|
+
const STALL_MS = 5 * 6e4;
|
|
5122
|
+
const RATE_WINDOW_MS = 5 * 6e4;
|
|
5123
|
+
const HISTORY_MAX = 120;
|
|
5124
|
+
const HISTORY_PERSIST_MS = 3e4;
|
|
5125
|
+
const HISTORY_PRUNE_AGE_MS = 120 * 6e4;
|
|
5126
|
+
const RANK = {
|
|
5127
|
+
green: 0,
|
|
5128
|
+
amber: 1,
|
|
5129
|
+
red: 2
|
|
5130
|
+
};
|
|
5131
|
+
const worst = (ls) => ls.reduce((a, b) => RANK[b] > RANK[a] ? b : a, "green");
|
|
5132
|
+
/** Host header without port; IPv6 literals keep their brackets stripped. */
|
|
5133
|
+
function hostName(header) {
|
|
5134
|
+
if (!header) return "";
|
|
5135
|
+
const h = header.trim().toLowerCase();
|
|
5136
|
+
if (h.startsWith("[")) return h.slice(1, h.indexOf("]"));
|
|
5137
|
+
return h.replace(/:\d+$/, "");
|
|
5138
|
+
}
|
|
5139
|
+
function hostAllowed(header, allowed) {
|
|
5140
|
+
const h = hostName(header);
|
|
5141
|
+
return h !== "" && new Set([...allowed].map((a) => a.toLowerCase())).has(h);
|
|
5142
|
+
}
|
|
5143
|
+
/** Tailnet DNS name and IPs of this machine from `tailscale status --json`; empty when unavailable. */
|
|
5144
|
+
async function tailnetHosts() {
|
|
5145
|
+
try {
|
|
5146
|
+
const { stdout } = await promisify(execFile)("tailscale", ["status", "--json"], { timeout: 3e3 });
|
|
5147
|
+
const self = JSON.parse(stdout).Self;
|
|
5148
|
+
return [self?.DNSName?.replace(/\.$/, ""), ...self?.TailscaleIPs ?? []].filter((x) => !!x);
|
|
5149
|
+
} catch {
|
|
5150
|
+
return [];
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
/** Stacked-bar numbers: done/embedded/total in, three non-negative segments out. `done` null = no re-embed. */
|
|
5154
|
+
function indexSegments(total, embedded, done) {
|
|
5155
|
+
const missing = Math.max(0, total - embedded);
|
|
5156
|
+
if (done === null) return {
|
|
5157
|
+
fresh: 0,
|
|
5158
|
+
old: Math.max(0, embedded),
|
|
5159
|
+
missing
|
|
5160
|
+
};
|
|
5161
|
+
return {
|
|
5162
|
+
fresh: done,
|
|
5163
|
+
old: Math.max(0, embedded - done),
|
|
5164
|
+
missing
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
function phaseOf(s) {
|
|
5168
|
+
if (s.finished) return "done";
|
|
5169
|
+
if (s.embedDone) return "finish";
|
|
5170
|
+
if (s.carryDone) return "embed";
|
|
5171
|
+
if (s.prepared) return "carry";
|
|
5172
|
+
return "prepare";
|
|
5173
|
+
}
|
|
5174
|
+
var DashboardState = class {
|
|
5175
|
+
cache = {
|
|
5176
|
+
counts: null,
|
|
5177
|
+
binding: null,
|
|
5178
|
+
bindingError: null,
|
|
5179
|
+
jobs: [],
|
|
5180
|
+
backend: null,
|
|
5181
|
+
loaded: null
|
|
5182
|
+
};
|
|
5183
|
+
samples = /* @__PURE__ */ new Map();
|
|
5184
|
+
lastExactAt;
|
|
5185
|
+
exactRunning = false;
|
|
5186
|
+
lastPersistAt;
|
|
5187
|
+
historyPath;
|
|
5188
|
+
constructor(src, now = Date.now, historyPath) {
|
|
5189
|
+
this.src = src;
|
|
5190
|
+
this.now = now;
|
|
5191
|
+
this.lastExactAt = now();
|
|
5192
|
+
this.lastPersistAt = now();
|
|
5193
|
+
this.historyPath = historyPath ?? paiHomePath("state", "dashboard-history.json");
|
|
5194
|
+
}
|
|
5195
|
+
async loadHistory() {
|
|
5196
|
+
try {
|
|
5197
|
+
if (!existsSync(this.historyPath)) return;
|
|
5198
|
+
const content = await readFile(this.historyPath, "utf8");
|
|
5199
|
+
const data = JSON.parse(content);
|
|
5200
|
+
const now = this.now();
|
|
5201
|
+
for (const [name, hist] of Object.entries(data.jobs ?? {})) {
|
|
5202
|
+
const pruned = hist.filter((s) => now - s.t <= HISTORY_PRUNE_AGE_MS);
|
|
5203
|
+
if (pruned.length > 0) {
|
|
5204
|
+
const s = this.samples.get(name) ?? {
|
|
5205
|
+
list: [],
|
|
5206
|
+
history: [],
|
|
5207
|
+
progressAt: now,
|
|
5208
|
+
lastDone: 0
|
|
5209
|
+
};
|
|
5210
|
+
s.history = pruned.slice(-HISTORY_MAX);
|
|
5211
|
+
this.samples.set(name, s);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
} catch {}
|
|
5215
|
+
}
|
|
5216
|
+
async persistHistory() {
|
|
5217
|
+
const t = this.now();
|
|
5218
|
+
if (t - this.lastPersistAt < HISTORY_PERSIST_MS) return;
|
|
5219
|
+
this.lastPersistAt = t;
|
|
5220
|
+
try {
|
|
5221
|
+
const jobs = {};
|
|
5222
|
+
for (const [name, s] of this.samples) if (s.history.length > 0) jobs[name] = s.history;
|
|
5223
|
+
const data = { jobs };
|
|
5224
|
+
await mkdir(join(this.historyPath, ".."), { recursive: true });
|
|
5225
|
+
await writeFile(this.historyPath + ".tmp", JSON.stringify(data), "utf8");
|
|
5226
|
+
await writeFile(this.historyPath, JSON.stringify(data), "utf8");
|
|
5227
|
+
} catch {}
|
|
5228
|
+
}
|
|
5229
|
+
/** Sample everything once. Never throws: a failing source shows up in the status instead. */
|
|
5230
|
+
async tick() {
|
|
5231
|
+
const t = this.now();
|
|
5232
|
+
const c = this.cache;
|
|
5233
|
+
const run = async (f, onErr) => {
|
|
5234
|
+
try {
|
|
5235
|
+
return await f();
|
|
5236
|
+
} catch (e) {
|
|
5237
|
+
onErr(e);
|
|
5238
|
+
return;
|
|
5239
|
+
}
|
|
5240
|
+
};
|
|
5241
|
+
if (!c.counts) {
|
|
5242
|
+
const est = await run(() => this.src.estimate(), () => {});
|
|
5243
|
+
if (est) c.counts = {
|
|
5244
|
+
...est,
|
|
5245
|
+
exact: false,
|
|
5246
|
+
at: t
|
|
5247
|
+
};
|
|
5248
|
+
}
|
|
5249
|
+
this.refreshExact(t);
|
|
5250
|
+
c.bindingError = null;
|
|
5251
|
+
const b = await run(() => this.src.binding(), (e) => {
|
|
5252
|
+
c.bindingError = e instanceof Error ? e.message : String(e);
|
|
5253
|
+
});
|
|
5254
|
+
if (b !== void 0) c.binding = b;
|
|
5255
|
+
const jobs = await run(() => this.src.jobs(), () => {});
|
|
5256
|
+
if (jobs) {
|
|
5257
|
+
c.jobs = jobs;
|
|
5258
|
+
for (const j of jobs) this.sample(j, t);
|
|
5259
|
+
}
|
|
5260
|
+
const be = this.src.backend();
|
|
5261
|
+
c.backend = await run(() => be.available(), () => {}) ?? {
|
|
5262
|
+
ok: false,
|
|
5263
|
+
reason: "availability probe threw"
|
|
5264
|
+
};
|
|
5265
|
+
c.loaded = be.id.startsWith("ollama") ? await run(() => this.src.loadedModels(), () => {}) ?? null : null;
|
|
5266
|
+
await this.persistHistory();
|
|
5267
|
+
}
|
|
5268
|
+
/** Real counts at most every COUNTS_TTL_MS, in the background; the tick never waits for them. */
|
|
5269
|
+
refreshExact(t) {
|
|
5270
|
+
if (this.exactRunning || t - this.lastExactAt < COUNTS_TTL_MS) return;
|
|
5271
|
+
this.exactRunning = true;
|
|
5272
|
+
this.lastExactAt = t;
|
|
5273
|
+
this.src.exact().then((n) => {
|
|
5274
|
+
this.cache.counts = {
|
|
5275
|
+
...n,
|
|
5276
|
+
exact: true,
|
|
5277
|
+
at: this.now()
|
|
5278
|
+
};
|
|
5279
|
+
}, () => {}).finally(() => {
|
|
5280
|
+
this.exactRunning = false;
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
sample(j, t) {
|
|
5284
|
+
const done = Number(j.state.done ?? 0);
|
|
5285
|
+
const s = this.samples.get(j.name) ?? {
|
|
5286
|
+
list: [],
|
|
5287
|
+
history: [],
|
|
5288
|
+
progressAt: t,
|
|
5289
|
+
lastDone: done
|
|
5290
|
+
};
|
|
5291
|
+
if (done !== s.lastDone) {
|
|
5292
|
+
s.progressAt = t;
|
|
5293
|
+
s.lastDone = done;
|
|
5294
|
+
}
|
|
5295
|
+
s.list.push({
|
|
5296
|
+
t,
|
|
5297
|
+
done
|
|
5298
|
+
});
|
|
5299
|
+
s.history.push({
|
|
5300
|
+
t,
|
|
5301
|
+
done
|
|
5302
|
+
});
|
|
5303
|
+
if (s.history.length > HISTORY_MAX) s.history.shift();
|
|
5304
|
+
while (s.list.length > 2 && t - s.list[0].t > RATE_WINDOW_MS) s.list.shift();
|
|
5305
|
+
this.samples.set(j.name, s);
|
|
5306
|
+
}
|
|
5307
|
+
jobView(j, t) {
|
|
5308
|
+
const total = Number(j.state.total ?? 0);
|
|
5309
|
+
const done = Number(j.state.done ?? 0);
|
|
5310
|
+
const phase = phaseOf(j.state);
|
|
5311
|
+
const s = this.samples.get(j.name);
|
|
5312
|
+
const first = s?.list[0];
|
|
5313
|
+
const last = s?.list[s.list.length - 1];
|
|
5314
|
+
const rate = first && last && last.t > first.t ? Math.max(0, (last.done - first.done) / ((last.t - first.t) / 1e3)) : null;
|
|
5315
|
+
const etaSecs = rate && rate > 0 && total > done ? Math.round((total - done) / rate) : null;
|
|
5316
|
+
const lastProgressAt = s?.progressAt ?? t;
|
|
5317
|
+
const stalled = phase !== "done" && t - lastProgressAt >= STALL_MS;
|
|
5318
|
+
return {
|
|
5319
|
+
name: j.name,
|
|
5320
|
+
phase,
|
|
5321
|
+
done,
|
|
5322
|
+
total,
|
|
5323
|
+
rate,
|
|
5324
|
+
etaSecs,
|
|
5325
|
+
lastProgressAt,
|
|
5326
|
+
stalled,
|
|
5327
|
+
level: stalled ? "red" : "green",
|
|
5328
|
+
history: s?.history ?? []
|
|
5329
|
+
};
|
|
5330
|
+
}
|
|
5331
|
+
status() {
|
|
5332
|
+
const t = this.now();
|
|
5333
|
+
const c = this.cache;
|
|
5334
|
+
const d = this.src.daemon();
|
|
5335
|
+
const be = this.src.backend();
|
|
5336
|
+
const jobs = c.jobs.filter((j) => Object.keys(j.state).length > 0).map((j) => this.jobView(j, t));
|
|
5337
|
+
const mismatch = !!c.binding && !bindingMatches(c.binding, be);
|
|
5338
|
+
const counts = c.counts;
|
|
5339
|
+
const reembedJob = this.findReembedJob(c.jobs, be.id);
|
|
5340
|
+
const reembedActive = reembedJob && !["done"].includes(phaseOf(reembedJob.state));
|
|
5341
|
+
const indexLevel = mismatch && !reembedActive || c.bindingError ? "red" : reembedActive ? "amber" : "green";
|
|
5342
|
+
const backendLevel = c.backend && !c.backend.ok ? "red" : c.backend ? "green" : "amber";
|
|
5343
|
+
const passes = this.src.passes().map((p) => ({
|
|
5344
|
+
...p,
|
|
5345
|
+
level: p.failure ? "red" : "green"
|
|
5346
|
+
}));
|
|
5347
|
+
let hint = null;
|
|
5348
|
+
if (mismatch && c.binding) if (reembedActive) hint = `re-embed to ${this.deriveBackendFromJobName(reembedJob.name)} in progress, semantic search keyword-only until it finishes`;
|
|
5349
|
+
else hint = mismatchReason(c.binding, be);
|
|
5350
|
+
const level = worst([
|
|
5351
|
+
indexLevel,
|
|
5352
|
+
backendLevel,
|
|
5353
|
+
...jobs.map((j) => j.level),
|
|
5354
|
+
...passes.map((p) => p.level)
|
|
5355
|
+
]);
|
|
5356
|
+
const reason = level === "green" ? "all systems healthy" : c.bindingError ? `binding check failed: ${c.bindingError}` : jobs.find((j) => j.stalled) ? `job ${jobs.find((j) => j.stalled).name} stalled` : c.backend && !c.backend.ok ? `backend unavailable: ${c.backend.reason}` : passes.find((p) => p.failure) ? `pass ${passes.find((p) => p.failure).name} failed` : hint ?? (reembedActive ? `re-embed ${reembedJob.name} running` : c.backend ? "degraded" : "backend not probed yet");
|
|
5357
|
+
const activeJob = reembedActive ? jobs.find((j) => j.name === reembedJob.name) : void 0;
|
|
5358
|
+
const segments = counts && counts.embedded !== null ? indexSegments(counts.chunks, counts.embedded, activeJob ? activeJob.done : null) : null;
|
|
5359
|
+
return {
|
|
5360
|
+
generatedAt: t,
|
|
5361
|
+
level,
|
|
5362
|
+
reason,
|
|
5363
|
+
daemon: {
|
|
5364
|
+
pid: d.pid,
|
|
5365
|
+
uptimeSecs: Math.floor((t - d.startTime) / 1e3),
|
|
5366
|
+
version: d.version,
|
|
5367
|
+
storage: this.src.storage
|
|
5368
|
+
},
|
|
5369
|
+
index: {
|
|
5370
|
+
level: indexLevel,
|
|
5371
|
+
files: counts?.files ?? null,
|
|
5372
|
+
chunks: counts?.chunks ?? null,
|
|
5373
|
+
embedded: counts?.embedded ?? null,
|
|
5374
|
+
coverage: counts && counts.embedded !== null && counts.chunks > 0 ? Math.min(1, counts.embedded / counts.chunks) : null,
|
|
5375
|
+
countsExact: counts?.exact ?? false,
|
|
5376
|
+
countsAt: counts?.at ?? null,
|
|
5377
|
+
segments,
|
|
5378
|
+
binding: c.binding,
|
|
5379
|
+
configured: {
|
|
5380
|
+
backend: be.id,
|
|
5381
|
+
model: be.model,
|
|
5382
|
+
dims: be.dims
|
|
5383
|
+
},
|
|
5384
|
+
mismatch,
|
|
5385
|
+
hint,
|
|
5386
|
+
error: c.bindingError
|
|
5387
|
+
},
|
|
5388
|
+
jobs,
|
|
5389
|
+
passes,
|
|
5390
|
+
backend: {
|
|
5391
|
+
level: backendLevel,
|
|
5392
|
+
id: be.id,
|
|
5393
|
+
model: be.model,
|
|
5394
|
+
available: c.backend?.ok ?? null,
|
|
5395
|
+
reason: c.backend?.reason ?? null,
|
|
5396
|
+
loaded: c.loaded?.map((m) => ({
|
|
5397
|
+
name: m.name,
|
|
5398
|
+
size: m.size,
|
|
5399
|
+
sizeVram: m.sizeVram,
|
|
5400
|
+
gpuShare: m.size > 0 ? m.sizeVram / m.size : null
|
|
5401
|
+
})) ?? null
|
|
5402
|
+
}
|
|
5403
|
+
};
|
|
5404
|
+
}
|
|
5405
|
+
findReembedJob(jobs, configuredBackendId) {
|
|
5406
|
+
return jobs.find((j) => j.name.endsWith("-reembed") && this.deriveBackendFromJobName(j.name) === configuredBackendId);
|
|
5407
|
+
}
|
|
5408
|
+
deriveBackendFromJobName(jobName) {
|
|
5409
|
+
const match = jobName.match(/^(.+)-reembed$/);
|
|
5410
|
+
return match ? match[1] : jobName;
|
|
5411
|
+
}
|
|
5412
|
+
};
|
|
5413
|
+
async function startDashboard(cfg, src, opts = {}) {
|
|
5414
|
+
const state = new DashboardState(src, Date.now, opts.historyPath);
|
|
5415
|
+
await state.loadHistory();
|
|
5416
|
+
const allowed = [
|
|
5417
|
+
cfg.bind,
|
|
5418
|
+
"localhost",
|
|
5419
|
+
"127.0.0.1",
|
|
5420
|
+
"::1",
|
|
5421
|
+
...opts.extraHosts ?? await tailnetHosts()
|
|
5422
|
+
];
|
|
5423
|
+
const server = createServer$1((req, res) => {
|
|
5424
|
+
const deny = (code, msg) => {
|
|
5425
|
+
res.writeHead(code, { "content-type": "text/plain" });
|
|
5426
|
+
res.end(msg);
|
|
5427
|
+
};
|
|
5428
|
+
if (!hostAllowed(req.headers.host, allowed)) return deny(403, "forbidden host");
|
|
5429
|
+
if (req.method !== "GET" && req.method !== "HEAD") return deny(405, "read-only");
|
|
5430
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
5431
|
+
if (path === "/api/status") {
|
|
5432
|
+
res.writeHead(200, {
|
|
5433
|
+
"content-type": "application/json",
|
|
5434
|
+
"cache-control": "no-store"
|
|
5435
|
+
});
|
|
5436
|
+
return res.end(JSON.stringify(state.status()));
|
|
5437
|
+
}
|
|
5438
|
+
if (path === "/") {
|
|
5439
|
+
res.writeHead(200, {
|
|
5440
|
+
"content-type": "text/html; charset=utf-8",
|
|
5441
|
+
"cache-control": "no-store"
|
|
5442
|
+
});
|
|
5443
|
+
return res.end(DASHBOARD_HTML);
|
|
5444
|
+
}
|
|
5445
|
+
deny(404, "not found");
|
|
5446
|
+
});
|
|
5447
|
+
await state.tick();
|
|
5448
|
+
const timer = setInterval(() => void state.tick(), TICK_MS);
|
|
5449
|
+
timer.unref();
|
|
5450
|
+
await new Promise((resolve, reject) => {
|
|
5451
|
+
server.once("error", reject);
|
|
5452
|
+
server.listen(cfg.port, cfg.bind, resolve);
|
|
5453
|
+
});
|
|
5454
|
+
return {
|
|
5455
|
+
server,
|
|
5456
|
+
url: `http://${cfg.bind}:${cfg.port}/`,
|
|
5457
|
+
stop: () => {
|
|
5458
|
+
clearInterval(timer);
|
|
5459
|
+
server.close();
|
|
5460
|
+
}
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
async function ollamaPs(baseUrl) {
|
|
5464
|
+
try {
|
|
5465
|
+
const r = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/ps`, { signal: AbortSignal.timeout(2e3) });
|
|
5466
|
+
if (!r.ok) return null;
|
|
5467
|
+
return ((await r.json()).models ?? []).map((m) => ({
|
|
5468
|
+
name: m.name,
|
|
5469
|
+
size: m.size,
|
|
5470
|
+
sizeVram: m.size_vram
|
|
5471
|
+
}));
|
|
5472
|
+
} catch {
|
|
5473
|
+
return null;
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
function liveSource(deps) {
|
|
5477
|
+
const st = deps.storage;
|
|
5478
|
+
const sqliteCounts = async () => ({
|
|
5479
|
+
...await deps.storage.getStats(),
|
|
5480
|
+
embedded: null
|
|
5481
|
+
});
|
|
5482
|
+
return {
|
|
5483
|
+
storage: deps.storage.backendType,
|
|
5484
|
+
daemon: deps.daemon,
|
|
5485
|
+
estimate: st.estimateCounts?.bind(st) ?? sqliteCounts,
|
|
5486
|
+
exact: st.exactCounts?.bind(st) ?? sqliteCounts,
|
|
5487
|
+
binding: () => deps.storage.getEmbeddingBinding(),
|
|
5488
|
+
jobs: st.embeddingJobs?.bind(st) ?? (async () => []),
|
|
5489
|
+
backend: deps.backend,
|
|
5490
|
+
loadedModels: () => ollamaPs(deps.ollamaBaseUrl),
|
|
5491
|
+
passes: deps.passes
|
|
5492
|
+
};
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
//#endregion
|
|
5496
|
+
//#region src/daemon/daemon/dashboard-live.ts
|
|
5497
|
+
/**
|
|
5498
|
+
* Wires the status dashboard to the daemon's live state (state.ts, scheduler
|
|
5499
|
+
* bookkeeping) and the configured embedding backend.
|
|
5500
|
+
*/
|
|
5501
|
+
function packageVersion() {
|
|
5502
|
+
try {
|
|
5503
|
+
return JSON.parse(readFileSync(resolveFromModule(import.meta.url, "package.json"), "utf8")).version ?? "0.0.0";
|
|
5504
|
+
} catch {
|
|
5505
|
+
return "0.0.0";
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
/** The index/embed/vault passes as the dashboard shows them, from the existing daemon state. */
|
|
5509
|
+
function livePasses(now = Date.now()) {
|
|
5510
|
+
const cfg = daemonConfig;
|
|
5511
|
+
const next = (last, intervalSecs) => cfg.maintenanceHour !== void 0 ? now + msUntilNextAnchor(cfg.maintenanceHour, intervalSecs * 1e3) : (last || now) + intervalSecs * 1e3;
|
|
5512
|
+
const snap = (name, running, lastEnd, nextAt) => {
|
|
5513
|
+
const f = passFailures.get(name);
|
|
5514
|
+
const delay = f ? PASS_RETRY_DELAYS_MS[f.attempts - 1] : void 0;
|
|
5515
|
+
return {
|
|
5516
|
+
name,
|
|
5517
|
+
running,
|
|
5518
|
+
lastEnd,
|
|
5519
|
+
failure: f ? {
|
|
5520
|
+
...f,
|
|
5521
|
+
retryAt: f.gaveUp || delay === void 0 ? null : f.at + delay
|
|
5522
|
+
} : null,
|
|
5523
|
+
nextAt
|
|
5524
|
+
};
|
|
5525
|
+
};
|
|
5526
|
+
return [
|
|
5527
|
+
snap("index", indexInProgress, lastIndexTime, next(lastIndexTime, cfg.indexIntervalSecs)),
|
|
5528
|
+
snap("embed", embedInProgress, lastEmbedTime, next(lastEmbedTime, cfg.embedIntervalSecs)),
|
|
5529
|
+
snap("vault", vaultIndexInProgress, lastVaultIndexTime, null)
|
|
5530
|
+
];
|
|
5531
|
+
}
|
|
5532
|
+
/** Start the dashboard when enabled; a failure to bind is logged, never fatal for the daemon. */
|
|
5533
|
+
async function startLiveDashboard(config) {
|
|
5534
|
+
if (!config.dashboard.enabled) return null;
|
|
5535
|
+
try {
|
|
5536
|
+
const handle = await startDashboard(config.dashboard, liveSource({
|
|
5537
|
+
storage: storageBackend,
|
|
5538
|
+
backend: () => getConfiguredBackend(config),
|
|
5539
|
+
ollamaBaseUrl: config.embedding?.ollama?.baseUrl ?? "http://127.0.0.1:11434",
|
|
5540
|
+
daemon: () => ({
|
|
5541
|
+
pid: process.pid,
|
|
5542
|
+
startTime,
|
|
5543
|
+
version: packageVersion()
|
|
5544
|
+
}),
|
|
5545
|
+
passes: livePasses
|
|
5546
|
+
}));
|
|
5547
|
+
process.stderr.write(`[pai-daemon] Dashboard: ${handle.url}\n`);
|
|
5548
|
+
return handle;
|
|
5549
|
+
} catch (e) {
|
|
5550
|
+
process.stderr.write(`[pai-daemon] Dashboard not started: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
5551
|
+
return null;
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
|
|
4822
5555
|
//#endregion
|
|
4823
5556
|
//#region src/storage/postgres/fts-migration.ts
|
|
4824
5557
|
const DONE_MARKER = "fts:simple";
|
|
@@ -4993,6 +5726,9 @@ async function serve(config) {
|
|
|
4993
5726
|
setStorageBackend(backend);
|
|
4994
5727
|
process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
|
|
4995
5728
|
startIndexScheduler();
|
|
5729
|
+
startLiveDashboard(config).then((h) => {
|
|
5730
|
+
dashboard = h;
|
|
5731
|
+
});
|
|
4996
5732
|
if (backend.supportsPostgresFeatures) {
|
|
4997
5733
|
startEmbedScheduler();
|
|
4998
5734
|
migrateFtsConfig(backend).catch((e) => {
|
|
@@ -5007,6 +5743,7 @@ async function serve(config) {
|
|
|
5007
5743
|
process.stderr.write(`[pai-daemon] Fatal: Could not open federation storage: ${msg}\n`);
|
|
5008
5744
|
process.exit(1);
|
|
5009
5745
|
});
|
|
5746
|
+
let dashboard = null;
|
|
5010
5747
|
const shutdown = async (signal) => {
|
|
5011
5748
|
process.stderr.write(`\n[pai-daemon] ${signal} received. Stopping.\n`);
|
|
5012
5749
|
setShutdownRequested(true);
|
|
@@ -5014,6 +5751,7 @@ async function serve(config) {
|
|
|
5014
5751
|
if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
|
|
5015
5752
|
if (cacheKeepaliveTimer) clearInterval(cacheKeepaliveTimer);
|
|
5016
5753
|
stopWorker();
|
|
5754
|
+
dashboard?.stop();
|
|
5017
5755
|
server.close();
|
|
5018
5756
|
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
5019
5757
|
const POLL_INTERVAL_MS = 100;
|
|
@@ -5045,4 +5783,4 @@ async function serve(config) {
|
|
|
5045
5783
|
|
|
5046
5784
|
//#endregion
|
|
5047
5785
|
export { serve as t };
|
|
5048
|
-
//# sourceMappingURL=daemon-
|
|
5786
|
+
//# sourceMappingURL=daemon-CsR1S_Qr.mjs.map
|