lightman-agent 1.0.22 → 1.0.24

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.
@@ -15,6 +15,7 @@ REM ================================================================
15
15
 
16
16
  set INSTALL_DIR=C:\Program Files\Lightman\Agent
17
17
  set CONFIG_FILE=%INSTALL_DIR%\agent.config.json
18
+ set URL_SIDECAR=C:\ProgramData\Lightman\kiosk-url.txt
18
19
  set CHROME_DATA=C:\ProgramData\Lightman\chrome-kiosk
19
20
  set LOG_FILE=C:\ProgramData\Lightman\logs\shell.log
20
21
 
@@ -68,14 +69,27 @@ if not "%DEVICE_SLUG%"=="" (
68
69
  echo [%date% %time%] WARNING: No slug in config! >> "%LOG_FILE%"
69
70
  )
70
71
 
72
+ REM If agent wrote a URL sidecar (includes deviceId/apiKey), prefer it.
73
+ if exist "%URL_SIDECAR%" (
74
+ for /f "usebackq delims=" %%u in ("%URL_SIDECAR%") do set SIDE_URL=%%u
75
+ if not "%SIDE_URL%"=="" (
76
+ set URL=%SIDE_URL%
77
+ echo [%date% %time%] Using sidecar URL >> "%LOG_FILE%"
78
+ )
79
+ )
80
+
71
81
  REM Fallback browser
