Bring the docs in line with the code

- python-backend/README: track analysis is implemented (2e0e6ad); the cache layout,
  the module table (lirc, screen, timers, podcasts, tippen) and the route table
  (track analysis, HA proxy, typing game) were missing pieces.
- docs/REPO_OVERVIEW: Mein Zimmer and the clock are implemented; Home Assistant is
  also reached over HTTP through the room page's proxy, not MQTT only; the IR remote,
  scripts/, the optional config sections and figure kind were not mentioned.
- web/README: the key table was the mockup's (Ctrl transport keys, TAB over 'all');
  ?pi=1 is now a preset with per-browser overrides, not a read-once flag; the room
  page and the typing game exist; eslint is part of the checks.
- Docstrings that described earlier designs: HaConfig (written as browser-direct in
  the same commit that built the proxy, a8ed350), services/__init__, events,
  playback, media, colors, tippen progress and AppHeader, lowPower.ts, and the
  'Hoerbuecher' spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 00:28:37 +02:00
parent a0ac48ccbf
commit a737ccf108
12 changed files with 105 additions and 77 deletions

View File

@@ -20,7 +20,9 @@ Assistant integration.
| `web/` | React + TypeScript front-end (Vite). Browse the whole library and play any of it from a browser. Built output is served by `python-backend` itself. | | `web/` | React + TypeScript front-end (Vite). Browse the whole library and play any of it from a browser. Built output is served by `python-backend` itself. |
| `esp-firmware/` | ESP32 firmware (C++, Arduino framework via PlatformIO). Reads the RFID reader and buttons, drives the LED strips, talks to `python-backend` over serial. | | `esp-firmware/` | ESP32 firmware (C++, Arduino framework via PlatformIO). Reads the RFID reader and buttons, drives the LED strips, talks to `python-backend` over serial. |
| `hardware/` | 3D-print models for the figurines and enclosure (FreeCAD/Blender/OBJ/STL), a Fritzing electronics sketch, datasheets, and `pinout.md` (RFID reader + button-board wiring). | | `hardware/` | 3D-print models for the figurines and enclosure (FreeCAD/Blender/OBJ/STL), a Fritzing electronics sketch, datasheets, and `pinout.md` (RFID reader + button-board wiring). |
| `claude-design/` | The interaction and visual spec the web front-end was built from ("Dolphin Beats"), as a standalone HTML mockup with hardcoded data. `web/` is the real implementation; the mockup is kept as the reference for the keyboard model and the styling. It also contains a second page, "Mein Zimmer" (room lights), which is **not** implemented. | | `claude-design/` | The interaction and visual specs the web front-end was built from, as standalone HTML mockups with hardcoded data: "Dolphin Beats" (the player), "Mein Zimmer" (the room page) and "Delphin Uhr" (the clock overlay). `web/` is the real implementation of all three; the mockups are kept as the reference for the keyboard model and the styling. |
| `scripts/` | `check.sh` (every check, see below) and `sync-library.sh` (seed a device's library and analysis cache). |
| `docs/` | This file. |
## How the pieces talk to each other ## How the pieces talk to each other
@@ -41,12 +43,18 @@ Assistant integration.
out of step with a figure someone puts on the reader. See out of step with a figure someone puts on the reader. See
`python-backend/README.md` for the endpoint list. `python-backend/README.md` for the endpoint list.
- **`python-backend` ↔ Home Assistant**: MQTT only. The backend publishes three - **`python-backend` ↔ Home Assistant**: two separate paths.
discoverable lights, a player sensor, a volume number, transport buttons, device - MQTT, for the mouse itself. The backend publishes three discoverable lights, a
triggers for every button and touch area, and a tag scanner for the RFID reader. It player sensor, a volume number, transport buttons, device triggers for every button
does *not* call Home Assistant services directly any more; behaviour like "the left and touch area, and a tag scanner for the RFID reader. Behaviour like "the left ear
ear turns the room light pink" is an HA automation. See `python-backend/README.md` turns the room light pink" is an HA automation, not backend code. See
for the trigger topics and the old colour mapping. `python-backend/README.md` for the trigger topics and the old colour mapping.
- HTTP, for the web front-end's room page only: `/api/ha/...` proxies state reads and
service calls to Home Assistant's REST API with the token from `general.ha`, so the
token never reaches the browser.
- **`python-backend` ↔ IR remote**: a TCP client for lircd (`general.lirc`), turned into
the same intents as every other front-end.
## Running it ## Running it
@@ -89,10 +97,13 @@ production so one process serves both the UI and the API.
- `general.{library.*, serial_port, baudrate, reconnect_interval, alsa_device, - `general.{library.*, serial_port, baudrate, reconnect_interval, alsa_device,
min_volume, max_volume, initial_volume, volume_increment, button_leds_brightness, min_volume, max_volume, initial_volume, volume_increment, button_leds_brightness,
audio_extensions, mqtt.*, web.*}` audio_extensions, podcast_episode_limit}`, plus optional sections `mqtt`, `web`, `ha`
- `figures.<name>.{id, colors}` — `id` is a 5-byte hex RFID tag, unique per figure; (room page), `lirc` (IR remote), `screen` (monitor on/off), `tippen` (typing game)
`colors` is exactly four (`primary, secondary, bg, accent`), each `"#rrggbb"` or and `clock` (school bus countdown).
`"wNN"`. - `figures.<name>.{id, colors, kind}` — `id` is a 5-byte hex RFID tag, unique per
figure; `colors` is exactly four (`primary, secondary, bg, accent`), each `"#rrggbb"`
or `"wNN"`; `kind` is `music` (default) or `book`.
- `remote.<digit>` — what each IR-remote number key plays. Written by the web UI.
`general.library.root` is the one path to the music. The shelves under it are fixed `general.library.root` is the one path to the music. The shelves under it are fixed
names, not settings, because each has quirks the scanner knows about: names, not settings, because each has quirks the scanner knows about:
@@ -118,9 +129,8 @@ reported at once.
the behaviour is end-to-end — those files are executed by the test suite. the behaviour is end-to-end — those files are executed by the test suite.
- `python-backend/notebooks/` is LED-effect scratch work, not part of the app. - `python-backend/notebooks/` is LED-effect scratch work, not part of the app.
- The firmware has no automated tests beyond a PlatformIO `native` env for LED effects. - The firmware has no automated tests beyond a PlatformIO `native` env for LED effects.
- The front-end has `vitest` over the pure modules (`web/src/lib/`) and `tsc --noEmit`; - The front-end has `vitest` over the pure modules (`web/src/lib/`), `tsc` and `eslint`;
there is no component-level test harness. `npm run test` and `npx tsc --noEmit` are there is no component-level test harness.
the two checks.
- The library index is cached under `general.library.cache`. It is keyed by file mtime - The library index is cached under `general.library.cache`. It is keyed by file mtime
and size, so **a change to how the scanner derives a title, artist or series is and size, so **a change to how the scanner derives a title, artist or series is
invisible until the cache is invalidated** — bump `_INDEX_VERSION` in invisible until the cache is invalidated** — bump `_INDEX_VERSION` in

View File

@@ -94,7 +94,10 @@ the prompt becomes a regression test by pasting it into a `.txt` file.
| `musicmouse/reactions/` | The policy. `@on(SomeEvent)` functions that get the app and act. | | `musicmouse/reactions/` | The policy. `@on(SomeEvent)` functions that get the app and act. |
| `musicmouse/library/` | The music collection: scanning, tags, cover art and its colours, the cache, and the seams for future track analysis. | | `musicmouse/library/` | The music collection: scanning, tags, cover art and its colours, the cache, and the seams for future track analysis. |
| `musicmouse/services/mqtt/` | Home Assistant entities: three lights, a player sensor, a volume number, transport buttons, device triggers, a tag scanner. | | `musicmouse/services/mqtt/` | Home Assistant entities: three lights, a player sensor, a volume number, transport buttons, device triggers, a tag scanner. |
| `musicmouse/services/web/` | The browser front-end's API: the library, a state websocket, command endpoints, and parent-mode settings. | | `musicmouse/services/web/` | The browser front-end's API: the library, a state websocket, command endpoints, parent-mode settings, the typing game and the Home Assistant proxy. |
| `musicmouse/services/lirc/` | The IR remote, as a lircd TCP client. |
| `musicmouse/services/` (`screen.py`, `timers.py`, `podcasts.py`) | The monitor's on/off and idle auto-off; the parents' timer and school-day calendar; the podcast feed poller. |
| `musicmouse/tippen/` | The typing game's curriculum, progress and track unlocks. |
| `musicmouse/simulator/` | Fake transport and player, the driver vocabulary, the REPL and the script runner. | | `musicmouse/simulator/` | Fake transport and player, the driver vocabulary, the REPL and the script runner. |
| `musicmouse/config.py` | Pydantic schema, validation, and human-readable error formatting. | | `musicmouse/config.py` | Pydantic schema, validation, and human-readable error formatting. |
@@ -186,8 +189,9 @@ different amounts to produce:
``` ```
index.json cheap: tags and structure. Rebuilt freely. index.json cheap: tags and structure. Rebuilt freely.
covers/<album_id>.jpg medium: art extracted from an ID3 APIC frame covers/<album_id>.jpg medium: the album's art, downscaled to 384 px
analysis/<track_key>.json expensive: reserved for offline audio analysis covers/<album_id>.thumb.jpg medium: the same at 256 px, for cards and rows
analysis/<track_key>.json expensive: track analysis (plus .beats.json, .curve.json)
``` ```
Deleting the whole directory is safe; deleting it throws away analysis that is minutes Deleting the whole directory is safe; deleting it throws away analysis that is minutes
@@ -199,13 +203,16 @@ change to how the scanner derives a title, artist or series is invisible until t
is invalidated: bump `_INDEX_VERSION` in `musicmouse/library/cache.py` when you touch is invalidated: bump `_INDEX_VERSION` in `musicmouse/library/cache.py` when you touch
that logic. that logic.
**Track analysis is not implemented.** `musicmouse/library/analysis.py` fixes the shape **Track analysis** drives the web front-end's ambient background. It is optional: install
of the results - scalars (`tempo`, `energy`, `valence`, `brightness`) travel inline with the `analysis` extra (`pip install -e '.[analysis]'`, librosa and numpy) and
the index, and a beat grid lives in its own file and is fetched per track - so an `musicmouse/library/librosa_analyzer.py` works through the music shelf in the
analyzer can be added later without touching the scanner, the API or the frontend. It background, in a pool of niced worker processes (`musicmouse/library/workers.py`), and
will go behind an optional dependency group, and because results are content-keyed pauses while anything is playing. Without the extra, `build_analyzer()` returns a no-op
files, they can equally well be computed on a workstation and the `analysis/` folder and the background stays static. Scalars (`tempo`, `energy`, `valence`, `brightness`,
copied to the device. `pulse`) travel inline with the index; the beat grid and per-second curves live in their
own files and are fetched only for the playing track. Results are content-keyed, so they
can be computed on a workstation and the `analysis/` folder copied to the device
(`scripts/sync-library.sh` does that).
## The web front-end ## The web front-end
@@ -226,6 +233,9 @@ copied to the device.
| `POST /api/screen/toggle` `/screen/activity` | the kiosk's Del key; its key presses and taps, holding off the idle screen-off | | `POST /api/screen/toggle` `/screen/activity` | the kiosk's Del key; its key presses and taps, holding off the idle screen-off |
| `GET` `PUT /api/settings` | parent mode | | `GET` `PUT /api/settings` | parent mode |
| `POST` `DELETE /api/timer` | parent mode's countdown: `{minutes}` or `{until: "HH:MM"}`; stop or silence it | | `POST` `DELETE /api/timer` | parent mode's countdown: `{minutes}` or `{until: "HH:MM"}`; stop or silence it |
| `GET /api/tracks/{album_id}/{index}/analysis` | beat grid and mood curves of one track, or 404 |
| `GET /api/ha`, `GET /api/ha/states/{entity}`, `POST /api/ha/services/{domain}/{service}` | the room page's Home Assistant proxy; 404 without an `ha` section |
| `GET /api/tippen/curriculum` `/progress`, `PUT /api/tippen/settings`, `POST /api/tippen/runs` | the typing game; 404 without a `tippen` section |
Two decisions worth knowing: Two decisions worth knowing:

View File

@@ -205,7 +205,7 @@ class ClockConfig(_Strict):
class LibraryConfig(_Strict): class LibraryConfig(_Strict):
"""Where the music lives. """Where the music lives.
One path. The shelves underneath it - ``Figuren``, ``Musik``, ``Hoerbuecher``, One path. The shelves underneath it - ``Figuren``, ``Musik``, ``Hörbücher``,
``Kinderpodcasts`` - are fixed names, not settings; see ``Kinderpodcasts`` - are fixed names, not settings; see
:mod:`musicmouse.library.sections`. :mod:`musicmouse.library.sections`.
""" """
@@ -259,10 +259,10 @@ class HaDeviceConfig(_Strict):
class HaConfig(_Strict): class HaConfig(_Strict):
"""Home Assistant integration for the room-control page ("Mein Zimmer"). """Home Assistant integration for the room-control page ("Mein Zimmer").
The backend never calls Home Assistant itself - it only hands the browser the The browser only gets the two ordered lists. Every state read and service call goes
server URL, the token, and these two ordered lists. Control happens directly from through the backend's ``/api/ha/...`` proxy, which attaches the token - so the token
the browser to Home Assistant's own REST API, so this token grants full HA control never leaves the backend. The proxy itself is unauthenticated, though: anything on
to anything on the LAN that can reach musicmouse. See config.yml.example. the LAN that can reach musicmouse can use it. See config.yml.example.
""" """
url: str url: str

View File

@@ -7,8 +7,8 @@ Three flavours, distinguished by base class:
* :class:`StateEvent` - something changed. * :class:`StateEvent` - something changed.
The intent layer is what lets several front-ends drive the same behaviour: a button The intent layer is what lets several front-ends drive the same behaviour: a button
press, an MQTT command and a future web request all emit ``NextTrackRequested`` and a press, an MQTT command, a web request and the IR remote all emit ``NextTrackRequested``
single reaction acts on it. and a single reaction acts on it.
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -2,7 +2,7 @@
The frontend paints an album card with these and the LED strips run an effect in them, The frontend paints an album card with these and the LED strips run an effect in them,
so a web-started album looks the same on the shelf as it does on the screen. Extraction so a web-started album looks the same on the shelf as it does on the screen. Extraction
happens once per album ever - the result is cached next to the cover. happens when an album is scanned; the result is stored in the cached index.
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -1,8 +1,8 @@
"""Playlist model. """Playlist model.
Deliberately a real type rather than a bare ``list[str]``: it is what the browse API What the player plays: a name and the files, in order. The browse API serves
serves, and it keeps track metadata in one place. Building one from a folder is :class:`~musicmouse.library.models.Album` instead, which carries the tag metadata;
:mod:`musicmouse.library`'s job. ``Album.to_playlist()`` builds one of these from it.
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -1,8 +1,8 @@
"""What the mouse plays, and when. """What the mouse plays, and when.
Physical inputs are turned into *intents*, and the intents are what actually drive the Physical inputs are turned into *intents*, and the intents are what actually drive the
player. That indirection is the point: an MQTT command or a future web request emits player. That indirection is the point: an MQTT command, a web request or an IR remote key
the same intent and lands in the same handler, so there is one place per behaviour. emits the same intent and lands in the same handler, so there is one place per behaviour.
""" """
from __future__ import annotations from __future__ import annotations

View File

@@ -1,7 +1,8 @@
"""Front-ends: things that mirror state outwards and turn requests into intents. """Front-ends: things that mirror state outwards and turn requests into intents.
``MqttService`` is the reference implementation. A web service would be another file MQTT (Home Assistant), web (the browser), lirc (the IR remote) and the screen, timer and
here plus one line in the app - devices and reactions would not change. podcast services each live here; adding another is a file here plus a line in
``__main__._build_services`` - devices and reactions do not change.
""" """
from musicmouse.services.base import Service from musicmouse.services.base import Service

View File

@@ -5,8 +5,8 @@ Same convention as everywhere else this codebase persists something outside
missing or corrupt file is just a fresh start, never a crash: nothing here is precious missing or corrupt file is just a fresh start, never a crash: nothing here is precious
enough to raise over. enough to raise over.
``record_run`` is a near-verbatim port of tippen's own (client-side, until now) ``record_run`` is a port of the ``recordRun`` the standalone typing app had client-side
``recordRun`` in ``tippen/src/lib/progress.ts`` - grading a keystroke-by-keystroke run before it was merged into the web front-end - grading a keystroke-by-keystroke run
into stars/points/an animal stays entirely client-side (see ``TippenRunIn`` in into stars/points/an animal stays entirely client-side (see ``TippenRunIn`` in
``musicmouse.services.web.schemas``); this only owns what happens to progress once a ``musicmouse.services.web.schemas``); this only owns what happens to progress once a
graded result arrives. graded result arrives.

View File

@@ -33,9 +33,11 @@ For the device, `npm run build` writes `dist/`, and `general.web.static_dir` in
```sh ```sh
npm run test # vitest over src/lib npm run test # vitest over src/lib
npx tsc --noEmit # strict, noUncheckedIndexedAccess npm run check # tsc (strict, noUncheckedIndexedAccess) and eslint
``` ```
`../scripts/check.sh web` runs both.
Only the pure modules are tested - the search, the keyboard machine and the time Only the pure modules are tested - the search, the keyboard machine and the time
formatting. That is where the behaviour lives; the components are mostly layout. formatting. That is where the behaviour lives; the components are mostly layout.
@@ -61,11 +63,12 @@ are local.
|---|---| |---|---|
| `src/lib/keyboard.ts` | The key map, as a pure function from a keypress to a list of actions. Testable without a DOM. | | `src/lib/keyboard.ts` | The key map, as a pure function from a keypress to a list of actions. Testable without a DOM. |
| `src/lib/search.ts` | Filtering and the three-level browse hierarchy. The whole library is in memory, so this is instant - see the note on `buildSearchIndex` below. | | `src/lib/search.ts` | Filtering and the three-level browse hierarchy. The whole library is in memory, so this is instant - see the note on `buildSearchIndex` below. |
| `src/lib/lowPower.ts` | The `?pi=1` profile: what gets cheaper, and by how much. | | `src/lib/perfSettings.ts` | The performance knobs: the `FULL` and `PI` presets, and per-browser overrides from the ⚡ dialog. |
| `src/lib/lowPower.ts` | The measurements behind the `?pi=1` preset, and the ambient-canvas quality numbers. |
| `src/lib/covers.ts` | Generated cover art from the album's three colours, and the book-spine treatment. | | `src/lib/covers.ts` | Generated cover art from the album's three colours, and the book-spine treatment. |
| `src/hooks/usePlayerState.ts` | The websocket, with reconnect-and-reseed. | | `src/hooks/usePlayerState.ts` | The websocket, with reconnect-and-reseed. |
| `src/hooks/usePlaybackClock.ts` | Interpolates the 2 Hz position onto animation frames. | | `src/hooks/usePlaybackClock.ts` | Interpolates the 2 Hz position onto animation frames. |
| `src/components/` | Layout only. | | `src/components/` | Layout only. `RoomView` is the "Mein Zimmer" page, `TippenApp` the typing game. |
`src/api/types.ts` mirrors `musicmouse/services/web/schemas.py`; the two are hand-kept in `src/api/types.ts` mirrors `musicmouse/services/web/schemas.py`; the two are hand-kept in
step. step.
@@ -73,26 +76,29 @@ step.
## Keyboard ## Keyboard
The device is meant to be driven from a keyboard, so every screen is reachable without a The device is meant to be driven from a keyboard, so every screen is reachable without a
pointer. `F1` shows the same list in-app. pointer. `F1` shows the full list in-app; `src/components/HelpOverlay.tsx` is that list
and `src/lib/keyboard.ts` is the binding - change both together.
| Key | | | Key | |
|---|---| |---|---|
| `SPACE` | play / pause | | `SPACE` | play / pause |
| `CTRL+H` / `CTRL+L` | previous / next track | | `SHIFT+H` / `SHIFT+L` | previous / next track |
| `CTRL+J` / `CTRL+K` | quieter / louder | | `SHIFT+J` / `SHIFT+K` | quieter / louder |
| `SHIFT+M` | mute |
| `SHIFT+←` / `SHIFT+→` | seek 15 s | | `SHIFT+←` / `SHIFT+→` | seek 15 s |
| `A-Z`, `0-9` | search albums as you type | | `0-9` | what the IR remote's number key does |
| `?` | switch to searching individual tracks | | `← ↑ ↓ →`, `CTRL+HJKL` | move the selection |
| `TAB` | cycle all / music / audiobooks | | `ENTER` / `SHIFT+ENTER` | play the selection / open its track list |
| `← ↑ ↓ →` | move the selection while browsing; transport and volume while playing | | `TAB` | cycle music / audiobooks / podcasts |
| `ENTER` | play the selection | | `A-Z` | search albums as you type |
| `ESC` | back out one layer at a time | | `?` | search individual tracks |
| `/` | back to browsing | | `/` | open search, or switch albums ↔ tracks |
| `A` then `0-9` | put the playing album on that remote key (`SHIFT+A`: just this song) |
| `INSERT` | clock on / off |
| `DELETE` | screen off / on (only on the device itself) |
| `ESC` | close, or clear the search |
| `F1` | this list | | `F1` | this list |
`SHIFT+arrows` is the one addition to the mockup: plain arrows were already taken, and
seeking is the one thing a real player can do that the mockup could not.
## The Raspberry Pi profile ## The Raspberry Pi profile
`?pi=1` loads the same app with nothing on screen animating itself. It exists for `?pi=1` loads the same app with nothing on screen animating itself. It exists for
@@ -125,15 +131,18 @@ sea behind everything - it just no longer moves or follows the track.
`AMBIENCE_QUALITY` in `src/lib/lowPower.ts` is kept accurate for whoever turns the `AMBIENCE_QUALITY` in `src/lib/lowPower.ts` is kept accurate for whoever turns the
canvas back on. canvas back on.
The flag is read once at module load from the URL and nowhere else - no persistence, no `?pi=1` selects the `PI` preset in `src/lib/perfSettings.ts`, and each knob can then be
auto-detection. That is what makes "is this the profile or the hardware?" answerable by overridden from the ⚡ dialog, stored in that browser's localStorage. Without `?pi=1`
editing the address bar. The kiosk gets it from `pi_kiosk_url` in the ansible repo. the stored values are ignored and the full version applies, which keeps "is this the
profile or the hardware?" answerable by editing the address bar. Settings are read once
at load, so a change applies on reload. The kiosk gets the flag from `pi_kiosk_url` in
the ansible repo.
## Parent mode ## Parent mode
`?parentMode=1` opens a settings panel - volume limits, the rotary step, button `?parentMode=1` adds a way into a settings panel - volume limits, the rotary step, button
brightness, and a button to re-read the library. Saving writes the device's brightness, the screen's auto-off, the parents' timer, and a button to re-read the
`config.yml`. library. Saving writes the device's `config.yml`.
It is hidden from the child, not protected from them: there is no authentication on the It is hidden from the child, not protected from them: there is no authentication on the
endpoints behind it. See `../python-backend/README.md`. endpoints behind it. See `../python-backend/README.md`.
@@ -190,7 +199,10 @@ than patching one element at a time:
home-indicator padding while the keyboard is up home-indicator padding while the keyboard is up
([safe-area-inset-bottom does not update for the keyboard](https://webventures.rejh.nl/blog/2025/safe-area-inset-bottom-does-not-update/)). ([safe-area-inset-bottom does not update for the keyboard](https://webventures.rejh.nl/blog/2025/safe-area-inset-bottom-does-not-update/)).
## Not implemented ## Other tabs
The mockup's second page, "Mein Zimmer" (Hue lights, a roller shutter and scenes), is - **Mein Zimmer** (`RoomView`): the mockup's room page - lights, a roller shutter and
not built. Nothing here depends on it. scenes - shown when the backend has an `ha` section. Every Home Assistant call goes
through the backend's `/api/ha/...` proxy, so the browser never sees the token.
- **Tippen** (`TippenApp`): the typing game, when the backend has a `tippen` section.
Passing lessons unlocks tracks in the library.

View File

@@ -1,5 +1,5 @@
/** The header, matching ../../web/src/components/AppHeader.tsx so the two apps open the /** The typing tab's header, matching ../AppHeader.tsx so both tabs open the same
* same way. The mascot bobs; `SHOW_DRIFT` doubles as the decorative-motion switch. */ * way. The mascot bobs; `SHOW_DRIFT` doubles as the decorative-motion switch. */
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { SKIN } from "../../lib/skins"; import { SKIN } from "../../lib/skins";

View File

@@ -41,19 +41,14 @@
* "decorative loops" row above: noise). Turning the canvas back on from the dialog * "decorative loops" row above: noise). Turning the canvas back on from the dialog
* wants the blur back on with it - the 118% row is that combination. * wants the blur back on with it - the 118% row is that combination.
* *
* Read once at module load, from the URL and nowhere else: no persistence, no * The kiosk points Firefox at `http://localhost:8080/?pi=1` (see the `pi_kiosk_url` host
* auto-detection. The kiosk points Firefox at `http://localhost:8080/?pi=1` (see the * var in the ansible repo). That flag is no longer a single switch: it selects the `PI`
* `pi_kiosk_url` host var in the ansible repo) and a laptop opening the same page gets * preset in `lib/perfSettings.ts`, which the ⚡ dialog can override per knob, and every
* the full-fat version, which is what makes "is this the profile or the hardware?" * knob here and in `theme.ts` reads `PERF`. This header is kept for the measurements.
* answerable by editing the address bar.
*/ */
import { PERF, PERF_ENABLED } from "./perfSettings"; import { PERF, PERF_ENABLED } from "./perfSettings";
// Superseded as a single switch: every knob below and in theme.ts now comes from
// `PERF` (lib/perfSettings.ts), which `?pi=1` seeds and the settings dialog overrides.
// This header is kept for the measurements it records.
export interface AmbienceQuality { export interface AmbienceQuality {
/** Multiplies the canvas backing store relative to its on-screen size, before /** Multiplies the canvas backing store relative to its on-screen size, before
* `devicePixelRatio`. At 0.5 the loop fills a quarter of the pixels and the browser * `devicePixelRatio`. At 0.5 the loop fills a quarter of the pixels and the browser