A DevTools trace recorded on musicdolphin put the whole question beyond argument:
ImageDecodeTask 29,939 ms x20 (~1.5 s each)
Decode LazyPixelRef 3,924 ms x20
Decode Image 3,530 ms x22
Paint 99.7 ms
Layout 77.3 ms
all app JavaScript <400 ms across 22 seconds
Image decoding is not the largest cost on that page, it is very nearly the only one.
The covers should already have been small - the backend downscales to 640 px now - but
the page was still decoding them at 1920, 1600, 1400 px, with deliveryType
"cache-storage" on every one. The service worker caches cover art cache-first, keyed on
URL, on the premise that it is "immutable per album id". The id did not change; the
bytes did. So every client that had ever loaded a cover kept serving the 3000 px
original from its own disk and never asked the backend for the new one.
Renaming the cache is the retirement mechanism the worker already has - `activate`
deletes every cache that is not one of the two current names - so COVERS becomes v2,
with a note saying that changing how covers are produced means bumping it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reward-gated album showed a bare ❓ over the generated gradient, which reads as
"something is broken" rather than "something is waiting". It is now a proper
placeholder cover, with a separate one for audiobooks and podcasts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moves tippen from a standalone app into web/ as a third tab (audio player /
smarthome / typing), replacing the old single room-toggle corner button with a
vertical icon tab rail. Curriculum and progress now come from the backend
(musicmouse/tippen/*) instead of a build-time YAML import and localStorage.
Adds reward-driven lock rendering: Cover/BrowseView/AlbumModal show a question
mark for locked albums/tracks with a hint on what unlocks them, and
ResultSheet gets a new unlock-animation block alongside the existing
lesson-unlock and aquarium-creature celebrations.
CSS from the two apps is merged carefully: identical rules (bubble/card/
key-cap/view-enter/backdrop-enter and their keyframes) are shared as-is,
while rules that bake in each app's own hue are kept separate under a
`tp-` prefix and scoped to the typing tab's own .tp-stage wrapper, so
neither app's look bleeds into the other's.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the room-control page from the design mockup: a scenes row above cards
for shutters, color lamps, and brightness-only lamps, all driven by a new
`general.ha` config section (server URL, token, ordered device/scene lists).
The backend proxies every Home Assistant call server-side (GET/POST /api/ha/...)
rather than the browser calling Home Assistant directly, so the long-lived token
never leaves the LAN device and Home Assistant's own CORS settings don't need to
know about musicmouse at all. Card kind (shutter/color/brightness-only) is
inferred at runtime from what Home Assistant reports about each entity, not
configured explicitly.
Also stops tracking python-backend/config.yml, which had drifted into the repo
despite its own header saying it shouldn't be - it now carries real credentials
locally and needs to stay untracked.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>