72
82
  if "%BROWSER%"=="" (
73
83
  if exist "C:\Program Files\Google\Chrome\Application\chrome.exe" (
74
84
  set "BROWSER=C:\Program Files\Google\Chrome\Application\chrome.exe"
75
85
  ) else if exist "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" (
76
86
  set "BROWSER=C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
87
+ ) else if exist "C:\Program Files\Microsoft\Edge\Application\msedge.exe" (
88
+ set "BROWSER=C:\Program Files\Microsoft\Edge\Application\msedge.exe"
89
+ ) else if exist "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" (
90
+ set "BROWSER=C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
77
91
  ) else (
78
- echo [%date% %time%] ERROR: Chrome not found! >> "%LOG_FILE%"
92
+ echo [%date% %time%] ERROR: No supported kiosk browser found! >> "%LOG_FILE%"
79
93
  start explorer.exe
80
94
  exit /b 1
81
95
  )
@@ -109,20 +123,27 @@ REM ----------------------------------------------------------------
109
123
  REM Infinite Chrome loop
110
124
  REM ----------------------------------------------------------------
111
125
  :loop
112
- REM Re-read slug from config on every loop iteration
113
- REM This way if someone changes agent.config.json, the next
114
- REM Chrome restart picks up the new slug automatically.
115
- if exist "%CONFIG_FILE%" (
116
- for /f "delims=" %%a in ('node -e "try{console.log(JSON.parse(require('fs').readFileSync(String.raw`%CONFIG_FILE%`,'utf8')).deviceSlug)}catch(e){console.log('')}" 2^>nul') do (
117
- if not "%%a"=="" set URL=http://localhost:3403/display/%%a
126
+ REM Prefer sidecar URL for auth params/device routing; fallback to slug URL.
127
+ set SIDE_URL=
128
+ if exist "%URL_SIDECAR%" (
129
+ for /f "usebackq delims=" %%u in ("%URL_SIDECAR%") do set SIDE_URL=%%u
130
+ )
131
+ if not "%SIDE_URL%"=="" (
132
+ set URL=%SIDE_URL%
133
+ ) else (
134
+ REM Re-read slug from config on every loop iteration.
135
+ if exist "%CONFIG_FILE%" (
136
+ for /f "delims=" %%a in ('node -e "try{console.log(JSON.parse(require('fs').readFileSync(String.raw`%CONFIG_FILE%`,'utf8')).deviceSlug)}catch(e){console.log('')}" 2^>nul') do (
137
+ if not "%%a"=="" set URL=http://localhost:3403/display/%%a
138
+ )
118
139
  )
119
140
  )
120
141
 
121
- echo [%date% %time%] Launching Chrome: %URL% >> "%LOG_FILE%"
142
+ echo [%date% %time%] Launching browser: %URL% >> "%LOG_FILE%"
122
143
 
123
144
  start /wait "" "%BROWSER%" --kiosk --noerrdialogs --disable-infobars --disable-session-crashed-bubble --no-first-run --no-default-browser-check --start-fullscreen --disable-translate --disable-extensions --autoplay-policy=no-user-gesture-required --disable-features=TranslateUI --user-data-dir="%CHROME_DATA%" "%URL%"
124
145
 
125
- echo [%date% %time%] Chrome exited (code: %errorlevel%). Restarting in 3s... >> "%LOG_FILE%"
146
+ echo [%date% %time%] Browser exited (code: %errorlevel%). Restarting in 3s... >> "%LOG_FILE%"
126
147
  timeout /t 3 /nobreak >nul
127
148
 
128
149
  goto loop
package/scripts/setup.ps1 CHANGED
@@ -2,8 +2,8 @@
2
2
  # Generates agent.config.json for this specific device.
3
3
  #
4
4
  # Usage (run from agent directory or scripts directory):
5
- # powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.1.100:3401"
6
- # powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.1.100:3401" -Timezone "Asia/Kolkata"
5
+ # powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.10.100:3401"
6
+ # powershell -ExecutionPolicy Bypass -File setup.ps1 -Slug "f-av01" -Server "http://192.168.10.100:3401" -Timezone "Asia/Kolkata"
7
7
  #
8
8
  # This script MUST be run once on every new device installation.
9
9
  # It clears any cached identity so the device provisions fresh.
@@ -42,6 +42,23 @@ Write-Host " Install dir: $InstallDir"
42
42
  Write-Host " Timezone: $Timezone"
43
43
  Write-Host ""
44
44
 
45
+ function Get-KioskBrowserPath {
46
+ $candidates = @(
47
+ "C:\Program Files\Google\Chrome\Application\chrome.exe",
48
+ "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
49
+ (Join-Path $env:LOCALAPPDATA "Google\Chrome\Application\chrome.exe"),
50
+ "C:\Program Files\Microsoft\Edge\Application\msedge.exe",
51
+ "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
52
+ (Join-Path $env:LOCALAPPDATA "Microsoft\Edge\Application\msedge.exe")
53
+ ) | Where-Object { $_ -and (Test-Path $_) }
54
+
55
+ if ($candidates.Count -gt 0) {
56
+ return $candidates[0]
57
+ }
58
+
59
+ throw "Neither Google Chrome nor Microsoft Edge was found. Install one of them and re-run setup."
60
+ }
61
+
45
62
  # 1. Clear cached identity (CRITICAL - prevents old device credentials leaking)
46
63
  $IdentityFile = Join-Path $InstallDir ".lightman-identity.json"
47
64
  if (Test-Path $IdentityFile) {
@@ -55,13 +72,12 @@ if (Test-Path $IdentityFile) {
55
72
  $KioskUrl = "http://localhost:3403/display/$Slug"
56
73
 
57
74
  # 3. Detect browser path
58
- $BrowserPath = "C:\Program Files\Google\Chrome\Application\chrome.exe"
59
- if (-not (Test-Path $BrowserPath)) {
60
- $BrowserPath = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
61
- }
62
- if (-not (Test-Path $BrowserPath)) {
63
- $BrowserPath = "chromium-browser"
64
- Write-Host "[WARN] Chrome not found - using 'chromium-browser'" -ForegroundColor Yellow
75
+ try {
76
+ $BrowserPath = Get-KioskBrowserPath
77
+ Write-Host "[OK] Using kiosk browser: $BrowserPath" -ForegroundColor Green
78
+ } catch {
79
+ Write-Host "[ERROR] $($_.Exception.Message)" -ForegroundColor Red
80
+ exit 1
65
81
  }
66
82
 
67
83
  $ChromeDataDir = "C:\ProgramData\Lightman\chrome-kiosk"
package/scripts/setup.sh CHANGED
@@ -3,8 +3,8 @@
3
3
  # Generates agent.config.json for this specific device.
4
4
  #
5
5
  # Usage:
6
- # sudo bash setup.sh --slug f-av01 --server http://192.168.1.100:3401
7
- # sudo bash setup.sh --slug f-av01 --server http://192.168.1.100:3401 --timezone Asia/Kolkata --dir /opt/lightman/agent
6
+ # sudo bash setup.sh --slug f-av01 --server http://192.168.10.100:3401
7
+ # sudo bash setup.sh --slug f-av01 --server http://192.168.10.100:3401 --timezone Asia/Kolkata --dir /opt/lightman/agent
8
8
  #
9
9
  # This script MUST be run once on every new device installation.
10
10
  # It clears any cached identity so the device provisions fresh.
@@ -37,13 +37,13 @@ done
37
37
 
38
38
  if [[ -z "$SLUG" ]]; then
39
39
  echo "Error: --slug is required"
40
- echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.1.100:3401"
40
+ echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.10.100:3401"
41
41
  exit 1
42
42
  fi
43
43
 
44
44
  if [[ -z "$SERVER" ]]; then
45
45
  echo "Error: --server is required"
46
- echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.1.100:3401"
46
+ echo "Usage: bash setup.sh --slug f-av01 --server http://192.168.10.100:3401"
47
47
  exit 1
48
48
  fi
49
49
 
package/src/index.ts CHANGED
@@ -10,11 +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 type { DetectedScreen } from './lib/screens.js';
16
- import { resolveScreenMap } from './lib/screenMap.js';
17
- 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';
18
18
  import { registerNetworkCommands } from './commands/network.js';
19
19
  import { Updater } from './services/updater.js';
20
20
  import { registerUpdateCommands } from './commands/update.js';
@@ -32,7 +32,7 @@ import { SerialBridge } from './services/serialBridge.js';
32
32
  import { OscBridge } from './services/oscBridge.js';
33
33
  import { LocalEventServer } from './services/localEvents.js';
34
34
  import { PresenceSensor } from './services/presenceSensor.js';
35
- import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
35
+ import type { WsMessage, KioskConfig, PowerScheduleConfig, Identity, ScreenMapping } from './lib/types.js';
36
36
 
37
37
  function getAgentVersion(): string {
38
38
  try {
@@ -111,10 +111,26 @@ async function main(): Promise<void> {
111
111
  serverUrl: config.serverUrl,
112
112
  identity,
113
113
  logger,
114
- onMessage: (msg: WsMessage) => {
115
- handleServerMessage(msg, commandExecutor, logger, powerScheduler, startSerialBridge, stopSerialBridge, startOscBridge, stopOscBridge, multiScreenKiosk, getIdentity, kioskManager, watchdog, startPresenceSensor, stopPresenceSensor);
116
- },
117
- });
114
+ onMessage: (msg: WsMessage) => {
115
+ handleServerMessage(
116
+ msg,
117
+ commandExecutor,
118
+ logger,
119
+ powerScheduler,
120
+ startSerialBridge,
121
+ stopSerialBridge,
122
+ startOscBridge,
123
+ stopOscBridge,
124
+ multiScreenKiosk,
125
+ getIdentity,
126
+ kioskManager,
127
+ watchdog,
128
+ startPresenceSensor,
129
+ stopPresenceSensor,
130
+ () => lastKnownTotalScreens
131
+ );
132
+ },
133
+ });
118
134
 
119
135
  // 4. Start health monitor
120
136
  const healthMonitor = new HealthMonitor(
@@ -163,65 +179,66 @@ async function main(): Promise<void> {
163
179
  const kioskManager = new KioskManager(kioskConfig, logger);
164
180
  registerKioskCommands(commandExecutor.register.bind(commandExecutor), kioskManager, logger);
165
181
 
166
- // Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
167
- const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
168
- const getIdentity = () => identity!;
169
- registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
170
-
182
+ // Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
183
+ const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
184
+ const getIdentity = () => identity!;
185
+ registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
186
+
171
187
  // Detect physical screens and keep them fresh (multi-display setups can change after boot).
188
+ let lastKnownTotalScreens = 0;
172
189
  let detectedScreens = detectScreens(logger);
173
190
  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);
191
+
192
+ const toScreenPayload = (screens: DetectedScreen[]) => (
193
+ screens.map((s) => ({
194
+ hardwareId: s.hardwareId,
195
+ name: s.name,
196
+ index: s.index,
197
+ width: s.width,
198
+ height: s.height,
199
+ x: s.x,
200
+ y: s.y,
201
+ primary: s.primary,
202
+ }))
203
+ );
204
+
205
+ const sendAgentRegister = () => {
206
+ wsClient.send({
207
+ type: 'agent:register',
208
+ payload: {
209
+ agentVersion: getAgentVersion(),
210
+ screens: toScreenPayload(detectedScreens),
211
+ },
212
+ timestamp: Date.now(),
213
+ });
214
+ };
215
+
216
+ const refreshDetectedScreens = async (reason: string) => {
217
+ const latest = detectScreens(logger);
218
+ if (!haveScreensChanged(detectedScreens, latest)) return;
219
+
220
+ logger.info(`[Screens] Topology changed (${reason}): ${detectedScreens.length} -> ${latest.length}`);
221
+ detectedScreens = latest;
222
+ multiScreenKiosk.setDetectedScreens(detectedScreens);
223
+
224
+ if (wsClient.isConnected()) {
225
+ sendAgentRegister();
226
+ }
227
+
228
+ if (multiScreenKiosk.hasDesiredScreenMap()) {
229
+ try {
230
+ await multiScreenKiosk.reapplyDesiredMap(identity);
231
+ } catch (err) {
232
+ logger.error('[MultiKiosk] Failed to reapply desired screen map after topology change:', err);
233
+ }
234
+ }
235
+ };
236
+
237
+ const screenRefreshInterval = setInterval(() => {
238
+ refreshDetectedScreens('periodic-refresh').catch((err) => {
239
+ logger.error('[Screens] Periodic refresh failed:', err);
240
+ });
241
+ }, 20_000);
225
242
 
226
243
  // Create Watchdog (Phase 20)
227
244
  const watchdog = new Watchdog(
@@ -464,11 +481,11 @@ async function main(): Promise<void> {
464
481
  watchdog.start();
465
482
  powerScheduler.start();
466
483
 
467
- // Send registration message once connected, then auto-launch kiosk
468
- const registerInterval = setInterval(() => {
469
- if (wsClient.isConnected()) {
470
- sendAgentRegister();
471
- clearInterval(registerInterval);
484
+ // Send registration message once connected, then auto-launch kiosk
485
+ const registerInterval = setInterval(() => {
486
+ if (wsClient.isConnected()) {
487
+ sendAgentRegister();
488
+ clearInterval(registerInterval);
472
489
 
473
490
  // Fetch device config first to decide single vs multi-screen kiosk
474
491
  fetchDeviceConfig(config.serverUrl, identity, logger).then((deviceCfg) => {
@@ -499,31 +516,30 @@ async function main(): Promise<void> {
499
516
  } else {
500
517
  logger.info('[Presence] Template is not presence-enabled — sensor not started');
501
518
  }
502
- // Multi-screen handling:
503
- // 1) Use explicit screenMap when present.
504
- // 2) For multi-screen apps without a saved map, auto-create placeholders.
519
+ // Multi-screen handling:
520
+ // 1) Use explicit screenMap when present.
521
+ // 2) For multi-screen apps without a saved map, auto-create placeholders.
505
522
  const requestedScreenMap = deviceCfg?.screenMap || [];
506
- const isMultiScreenApp = !!deviceCfg && deviceCfg.totalScreens > 1;
507
- const effectiveRequestedMap = requestedScreenMap.length > 0
508
- ? requestedScreenMap
509
- : (isMultiScreenApp ? createPlaceholderScreenMap(deviceCfg.totalScreens) : []);
523
+ const totalScreens = Math.max(deviceCfg?.totalScreens || 0, requestedScreenMap.length);
524
+ lastKnownTotalScreens = totalScreens;
525
+ const effectiveRequestedMap = normalizeScreenMapForTotalScreens(requestedScreenMap, totalScreens);
510
526
 
511
527
  if (effectiveRequestedMap.length > 0) {
512
528
  const resolved = resolveScreenMap({
513
529
  requestedScreenMap: effectiveRequestedMap,
514
530
  detectedScreens,
515
- totalScreens: Math.max(deviceCfg?.totalScreens || 0, effectiveRequestedMap.length),
531
+ totalScreens,
516
532
  });
517
533
  logger.info(
518
- `[MultiKiosk] Effective map ready: requested=${effectiveRequestedMap.length}, mode=${resolved.mode}, totalScreens=${deviceCfg?.totalScreens || 0}`
534
+ `[MultiKiosk] Effective map ready: requested=${requestedScreenMap.length}, effective=${effectiveRequestedMap.length}, mode=${resolved.mode}, totalScreens=${totalScreens}`
519
535
  );
520
536
  watchdog.setMultiScreenActive(true);
521
537
  multiScreenKiosk.applyScreenMap(effectiveRequestedMap, identity).catch((err) => {
522
538
  logger.error('[MultiKiosk] Failed to apply effective screenMap from config:', err);
523
- });
524
- return;
525
- }
526
-
539
+ });
540
+ return;
541
+ }
542
+
527
543
  // No effective multi-screen map - launch single-screen kiosk as before
528
544
  if (config.kiosk) {
529
545
  if (config.kiosk.shellMode) {
@@ -552,10 +568,10 @@ async function main(): Promise<void> {
552
568
  }, 1000);
553
569
 
554
570
  // 7. Graceful shutdown
555
- const shutdown = async (signal: string): Promise<void> => {
556
- logger.info(`${signal} received. Shutting down...`);
557
- clearInterval(registerInterval);
558
- clearInterval(screenRefreshInterval);
571
+ const shutdown = async (signal: string): Promise<void> => {
572
+ logger.info(`${signal} received. Shutting down...`);
573
+ clearInterval(registerInterval);
574
+ clearInterval(screenRefreshInterval);
559
575
 
560
576
  // Wait for updater to finish if it's busy (max 60s)
561
577
  if (updater.isBusy()) {
@@ -603,50 +619,74 @@ async function main(): Promise<void> {
603
619
  process.exit(1);
604
620
  });
605
621
 
606
- logger.info('LIGHTMAN Agent running.');
607
- }
608
-
622
+ logger.info('LIGHTMAN Agent running.');
623
+ }
624
+
609
625
  function createPlaceholderScreenMap(totalScreens: number): ScreenMapping[] {
610
626
  const count = Math.max(0, Math.floor(totalScreens || 0));
611
627
  return Array.from({ length: count }, () => ({ hardwareId: '', url: '' }));
612
628
  }
613
629
 
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
- }
630
+ function normalizeScreenMapForTotalScreens(
631
+ screenMap: ScreenMapping[] | undefined,
632
+ totalScreens: number
633
+ ): ScreenMapping[] {
634
+ const requested = Array.isArray(screenMap)
635
+ ? screenMap.map((m) => ({
636
+ hardwareId: String(m.hardwareId || ''),
637
+ url: String(m.url || ''),
638
+ ...(m.label ? { label: String(m.label) } : {}),
639
+ }))
640
+ : [];
633
641
 
642
+ const targetCount = Math.max(requested.length, Math.max(0, Math.floor(totalScreens || 0)));
643
+ if (targetCount === 0) return [];
644
+
645
+ if (requested.length >= targetCount) return requested;
646
+
647
+ return [
648
+ ...requested,
649
+ ...createPlaceholderScreenMap(targetCount - requested.length),
650
+ ];
651
+ }
652
+
653
+ function haveScreensChanged(prev: DetectedScreen[], next: DetectedScreen[]): boolean {
654
+ if (prev.length !== next.length) return true;
655
+ for (let i = 0; i < prev.length; i++) {
656
+ const a = prev[i];
657
+ const b = next[i];
658
+ if (
659
+ a.hardwareId !== b.hardwareId
660
+ || a.index !== b.index
661
+ || a.x !== b.x
662
+ || a.y !== b.y
663
+ || a.width !== b.width
664
+ || a.height !== b.height
665
+ || a.primary !== b.primary
666
+ ) {
667
+ return true;
668
+ }
669
+ }
670
+ return false;
671
+ }
672
+
634
673
  function handleServerMessage(
635
674
  msg: WsMessage,
636
- commandExecutor: CommandExecutor,
675
+ commandExecutor: CommandExecutor,
637
676
  logger: Logger,
638
677
  powerScheduler?: PowerScheduler,
639
678
  startSerialBridge?: (comPort: string, controllerId: string, baudRate?: number) => void,
640
679
  stopSerialBridge?: () => void,
641
680
  startOscBridgeFn?: (oscPort: number, oscAddress: string, oscHost?: string) => void,
642
681
  stopOscBridgeFn?: () => void,
643
- multiScreenKiosk?: MultiScreenKioskManager,
644
- getIdentity?: () => Identity,
645
- kioskManager?: KioskManager,
646
- watchdog?: Watchdog,
647
- startPresenceSensorFn?: (port?: string) => void,
648
- stopPresenceSensorFn?: () => void
649
- ): void {
682
+ multiScreenKiosk?: MultiScreenKioskManager,
683
+ getIdentity?: () => Identity,
684
+ kioskManager?: KioskManager,
685
+ watchdog?: Watchdog,
686
+ startPresenceSensorFn?: (port?: string) => void,
687
+ stopPresenceSensorFn?: () => void,
688
+ getTotalScreensHint?: () => number
689
+ ): void {
650
690
  switch (msg.type) {
651
691
  case 'connected':
652
692
  logger.info('Server acknowledged connection');
@@ -698,16 +738,25 @@ function handleServerMessage(
698
738
  }
699
739
 
700
740
  // Admin pushed updated screenMap via device config save
701
- const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
702
- if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
703
- if (screenMap.length > 0) {
704
- logger.info(`[MultiKiosk] Received screenMap update: ${screenMap.length} mapping(s) — killing single kiosk`);
705
- if (kioskManager) kioskManager.kill().catch(() => {});
706
- if (watchdog) watchdog.setMultiScreenActive(true);
707
- multiScreenKiosk.applyScreenMap(screenMap, getIdentity()).catch((err) => {
708
- logger.error('[MultiKiosk] Failed to apply screenMap:', err);
709
- });
710
- } else {
741
+ const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
742
+ if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
743
+ const payloadTotalScreens = Number(msg.payload.totalScreens || 0);
744
+ const hintTotalScreens = Math.max(
745
+ Number.isFinite(payloadTotalScreens) ? payloadTotalScreens : 0,
746
+ getTotalScreensHint ? getTotalScreensHint() : 0
747
+ );
748
+ const effectiveScreenMap = screenMap.length > 0
749
+ ? normalizeScreenMapForTotalScreens(screenMap, hintTotalScreens)
750
+ : screenMap;
751
+
752
+ if (screenMap.length > 0) {
753
+ logger.info(`[MultiKiosk] Received screenMap update: requested=${screenMap.length}, effective=${effectiveScreenMap.length}, totalScreens=${hintTotalScreens} — killing single kiosk`);
754
+ if (kioskManager) kioskManager.kill().catch(() => {});
755
+ if (watchdog) watchdog.setMultiScreenActive(true);
756
+ multiScreenKiosk.applyScreenMap(effectiveScreenMap, getIdentity()).catch((err) => {
757
+ logger.error('[MultiKiosk] Failed to apply screenMap:', err);
758
+ });
759
+ } else {
711
760
  // Empty screenMap — deactivate multi-screen, resume single kiosk
712
761
  logger.info('[MultiKiosk] Empty screenMap received — deactivating multi-screen');
713
762
  multiScreenKiosk.killAll().catch(() => {});
@@ -719,16 +768,25 @@ function handleServerMessage(
719
768
  case 'agent:screenMap':
720
769
  // Direct screenMap push from server
721
770
  if (msg.payload && multiScreenKiosk && getIdentity) {
722
- const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
723
- if (screenMap && Array.isArray(screenMap)) {
724
- if (screenMap.length > 0) {
725
- logger.info(`[MultiKiosk] Received agent:screenMap: ${screenMap.length} mapping(s) — killing single kiosk`);
726
- if (kioskManager) kioskManager.kill().catch(() => {});
727
- if (watchdog) watchdog.setMultiScreenActive(true);
728
- multiScreenKiosk.applyScreenMap(screenMap, getIdentity()).catch((err) => {
729
- logger.error('[MultiKiosk] Failed to apply screenMap:', err);
730
- });
731
- } else {
771
+ const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
772
+ if (screenMap && Array.isArray(screenMap)) {
773
+ const payloadTotalScreens = Number(msg.payload.totalScreens || 0);
774
+ const hintTotalScreens = Math.max(
775
+ Number.isFinite(payloadTotalScreens) ? payloadTotalScreens : 0,
776
+ getTotalScreensHint ? getTotalScreensHint() : 0
777
+ );
778
+ const effectiveScreenMap = screenMap.length > 0
779
+ ? normalizeScreenMapForTotalScreens(screenMap, hintTotalScreens)
780
+ : screenMap;
781
+
782
+ if (screenMap.length > 0) {
783
+ logger.info(`[MultiKiosk] Received agent:screenMap: requested=${screenMap.length}, effective=${effectiveScreenMap.length}, totalScreens=${hintTotalScreens} — killing single kiosk`);
784
+ if (kioskManager) kioskManager.kill().catch(() => {});
785
+ if (watchdog) watchdog.setMultiScreenActive(true);
786
+ multiScreenKiosk.applyScreenMap(effectiveScreenMap, getIdentity()).catch((err) => {
787
+ logger.error('[MultiKiosk] Failed to apply screenMap:', err);
788
+ });
789
+ } else {
732
790
  logger.info('[MultiKiosk] Empty agent:screenMap — deactivating multi-screen');
733
791
  multiScreenKiosk.killAll().catch(() => {});
734
792
  if (watchdog) watchdog.setMultiScreenActive(false);
@@ -756,17 +814,17 @@ async function fetchDeviceConfig(
756
814
  serverUrl: string,
757
815
  identity: Identity,
758
816
  logger: Logger
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> {
817
+ ): Promise<{
818
+ comPort: string;
819
+ controllerId: string;
820
+ baudRate: number;
821
+ screenMap: ScreenMapping[];
822
+ totalScreens: number;
823
+ oscPort: number;
824
+ oscAddress: string;
825
+ oscHost: string;
826
+ templateType: string;
827
+ } | null> {
770
828
  try {
771
829
  const url = `${serverUrl}/api/devices/${identity.deviceId}/config`;
772
830
  const res = await fetch(url, {
@@ -788,12 +846,12 @@ async function fetchDeviceConfig(
788
846
  const controllerId = (appConfig.controllerId as string) || comPort;
789
847
  const baudRate = (device?.baud_rate as number) || 115200;
790
848
 
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);
849
+ // Screen map from device config (set by admin)
850
+ const screenMap = (device?.screenMap as ScreenMapping[]) || [];
851
+ const appScreens = (appConfig.screens as Array<Record<string, unknown>>) || [];
852
+ const totalScreens = appScreens.length > 0
853
+ ? appScreens.length
854
+ : ((appConfig.totalScreens as number) || 0);
797
855
 
798
856
  // OSC settings from app config (custom07-osc template)
799
857
  const inputSource = appConfig.inputSource as string;
@@ -803,7 +861,7 @@ async function fetchDeviceConfig(
803
861
 
804
862
  const templateType = (assignedApp?.templateType as string) || '';
805
863
 
806
- return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
864
+ return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
807
865
  } catch (err) {
808
866
  logger.debug('Failed to fetch device config:', err);
809
867
  return null;