react-native-debug-toolkit 2.3.0 → 3.1.2

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 (93) hide show
  1. package/README.md +115 -97
  2. package/README.zh-CN.md +113 -95
  3. package/bin/debug-toolkit.js +114 -0
  4. package/lib/commonjs/core/initialize.js +5 -0
  5. package/lib/commonjs/core/initialize.js.map +1 -1
  6. package/lib/commonjs/features/network/index.js +28 -2
  7. package/lib/commonjs/features/network/index.js.map +1 -1
  8. package/lib/commonjs/features/network/networkInterceptor.js +14 -6
  9. package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
  10. package/lib/commonjs/index.js +56 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
  13. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  14. package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
  15. package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
  16. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +495 -0
  17. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
  18. package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
  19. package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
  20. package/lib/commonjs/utils/DaemonClient.js +721 -0
  21. package/lib/commonjs/utils/DaemonClient.js.map +1 -0
  22. package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
  23. package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
  24. package/lib/commonjs/utils/deviceReport.js +132 -0
  25. package/lib/commonjs/utils/deviceReport.js.map +1 -0
  26. package/lib/module/core/initialize.js +6 -0
  27. package/lib/module/core/initialize.js.map +1 -1
  28. package/lib/module/features/network/index.js +25 -1
  29. package/lib/module/features/network/index.js.map +1 -1
  30. package/lib/module/features/network/networkInterceptor.js +14 -6
  31. package/lib/module/features/network/networkInterceptor.js.map +1 -1
  32. package/lib/module/index.js +3 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/ui/panel/DebugPanel.js +26 -1
  35. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  36. package/lib/module/ui/panel/FloatPanelView.js +16 -63
  37. package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
  38. package/lib/module/ui/panel/StreamingSettingsModal.js +490 -0
  39. package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
  40. package/lib/module/ui/panel/useTabAnimation.js +67 -0
  41. package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
  42. package/lib/module/utils/DaemonClient.js +703 -0
  43. package/lib/module/utils/DaemonClient.js.map +1 -0
  44. package/lib/module/utils/createPersistedObservableStore.js +23 -3
  45. package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
  46. package/lib/module/utils/deviceReport.js +128 -0
  47. package/lib/module/utils/deviceReport.js.map +1 -0
  48. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  49. package/lib/typescript/src/features/network/index.d.ts +2 -0
  50. package/lib/typescript/src/features/network/index.d.ts.map +1 -1
  51. package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
  52. package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
  53. package/lib/typescript/src/index.d.ts +5 -0
  54. package/lib/typescript/src/index.d.ts.map +1 -1
  55. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  56. package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
  57. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
  58. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
  59. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
  60. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
  61. package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
  62. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
  63. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
  64. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
  65. package/lib/typescript/src/utils/deviceReport.d.ts +18 -0
  66. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
  67. package/node/daemon/src/cli.js +82 -0
  68. package/node/daemon/src/console/console.html +1662 -0
  69. package/node/daemon/src/console/index.js +47 -0
  70. package/node/daemon/src/constants.js +38 -0
  71. package/node/daemon/src/index.js +11 -0
  72. package/node/daemon/src/server.js +447 -0
  73. package/node/daemon/src/store.js +187 -0
  74. package/node/mcp/src/cli.js +31 -0
  75. package/node/mcp/src/constants.js +13 -0
  76. package/node/mcp/src/daemonClient.js +132 -0
  77. package/node/mcp/src/httpClient.js +49 -0
  78. package/node/mcp/src/index.js +15 -0
  79. package/node/mcp/src/logs.js +96 -0
  80. package/node/mcp/src/server.js +144 -0
  81. package/node/mcp/src/tools.js +84 -0
  82. package/package.json +8 -3
  83. package/src/core/initialize.ts +8 -0
  84. package/src/features/network/index.ts +30 -3
  85. package/src/features/network/networkInterceptor.ts +19 -6
  86. package/src/index.ts +22 -0
  87. package/src/ui/panel/DebugPanel.tsx +23 -1
  88. package/src/ui/panel/FloatPanelView.tsx +10 -68
  89. package/src/ui/panel/StreamingSettingsModal.tsx +528 -0
  90. package/src/ui/panel/useTabAnimation.ts +77 -0
  91. package/src/utils/DaemonClient.ts +887 -0
  92. package/src/utils/createPersistedObservableStore.ts +16 -3
  93. package/src/utils/deviceReport.ts +203 -0
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function sendUnauthorized(res) {
7
+ const body = 'Unauthorized';
8
+ res.writeHead(401, {
9
+ 'content-type': 'text/plain; charset=utf-8',
10
+ 'content-length': Buffer.byteLength(body),
11
+ });
12
+ res.end(body);
13
+ }
14
+
15
+ function createConsoleHandler(options = {}) {
16
+ const htmlPath = path.join(__dirname, 'console.html');
17
+ const html = fs.readFileSync(htmlPath, 'utf8');
18
+ const htmlBytes = Buffer.byteLength(html);
19
+ const authorize = options.authorize || (() => true);
20
+
21
+ return function handleConsoleRequest(req, res, url, method) {
22
+ if (method === 'GET' && url.pathname === '/') {
23
+ res.writeHead(302, { location: `/console${url.search || ''}` });
24
+ res.end();
25
+ return true;
26
+ }
27
+
28
+ if (method === 'GET' && url.pathname === '/console') {
29
+ if (!authorize(req, res, url)) {
30
+ if (!res.headersSent) sendUnauthorized(res);
31
+ return true;
32
+ }
33
+
34
+ res.writeHead(200, {
35
+ 'content-type': 'text/html; charset=utf-8',
36
+ 'content-length': htmlBytes,
37
+ 'cache-control': 'no-store',
38
+ });
39
+ res.end(html);
40
+ return true;
41
+ }
42
+
43
+ return false;
44
+ };
45
+ }
46
+
47
+ module.exports = { createConsoleHandler };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ const DEFAULT_HOST = '0.0.0.0';
7
+ const DEFAULT_PORT = 3799;
8
+ const DAEMON_NAME = 'react-native-debug-toolkit-daemon';
9
+ const DAEMON_VERSION = '0.1.0';
10
+ const REPORT_PROTOCOL_VERSION = 2;
11
+
12
+ function getLanIPs() {
13
+ const interfaces = os.networkInterfaces();
14
+ const ips = [];
15
+ for (const entries of Object.values(interfaces)) {
16
+ if (!entries) continue;
17
+ for (const entry of entries) {
18
+ if (entry.family === 'IPv4' && !entry.internal) {
19
+ ips.push(entry.address);
20
+ }
21
+ }
22
+ }
23
+ return ips;
24
+ }
25
+
26
+ function getDefaultDeviceStorePath() {
27
+ return path.join(os.homedir(), '.react-native-debug-toolkit', 'daemon-devices.json');
28
+ }
29
+
30
+ module.exports = {
31
+ DAEMON_NAME,
32
+ DAEMON_VERSION,
33
+ DEFAULT_HOST,
34
+ DEFAULT_PORT,
35
+ REPORT_PROTOCOL_VERSION,
36
+ getDefaultDeviceStorePath,
37
+ getLanIPs,
38
+ };
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ const { startDaemonFromCli } = require('./cli');
4
+ const { createDaemonServer } = require('./server');
5
+ const { createMemoryStore } = require('./store');
6
+
7
+ module.exports = {
8
+ createDaemonServer,
9
+ createMemoryStore,
10
+ startDaemonFromCli,
11
+ };
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const { URL } = require('url');
5
+
6
+ const {
7
+ DAEMON_NAME,
8
+ DAEMON_VERSION,
9
+ REPORT_PROTOCOL_VERSION,
10
+ getLanIPs,
11
+ } = require('./constants');
12
+ const { createMemoryStore } = require('./store');
13
+ const { createConsoleHandler } = require('./console');
14
+
15
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
16
+ const MAX_SSE_CLIENTS = 20;
17
+
18
+ function sendJson(res, statusCode, payload) {
19
+ const body = JSON.stringify(payload);
20
+ res.writeHead(statusCode, {
21
+ 'content-type': 'application/json; charset=utf-8',
22
+ 'content-length': Buffer.byteLength(body),
23
+ });
24
+ res.end(body);
25
+ }
26
+
27
+ function readJsonBody(req) {
28
+ return new Promise((resolve, reject) => {
29
+ let size = 0;
30
+ const chunks = [];
31
+
32
+ req.on('data', (chunk) => {
33
+ size += chunk.length;
34
+ if (size > MAX_BODY_BYTES) {
35
+ reject(Object.assign(new Error('Request body is too large'), { statusCode: 413 }));
36
+ req.destroy();
37
+ return;
38
+ }
39
+ chunks.push(chunk);
40
+ });
41
+
42
+ req.on('end', () => {
43
+ const raw = Buffer.concat(chunks).toString('utf8');
44
+ if (!raw) {
45
+ reject(Object.assign(new Error('Request body is required'), { statusCode: 400 }));
46
+ return;
47
+ }
48
+
49
+ try {
50
+ resolve(JSON.parse(raw));
51
+ } catch {
52
+ reject(Object.assign(new Error('Request body must be valid JSON'), { statusCode: 400 }));
53
+ }
54
+ });
55
+
56
+ req.on('error', reject);
57
+ });
58
+ }
59
+
60
+ function isValidReport(report) {
61
+ return Boolean(
62
+ report &&
63
+ typeof report === 'object' &&
64
+ report.version === REPORT_PROTOCOL_VERSION &&
65
+ report.logs &&
66
+ typeof report.logs === 'object' &&
67
+ !Array.isArray(report.logs),
68
+ );
69
+ }
70
+
71
+ function getBearerToken(req) {
72
+ const header = req.headers.authorization;
73
+ if (!header || Array.isArray(header)) {
74
+ return null;
75
+ }
76
+
77
+ const match = header.match(/^Bearer\s+(.+)$/i);
78
+ return match ? match[1] : null;
79
+ }
80
+
81
+ function requireToken(req, url, token) {
82
+ if (!token) {
83
+ return true;
84
+ }
85
+ return getBearerToken(req) === token || url.searchParams.get('token') === token;
86
+ }
87
+
88
+ function authorizeRequest(req, res, url, token) {
89
+ if (requireToken(req, url, token)) {
90
+ return true;
91
+ }
92
+
93
+ sendJson(res, 401, { ok: false, error: 'Unauthorized' });
94
+ return false;
95
+ }
96
+
97
+ function normalizeIpAddress(value) {
98
+ if (!value || typeof value !== 'string') {
99
+ return null;
100
+ }
101
+
102
+ if (value.startsWith('::ffff:')) {
103
+ return value.slice(7);
104
+ }
105
+
106
+ return value;
107
+ }
108
+
109
+ function getRequestSource(req) {
110
+ const forwarded = req.headers['x-forwarded-for'];
111
+ const forwardedIp = Array.isArray(forwarded)
112
+ ? forwarded[0]
113
+ : typeof forwarded === 'string'
114
+ ? forwarded.split(',')[0]
115
+ : null;
116
+ const ip = normalizeIpAddress((forwardedIp || '').trim()) ||
117
+ normalizeIpAddress(req.socket && req.socket.remoteAddress);
118
+ const userAgent = req.headers['user-agent'];
119
+
120
+ return {
121
+ ip: ip || 'unknown',
122
+ userAgent: typeof userAgent === 'string' ? userAgent : '',
123
+ };
124
+ }
125
+
126
+ function toDevicePayload(deviceLog) {
127
+ if (!deviceLog) {
128
+ return null;
129
+ }
130
+
131
+ return {
132
+ deviceId: deviceLog.deviceId,
133
+ firstSeenAt: deviceLog.firstSeenAt,
134
+ lastSeenAt: deviceLog.lastSeenAt,
135
+ receivedAt: deviceLog.receivedAt,
136
+ device: deviceLog.device || null,
137
+ source: deviceLog.source || null,
138
+ logCount: deviceLog.logCount,
139
+ report: deviceLog.report,
140
+ };
141
+ }
142
+
143
+ function selectLogs(deviceLog, searchParams) {
144
+ if (!deviceLog) {
145
+ return [];
146
+ }
147
+
148
+ const type = searchParams.get('type');
149
+ const limitParam = Number(searchParams.get('limit') || 50);
150
+ const limit = Number.isFinite(limitParam) && limitParam > 0
151
+ ? Math.min(Math.floor(limitParam), 500)
152
+ : 50;
153
+ const failedOnly = searchParams.get('failedOnly') === 'true';
154
+ const logs = deviceLog.report.logs || {};
155
+
156
+ let entries = [];
157
+ if (type) {
158
+ entries = Array.isArray(logs[type]) ? logs[type] : [];
159
+ } else {
160
+ entries = Object.values(logs).flatMap((value) => Array.isArray(value) ? value : []);
161
+ }
162
+
163
+ if (failedOnly) {
164
+ entries = entries.filter((entry) => (
165
+ entry &&
166
+ typeof entry === 'object' &&
167
+ (
168
+ Boolean(entry.error) ||
169
+ entry.level === 'error' ||
170
+ entry.response?.success === false ||
171
+ entry.response?.status >= 400
172
+ )
173
+ ));
174
+ }
175
+
176
+ return entries.slice(-limit);
177
+ }
178
+
179
+ function broadcastSSE(clients, eventType, deviceLog, delta) {
180
+ if (clients.size === 0) return;
181
+
182
+ const payload = delta
183
+ ? { type: 'delta', deviceId: deviceLog.deviceId, delta, logCount: deviceLog.logCount }
184
+ : {
185
+ type: 'full',
186
+ deviceId: deviceLog.deviceId,
187
+ logCount: deviceLog.logCount,
188
+ device: deviceLog.device || null,
189
+ source: deviceLog.source || null,
190
+ };
191
+ const data = JSON.stringify(payload);
192
+
193
+ clients.forEach((client) => {
194
+ try {
195
+ client.write(`event: ${eventType}\ndata: ${data}\n\n`);
196
+ } catch {
197
+ clients.delete(client);
198
+ }
199
+ });
200
+ }
201
+
202
+ // --- Route Handlers ---
203
+
204
+ function handleHealth(req, res, ctx) {
205
+ sendJson(res, 200, {
206
+ ok: true,
207
+ name: DAEMON_NAME,
208
+ version: DAEMON_VERSION,
209
+ protocolVersion: REPORT_PROTOCOL_VERSION,
210
+ ips: getLanIPs(),
211
+ deviceStore: ctx.options.deviceStorePath || null,
212
+ });
213
+ }
214
+
215
+ async function handleReport(req, res, ctx) {
216
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
217
+
218
+ try {
219
+ const report = await readJsonBody(req);
220
+ if (!isValidReport(report)) {
221
+ sendJson(res, 400, {
222
+ ok: false,
223
+ error: `Report must include version ${REPORT_PROTOCOL_VERSION} and logs object`,
224
+ });
225
+ return;
226
+ }
227
+
228
+ const deviceLog = ctx.store.saveReport(report, { source: getRequestSource(req) });
229
+ sendJson(res, 200, {
230
+ ok: true,
231
+ deviceId: deviceLog.deviceId,
232
+ receivedAt: deviceLog.receivedAt,
233
+ logCount: deviceLog.logCount,
234
+ });
235
+ } catch (error) {
236
+ sendJson(res, error.statusCode || 500, {
237
+ ok: false,
238
+ error: error.message || 'Failed to read report',
239
+ });
240
+ }
241
+ }
242
+
243
+ async function handleIngest(req, res, ctx) {
244
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
245
+
246
+ try {
247
+ const body = await readJsonBody(req);
248
+ if (!body || typeof body.deviceId !== 'string' || !body.delta) {
249
+ sendJson(res, 400, { ok: false, error: 'Must include deviceId and delta' });
250
+ return;
251
+ }
252
+
253
+ const deviceLog = ctx.store.appendLogs(body.deviceId, body.delta);
254
+ if (!deviceLog) {
255
+ sendJson(res, 404, { ok: false, error: 'Device not found' });
256
+ return;
257
+ }
258
+
259
+ sendJson(res, 200, { ok: true, deviceId: deviceLog.deviceId, logCount: deviceLog.logCount });
260
+ } catch (error) {
261
+ sendJson(res, error.statusCode || 500, {
262
+ ok: false,
263
+ error: error.message || 'Failed to ingest',
264
+ });
265
+ }
266
+ }
267
+
268
+ function handleLatestDevice(req, res, ctx) {
269
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
270
+
271
+ const deviceLog = ctx.store.getLatestDevice();
272
+ if (!deviceLog) {
273
+ sendJson(res, 404, { ok: false, error: 'No device logs have been received' });
274
+ return;
275
+ }
276
+
277
+ sendJson(res, 200, {
278
+ ok: true,
279
+ ...toDevicePayload(deviceLog),
280
+ });
281
+ }
282
+
283
+ function handleDevicesList(req, res, ctx) {
284
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
285
+
286
+ sendJson(res, 200, { ok: true, devices: ctx.store.listDevices() });
287
+ }
288
+
289
+ function handleDevicesClear(req, res, ctx) {
290
+ if (!authorizeRequest(req, res, req.url, ctx.token)) return;
291
+
292
+ ctx.store.clear();
293
+ sendJson(res, 200, { ok: true });
294
+ }
295
+
296
+ function handleDeviceMatch(req, res, ctx, url, deviceMatch) {
297
+ if (!authorizeRequest(req, res, url, ctx.token)) return;
298
+
299
+ const deviceLog = ctx.store.getDevice(decodeURIComponent(deviceMatch[1]));
300
+ if (!deviceLog) {
301
+ sendJson(res, 404, { ok: false, error: 'Device not found' });
302
+ return;
303
+ }
304
+
305
+ if (url.pathname.endsWith('/logs')) {
306
+ sendJson(res, 200, {
307
+ ok: true,
308
+ deviceId: deviceLog.deviceId,
309
+ receivedAt: deviceLog.receivedAt,
310
+ logs: selectLogs(deviceLog, url.searchParams),
311
+ });
312
+ return;
313
+ }
314
+
315
+ sendJson(res, 200, {
316
+ ok: true,
317
+ ...toDevicePayload(deviceLog),
318
+ });
319
+ }
320
+
321
+ // --- Server Factory ---
322
+
323
+ function createDaemonServer(options = {}) {
324
+ const sseClients = new Set();
325
+ const maxSseClients = options.maxSseClients || MAX_SSE_CLIENTS;
326
+ let keepaliveTimer = null;
327
+
328
+ const store = options.store || createMemoryStore({
329
+ storagePath: options.deviceStorePath || null,
330
+ onUpdate(deviceLog, type, delta) {
331
+ broadcastSSE(sseClients, 'logs', deviceLog, type === 'delta' ? delta : null);
332
+ },
333
+ });
334
+ const token = options.token || null;
335
+ const handleConsole = createConsoleHandler({
336
+ authorize: (req, res, url) => authorizeRequest(req, res, url, token),
337
+ });
338
+
339
+ const ctx = { store, token, options };
340
+
341
+ const server = http.createServer(async (req, res) => {
342
+ const url = new URL(req.url || '/', 'http://127.0.0.1');
343
+ const method = req.method || 'GET';
344
+ req.url = url;
345
+
346
+ if (handleConsole(req, res, url, method)) return;
347
+
348
+ if (method === 'GET' && url.pathname === '/events') {
349
+ if (!authorizeRequest(req, res, url, token)) return;
350
+ if (sseClients.size >= maxSseClients) {
351
+ sendJson(res, 503, { ok: false, error: 'Too many SSE clients' });
352
+ return;
353
+ }
354
+
355
+ res.writeHead(200, {
356
+ 'Content-Type': 'text/event-stream',
357
+ 'Cache-Control': 'no-cache',
358
+ Connection: 'keep-alive',
359
+ });
360
+
361
+ sseClients.add(res);
362
+ req.on('close', () => { sseClients.delete(res); });
363
+
364
+ const latest = store.getLatestDevice();
365
+ if (latest) {
366
+ const data = JSON.stringify({
367
+ type: 'full',
368
+ deviceId: latest.deviceId,
369
+ logCount: latest.logCount,
370
+ device: latest.device || null,
371
+ source: latest.source || null,
372
+ });
373
+ res.write(`event: logs\ndata: ${data}\n\n`);
374
+ }
375
+
376
+ if (!keepaliveTimer) {
377
+ keepaliveTimer = setInterval(() => {
378
+ if (sseClients.size > 0) {
379
+ sseClients.forEach((client) => {
380
+ try { client.write(':keepalive\n\n'); } catch { sseClients.delete(client); }
381
+ });
382
+ } else {
383
+ clearInterval(keepaliveTimer);
384
+ keepaliveTimer = null;
385
+ }
386
+ }, 30000);
387
+ }
388
+ return;
389
+ }
390
+
391
+ if (method === 'GET' && url.pathname === '/health') {
392
+ return handleHealth(req, res, ctx);
393
+ }
394
+
395
+ if (method === 'POST' && url.pathname === '/report') {
396
+ return await handleReport(req, res, ctx);
397
+ }
398
+
399
+ if (method === 'POST' && url.pathname === '/ingest') {
400
+ return await handleIngest(req, res, ctx);
401
+ }
402
+
403
+ if (method === 'GET' && url.pathname === '/devices/latest') {
404
+ return handleLatestDevice(req, res, ctx);
405
+ }
406
+
407
+ if (method === 'GET' && url.pathname === '/devices') {
408
+ return handleDevicesList(req, res, ctx);
409
+ }
410
+
411
+ if (method === 'DELETE' && url.pathname === '/devices') {
412
+ return handleDevicesClear(req, res, ctx);
413
+ }
414
+
415
+ const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)(?:\/logs)?$/);
416
+ if (method === 'GET' && deviceMatch) {
417
+ return handleDeviceMatch(req, res, ctx, url, deviceMatch);
418
+ }
419
+
420
+ sendJson(res, 404, { ok: false, error: 'Not found' });
421
+ });
422
+
423
+ server.on('close', () => {
424
+ if (keepaliveTimer) {
425
+ clearInterval(keepaliveTimer);
426
+ keepaliveTimer = null;
427
+ }
428
+ sseClients.forEach((client) => {
429
+ try { client.destroy(); } catch {}
430
+ });
431
+ sseClients.clear();
432
+ });
433
+
434
+ return { server, store };
435
+ }
436
+
437
+ module.exports = {
438
+ createDaemonServer,
439
+ handleHealth,
440
+ handleReport,
441
+ handleIngest,
442
+ handleLatestDevice,
443
+ handleDevicesList,
444
+ handleDevicesClear,
445
+ handleDeviceMatch,
446
+ isValidReport,
447
+ };