# render/ — shared frontend surfaces Two pages that every endpoint can show, kept here rather than inside one component because they belong to no single service: the now-playing screen reads Home Assistant, the floorplan reads `identity`, and both are shown on thin clients, touch panels, the kitchen and door panels alike. Same rules as `digest-canvas-sdk` and `canvas-sdk`: **vendored, dependency-free, no build step**, config from `?query=params` so nothing here holds a secret. Copy the directory into a host's image; do not serve it from one place, or a kiosk that cannot reach the container host shows nothing. | | What | Reads | |---|---|---| | `media-visualiser/` | now-playing: circular spectrum, album-art colours, lyrics | HA `media_player` state | | `floorplan-3d/` | who is in which room, extruded | `identity` `GET /floorplan/presence` | ## media-visualiser ``` nowplaying.html?ha=http://ha:8123&token=&entity=media_player.living_room ``` **Two tiers, and the distinction is load-bearing.** CAVA reads an audio stream; most endpoints do not have one — a kitchen panel showing what the *living room* is playing has no audio to analyse and never will. So: - **reactive** — audio is local. Real FFT via WebAudio (`attachAudio(el)`), or levels pushed from `cava -r` by the host's agent (`pushLevels([...])`). - **synthetic** — everywhere else. The ring breathes from track position and tempo. The synthetic tier **says so on screen** ("visual rhythm — not an audio analysis") and sets `data-tier` on the canvas. That matters: the moment somebody believes it is a spectrum, every bass drop it misses becomes a bug report. Palette extraction rejects near-greys and near-blacks *before* ranking (album art is full of both, and a naive palette off a dark cover is four indistinguishable greys), then lifts each colour until it clears the background — the step that gets skipped, and why so many art-coloured visualisers are invisible on dark covers. Lyrics: synced LRC highlights and scrolls; **plain text is shown but never auto-scrolled at a guessed rate**, which is wrong within ten seconds and stays wrong. Most tracks have none, so the layout treats absent as the normal case. ## floorplan-3d ``` floorplan.html?api=http://identity:8097&token=[&level=] ``` **Canvas 2D, not three.js** — a change from what `docs/endpoint-surfaces.md` planned, made while building it. The scene is prisms on a plane: no lighting model, no textures, no model import. An isometric projection with painter's-algorithm sorting draws exactly that in ~200 lines, runs predictably on the small panels (where a WebGL context is much less predictable), and keeps the frontend at zero dependencies. If this ever needs real lighting or an imported model, three.js becomes right and this becomes the fallback. Inherited from the Pebble watchapp deliberately, because they render the same payload and divergence would mean one of them is lying: - Occupied rooms **lit**, empty rooms **dark** — carried in lightness, not hue, because colour already means *who*. - A **third state**, dashed, for rooms HA never reports on. Drawing "no data" as "empty" is a quiet lie. - Occupants as **colour + initial**, upgraded to their photo where there is room, with the colour ring kept either way — it is what ties this marker to the same person on the watch and in the admin panel. - `unplaced` people get a **visible shelf**. They are who you are most often looking for. Drag to orbit; pitch is clamped and there is no free-fly, because an unconstrained camera on a wall panel is something people knock askew and cannot get back. **The caveat no rendering fixes:** this is only as right as room-level presence, which has never been measured in this house. A beautiful 3D house with everybody on the unplaced shelf is a beautiful picture of nothing — test that first with the admin panel's floorplan tab, **Live** ticked.