Kiosk: show weather compensation summary in the header
Same info the Dashboard's banner shows (forecast high, AC default power/mode/ setpoint + active weather tier), plus TRV default temp alongside it since the kiosk has no Settings page to check that separately. Weather/AC data reuses the existing /api/status poll; TRV default is a one-time /api/config fetch on login (rarely changes, not worth polling every 12s). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9b4e49b801
commit
371909bded
3 changed files with 59 additions and 10 deletions
|
|
@ -218,6 +218,9 @@ export function kioskFetchStatus(): Promise<{
|
|||
}> {
|
||||
return krequest('/status')
|
||||
}
|
||||
export function kioskFetchConfig(): Promise<AppConfig> {
|
||||
return krequest('/config')
|
||||
}
|
||||
export function kioskFetchTrvStatus(deviceId: number): Promise<TrvStatus> {
|
||||
return krequest(`/devices/${deviceId}/trv-status`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <link
|
||||
// rel="manifest"> 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<ZoneStatus[]>([])
|
||||
const [loadError, setLoadError] = useState('')
|
||||
const [weatherPeakC, setWeatherPeakC] = useState<number | null>(null)
|
||||
const [activeAcTemplate, setActiveAcTemplate] = useState<ActiveAcTemplate | null>(null)
|
||||
const [trvDefaultTemp, setTrvDefaultTemp] = useState<number | null>(null)
|
||||
|
||||
const [selectionMode, setSelectionMode] = useState(false)
|
||||
const [selectedZoneIds, setSelectedZoneIds] = useState<Set<number>>(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 (
|
||||
<div className="kiosk-shell">
|
||||
<header className="kiosk-header">
|
||||
<div className="kiosk-header-row">
|
||||
<span className="kiosk-header-title">
|
||||
<Thermometer size={18} strokeWidth={1.75} /> HVAC Kiosk
|
||||
</span>
|
||||
<button className={`btn btn-sm${selectionMode ? ' btn-primary' : ''}`} onClick={toggleSelectionMode}>
|
||||
<CheckSquare size={14} strokeWidth={1.75} /> {selectionMode ? 'Cancel select' : 'Select'}
|
||||
</button>
|
||||
</div>
|
||||
{infoParts.length > 0 && <div className="kiosk-header-info">{infoParts.join(' · ')}</div>}
|
||||
</header>
|
||||
|
||||
{loadError && <div className="error-banner kiosk-error">{loadError}</div>}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue