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": "el umbral",
|
||||
"codex": "códice",
|
||||
"shop": "relicario",
|
||||
"devices": "dispositivos",
|
||||
"seance": "sesión",
|
||||
"enter": "entrar",
|
||||
"logout": "partir"
|
||||
@@ -333,6 +334,39 @@
|
||||
},
|
||||
"honesty": "la Ultimate Quantum Box está en desarrollo activo — esto es un manifiesto de reserva, no se cobra nada. el sitio funciona por completo sin ella; el hardware solo golpea más fuerte."
|
||||
},
|
||||
"devices": {
|
||||
"back": "← el umbral",
|
||||
"title": "BAHÍA DE DISPOSITIVOS",
|
||||
"subtitle": "empareja hardware con tu cuenta y observa su telemetría en vivo",
|
||||
"pair": {
|
||||
"title": "EMPAREJAR UN NUEVO DISPOSITIVO",
|
||||
"nameLabel": "nombre del dispositivo",
|
||||
"namePlaceholder": "p. ej. nodo-ático-01",
|
||||
"submit": "emparejar dispositivo",
|
||||
"pending": "emparejando…",
|
||||
"nameRequired": "nombra el dispositivo antes de emparejarlo",
|
||||
"error": "el emparejamiento falló — inténtalo de nuevo"
|
||||
},
|
||||
"token": {
|
||||
"warning": "cópialo ahora — no volverá a mostrarse. perderlo significa volver a emparejar el dispositivo.",
|
||||
"copy": "copiar",
|
||||
"copied": "copiado",
|
||||
"confirm": "ya lo guardé — ocultar token"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "TRANSMISIÓN EN VIVO",
|
||||
"empty": "aún no hay dispositivos emparejados — empareja uno arriba para verlo aquí",
|
||||
"lastSeen": "visto por última vez",
|
||||
"lastSeenNever": "nunca",
|
||||
"noReadings": "esperando la primera lectura…",
|
||||
"connection": {
|
||||
"connecting": "alcanzando la transmisión…",
|
||||
"open": "transmisión en vivo",
|
||||
"unstable": "transmisión inestable…",
|
||||
"closed": "transmisión desconectada"
|
||||
}
|
||||
}
|
||||
},
|
||||
"haunting": {
|
||||
"whisper": {
|
||||
"behindYou": "detrás de ti",
|
||||
|
||||
Reference in New Issue
Block a user