lightman-agent 1.0.22 → 1.0.23

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 {
@@ -168,60 +168,60 @@ async function main(): Promise<void> {
168
168
  const getIdentity = () => identity!;
169
169
  registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
170
170
 
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);
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);
225
225
 
226
226
  // Create Watchdog (Phase 20)
227
227
  const watchdog = new Watchdog(
@@ -464,11 +464,11 @@ async function main(): Promise<void> {
464
464
  watchdog.start();
465
465
  powerScheduler.start();
466
466
 
467
- // Send registration message once connected, then auto-launch kiosk
468
- const registerInterval = setInterval(() => {
469
- if (wsClient.isConnected()) {
470
- sendAgentRegister();
471
- 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);
472
472
 
473
473
  // Fetch device config first to decide single vs multi-screen kiosk
474
474
  fetchDeviceConfig(config.serverUrl, identity, logger).then((deviceCfg) => {
@@ -499,31 +499,31 @@ async function main(): Promise<void> {
499
499
  } else {
500
500
  logger.info('[Presence] Template is not presence-enabled — sensor not started');
501
501
  }
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
-
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
527
  // No effective multi-screen map - launch single-screen kiosk as before
528
528
  if (config.kiosk) {
529
529
  if (config.kiosk.shellMode) {
@@ -552,10 +552,10 @@ async function main(): Promise<void> {
552
552
  }, 1000);
553
553
 
554
554
  // 7. Graceful shutdown
555
- const shutdown = async (signal: string): Promise<void> => {
556
- logger.info(`${signal} received. Shutting down...`);
557
- clearInterval(registerInterval);
558
- clearInterval(screenRefreshInterval);
555
+ const shutdown = async (signal: string): Promise<void> => {
556
+ logger.info(`${signal} received. Shutting down...`);
557
+ clearInterval(registerInterval);
558
+ clearInterval(screenRefreshInterval);
559
559
 
560
560
  // Wait for updater to finish if it's busy (max 60s)
561
561
  if (updater.isBusy()) {
@@ -603,36 +603,36 @@ async function main(): Promise<void> {
603
603
  process.exit(1);
604
604
  });
605
605
 
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,
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,
636
636
  commandExecutor: CommandExecutor,
637
637
  logger: Logger,
638
638
  powerScheduler?: PowerScheduler,
@@ -756,17 +756,17 @@ async function fetchDeviceConfig(
756
756
  serverUrl: string,
757
757
  identity: Identity,
758
758
  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> {
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> {
770
770
  try {
771
771
  const url = `${serverUrl}/api/devices/${identity.deviceId}/config`;
772
772
  const res = await fetch(url, {
@@ -788,12 +788,12 @@ async function fetchDeviceConfig(
788
788
  const controllerId = (appConfig.controllerId as string) || comPort;
789
789
  const baudRate = (device?.baud_rate as number) || 115200;
790
790
 
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);
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);
797
797
 
798
798
  // OSC settings from app config (custom07-osc template)
799
799
  const inputSource = appConfig.inputSource as string;
@@ -803,7 +803,7 @@ async function fetchDeviceConfig(
803
803
 
804
804
  const templateType = (assignedApp?.templateType as string) || '';
805
805
 
806
- return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
806
+ return { comPort, controllerId, baudRate, screenMap, totalScreens, oscPort, oscAddress, oscHost, templateType };
807
807
  } catch (err) {
808
808
  logger.debug('Failed to fetch device config:', err);
809
809
  return null;