lightman-agent 1.0.18 → 1.0.21

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 (52) hide show
  1. package/agent.config.json +22 -23
  2. package/agent.config.template.json +30 -31
  3. package/bin/cms-agent.js +269 -248
  4. package/package.json +1 -1
  5. package/public/assets/index-CcBNCz6h.css +1 -1
  6. package/public/assets/index-D9QHMG8k.js +1 -1
  7. package/public/assets/index-H-8HDl46.js +1 -1
  8. package/public/assets/index-YodeiCia.css +1 -1
  9. package/public/assets/index-legacy-DWtNM8y7.js +41 -41
  10. package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -4
  11. package/scripts/guardian.ps1 +50 -124
  12. package/scripts/install-windows.ps1 +60 -116
  13. package/scripts/lightman-agent.logrotate +12 -12
  14. package/scripts/lightman-agent.service +38 -38
  15. package/scripts/reinstall-windows.ps1 +26 -26
  16. package/scripts/restore-desktop.ps1 +32 -32
  17. package/scripts/setup.ps1 +17 -22
  18. package/scripts/sync-display.mjs +20 -20
  19. package/scripts/uninstall-windows.ps1 +54 -54
  20. package/src/commands/display.ts +177 -177
  21. package/src/commands/kiosk.ts +113 -113
  22. package/src/commands/maintenance.ts +106 -106
  23. package/src/commands/network.ts +129 -129
  24. package/src/commands/power.ts +163 -163
  25. package/src/commands/rpi.ts +45 -45
  26. package/src/commands/screenshot.ts +166 -166
  27. package/src/commands/serial.ts +17 -17
  28. package/src/commands/update.ts +124 -124
  29. package/src/index.ts +173 -90
  30. package/src/lib/config.ts +2 -3
  31. package/src/lib/identity.ts +40 -40
  32. package/src/lib/logger.ts +137 -137
  33. package/src/lib/platform.ts +10 -10
  34. package/src/lib/rpi.ts +180 -180
  35. package/src/lib/screenMap.ts +135 -0
  36. package/src/lib/screens.ts +128 -128
  37. package/src/lib/types.ts +176 -177
  38. package/src/services/commands.ts +107 -107
  39. package/src/services/health.ts +161 -161
  40. package/src/services/localEvents.ts +60 -60
  41. package/src/services/logForwarder.ts +72 -72
  42. package/src/services/multiScreenKiosk.ts +116 -83
  43. package/src/services/oscBridge.ts +186 -186
  44. package/src/services/powerScheduler.ts +260 -260
  45. package/src/services/provisioning.ts +120 -122
  46. package/src/services/serialBridge.ts +230 -230
  47. package/src/services/serviceLauncher.ts +183 -183
  48. package/src/services/staticServer.ts +226 -226
  49. package/src/services/updater.ts +249 -249
  50. package/src/services/watchdog.ts +310 -310
  51. package/src/services/websocket.ts +152 -152
  52. package/tsconfig.json +28 -28
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'fs';
1
+ import { readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
3
  import { loadConfig } from './lib/config.js';
4
4
  import { Logger } from './lib/logger.js';
@@ -10,9 +10,11 @@ import { KioskManager } from './services/kiosk.js';
10
10
  import { registerPowerCommands } from './commands/power.js';
11
11
  import { registerKioskCommands, registerMultiScreenKioskCommands } from './commands/kiosk.js';
12
12
  import { registerScreenshotCommands } from './commands/screenshot.js';
13
- import { MultiScreenKioskManager } from './services/multiScreenKiosk.js';
14
- import { detectScreens } from './lib/screens.js';
15
- import { registerDisplayCommands } from './commands/display.js';
13
+ import { MultiScreenKioskManager } from './services/multiScreenKiosk.js';
14
+ import { detectScreens } from './lib/screens.js';
15
+ import type { DetectedScreen } from './lib/screens.js';
16
+ import { resolveScreenMap } from './lib/screenMap.js';
17
+ import { registerDisplayCommands } from './commands/display.js';
16
18
  import { registerNetworkCommands } from './commands/network.js';
17
19
  import { Updater } from './services/updater.js';
18
20
  import { registerUpdateCommands } from './commands/update.js';
@@ -30,7 +32,7 @@ import { SerialBridge } from './services/serialBridge.js';
30
32
  import { OscBridge } from './services/oscBridge.js';
31
33
  import { LocalEventServer } from './services/localEvents.js';
32
34
  import { PresenceSensor } from './services/presenceSensor.js';
33
- import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
35
+ import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
34
36
 
35
37
  function getAgentVersion(): string {
36
38
  try {
@@ -71,7 +73,7 @@ async function main(): Promise<void> {
71
73
  }
72
74
 
73
75
  // 2. Provision (get identity)
74
- // Provision with retry never crash, just keep trying.
76
+ // Provision with retry — never crash, just keep trying.
75
77
  // This prevents NSSM restart loops that kill Chrome (blinking screen).
76
78
  let identity: Identity | null = null;
77
79
  const MAX_PROVISION_ATTEMPTS = 999;
@@ -97,7 +99,7 @@ async function main(): Promise<void> {
97
99
  }
98
100
 
99
101
  if (!identity) {
100
- logger.error('Provisioning failed no identity. Exiting.');
102
+ logger.error('Provisioning failed — no identity. Exiting.');
101
103
  process.exit(1);
102
104
  }
103
105
 
@@ -161,14 +163,65 @@ async function main(): Promise<void> {
161
163
  const kioskManager = new KioskManager(kioskConfig, logger);
162
164
  registerKioskCommands(commandExecutor.register.bind(commandExecutor), kioskManager, logger);
163
165
 
164
- // Multi-screen kiosk manager handles multiple Chrome instances on multi-display devices
166
+ // Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
165
167
  const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
166
168
  const getIdentity = () => identity!;
167
169
  registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
168
170
 
169
- // Detect physical screens and report to server
170
- const detectedScreens = detectScreens(logger);
171
- multiScreenKiosk.setDetectedScreens(detectedScreens);
171
+ // Detect physical screens and keep them fresh (multi-display setups can change after boot).
172
+ let detectedScreens = detectScreens(logger);
173
+ multiScreenKiosk.setDetectedScreens(detectedScreens);
174
+
175
+ const toScreenPayload = (screens: DetectedScreen[]) => (
176
+ screens.map((s) => ({
177
+ hardwareId: s.hardwareId,
178
+ name: s.name,
179
+ index: s.index,
180
+ width: s.width,
181
+ height: s.height,
182
+ x: s.x,
183
+ y: s.y,
184
+ primary: s.primary,
185
+ }))
186
+ );
187
+
188
+ const sendAgentRegister = () => {
189
+ wsClient.send({
190
+ type: 'agent:register',
191
+ payload: {
192
+ agentVersion: getAgentVersion(),
193
+ screens: toScreenPayload(detectedScreens),
194
+ },
195
+ timestamp: Date.now(),
196
+ });
197
+ };
198
+
199
+ const refreshDetectedScreens = async (reason: string) => {
200
+ const latest = detectScreens(logger);
201
+ if (!haveScreensChanged(detectedScreens, latest)) return;
202
+
203
+ logger.info(`[Screens] Topology changed (${reason}): ${detectedScreens.length} -> ${latest.length}`);
204
+ detectedScreens = latest;
205
+ multiScreenKiosk.setDetectedScreens(detectedScreens);
206
+
207
+ if (wsClient.isConnected()) {
208
+ sendAgentRegister();
209
+ }
210
+
211
+ if (multiScreenKiosk.hasDesiredScreenMap()) {
212
+ try {
213
+ await multiScreenKiosk.reapplyDesiredMap(identity);
214
+ } catch (err) {
215
+ logger.error('[MultiKiosk] Failed to reapply desired screen map after topology change:', err);
216
+ }
217
+ }
218
+ };
219
+
220
+ const screenRefreshInterval = setInterval(() => {
221
+ refreshDetectedScreens('periodic-refresh').catch((err) => {
222
+ logger.error('[Screens] Periodic refresh failed:', err);
223
+ });
224
+ }, 20_000);
172
225
 
173
226
  // Create Watchdog (Phase 20)
174
227
  const watchdog = new Watchdog(
@@ -210,11 +263,11 @@ async function main(): Promise<void> {
210
263
  // Register serial/COM port commands (works on all platforms)
211
264
  registerSerialCommands(commandExecutor.register.bind(commandExecutor), logger);
212
265
 
213
- // Local hardware event server broadcasts directly to Chrome on this device
266
+ // Local hardware event server — broadcasts directly to Chrome on this device
214
267
  const localEventServer = new LocalEventServer(config.localEventsPort || 3402, logger);
215
268
  localEventServer.start();
216
269
 
217
- // OSC bridge listens on UDP for OSC messages and forwards triggers to display
270
+ // OSC bridge — listens on UDP for OSC messages and forwards triggers to display
218
271
  let oscBridge: OscBridge | null = null;
219
272
 
220
273
  const startOscBridge = (oscPort: number, oscAddress: string, oscHost?: string) => {
@@ -223,7 +276,7 @@ async function main(): Promise<void> {
223
276
  oscBridge.stop();
224
277
  oscBridge = null;
225
278
  }
226
- logger.info(`[OSC] Starting bridge UDP ${oscHost || '0.0.0.0'}:${oscPort} address: ${oscAddress}`);
279
+ logger.info(`[OSC] Starting bridge — UDP ${oscHost || '0.0.0.0'}:${oscPort} address: ${oscAddress}`);
227
280
  oscBridge = new OscBridge({
228
281
  wsClient,
229
282
  logger,
@@ -239,7 +292,7 @@ async function main(): Promise<void> {
239
292
  if (oscBridge) { oscBridge.stop(); oscBridge = null; }
240
293
  };
241
294
 
242
- // Serial bridge reads COM port chars (* pickup, # hangup) and forwards to server
295
+ // Serial bridge — reads COM port chars (* → pickup, # → hangup) and forwards to server
243
296
  let serialBridge: SerialBridge | null = null;
244
297
 
245
298
  /** Start or restart the serial bridge with given COM port and controllerId */
@@ -260,14 +313,14 @@ async function main(): Promise<void> {
260
313
  onEvent: (event) => localEventServer.broadcast({ type: 'hardware:event', payload: event }),
261
314
  });
262
315
  serialBridge.start();
263
- logger.info(`[SERIAL] Bridge started waiting for hardware events on ${comPort}`);
316
+ logger.info(`[SERIAL] Bridge started — waiting for hardware events on ${comPort}`);
264
317
  };
265
318
 
266
319
  const stopSerialBridge = () => {
267
320
  if (serialBridge) { serialBridge.stop(); serialBridge = null; }
268
321
  };
269
322
 
270
- // Presence sensor auto-detects HLK-LD2410B via USB serial
323
+ // Presence sensor — auto-detects HLK-LD2410B via USB serial
271
324
  let presenceSensor: PresenceSensor | null = null;
272
325
 
273
326
  const startPresenceSensor = (port?: string) => {
@@ -411,27 +464,11 @@ async function main(): Promise<void> {
411
464
  watchdog.start();
412
465
  powerScheduler.start();
413
466
 
414
- // Send registration message once connected, then auto-launch kiosk
415
- const registerInterval = setInterval(() => {
416
- if (wsClient.isConnected()) {
417
- wsClient.send({
418
- type: 'agent:register',
419
- payload: {
420
- agentVersion: getAgentVersion(),
421
- screens: detectedScreens.map(s => ({
422
- hardwareId: s.hardwareId,
423
- name: s.name,
424
- index: s.index,
425
- width: s.width,
426
- height: s.height,
427
- x: s.x,
428
- y: s.y,
429
- primary: s.primary,
430
- })),
431
- },
432
- timestamp: Date.now(),
433
- });
434
- clearInterval(registerInterval);
467
+ // Send registration message once connected, then auto-launch kiosk
468
+ const registerInterval = setInterval(() => {
469
+ if (wsClient.isConnected()) {
470
+ sendAgentRegister();
471
+ clearInterval(registerInterval);
435
472
 
436
473
  // Fetch device config first to decide single vs multi-screen kiosk
437
474
  fetchDeviceConfig(config.serverUrl, identity, logger).then((deviceCfg) => {
@@ -441,39 +478,53 @@ async function main(): Promise<void> {
441
478
  const controllerId = deviceCfg.controllerId || comPort;
442
479
  const bridgeBaud = deviceCfg.baudRate || 115200;
443
480
  logger.info(`[SERIAL] com_port found: ${comPort} | controllerId: ${controllerId} | baud: ${bridgeBaud}`);
444
- logger.info(`[SERIAL] Starting serial bridge listening on ${comPort}...`);
481
+ logger.info(`[SERIAL] Starting serial bridge — listening on ${comPort}...`);
445
482
  startSerialBridge(comPort, controllerId, bridgeBaud);
446
483
  } else {
447
- logger.info('[SERIAL] No com_port configured on this device serial bridge not started');
484
+ logger.info('[SERIAL] No com_port configured on this device — serial bridge not started');
448
485
  }
449
486
 
450
- // OSC bridge auto-start if app config has inputSource === 'osc'
487
+ // OSC bridge — auto-start if app config has inputSource === 'osc'
451
488
  if (deviceCfg && deviceCfg.oscPort && deviceCfg.oscAddress) {
452
489
  logger.info(`[OSC] Config found: port=${deviceCfg.oscPort} address=${deviceCfg.oscAddress}`);
453
490
  startOscBridge(deviceCfg.oscPort, deviceCfg.oscAddress, deviceCfg.oscHost);
454
491
  } else {
455
- logger.info('[OSC] No OSC config on this device OSC bridge not started');
492
+ logger.info('[OSC] No OSC config on this device — OSC bridge not started');
456
493
  }
457
494
 
458
- // Presence sensor auto-start if template is presence-enabled (e.g. wipro-timeline)
495
+ // Presence sensor — auto-start if template is presence-enabled (e.g. wipro-timeline)
459
496
  if (deviceCfg && deviceCfg.templateType === 'custom01-wipro-timeline') {
460
- logger.info('[Presence] Template is custom01-wipro-timeline auto-starting presence sensor');
497
+ logger.info('[Presence] Template is custom01-wipro-timeline — auto-starting presence sensor');
461
498
  startPresenceSensor();
462
499
  } else {
463
- logger.info('[Presence] Template is not presence-enabled sensor not started');
464
- }
465
-
466
- // Multi-screen: if screenMap exists, use MultiScreenKioskManager — do NOT launch single kiosk
467
- if (deviceCfg && deviceCfg.screenMap && deviceCfg.screenMap.length > 0) {
468
- logger.info(`[MultiKiosk] Found screenMap in device config: ${deviceCfg.screenMap.length} mapping(s) — skipping single kiosk`);
469
- watchdog.setMultiScreenActive(true);
470
- multiScreenKiosk.applyScreenMap(deviceCfg.screenMap, identity).catch((err) => {
471
- logger.error('[MultiKiosk] Failed to apply screenMap from config:', err);
472
- });
473
- return;
500
+ logger.info('[Presence] Template is not presence-enabled — sensor not started');
474
501
  }
475
-
476
- // No screenMap launch single-screen kiosk as before
502
+ // Multi-screen handling:
503
+ // 1) Use explicit screenMap when present.
504
+ // 2) For multi-screen apps without a saved map, auto-create placeholders.
505
+ const requestedScreenMap = deviceCfg?.screenMap || [];
506
+ const isMultiScreenApp = !!deviceCfg && deviceCfg.totalScreens > 1;
507
+ const effectiveRequestedMap = requestedScreenMap.length > 0
508
+ ? requestedScreenMap
509
+ : (isMultiScreenApp ? createPlaceholderScreenMap(deviceCfg.totalScreens) : []);
510
+
511
+ if (effectiveRequestedMap.length > 0) {
512
+ const resolved = resolveScreenMap({
513
+ requestedScreenMap: effectiveRequestedMap,
514
+ detectedScreens,
515
+ totalScreens: Math.max(deviceCfg?.totalScreens || 0, effectiveRequestedMap.length),
516
+ });
517
+ logger.info(
518
+ `[MultiKiosk] Effective map ready: requested=${effectiveRequestedMap.length}, mode=${resolved.mode}, totalScreens=${deviceCfg?.totalScreens || 0}`
519
+ );
520
+ watchdog.setMultiScreenActive(true);
521
+ multiScreenKiosk.applyScreenMap(effectiveRequestedMap, identity).catch((err) => {
522
+ logger.error('[MultiKiosk] Failed to apply effective screenMap from config:', err);
523
+ });
524
+ return;
525
+ }
526
+
527
+ // No effective multi-screen map - launch single-screen kiosk as before
477
528
  if (config.kiosk) {
478
529
  if (config.kiosk.shellMode) {
479
530
  logger.info('Shell mode: skipping Chrome launch (managed by Windows shell)');
@@ -501,9 +552,10 @@ async function main(): Promise<void> {
501
552
  }, 1000);
502
553
 
503
554
  // 7. Graceful shutdown
504
- const shutdown = async (signal: string): Promise<void> => {
505
- logger.info(`${signal} received. Shutting down...`);
506
- clearInterval(registerInterval);
555
+ const shutdown = async (signal: string): Promise<void> => {
556
+ logger.info(`${signal} received. Shutting down...`);
557
+ clearInterval(registerInterval);
558
+ clearInterval(screenRefreshInterval);
507
559
 
508
560
  // Wait for updater to finish if it's busy (max 60s)
509
561
  if (updater.isBusy()) {
@@ -551,11 +603,36 @@ async function main(): Promise<void> {
551
603
  process.exit(1);
552
604
  });
553
605
 
554
- logger.info('LIGHTMAN Agent running.');
555
- }
556
-
557
- function handleServerMessage(
558
- msg: WsMessage,
606
+ logger.info('LIGHTMAN Agent running.');
607
+ }
608
+
609
+ function createPlaceholderScreenMap(totalScreens: number): ScreenMapping[] {
610
+ const count = Math.max(0, Math.floor(totalScreens || 0));
611
+ return Array.from({ length: count }, () => ({ hardwareId: '', url: '' }));
612
+ }
613
+
614
+ function haveScreensChanged(prev: DetectedScreen[], next: DetectedScreen[]): boolean {
615
+ if (prev.length !== next.length) return true;
616
+ for (let i = 0; i < prev.length; i++) {
617
+ const a = prev[i];
618
+ const b = next[i];
619
+ if (
620
+ a.hardwareId !== b.hardwareId
621
+ || a.index !== b.index
622
+ || a.x !== b.x
623
+ || a.y !== b.y
624
+ || a.width !== b.width
625
+ || a.height !== b.height
626
+ || a.primary !== b.primary
627
+ ) {
628
+ return true;
629
+ }
630
+ }
631
+ return false;
632
+ }
633
+
634
+ function handleServerMessage(
635
+ msg: WsMessage,
559
636
  commandExecutor: CommandExecutor,
560
637
  logger: Logger,
561
638
  powerScheduler?: PowerScheduler,
@@ -589,11 +666,11 @@ function handleServerMessage(
589
666
  const comPort = msg.payload.com_port as string | undefined;
590
667
  if (comPort && startSerialBridge) {
591
668
  const controllerId = (msg.payload.controllerId as string) || comPort;
592
- logger.info(`[SERIAL] Admin updated com_port ${comPort} | Restarting serial bridge...`);
669
+ logger.info(`[SERIAL] Admin updated com_port → ${comPort} | Restarting serial bridge...`);
593
670
  startSerialBridge(comPort, controllerId);
594
671
  logger.info(`[SERIAL] Serial bridge now listening on ${comPort}`);
595
672
  } else if (comPort === '' && stopSerialBridge) {
596
- logger.info('[SERIAL] Admin cleared com_port stopping serial bridge');
673
+ logger.info('[SERIAL] Admin cleared com_port — stopping serial bridge');
597
674
  stopSerialBridge();
598
675
  }
599
676
 
@@ -603,20 +680,20 @@ function handleServerMessage(
603
680
  const inputSource = msg.payload.inputSource as string | undefined;
604
681
  if (inputSource === 'osc' && oscPort && oscAddress && startOscBridgeFn) {
605
682
  const oscHost = (msg.payload.oscHost as string) || '0.0.0.0';
606
- logger.info(`[OSC] Admin updated OSC config port=${oscPort} address=${oscAddress} restarting bridge...`);
683
+ logger.info(`[OSC] Admin updated OSC config → port=${oscPort} address=${oscAddress} — restarting bridge...`);
607
684
  startOscBridgeFn(oscPort, oscAddress, oscHost);
608
685
  } else if (inputSource === 'com' && stopOscBridgeFn) {
609
- logger.info('[OSC] Admin switched to COM input stopping OSC bridge');
686
+ logger.info('[OSC] Admin switched to COM input — stopping OSC bridge');
610
687
  stopOscBridgeFn();
611
688
  }
612
689
 
613
- // Presence sensor auto-start/stop based on template type change
690
+ // Presence sensor — auto-start/stop based on template type change
614
691
  const templateType = msg.payload.templateType as string | undefined;
615
692
  if (templateType === 'custom01-wipro-timeline' && startPresenceSensorFn) {
616
- logger.info('[Presence] Template changed to custom01-wipro-timeline starting sensor');
693
+ logger.info('[Presence] Template changed to custom01-wipro-timeline — starting sensor');
617
694
  startPresenceSensorFn();
618
695
  } else if (templateType && templateType !== 'custom01-wipro-timeline' && stopPresenceSensorFn) {
619
- logger.info('[Presence] Template changed away from wipro-timeline stopping sensor');
696
+ logger.info('[Presence] Template changed away from wipro-timeline — stopping sensor');
620
697
  stopPresenceSensorFn();
621
698
  }
622
699
 
@@ -624,15 +701,15 @@ function handleServerMessage(
624
701
  const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
625
702
  if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
626
703
  if (screenMap.length > 0) {
627
- logger.info(`[MultiKiosk] Received screenMap update: ${screenMap.length} mapping(s) killing single kiosk`);
704
+ logger.info(`[MultiKiosk] Received screenMap update: ${screenMap.length} mapping(s) — killing single kiosk`);
628
705
  if (kioskManager) kioskManager.kill().catch(() => {});
629
706
  if (watchdog) watchdog.setMultiScreenActive(true);
630
707
  multiScreenKiosk.applyScreenMap(screenMap, getIdentity()).catch((err) => {
631
708
  logger.error('[MultiKiosk] Failed to apply screenMap:', err);
632
709
  });
633
710
  } else {
634
- // Empty screenMap deactivate multi-screen, resume single kiosk
635
- logger.info('[MultiKiosk] Empty screenMap received deactivating multi-screen');
711
+ // Empty screenMap — deactivate multi-screen, resume single kiosk
712
+ logger.info('[MultiKiosk] Empty screenMap received — deactivating multi-screen');
636
713
  multiScreenKiosk.killAll().catch(() => {});
637
714
  if (watchdog) watchdog.setMultiScreenActive(false);
638
715
  }
@@ -645,14 +722,14 @@ function handleServerMessage(
645
722
  const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
646
723
  if (screenMap && Array.isArray(screenMap)) {
647
724
  if (screenMap.length > 0) {
648
- logger.info(`[MultiKiosk] Received agent:screenMap: ${screenMap.length} mapping(s) killing single kiosk`);
725
+ logger.info(`[MultiKiosk] Received agent:screenMap: ${screenMap.length} mapping(s) — killing single kiosk`);
649
726
  if (kioskManager) kioskManager.kill().catch(() => {});
650
727
  if (watchdog) watchdog.setMultiScreenActive(true);
651
728
  multiScreenKiosk.applyScreenMap(screenMap, getIdentity()).catch((err) => {
652
729
  logger.error('[MultiKiosk] Failed to apply screenMap:', err);
653
730
  });
654
731
  } else {
655
- logger.info('[MultiKiosk] Empty agent:screenMap deactivating multi-screen');
732
+ logger.info('[MultiKiosk] Empty agent:screenMap — deactivating multi-screen');
656
733
  multiScreenKiosk.killAll().catch(() => {});
657
734
  if (watchdog) watchdog.setMultiScreenActive(false);
658
735
  }
@@ -679,16 +756,17 @@ async function fetchDeviceConfig(
679
756
  serverUrl: string,
680
757
  identity: Identity,
681
758
  logger: Logger
682
- ): Promise<{
683
- comPort: string;
684
- controllerId: string;
685
- baudRate: number;
686
- screenMap: ScreenMapping[];
687
- oscPort: number;
688
- oscAddress: string;
689
- oscHost: string;
690
- templateType: string;
691
- } | null> {
759
+ ): Promise<{
760
+ comPort: string;
761
+ controllerId: string;
762
+ baudRate: number;
763
+ screenMap: ScreenMapping[];
764
+ totalScreens: number;
765
+ oscPort: number;
766
+ oscAddress: string;
767
+ oscHost: string;
768
+ templateType: string;
769
+ } | null> {
692
770
  try {
693
771
  const url = `${serverUrl}/api/devices/${identity.deviceId}/config`;
694
772
  const res = await fetch(url, {
@@ -710,8 +788,12 @@ async function fetchDeviceConfig(
710
788
  const controllerId = (appConfig.controllerId as string) || comPort;
711
789
  const baudRate = (device?.baud_rate as number) || 115200;
712
790
 
713
- // Screen map from device config (set by admin)
714
- const screenMap = (device?.screenMap as ScreenMapping[]) || [];
791
+ // Screen map from device config (set by admin)
792
+ const screenMap = (device?.screenMap as ScreenMapping[]) || [];
793
+ const appScreens = (appConfig.screens as Array<Record<string, unknown>>) || [];
794
+ const totalScreens = appScreens.length > 0
795
+ ? appScreens.length
796
+ : ((appConfig.totalScreens as number) || 0);
715
797
 
716
798
  // OSC settings from app config (custom07-osc template)
717
799
  const inputSource = appConfig.inputSource as string;
@@ -721,7 +803,7 @@ async function fetchDeviceConfig(
721
803
 
722
804
  const templateType = (assignedApp?.templateType as string) || '';
723
805
 
724
- return { comPort, controllerId, baudRate, screenMap, oscPort, oscAddress, oscHost, templateType };
806
+ return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
725
807
  } catch (err) {
726
808
  logger.debug('Failed to fetch device config:', err);
727
809
  return null;
@@ -732,3 +814,4 @@ main().catch((err) => {
732
814
  console.error('Fatal error:', err);
733
815
  process.exit(1);
734
816
  });
817
+
package/src/lib/config.ts CHANGED
@@ -32,9 +32,8 @@ const configSchema = z.object({
32
32
  healthIntervalMs: z.number().int().min(5000).default(60000),
33
33
  logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
34
34
  logFile: z.string().default('agent.log'),
35
- identityFile: z.string().default('.lightman-identity.json'),
36
- pairingTimeoutSeconds: z.number().int().min(0).default(900),
37
- localServices: z.boolean().default(true),
35
+ identityFile: z.string().default('.lightman-identity.json'),
36
+ localServices: z.boolean().default(true),
38
37
  kiosk: kioskSchema.optional(),
39
38
  screenshot: screenshotSchema.optional(),
40
39
  powerSchedule: powerScheduleSchema.optional(),
@@ -1,40 +1,40 @@
1
- import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
2
- import { resolve, dirname } from 'path';
3
- import { mkdirSync } from 'fs';
4
- import type { Identity } from './types.js';
5
-
6
- export function readIdentity(filePath: string): Identity | null {
7
- const fullPath = resolve(process.cwd(), filePath);
8
-
9
- if (!existsSync(fullPath)) {
10
- return null;
11
- }
12
-
13
- try {
14
- const raw = JSON.parse(readFileSync(fullPath, 'utf-8'));
15
- if (raw.deviceId && raw.apiKey) {
16
- return { deviceId: raw.deviceId, apiKey: raw.apiKey };
17
- }
18
- return null;
19
- } catch {
20
- return null;
21
- }
22
- }
23
-
24
- export function writeIdentity(filePath: string, identity: Identity): void {
25
- const fullPath = resolve(process.cwd(), filePath);
26
- const dir = dirname(fullPath);
27
-
28
- if (!existsSync(dir)) {
29
- mkdirSync(dir, { recursive: true });
30
- }
31
-
32
- writeFileSync(fullPath, JSON.stringify(identity, null, 2), { mode: 0o600 });
33
-
34
- // Ensure permissions are correct even if file already existed
35
- try {
36
- chmodSync(fullPath, 0o600);
37
- } catch {
38
- // Ignore permission errors on Windows
39
- }
40
- }
1
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+ import { mkdirSync } from 'fs';
4
+ import type { Identity } from './types.js';
5
+
6
+ export function readIdentity(filePath: string): Identity | null {
7
+ const fullPath = resolve(process.cwd(), filePath);
8
+
9
+ if (!existsSync(fullPath)) {
10
+ return null;
11
+ }
12
+
13
+ try {
14
+ const raw = JSON.parse(readFileSync(fullPath, 'utf-8'));
15
+ if (raw.deviceId && raw.apiKey) {
16
+ return { deviceId: raw.deviceId, apiKey: raw.apiKey };
17
+ }
18
+ return null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function writeIdentity(filePath: string, identity: Identity): void {
25
+ const fullPath = resolve(process.cwd(), filePath);
26
+ const dir = dirname(fullPath);
27
+
28
+ if (!existsSync(dir)) {
29
+ mkdirSync(dir, { recursive: true });
30
+ }
31
+
32
+ writeFileSync(fullPath, JSON.stringify(identity, null, 2), { mode: 0o600 });
33
+
34
+ // Ensure permissions are correct even if file already existed
35
+ try {
36
+ chmodSync(fullPath, 0o600);
37
+ } catch {
38
+ // Ignore permission errors on Windows
39
+ }
40
+ }