diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 3d8de7b..0bf0d27 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -218,6 +218,9 @@ export function kioskFetchStatus(): Promise<{ }> { return krequest('/status') } +export function kioskFetchConfig(): Promise { + return krequest('/config') +} export function kioskFetchTrvStatus(deviceId: number): Promise { return krequest(`/devices/${deviceId}/trv-status`) } diff --git a/frontend/src/index.css b/frontend/src/index.css index 16e20d6..5e2a328 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -446,10 +446,15 @@ table.data tr:last-child td { border-bottom: none; } } .kiosk-header { - display: flex; align-items: center; justify-content: space-between; + display: flex; flex-direction: column; gap: 6px; padding: 14px 18px; background: var(--navy); color: var(--text); flex-shrink: 0; } +.kiosk-header-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .kiosk-header-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 16px; color: var(--gold); } +/* Same weather-compensation summary as the Dashboard's banner (plus TRV + default, which the kiosk has no Settings page to show separately) — see + buildInfoLine() in Kiosk.tsx. */ +.kiosk-header-info { font-size: 12px; color: var(--text-muted); } .kiosk-error { margin: 12px 16px 0; } diff --git a/frontend/src/pages/Kiosk.tsx b/frontend/src/pages/Kiosk.tsx index dc7047f..948664e 100644 --- a/frontend/src/pages/Kiosk.tsx +++ b/frontend/src/pages/Kiosk.tsx @@ -1,8 +1,8 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { Thermometer, CheckSquare, X } from 'lucide-react' -import { kioskLogin, kioskFetchStatus, KioskSessionError } from '../api' -import type { DeviceSummary, ZoneStatus } from '../types' -import { KIOSK_DEVICE_TYPES } from '../types' +import { kioskLogin, kioskFetchStatus, kioskFetchConfig, KioskSessionError } from '../api' +import type { ActiveAcTemplate, DeviceSummary, ZoneStatus } from '../types' +import { KIOSK_DEVICE_TYPES, DEFAULT_HVAC_TEMPLATE } from '../types' import KioskZoneBlock from '../components/KioskZoneBlock' import KioskZonePanel from '../components/KioskZonePanel' import KioskModal from '../components/KioskModal' @@ -17,6 +17,28 @@ function liveDevicesOf(zone: ZoneStatus): DeviceSummary[] { return zone.devices.filter(d => KIOSK_DEVICE_TYPES.includes(d.device_type)) } +function cap(s: string) { + return s[0].toUpperCase() + s.slice(1) +} + +// Same weather-compensation summary the Dashboard shows (Settings → Weather +// Compensation drives it) — "what would a room transitioning to occupied get +// right now", plus the TRV occupied default alongside it since the kiosk has +// no separate Settings page to check it on. Renders only the parts that have +// loaded, so a slow weather fetch doesn't hide the AC/TRV defaults too. +function buildInfoLine(weatherPeakC: number | null, activeAcTemplate: ActiveAcTemplate | null, trvDefaultTemp: number | null): string[] { + const parts: string[] = [] + if (weatherPeakC != null) parts.push(`Forecast high ${weatherPeakC}°C`) + if (activeAcTemplate) { + const bits = [activeAcTemplate.power === 'on' ? 'ON' : 'OFF'] + if (activeAcTemplate.mode) bits.push(cap(activeAcTemplate.mode)) + if (activeAcTemplate.setpoint != null) bits.push(`${activeAcTemplate.setpoint}°C`) + parts.push(`AC default ${bits.join(' ')}${activeAcTemplate.tierApplied ? ` (${cap(activeAcTemplate.tierApplied)} tier)` : ''}`) + } + if (trvDefaultTemp != null) parts.push(`TRV default ${trvDefaultTemp}°C`) + return parts +} + // The whole hvac SPA shares one index.html, so there's only one in the document — "Install" from any route otherwise always // installs the main app (start_url '/hvac/', staff login). Swap it to a @@ -51,6 +73,9 @@ export default function Kiosk() { const [zones, setZones] = useState([]) const [loadError, setLoadError] = useState('') + const [weatherPeakC, setWeatherPeakC] = useState(null) + const [activeAcTemplate, setActiveAcTemplate] = useState(null) + const [trvDefaultTemp, setTrvDefaultTemp] = useState(null) const [selectionMode, setSelectionMode] = useState(false) const [selectedZoneIds, setSelectedZoneIds] = useState>(new Set()) @@ -63,6 +88,8 @@ export default function Kiosk() { kioskFetchStatus() .then(s => { setZones(s.zones.filter(z => liveDevicesOf(z).length > 0)) + setWeatherPeakC(s.weather_peak_c) + setActiveAcTemplate(s.active_ac_template) setLoadError('') setAuthState('authed') }) @@ -74,6 +101,15 @@ export default function Kiosk() { useEffect(() => { load() }, [load]) + // TRV default temp doesn't come back from /api/status (see buildInfoLine) and + // rarely changes — fetch it once on login rather than on every POLL_MS tick. + useEffect(() => { + if (authState !== 'authed') return + kioskFetchConfig() + .then(cfg => setTrvDefaultTemp(cfg.hvac_defaults?.trv_occupied_temp ?? DEFAULT_HVAC_TEMPLATE.trv_occupied_temp)) + .catch(() => {}) + }, [authState]) + useEffect(() => { if (authState !== 'authed') { if (pollRef.current) clearInterval(pollRef.current) @@ -150,15 +186,20 @@ export default function Kiosk() { ) } + const infoParts = buildInfoLine(weatherPeakC, activeAcTemplate, trvDefaultTemp) + return (
- - HVAC Kiosk - - +
+ + HVAC Kiosk + + +
+ {infoParts.length > 0 &&
{infoParts.join(' · ')}
}
{loadError &&
{loadError}
}