Add kiosk wall-panel control view

Full-screen /kiosk route, separate from the staff SPA: dedicated long-lived
kiosk login (own password, hashed, mints its own hnf_session cookie locally
via CENTRAL_AUTH_SECRET -- no central auth changes needed), scrollable grid
of small mode/setpoint blocks per live-controllable device (shelly_trv,
mhi_mqtt, daikin), tap-to-edit modal reusing the existing per-type control
panels, and a type-aware bulk multiselect (separate TRV/AC sections, only
applied to matching selected devices, TRV commands staggered).

- backend/src/routes/kiosk.js: public POST /api/kiosk/login
- backend/src/routes/settings.js: admin set/check kiosk password (bcrypt)
- backend/src/routes/config.js: exclude/protect kiosk_password_hash
- frontend/src/pages/Kiosk.tsx + components/Kiosk*: the kiosk UI
- reuses GET /api/status for the combined device list -- no new status
  endpoint needed since the kiosk token now also carries the 'view' cap

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 20:44:50 +00:00
parent f0b4ac3c41
commit 2db7d5f4d0
16 changed files with 757 additions and 14 deletions

View file

@ -430,4 +430,60 @@ table.data tr:last-child td { border-bottom: none; }
.sidebar::-webkit-scrollbar-track, .sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.sidebar::-webkit-scrollbar-thumb, .sidebar-nav::-webkit-scrollbar-thumb { background: rgba(201,168,76,0.35); border-radius: 2px; }
.sidebar::-webkit-scrollbar-thumb:hover, .sidebar-nav::-webkit-scrollbar-thumb:hover { background: rgba(201,168,76,0.65); }
/* ── Kiosk wall-panel view ─────────────────────────────────── */
/* Deliberately NOT modelled on weather's fixed-grid no-scroll layout — a device
grid can outgrow one screen the way a single day's forecast never does, so
this is a normal scrollable page, just full-screen and touch-sized. */
.kiosk-shell { height: 100dvh; display: flex; flex-direction: column; background: var(--body-bg); }
.kiosk-center { align-items: center; justify-content: center; }
.kiosk-login-card {
background: var(--card-bg); border: 1px solid var(--card-border); border-radius: var(--radius);
box-shadow: var(--shadow-md); padding: 28px 26px; width: 100%; max-width: 340px; text-align: center;
}
.kiosk-login-card input {
border: 1px solid var(--card-border); border-radius: 8px; padding: 10px 12px; font-size: 16px;
}
.kiosk-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; background: var(--navy); color: var(--text); flex-shrink: 0;
}
.kiosk-header-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 16px; color: var(--gold); }
.kiosk-error { margin: 12px 16px 0; }
.kiosk-grid-wrap { flex: 1; overflow-y: auto; padding: 16px; }
.kiosk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.kiosk-block {
position: relative; cursor: pointer; background: var(--card-bg);
border: 1px solid var(--card-border); border-radius: var(--radius);
box-shadow: var(--shadow-sm); padding: 12px; min-height: 96px;
display: flex; flex-direction: column; gap: 6px; user-select: none;
}
.kiosk-block.on { border-color: var(--app-primary); }
.kiosk-block.selected { outline: 2px solid var(--gold); outline-offset: -2px; }
.kiosk-block-check {
position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; border-radius: 5px;
border: 1.5px solid var(--card-border); display: flex; align-items: center; justify-content: center;
color: var(--navy); background: var(--card-bg);
}
.kiosk-block.selected .kiosk-block-check { background: var(--gold); border-color: var(--gold); color: var(--navy); }
.kiosk-block-zone { font-size: 12px; font-weight: 700; color: var(--text-dark); padding-right: 22px; }
.kiosk-block-location { font-weight: 500; color: var(--text-mid); }
.kiosk-block-main { display: flex; align-items: center; gap: 8px; font-size: 20px; font-weight: 700; color: var(--text-dark); }
.kiosk-block.on .kiosk-block-main { color: var(--app-primary); }
.kiosk-block-sub { display: flex; align-items: center; gap: 4px; font-size: 11.5px; color: var(--text-mid); }
.kiosk-block-fan { margin-left: auto; text-transform: capitalize; }
.kiosk-bulk-bar {
position: sticky; bottom: 0; display: flex; align-items: center; gap: 12px;
padding: 12px 18px; background: var(--navy); color: var(--text); flex-shrink: 0;
}
/* 30s duration must match KioskModal.tsx's IDLE_MS */
.kiosk-modal-timeout-track { height: 3px; background: var(--card-border); border-radius: 2px; margin-top: 18px; overflow: hidden; }
.kiosk-modal-timeout-bar { height: 100%; width: 100%; background: var(--gold); transform-origin: left; animation: kiosk-timeout-shrink 30s linear forwards; }
@keyframes kiosk-timeout-shrink { from { transform: scaleX(1); } to { transform: scaleX(0); } }
.sidebar, .sidebar-nav { scrollbar-width: thin; scrollbar-color: rgba(201,168,76,0.35) transparent; }