From c84b3adf04eacba527479d75985e1c0e93bbfb68 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 9 Mar 2026 10:05:18 -0700 Subject: [PATCH 1/5] feat(peripheral-demo): add Sensor Data card to operator view - Add humidity and airPressure fields to AppState (nullable, default null) - Add setHumidity and setAirPressure mutations to state - Parse humidity and air_pressure readings from peripheral WS messages - Add Sensor Data glass card to operator screen with temperature, humidity, air pressure - Show "No Data" fallback until first WS message arrives - Handle nullable temperature in public view temperature pill Co-Authored-By: Claude Sonnet 4.6 --- .../peripheral-integration-demo/index.html | 42 +++++++++++++++++++ .../src/core/screen.ts | 3 +- .../src/core/state.ts | 18 +++++++- .../src/features/operator-dashboard.ts | 18 +++++++- .../src/features/peripherals.ts | 17 +++++++- 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/edge-apps/peripheral-integration-demo/index.html b/edge-apps/peripheral-integration-demo/index.html index 16a1c3b40..a08245563 100644 --- a/edge-apps/peripheral-integration-demo/index.html +++ b/edge-apps/peripheral-integration-demo/index.html @@ -251,6 +251,48 @@ + + +
+
+ + + + + Sensor Data +
+ +
+
+ Temperature + No Data +
+
+ Humidity + No Data +
+
+ Air Pressure + No Data +
+
+
diff --git a/edge-apps/peripheral-integration-demo/src/core/screen.ts b/edge-apps/peripheral-integration-demo/src/core/screen.ts index ed4db3291..0820c775a 100644 --- a/edge-apps/peripheral-integration-demo/src/core/screen.ts +++ b/edge-apps/peripheral-integration-demo/src/core/screen.ts @@ -52,7 +52,8 @@ function syncScreensToState(state: ReturnType) { } const publicTemp = getEl('public-temperature') - publicTemp.textContent = `${Math.round(state.temperature)}°C` + publicTemp.textContent = + state.temperature !== null ? `${Math.round(state.temperature)}°C` : '--' if (state.currentScreen === 'maintenance') { getEl('maintenance-network').textContent = getNetworkStatus() diff --git a/edge-apps/peripheral-integration-demo/src/core/state.ts b/edge-apps/peripheral-integration-demo/src/core/state.ts index 5e5e7aa6c..b73006e46 100644 --- a/edge-apps/peripheral-integration-demo/src/core/state.ts +++ b/edge-apps/peripheral-integration-demo/src/core/state.ts @@ -4,14 +4,18 @@ export interface AppState { currentScreen: ScreenType timezone: string locale: string - temperature: number + temperature: number | null + humidity: number | null + airPressure: number | null } const state: AppState = { currentScreen: 'public', timezone: 'UTC', locale: 'en', - temperature: 22, + temperature: null, + humidity: null, + airPressure: null, } type Listener = (state: AppState) => void @@ -50,6 +54,16 @@ export function setTemperature(value: number) { notify() } +export function setHumidity(value: number) { + state.humidity = value + notify() +} + +export function setAirPressure(value: number) { + state.airPressure = value + notify() +} + export function getState(): Readonly { return { ...state } } diff --git a/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts b/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts index e2e8f3737..802e6f03c 100644 --- a/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts +++ b/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts @@ -92,17 +92,33 @@ function stopUpdates() { } } +function updateSensorData(state: ReturnType) { + getEl('sensor-temperature').textContent = + state.temperature !== null + ? `${Math.round(state.temperature)}°C` + : 'No Data' + getEl('sensor-humidity').textContent = + state.humidity !== null ? `${Math.round(state.humidity)}%` : 'No Data' + getEl('sensor-air-pressure').textContent = + state.airPressure !== null + ? `${Math.round(state.airPressure)} hPa` + : 'No Data' +} + function onStateChange(state: ReturnType) { if (state.currentScreen === 'operator') { startUpdates() } else { stopUpdates() } + updateSensorData(state) } export function initOperatorDashboard() { subscribe(onStateChange) - if (getState().currentScreen === 'operator') startUpdates() + const state = getState() + if (state.currentScreen === 'operator') startUpdates() + updateSensorData(state) } export function updateOperatorDashboard() { diff --git a/edge-apps/peripheral-integration-demo/src/features/peripherals.ts b/edge-apps/peripheral-integration-demo/src/features/peripherals.ts index 68f844f4c..9a4ffe470 100644 --- a/edge-apps/peripheral-integration-demo/src/features/peripherals.ts +++ b/edge-apps/peripheral-integration-demo/src/features/peripherals.ts @@ -1,7 +1,12 @@ import { createPeripheralClient } from '@screenly/edge-apps' import type { PeripheralStateMessage } from '@screenly/edge-apps' -import { getState, setTemperature } from '../core/state' +import { + getState, + setTemperature, + setHumidity, + setAirPressure, +} from '../core/state' import { showWelcomeThenSwitch } from '../core/screen' import { restartLogoutTimer } from '../core/timer' import { authenticate } from './authenticate' @@ -21,6 +26,16 @@ export function initPeripherals() { setTemperature(tempReading.ambient_temperature as number) } + const humidityReading = readings.find((r) => 'humidity' in r) + if (humidityReading) { + setHumidity(humidityReading.humidity as number) + } + + const pressureReading = readings.find((r) => 'air_pressure' in r) + if (pressureReading) { + setAirPressure(pressureReading.air_pressure as number) + } + const cardReading = readings.find((r) => 'secure_card' in r) if (cardReading) { const MAX_CARD_AGE_MS = 60_000 From ee6cce68e4a5e6a6173ae2a1c07fd3869fe5e417 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 9 Mar 2026 11:37:30 -0700 Subject: [PATCH 2/5] fix(peripheral-demo): display sensor values with 2 decimal places Co-Authored-By: Claude Sonnet 4.6 --- .../src/features/operator-dashboard.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts b/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts index 802e6f03c..edb813e05 100644 --- a/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts +++ b/edge-apps/peripheral-integration-demo/src/features/operator-dashboard.ts @@ -94,14 +94,12 @@ function stopUpdates() { function updateSensorData(state: ReturnType) { getEl('sensor-temperature').textContent = - state.temperature !== null - ? `${Math.round(state.temperature)}°C` - : 'No Data' + state.temperature !== null ? `${state.temperature.toFixed(2)}°C` : 'No Data' getEl('sensor-humidity').textContent = - state.humidity !== null ? `${Math.round(state.humidity)}%` : 'No Data' + state.humidity !== null ? `${state.humidity.toFixed(2)}%` : 'No Data' getEl('sensor-air-pressure').textContent = state.airPressure !== null - ? `${Math.round(state.airPressure)} hPa` + ? `${state.airPressure.toFixed(2)} hPa` : 'No Data' } From 6285ffa1c8d0038765873b44ae1d6b7ad2a12d41 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 9 Mar 2026 11:44:40 -0700 Subject: [PATCH 3/5] refactor(peripheral-demo): widen sensor state setters to accept null - Update setTemperature, setHumidity, setAirPressure to accept number | null - Keeps state API consistent with the nullable AppState field types Co-Authored-By: Claude Sonnet 4.6 --- edge-apps/peripheral-integration-demo/src/core/state.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/edge-apps/peripheral-integration-demo/src/core/state.ts b/edge-apps/peripheral-integration-demo/src/core/state.ts index b73006e46..49be2d602 100644 --- a/edge-apps/peripheral-integration-demo/src/core/state.ts +++ b/edge-apps/peripheral-integration-demo/src/core/state.ts @@ -49,17 +49,17 @@ export function setLocale(locale: string) { notify() } -export function setTemperature(value: number) { +export function setTemperature(value: number | null) { state.temperature = value notify() } -export function setHumidity(value: number) { +export function setHumidity(value: number | null) { state.humidity = value notify() } -export function setAirPressure(value: number) { +export function setAirPressure(value: number | null) { state.airPressure = value notify() } From 86e5699f1bdd6b73019b15210edaad26b1d98ca0 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 9 Mar 2026 11:58:26 -0700 Subject: [PATCH 4/5] refactor(peripheral-demo): batch sensor state updates into single notify call - Replace individual setTemperature/setHumidity/setAirPressure setters with a single setSensorReadings batch mutation - Update peripherals.ts to use setSensorReadings, reducing notify() calls from 3 to 1 per WebSocket message Co-Authored-By: Claude Sonnet 4.6 --- .../src/core/state.ts | 22 +++++++------- .../src/features/peripherals.ts | 30 ++++++++----------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/edge-apps/peripheral-integration-demo/src/core/state.ts b/edge-apps/peripheral-integration-demo/src/core/state.ts index 49be2d602..a79aa6c14 100644 --- a/edge-apps/peripheral-integration-demo/src/core/state.ts +++ b/edge-apps/peripheral-integration-demo/src/core/state.ts @@ -49,18 +49,16 @@ export function setLocale(locale: string) { notify() } -export function setTemperature(value: number | null) { - state.temperature = value - notify() -} - -export function setHumidity(value: number | null) { - state.humidity = value - notify() -} - -export function setAirPressure(value: number | null) { - state.airPressure = value +export function setSensorReadings(readings: { + temperature?: number | null + humidity?: number | null + airPressure?: number | null +}) { + if (readings.temperature !== undefined) + state.temperature = readings.temperature + if (readings.humidity !== undefined) state.humidity = readings.humidity + if (readings.airPressure !== undefined) + state.airPressure = readings.airPressure notify() } diff --git a/edge-apps/peripheral-integration-demo/src/features/peripherals.ts b/edge-apps/peripheral-integration-demo/src/features/peripherals.ts index 9a4ffe470..aeacdde68 100644 --- a/edge-apps/peripheral-integration-demo/src/features/peripherals.ts +++ b/edge-apps/peripheral-integration-demo/src/features/peripherals.ts @@ -1,12 +1,7 @@ import { createPeripheralClient } from '@screenly/edge-apps' import type { PeripheralStateMessage } from '@screenly/edge-apps' -import { - getState, - setTemperature, - setHumidity, - setAirPressure, -} from '../core/state' +import { getState, setSensorReadings } from '../core/state' import { showWelcomeThenSwitch } from '../core/screen' import { restartLogoutTimer } from '../core/timer' import { authenticate } from './authenticate' @@ -22,19 +17,20 @@ export function initPeripherals() { const readings = msg.request.edge_app_source_state.states const tempReading = readings.find((r) => 'ambient_temperature' in r) - if (tempReading) { - setTemperature(tempReading.ambient_temperature as number) - } - const humidityReading = readings.find((r) => 'humidity' in r) - if (humidityReading) { - setHumidity(humidityReading.humidity as number) - } - const pressureReading = readings.find((r) => 'air_pressure' in r) - if (pressureReading) { - setAirPressure(pressureReading.air_pressure as number) - } + + setSensorReadings({ + temperature: tempReading + ? (tempReading.ambient_temperature as number) + : undefined, + humidity: humidityReading + ? (humidityReading.humidity as number) + : undefined, + airPressure: pressureReading + ? (pressureReading.air_pressure as number) + : undefined, + }) const cardReading = readings.find((r) => 'secure_card' in r) if (cardReading) { From 092e6fe394c66c0ce072068e51b95fda537c1100 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 9 Mar 2026 21:29:05 -0700 Subject: [PATCH 5/5] feat(peripheral-demo): add Raw Peripheral State card to maintenance view - Install highlight.js for JSON syntax highlighting - Store last peripheral WebSocket message in module-level state - Add Raw Peripheral State glass card to maintenance screen - Render highlighted JSON via highlight.js (Monokai theme) on maintenance view load Co-Authored-By: Claude Sonnet 4.6 --- .../peripheral-integration-demo/bun.lock | 5 ++++ .../peripheral-integration-demo/index.html | 24 ++++++++++++++++++ .../peripheral-integration-demo/package.json | 3 +++ .../src/core/screen.ts | 20 ++++++++++++++- .../src/core/state.ts | 10 ++++++++ .../src/css/style.css | 25 +++++++++++++++++++ .../src/features/peripherals.ts | 8 +++++- .../peripheral-integration-demo/src/main.ts | 1 + 8 files changed, 94 insertions(+), 2 deletions(-) diff --git a/edge-apps/peripheral-integration-demo/bun.lock b/edge-apps/peripheral-integration-demo/bun.lock index 88f957150..3ca4acfc5 100644 --- a/edge-apps/peripheral-integration-demo/bun.lock +++ b/edge-apps/peripheral-integration-demo/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "peripheral-integration-demo", + "dependencies": { + "highlight.js": "^11.11.1", + }, "devDependencies": { "@playwright/test": "^1.58.0", "@screenly/edge-apps": "workspace:../edge-apps-library", @@ -725,6 +728,8 @@ "heap": ["heap@0.2.7", "", {}, "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="], + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], diff --git a/edge-apps/peripheral-integration-demo/index.html b/edge-apps/peripheral-integration-demo/index.html index a08245563..a64ed6d06 100644 --- a/edge-apps/peripheral-integration-demo/index.html +++ b/edge-apps/peripheral-integration-demo/index.html @@ -363,6 +363,30 @@ + +
+
+ + + + + Raw Peripheral State +
+
No data yet
+
diff --git a/edge-apps/peripheral-integration-demo/package.json b/edge-apps/peripheral-integration-demo/package.json index 1c6eb7055..de2baa13b 100644 --- a/edge-apps/peripheral-integration-demo/package.json +++ b/edge-apps/peripheral-integration-demo/package.json @@ -31,5 +31,8 @@ "npm-run-all2": "^8.0.4", "prettier": "^3.8.1", "typescript": "^5.9.3" + }, + "dependencies": { + "highlight.js": "^11.11.1" } } diff --git a/edge-apps/peripheral-integration-demo/src/core/screen.ts b/edge-apps/peripheral-integration-demo/src/core/screen.ts index 0820c775a..76149ecf1 100644 --- a/edge-apps/peripheral-integration-demo/src/core/screen.ts +++ b/edge-apps/peripheral-integration-demo/src/core/screen.ts @@ -6,8 +6,16 @@ import { getTimeZone, } from '@screenly/edge-apps' import { Hardware } from '@screenly/edge-apps' +import hljs from 'highlight.js/lib/core' +import json from 'highlight.js/lib/languages/json' -import { subscribe, getState, setScreen, type ScreenType } from './state' +import { + subscribe, + getState, + setScreen, + type ScreenType, + getLastPeripheralState, +} from './state' import { waitForScreenDataPrepared, dispatchScreenDataPrepared, @@ -15,6 +23,8 @@ import { import { getNetworkStatus } from '../utils/network' import { updateOperatorDashboard } from '../features/operator-dashboard' +hljs.registerLanguage('json', json) + function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } @@ -79,6 +89,14 @@ async function loadMaintenanceInfo() { } catch { getEl('maintenance-timezone').textContent = '—' } + + const rawState = getLastPeripheralState() + const codeEl = getEl('maintenance-raw-state').querySelector('code')! + if (rawState) { + codeEl.innerHTML = hljs.highlight(JSON.stringify(rawState, null, 2), { + language: 'json', + }).value + } } async function preloadScreenData(role: ScreenType): Promise { diff --git a/edge-apps/peripheral-integration-demo/src/core/state.ts b/edge-apps/peripheral-integration-demo/src/core/state.ts index a79aa6c14..4c162d272 100644 --- a/edge-apps/peripheral-integration-demo/src/core/state.ts +++ b/edge-apps/peripheral-integration-demo/src/core/state.ts @@ -65,3 +65,13 @@ export function setSensorReadings(readings: { export function getState(): Readonly { return { ...state } } + +let lastPeripheralState: unknown = null + +export function setLastPeripheralState(raw: unknown) { + lastPeripheralState = raw +} + +export function getLastPeripheralState(): unknown { + return lastPeripheralState +} diff --git a/edge-apps/peripheral-integration-demo/src/css/style.css b/edge-apps/peripheral-integration-demo/src/css/style.css index e3d3690b9..db998c5da 100644 --- a/edge-apps/peripheral-integration-demo/src/css/style.css +++ b/edge-apps/peripheral-integration-demo/src/css/style.css @@ -370,6 +370,14 @@ auto-scaler { /* Maintenance rows */ +#screen-maintenance { + flex: 1; +} + +#screen-maintenance .glass-card:last-child { + flex: 0 1 auto; +} + .maintenance-rows { display: flex; flex-direction: column; @@ -425,3 +433,20 @@ auto-scaler { font-weight: 600; color: #e33876; } + +.maintenance-raw-state { + width: 100%; + flex: 1; + overflow: hidden; + border-radius: 16px; + font-size: 18px; + line-height: 1.6; + margin: 0; +} + +.maintenance-raw-state code { + display: block; + height: 100%; + padding: 1.5rem; + white-space: pre; +} diff --git a/edge-apps/peripheral-integration-demo/src/features/peripherals.ts b/edge-apps/peripheral-integration-demo/src/features/peripherals.ts index aeacdde68..b8993c1a0 100644 --- a/edge-apps/peripheral-integration-demo/src/features/peripherals.ts +++ b/edge-apps/peripheral-integration-demo/src/features/peripherals.ts @@ -1,7 +1,11 @@ import { createPeripheralClient } from '@screenly/edge-apps' import type { PeripheralStateMessage } from '@screenly/edge-apps' -import { getState, setSensorReadings } from '../core/state' +import { + getState, + setSensorReadings, + setLastPeripheralState, +} from '../core/state' import { showWelcomeThenSwitch } from '../core/screen' import { restartLogoutTimer } from '../core/timer' import { authenticate } from './authenticate' @@ -14,6 +18,8 @@ export function initPeripherals() { client.register(edgeAppId) client.watchState((msg: PeripheralStateMessage) => { + setLastPeripheralState(msg.request.edge_app_source_state) + const readings = msg.request.edge_app_source_state.states const tempReading = readings.find((r) => 'ambient_temperature' in r) diff --git a/edge-apps/peripheral-integration-demo/src/main.ts b/edge-apps/peripheral-integration-demo/src/main.ts index c0fbf2e17..93e9a1f83 100644 --- a/edge-apps/peripheral-integration-demo/src/main.ts +++ b/edge-apps/peripheral-integration-demo/src/main.ts @@ -1,4 +1,5 @@ import './css/style.css' +import 'highlight.js/styles/monokai.css' import '@screenly/edge-apps/components' import {