feat(frontend): device pairing UI + live telemetry dashboard (Workstream H)
Adds /devices — pair an ESP32 sensor node (POST /api/device), reveal its
raw pairing token exactly once with a hard-to-miss "cannot be shown again"
warning (styled like a real API-key-reveal UI), then a live dashboard
subscribing to /ws/device-feed: the initial `devices` frame seeds paired
devices, and `reading` frames update one row per distinct sensor_type in
place. sensor_type/value/unit are rendered fully generically per the
contract (free-form, open-ended) — an unrecognized sensor_type renders
safely with no special-casing.
- frontend/src/lib/deviceFeed.ts: reconnecting WS client for
/ws/device-feed, mirroring VeilSocket's backoff shape (receive-only, no
outbox needed).
- frontend/src/pages/DevicesPage.{tsx,css}: pairing form + one-time token
reveal + live device-card grid. Leans into "hacker" terminal styling
(monospace readouts, terminal device cards) over the app's usual gothic
chrome, per the design spec, while keeping the existing dark/violet
palette tokens from App.css.
- Route + nav link wired into App.tsx / SeancePage.tsx.
- i18n: new `devices.*` / `nav.devices` keys in en.json + es.json; added a
coverage-check.mjs domain rule for the dynamic connection-state key,
mirroring the existing `seance.connection.` rule.
Tests: deviceFeed.test.ts (backoff/reconnect/frame delivery) and
DevicesPage.test.tsx (empty state, name validation, one-time token reveal
and dismissal, live frame updates in place without duplicating rows,
multi-device/multi-sensor rendering, and a mocked unrecognized sensor_type
that must not crash). Full suite: 154 passed (137 pre-existing + 17 new).
`npx tsc -b` and `npm run build` both clean.
Assumption (undocumented in spec): POST /api/device's JSON response shape
is inferred as `{id, name, token, last_seen_at}` since the Contract section
only describes the endpoint in prose. GET /api/device is intentionally not
called — the live dashboard is fully seeded by /ws/device-feed's initial
`devices` frame per the contract, so it's redundant for this page's scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"home": "the threshold",
|
||||
"codex": "codex",
|
||||
"shop": "shop",
|
||||
"devices": "devices",
|
||||
"seance": "séance",
|
||||
"enter": "enter",
|
||||
"logout": "depart"
|
||||
@@ -333,6 +334,39 @@
|
||||
},
|
||||
"honesty": "the Ultimate Quantum Box is in active development — this is a reservation manifest, no payment is taken. the website works fully without it; hardware only knocks louder."
|
||||
},
|
||||
"devices": {
|
||||
"back": "← the threshold",
|
||||
"title": "DEVICE BAY",
|
||||
"subtitle": "pair hardware to your account and watch its telemetry live",
|
||||
"pair": {
|
||||
"title": "PAIR A NEW DEVICE",
|
||||
"nameLabel": "device name",
|
||||
"namePlaceholder": "e.g. attic-node-01",
|
||||
"submit": "pair device",
|
||||
"pending": "pairing…",
|
||||
"nameRequired": "name the device before pairing",
|
||||
"error": "the pairing handshake failed — try again"
|
||||
},
|
||||
"token": {
|
||||
"warning": "copy this now — it cannot be shown again. losing it means re-pairing the device.",
|
||||
"copy": "copy",
|
||||
"copied": "copied",
|
||||
"confirm": "I've saved it — hide token"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "LIVE FEED",
|
||||
"empty": "no devices paired yet — pair one above to see it here",
|
||||
"lastSeen": "last seen",
|
||||
"lastSeenNever": "never",
|
||||
"noReadings": "awaiting first reading…",
|
||||
"connection": {
|
||||
"connecting": "reaching the feed…",
|
||||
"open": "feed live",
|
||||
"unstable": "feed unstable…",
|
||||
"closed": "feed offline"
|
||||
}
|
||||
}
|
||||
},
|
||||
"haunting": {
|
||||
"whisper": {
|
||||
"behindYou": "behind you",
|
||||
|
||||
Reference in New Issue
Block a user