@blackbelt-technology/pi-agent-dashboard 0.2.0
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/AGENTS.md +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- package/packages/shared/tsconfig.json +8 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System REST API routes: config, health, shutdown, tunnel, editors.
|
|
3
|
+
*/
|
|
4
|
+
import type { FastifyInstance } from "fastify";
|
|
5
|
+
import type { SessionManager } from "../memory-session-manager.js";
|
|
6
|
+
import type { PreferencesStore } from "../preferences-store.js";
|
|
7
|
+
import type { MetaPersistence } from "../meta-persistence.js";
|
|
8
|
+
import type { ServerConfig } from "../server.js";
|
|
9
|
+
import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
10
|
+
import type { NetworkGuard } from "./route-deps.js";
|
|
11
|
+
import { detectEditors, EDITORS } from "../editor-registry.js";
|
|
12
|
+
import { detectCodeServerBinary, resetDetectionCache } from "../editor-detection.js";
|
|
13
|
+
import { readConfigRedacted, writeConfigPartial } from "../config-api.js";
|
|
14
|
+
import { createTunnel, deleteTunnel, getTunnelStatus } from "../tunnel.js";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { localhostGuard, netmaskToCidrBits, networkAddress } from "../localhost-guard.js";
|
|
19
|
+
import type { NetworkInterface } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
20
|
+
|
|
21
|
+
export function registerSystemRoutes(
|
|
22
|
+
fastify: FastifyInstance,
|
|
23
|
+
deps: {
|
|
24
|
+
sessionManager: SessionManager;
|
|
25
|
+
preferencesStore: PreferencesStore;
|
|
26
|
+
metaPersistence: MetaPersistence;
|
|
27
|
+
config: ServerConfig;
|
|
28
|
+
networkGuard: NetworkGuard;
|
|
29
|
+
},
|
|
30
|
+
) {
|
|
31
|
+
const { sessionManager, preferencesStore, metaPersistence, config, networkGuard } = deps;
|
|
32
|
+
const serverStartTime = Date.now();
|
|
33
|
+
|
|
34
|
+
// Editor detection endpoint
|
|
35
|
+
fastify.get<{ Querystring: { path?: string } }>(
|
|
36
|
+
"/api/editors",
|
|
37
|
+
{ preHandler: networkGuard },
|
|
38
|
+
async (request) => {
|
|
39
|
+
const cwd = request.query.path;
|
|
40
|
+
if (!cwd) {
|
|
41
|
+
return { success: false, error: "path parameter required" } satisfies ApiResponse;
|
|
42
|
+
}
|
|
43
|
+
const editors = detectEditors(cwd);
|
|
44
|
+
return { success: true, data: editors } satisfies ApiResponse;
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// code-server binary detection endpoint
|
|
49
|
+
fastify.get(
|
|
50
|
+
"/api/editor/detect",
|
|
51
|
+
{ preHandler: networkGuard },
|
|
52
|
+
async () => {
|
|
53
|
+
resetDetectionCache();
|
|
54
|
+
const result = detectCodeServerBinary(config.editor);
|
|
55
|
+
return { success: true, data: result } satisfies ApiResponse;
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Open editor endpoint
|
|
60
|
+
fastify.post<{ Body: { path?: string; editor?: string; file?: string; line?: number } }>(
|
|
61
|
+
"/api/open-editor",
|
|
62
|
+
{ preHandler: networkGuard },
|
|
63
|
+
async (request) => {
|
|
64
|
+
const { path: cwd, editor: editorId, file, line } = request.body ?? {};
|
|
65
|
+
if (!cwd || !editorId) {
|
|
66
|
+
return { success: false, error: "path and editor required" } satisfies ApiResponse;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const allSessions = sessionManager.listAll();
|
|
70
|
+
if (!allSessions.some((s) => s.cwd === cwd)) {
|
|
71
|
+
return { success: false, error: "unknown session path" } satisfies ApiResponse;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const editorEntry = EDITORS.find((e) => e.id === editorId);
|
|
75
|
+
if (!editorEntry) {
|
|
76
|
+
return { success: false, error: "unknown editor" } satisfies ApiResponse;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const target = file ? path.resolve(cwd, file) : cwd;
|
|
80
|
+
const args = line && file ? [`${target}:${line}`] : [target];
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const child = spawn(editorEntry.cli, args, {
|
|
84
|
+
detached: true,
|
|
85
|
+
stdio: "ignore",
|
|
86
|
+
});
|
|
87
|
+
child.unref();
|
|
88
|
+
return { success: true } satisfies ApiResponse;
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
return { success: false, error: `failed to open editor: ${err.message}` } satisfies ApiResponse;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Config endpoints
|
|
96
|
+
fastify.get(
|
|
97
|
+
"/api/config",
|
|
98
|
+
{ preHandler: networkGuard },
|
|
99
|
+
async () => {
|
|
100
|
+
return { success: true, data: readConfigRedacted() };
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
fastify.put(
|
|
105
|
+
"/api/config",
|
|
106
|
+
{ preHandler: networkGuard },
|
|
107
|
+
async (request, reply) => {
|
|
108
|
+
const partial = request.body as Record<string, any>;
|
|
109
|
+
if (!partial || typeof partial !== "object") {
|
|
110
|
+
return reply.code(400).send({ success: false, error: "Invalid body" });
|
|
111
|
+
}
|
|
112
|
+
const result = writeConfigPartial(partial);
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
return reply.code(500).send({ success: false, error: result.error });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Apply runtime-safe changes
|
|
118
|
+
const reloaded = (await import("@blackbelt-technology/pi-dashboard-shared/config.js")).loadConfig();
|
|
119
|
+
if (partial.autoShutdown !== undefined || partial.shutdownIdleSeconds !== undefined) {
|
|
120
|
+
config.autoShutdown = reloaded.autoShutdown;
|
|
121
|
+
config.shutdownIdleSeconds = reloaded.shutdownIdleSeconds;
|
|
122
|
+
}
|
|
123
|
+
if (partial.auth !== undefined) {
|
|
124
|
+
config.authConfig = reloaded.auth;
|
|
125
|
+
if (reloaded.auth && (fastify as any)._reloadAuth) {
|
|
126
|
+
await (fastify as any)._reloadAuth(reloaded.auth);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { success: true, restartRequired: result.restartRequired };
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Tunnel endpoints
|
|
135
|
+
fastify.get("/api/tunnel-status", async () => {
|
|
136
|
+
return getTunnelStatus();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
fastify.post("/api/tunnel-connect", async () => {
|
|
140
|
+
const status = getTunnelStatus();
|
|
141
|
+
if (status.status === "active") return { ok: true, url: status.url };
|
|
142
|
+
if (status.status === "unavailable") return { ok: false, error: "zrok not installed" };
|
|
143
|
+
const url = await createTunnel(config.port, config.tunnelReservedToken);
|
|
144
|
+
if (url) return { ok: true, url };
|
|
145
|
+
return { ok: false, error: "Failed to create tunnel" };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
fastify.post("/api/tunnel-disconnect", async () => {
|
|
149
|
+
await deleteTunnel();
|
|
150
|
+
return { ok: true };
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Health endpoint — includes server + agent process metrics
|
|
154
|
+
fastify.get("/api/health", async () => {
|
|
155
|
+
const mem = process.memoryUsage();
|
|
156
|
+
const activeSessions = sessionManager.listActive();
|
|
157
|
+
const agentMetrics = activeSessions
|
|
158
|
+
.filter(s => s.processMetrics)
|
|
159
|
+
.map(s => ({
|
|
160
|
+
sessionId: s.id,
|
|
161
|
+
cwd: s.cwd,
|
|
162
|
+
...s.processMetrics,
|
|
163
|
+
}));
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
pid: process.pid,
|
|
167
|
+
uptime: Math.floor((Date.now() - serverStartTime) / 1000),
|
|
168
|
+
mode: config.dev ? "dev" : "production",
|
|
169
|
+
server: {
|
|
170
|
+
rss: mem.rss,
|
|
171
|
+
heapUsed: mem.heapUsed,
|
|
172
|
+
heapTotal: mem.heapTotal,
|
|
173
|
+
activeSessions: activeSessions.length,
|
|
174
|
+
totalSessions: sessionManager.listAll().length,
|
|
175
|
+
},
|
|
176
|
+
agents: agentMetrics,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Shutdown endpoint — used by devBuildOnReload
|
|
181
|
+
fastify.post(
|
|
182
|
+
"/api/shutdown",
|
|
183
|
+
{ preHandler: networkGuard },
|
|
184
|
+
async () => {
|
|
185
|
+
metaPersistence.flushAll();
|
|
186
|
+
preferencesStore.flush();
|
|
187
|
+
setTimeout(() => process.exit(0), 100);
|
|
188
|
+
return { ok: true };
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Restart endpoint — flush state, spawn new server, then exit
|
|
193
|
+
fastify.post<{ Body: { dev?: boolean } }>(
|
|
194
|
+
"/api/restart",
|
|
195
|
+
{ preHandler: networkGuard },
|
|
196
|
+
async (request) => {
|
|
197
|
+
metaPersistence.flushAll();
|
|
198
|
+
preferencesStore.flush();
|
|
199
|
+
|
|
200
|
+
const cliPath = process.argv[1];
|
|
201
|
+
if (!cliPath) return { ok: false, error: "Cannot determine CLI path" };
|
|
202
|
+
|
|
203
|
+
// Find the TypeScript loader from process.execArgv (--import <loader>)
|
|
204
|
+
const importIdx = process.execArgv.indexOf("--import");
|
|
205
|
+
const loaderArgs = importIdx >= 0 ? ["--import", process.execArgv[importIdx + 1]] : [];
|
|
206
|
+
|
|
207
|
+
// Allow overriding dev mode via request body
|
|
208
|
+
const useDev = request.body?.dev ?? config.dev;
|
|
209
|
+
const args = ["start"];
|
|
210
|
+
if (useDev) args.push("--dev");
|
|
211
|
+
|
|
212
|
+
// Spawn a shell script that:
|
|
213
|
+
// 1. Waits for the old server's port to be free (up to 10s)
|
|
214
|
+
// 2. Starts the new server
|
|
215
|
+
// 3. Verifies health (up to 10s)
|
|
216
|
+
// 4. If health check fails, logs error
|
|
217
|
+
const port = config.port;
|
|
218
|
+
const nodeCmd = `${JSON.stringify(process.execPath)} ${loaderArgs.map(a => JSON.stringify(a)).join(" ")} ${JSON.stringify(cliPath)} ${args.join(" ")}`;
|
|
219
|
+
const script = [
|
|
220
|
+
// Wait for port to be free
|
|
221
|
+
`for i in $(seq 1 20); do`,
|
|
222
|
+
` lsof -i :${port} -sTCP:LISTEN >/dev/null 2>&1 || break`,
|
|
223
|
+
` sleep 0.5`,
|
|
224
|
+
`done`,
|
|
225
|
+
// Start new server
|
|
226
|
+
nodeCmd,
|
|
227
|
+
// Verify health
|
|
228
|
+
`for i in $(seq 1 20); do`,
|
|
229
|
+
` curl -sf http://localhost:${port}/api/health >/dev/null 2>&1 && exit 0`,
|
|
230
|
+
` sleep 0.5`,
|
|
231
|
+
`done`,
|
|
232
|
+
`echo "[dashboard] Restart health check failed" >&2`,
|
|
233
|
+
].join("\n");
|
|
234
|
+
|
|
235
|
+
const child = spawn("sh", ["-c", script], {
|
|
236
|
+
detached: true,
|
|
237
|
+
stdio: "ignore",
|
|
238
|
+
env: { ...process.env },
|
|
239
|
+
});
|
|
240
|
+
child.unref();
|
|
241
|
+
|
|
242
|
+
setTimeout(() => process.exit(0), 200);
|
|
243
|
+
return { ok: true };
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Network interfaces for trusted networks UI (localhost-only for security)
|
|
248
|
+
fastify.get(
|
|
249
|
+
"/api/network-interfaces",
|
|
250
|
+
{ preHandler: localhostGuard },
|
|
251
|
+
async () => {
|
|
252
|
+
const interfaces = os.networkInterfaces();
|
|
253
|
+
const result: NetworkInterface[] = [];
|
|
254
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
255
|
+
if (!addrs) continue;
|
|
256
|
+
for (const info of addrs) {
|
|
257
|
+
if (info.internal || info.family !== "IPv4") continue;
|
|
258
|
+
const bits = netmaskToCidrBits(info.netmask);
|
|
259
|
+
const net = networkAddress(info.address, info.netmask);
|
|
260
|
+
result.push({
|
|
261
|
+
name,
|
|
262
|
+
address: info.address,
|
|
263
|
+
netmask: info.netmask,
|
|
264
|
+
cidr: `${net}/${bits}`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { success: true, data: result };
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID file management for the dashboard server process.
|
|
3
|
+
* Writes/reads/removes ~/.pi/dashboard/server.pid to track the running server.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { isDashboardRunning } from "@blackbelt-technology/pi-dashboard-shared/server-identity.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PID_PATH = path.join(os.homedir(), ".pi", "dashboard", "server.pid");
|
|
11
|
+
|
|
12
|
+
export interface ServerPidOptions {
|
|
13
|
+
pidPath?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if a process with the given PID is alive.
|
|
18
|
+
*/
|
|
19
|
+
export function isProcessAlive(pid: number): boolean {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Write the current process PID to the PID file.
|
|
30
|
+
*/
|
|
31
|
+
export function writePid(pid: number, options?: ServerPidOptions): void {
|
|
32
|
+
const pidPath = options?.pidPath ?? DEFAULT_PID_PATH;
|
|
33
|
+
const dir = path.dirname(pidPath);
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
fs.writeFileSync(pidPath, String(pid) + "\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read the PID from the PID file. Returns null if file doesn't exist or is invalid.
|
|
40
|
+
*/
|
|
41
|
+
export function readPid(options?: ServerPidOptions): number | null {
|
|
42
|
+
const pidPath = options?.pidPath ?? DEFAULT_PID_PATH;
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(pidPath, "utf-8").trim();
|
|
45
|
+
const pid = parseInt(content, 10);
|
|
46
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove the PID file.
|
|
54
|
+
*/
|
|
55
|
+
export function removePid(options?: ServerPidOptions): void {
|
|
56
|
+
const pidPath = options?.pidPath ?? DEFAULT_PID_PATH;
|
|
57
|
+
try {
|
|
58
|
+
fs.unlinkSync(pidPath);
|
|
59
|
+
} catch {
|
|
60
|
+
// File may not exist — that's fine
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if the dashboard server is currently running.
|
|
66
|
+
* Returns the PID if running, null otherwise.
|
|
67
|
+
* Cleans up stale PID files automatically.
|
|
68
|
+
*/
|
|
69
|
+
export async function isServerRunning(port: number, options?: ServerPidOptions): Promise<number | null> {
|
|
70
|
+
const pid = readPid(options);
|
|
71
|
+
|
|
72
|
+
if (pid === null) return null;
|
|
73
|
+
|
|
74
|
+
// Process alive — verify it's actually our server via health check
|
|
75
|
+
if (isProcessAlive(pid)) {
|
|
76
|
+
const status = await isDashboardRunning(port);
|
|
77
|
+
if (status.running) return pid;
|
|
78
|
+
// Process alive but dashboard not responding — could be a recycled PID, treat as stale
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Stale PID file — clean up
|
|
82
|
+
removePid(options);
|
|
83
|
+
return null;
|
|
84
|
+
}
|