@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.
Files changed (212) hide show
  1. package/AGENTS.md +342 -0
  2. package/README.md +619 -0
  3. package/docs/architecture.md +646 -0
  4. package/package.json +92 -0
  5. package/packages/extension/package.json +33 -0
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  8. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  9. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  10. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  11. package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
  12. package/packages/extension/src/__tests__/git-info.test.ts +112 -0
  13. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  14. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
  15. package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
  16. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  17. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  18. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  19. package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
  20. package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
  21. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  22. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  23. package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
  24. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  25. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  26. package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
  27. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  28. package/packages/extension/src/ask-user-tool.ts +63 -0
  29. package/packages/extension/src/bridge-context.ts +64 -0
  30. package/packages/extension/src/bridge.ts +926 -0
  31. package/packages/extension/src/command-handler.ts +538 -0
  32. package/packages/extension/src/connection.ts +204 -0
  33. package/packages/extension/src/dev-build.ts +39 -0
  34. package/packages/extension/src/event-forwarder.ts +40 -0
  35. package/packages/extension/src/flow-event-wiring.ts +102 -0
  36. package/packages/extension/src/git-info.ts +65 -0
  37. package/packages/extension/src/git-link-builder.ts +112 -0
  38. package/packages/extension/src/model-tracker.ts +56 -0
  39. package/packages/extension/src/pi-env.d.ts +23 -0
  40. package/packages/extension/src/process-metrics.ts +70 -0
  41. package/packages/extension/src/process-scanner.ts +396 -0
  42. package/packages/extension/src/prompt-expander.ts +87 -0
  43. package/packages/extension/src/provider-register.ts +276 -0
  44. package/packages/extension/src/server-auto-start.ts +87 -0
  45. package/packages/extension/src/server-launcher.ts +82 -0
  46. package/packages/extension/src/server-probe.ts +33 -0
  47. package/packages/extension/src/session-sync.ts +154 -0
  48. package/packages/extension/src/source-detector.ts +26 -0
  49. package/packages/extension/src/ui-proxy.ts +269 -0
  50. package/packages/extension/tsconfig.json +11 -0
  51. package/packages/server/package.json +37 -0
  52. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  53. package/packages/server/src/__tests__/auth.test.ts +224 -0
  54. package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
  55. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  56. package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
  57. package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
  58. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  59. package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
  60. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  61. package/packages/server/src/__tests__/config-api.test.ts +104 -0
  62. package/packages/server/src/__tests__/cors.test.ts +48 -0
  63. package/packages/server/src/__tests__/directory-service.test.ts +240 -0
  64. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  65. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  66. package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
  67. package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
  68. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  69. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  70. package/packages/server/src/__tests__/extension-register.test.ts +61 -0
  71. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  72. package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
  73. package/packages/server/src/__tests__/git-operations.test.ts +251 -0
  74. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  75. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  76. package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
  77. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  78. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  79. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  80. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  81. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  82. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  83. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  84. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  85. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  86. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  87. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
  88. package/packages/server/src/__tests__/package-routes.test.ts +172 -0
  89. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  90. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  91. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  92. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  93. package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
  94. package/packages/server/src/__tests__/process-manager.test.ts +184 -0
  95. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  96. package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
  97. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  98. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  99. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  100. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  101. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  102. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  103. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  104. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  105. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
  106. package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
  107. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  108. package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
  109. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  110. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  111. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
  112. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  113. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  114. package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
  115. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  116. package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
  117. package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
  118. package/packages/server/src/__tests__/tunnel.test.ts +206 -0
  119. package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
  120. package/packages/server/src/auth-plugin.ts +302 -0
  121. package/packages/server/src/auth.ts +323 -0
  122. package/packages/server/src/browse.ts +55 -0
  123. package/packages/server/src/browser-gateway.ts +495 -0
  124. package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
  125. package/packages/server/src/browser-handlers/handler-context.ts +45 -0
  126. package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
  127. package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
  128. package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
  129. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  130. package/packages/server/src/cli.ts +347 -0
  131. package/packages/server/src/config-api.ts +130 -0
  132. package/packages/server/src/directory-service.ts +162 -0
  133. package/packages/server/src/editor-detection.ts +60 -0
  134. package/packages/server/src/editor-manager.ts +352 -0
  135. package/packages/server/src/editor-proxy.ts +134 -0
  136. package/packages/server/src/editor-registry.ts +108 -0
  137. package/packages/server/src/event-status-extraction.ts +131 -0
  138. package/packages/server/src/event-wiring.ts +589 -0
  139. package/packages/server/src/extension-register.ts +92 -0
  140. package/packages/server/src/git-operations.ts +200 -0
  141. package/packages/server/src/headless-pid-registry.ts +207 -0
  142. package/packages/server/src/idle-timer.ts +61 -0
  143. package/packages/server/src/json-store.ts +32 -0
  144. package/packages/server/src/localhost-guard.ts +117 -0
  145. package/packages/server/src/memory-event-store.ts +193 -0
  146. package/packages/server/src/memory-session-manager.ts +123 -0
  147. package/packages/server/src/meta-persistence.ts +64 -0
  148. package/packages/server/src/migrate-persistence.ts +195 -0
  149. package/packages/server/src/npm-search-proxy.ts +143 -0
  150. package/packages/server/src/oauth-callback-server.ts +177 -0
  151. package/packages/server/src/openspec-archive.ts +60 -0
  152. package/packages/server/src/package-manager-wrapper.ts +200 -0
  153. package/packages/server/src/pending-fork-registry.ts +53 -0
  154. package/packages/server/src/pending-load-manager.ts +110 -0
  155. package/packages/server/src/pending-resume-registry.ts +69 -0
  156. package/packages/server/src/pi-gateway.ts +419 -0
  157. package/packages/server/src/pi-resource-scanner.ts +369 -0
  158. package/packages/server/src/preferences-store.ts +116 -0
  159. package/packages/server/src/process-manager.ts +311 -0
  160. package/packages/server/src/provider-auth-handlers.ts +438 -0
  161. package/packages/server/src/provider-auth-storage.ts +200 -0
  162. package/packages/server/src/resolve-path.ts +12 -0
  163. package/packages/server/src/routes/editor-routes.ts +86 -0
  164. package/packages/server/src/routes/file-routes.ts +116 -0
  165. package/packages/server/src/routes/git-routes.ts +89 -0
  166. package/packages/server/src/routes/openspec-routes.ts +99 -0
  167. package/packages/server/src/routes/package-routes.ts +172 -0
  168. package/packages/server/src/routes/provider-auth-routes.ts +244 -0
  169. package/packages/server/src/routes/provider-routes.ts +101 -0
  170. package/packages/server/src/routes/route-deps.ts +23 -0
  171. package/packages/server/src/routes/session-routes.ts +91 -0
  172. package/packages/server/src/routes/system-routes.ts +271 -0
  173. package/packages/server/src/server-pid.ts +84 -0
  174. package/packages/server/src/server.ts +554 -0
  175. package/packages/server/src/session-api.ts +330 -0
  176. package/packages/server/src/session-bootstrap.ts +80 -0
  177. package/packages/server/src/session-diff.ts +178 -0
  178. package/packages/server/src/session-discovery.ts +134 -0
  179. package/packages/server/src/session-file-reader.ts +135 -0
  180. package/packages/server/src/session-order-manager.ts +73 -0
  181. package/packages/server/src/session-scanner.ts +233 -0
  182. package/packages/server/src/session-stats-reader.ts +99 -0
  183. package/packages/server/src/terminal-gateway.ts +51 -0
  184. package/packages/server/src/terminal-manager.ts +241 -0
  185. package/packages/server/src/tunnel.ts +329 -0
  186. package/packages/server/tsconfig.json +11 -0
  187. package/packages/shared/package.json +15 -0
  188. package/packages/shared/src/__tests__/config.test.ts +358 -0
  189. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  190. package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
  191. package/packages/shared/src/__tests__/protocol.test.ts +243 -0
  192. package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
  193. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  194. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  195. package/packages/shared/src/archive-types.ts +11 -0
  196. package/packages/shared/src/browser-protocol.ts +534 -0
  197. package/packages/shared/src/config.ts +245 -0
  198. package/packages/shared/src/diff-types.ts +41 -0
  199. package/packages/shared/src/editor-types.ts +18 -0
  200. package/packages/shared/src/mdns-discovery.ts +248 -0
  201. package/packages/shared/src/openspec-activity-detector.ts +109 -0
  202. package/packages/shared/src/openspec-poller.ts +96 -0
  203. package/packages/shared/src/protocol.ts +369 -0
  204. package/packages/shared/src/resolve-jiti.ts +43 -0
  205. package/packages/shared/src/rest-api.ts +255 -0
  206. package/packages/shared/src/server-identity.ts +51 -0
  207. package/packages/shared/src/session-meta.ts +86 -0
  208. package/packages/shared/src/state-replay.ts +174 -0
  209. package/packages/shared/src/stats-extractor.ts +54 -0
  210. package/packages/shared/src/terminal-types.ts +18 -0
  211. package/packages/shared/src/types.ts +351 -0
  212. 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
+ }