From sleep loop to portable screensaver
Left alone, the showroom TV plays a 22-slide loop that sells the life, not the map. When another app wanted it, it became one HTML file built from the same code, by a script that stops if that code changes shape.
The problem
The TV’s idle loop was wanted inside another app, without the login, the map or the CRM.
The solution
Build a standalone page from the TV’s own code, and stop the build if that code changes shape.
A showroom TV has to earn its place when nobody is touching it. Ours goes to sleep after 20 seconds without a button press, and sleep is a loop with one job. The first line of its notes says it plainly: “It sells the life, not the map.”
What the loop plays
One pass is 22 slides, about 4½ minutes: the AHA logo, all eleven AHA moments in the order of a day, five products (the next five on the next pass), two client testimonials, four buildings with AHA homes, both experience centres with live opening hours, and a QR code to the experiences page.
It opens on the moment that fits the hour. At 6 pm that’s Grand Welcome, arriving home; at 10 am it’s Goodbye Routine, leaving. On a laptop the loop waits until nothing is open, so a page someone is reading is never swept away. The TV sleeps from anywhere: its viewers have walked off.
Smooth on a 2 GB TV
Slide changes used to stutter on the TV, waiting on the network or the image decoder. Now the next slide’s photo is fetched and decoded while the current one shows. The new photo dissolves in over the old one, which is removed only once covered. A few decoded images stay in hand (the one on screen, the next, and the one a ◀ press would bring back), and older ones are let go so the TV doesn’t run out of memory. The progress line is scaled rather than resized, so the graphics chip has little to redo.
Building photos, sorted by eye
Most buildings use their public map photo, which is whatever a visitor uploaded. All 229 were sorted by eye. 65 were construction sites, hoardings, street scenes or not the building at all (a shop, a temple, a chai van), and are never shown. 76 were good enough for the loop. Each verdict is tied to that exact photo, so a photo swapped later is judged afresh instead of inheriting the old verdict.
The same loop, somewhere else
In October the loop was wanted for the AHA camera app, as a screensaver in its web view. Rather than keep a second copy that could drift, a script builds the standalone page from the TV’s own code, copying the loop’s functions, photos, words and styles verbatim, so it plays exactly as the TV does.
It leaves out the login, the map and the CRM data, so the buildings slide goes: 21 slides, about 4 minutes. A tap wakes nothing, because there’s no app underneath; it tells the app showing the page, which usually closes it.
The script is strict. Every function and style rule it copies must be found exactly once, and so must every edit it makes:
// each [old, new] edit must match exactly once, or the build stops
pairs.forEach(([a, b]) => {
const n = text.split(a).length - 1;
if (n !== 1) fail(label + ': expected to find once, found ' + n);
text = text.replace(a, () => b);
});
If the TV’s code changes shape, the build stops and says where, instead of shipping a screensaver that quietly differs.
What the handover taught
The first handover was three files, and the page opened alone as a blank white screen. Now aha-screensaver.html holds everything in one file, so it plays however it’s opened: from a chat, a download, an app’s own files, or handed to a web view as text.
Older web views needed two more guards. CSS inset is unknown before Chrome 87, and without it the whole page would collapse to nothing, so the script writes it out as top, right, bottom and left. And if the page can’t start, with JavaScript off (an Android web view’s default) or files missing, it says so on screen after two seconds instead of staying blank.
What I took from it
- Idle time is airtime. A screen nobody is touching should still be selling.
- Decode before you show. A change should never wait on the network or the decoder.
- Generate the copy, don’t fork it. And make the generator fail loudly when its source moves.
- Hand over one file. Assume it will be opened in the least helpful way.
I directed the work and Claude Code wrote much of the code. There is still only one loop: the screensaver is rebuilt from it, and the build stops rather than copy something that has changed shape.