The Screen Kept Going to Sleep
A fullscreen clock is a trivial app right up until the screen has to stay on for eight hours.
I wanted an old iPad to sit on my desk and be a clock. Big digits, dark background, nothing else. Clock is that, and it is a single page with no backend.
Drawing the time was not the problem. Keeping the screen awake was.
Drawing a clock, briefly
The instinct is setInterval at one second. It works and it is subtly wrong. Timers drift, they fire late when the tab is busy, and a clock that updates 40ms after the second changes will visibly skip a number now and then.
Update on requestAnimationFrame instead and read the clock fresh each frame. You are aligned to the display rather than to a timer you hope is punctual, and the seconds turn over when they actually turn over.
Wake Lock is a lease, not a switch
The Screen Wake Lock API is the supported way to ask a browser not to dim the display. You request it, you get a sentinel back, the screen stays on.
Then you switch to another app for ten seconds, come back, and the screen dims a minute later.
The lock is released automatically whenever the page stops being visible. Switch tabs, switch apps, lock the device, and it is gone. It does not return when you come back, and nothing tells you it went. Your sentinel object is still sitting there in memory looking perfectly healthy.
The fix is to listen for visibilitychange and request a new lock every time the page becomes visible again:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') requestWakeLock();
});
This is the kind of bug that survives testing, because testing means loading the page and looking at it. The failure needs you to look away, which is the one thing you do not do while checking whether your clock works. I found it the way you would expect, by walking off and coming back to a dark iPad.
Settings have to survive a refresh
The other thing a wall clock needs is to not ask you anything. Theme, seconds on or off, size, and format all persist to localStorage and are read before first paint.
An appliance that greets you with defaults after a reload is not an appliance. It is a webpage.
What I took from it
Read the lifecycle, not just the API. The signature told me how to acquire a wake lock. It did not tell me the platform would quietly take it back.
Bugs that need absence are invisible during development. Anything involving backgrounding, sleeping, or long idle periods has to be tested by actually leaving.
Persistence is what separates a tool from a page. Remembering the choice is most of the product.