One feature looked broken. Nothing had initialized.
Signature upload did nothing. The real problem was that the whole app never started, because a DOMContentLoaded listener was attached after the event had already fired. Here is the commit.
Builds production software, and cleans up the AI-generated code that breaks it. pdflokal, his open-source PDF toolkit, is one of the repos these notes come from.
A bug report: signature upload does nothing. You open the signature tool, the file picker appears, you choose a PNG, and nothing happens. Narrow, specific, obviously about the signature feature. So you go read the signature code.
The signature code was fine. It had never run. Nothing had.
The listener that fired before anyone was listening
pdflokal, our own PDF editor, wired up its whole front end in one function, initApp: the dropzone, the file inputs, the tool cards, the signature pad, all of it. That function was scheduled the way front-end code almost always is: document.addEventListener('DOMContentLoaded', initApp).
DOMContentLoaded fires once, at the moment the HTML finishes parsing. Add a listener before that moment and it runs. Add it after, and the event has already passed; your listener sits there, correct and idle, waiting for a thing that already happened. When the script ended up running after the DOM was already complete, that is exactly what occurred. initApp was never called. Not once.
A total failure wearing a local face
Here is the part worth keeping. Everything was broken. The dropzone did not initialize, the file inputs did not initialize, the tool cards did not initialize. But a user does not test everything. They reach for one thing, the signature upload, and they report the one thing that did not work. The bug arrives labelled “signature upload does nothing,” which points you at the signature code, which is the one place the problem is not. The disguise is not a lie. It is just the smallest true thing the user could see.
This prevented ALL initialization (dropzone, file inputs, signature pad, etc.).From the fix commit’s own description. Every feature was dead. Exactly one of them got reported.
The fix touched none of the feature code
The repair (c1d1207) did not go near the signature upload. It changed how initApp is scheduled: if document.readyState is 'loading', wait for DOMContentLoaded; otherwise call initApp() right now. Read the DOM’s current state instead of trusting that the event is still coming. Nine lines added, two removed, and the signature code was never opened. It had worked the whole time. It had simply never been switched on.
What this actually teaches
DOMContentLoaded is a mechanism everyone assumes they understand, and most of the time you can hold a slightly wrong model of it and never pay for it, because the script happens to load early enough. The wrong model is “add a DOMContentLoaded listener and your code runs on load.” The accurate model is “a DOMContentLoaded listener runs only if the event has not already fired.” The gap between those two is invisible until timing shifts: a script moves, a defer attribute appears, a bundler reorders things, and then the event is gone before you ask for it and your init silently never runs.
AI writes the bare listener by default, because it is the pattern that appears most often in its training and it is right most of the time. What it does not do unprompted is defend against the case where the DOM is already loaded, because that case is a property of when the script executes, not of the code in front of it. And it will not connect “signature upload does nothing” to “nothing initialized,” because you handed it a local symptom and a local symptom is what it will reason about. Paste it the bug as reported and it will confidently investigate the signature code, the one place the fix is not.
So when one feature does nothing, check whether its neighbours work before you read its code. A single dead feature is a bug in that feature. A dead feature next to other dead features is a bug in the thing that was supposed to start all of them. The most misleading failures are the global ones, because the user can only ever report the corner they happened to touch.
- The fix: guard initApp() with a document.readyState check, in a commit titled “Fix signature upload bug caused by DOMContentLoaded race condition” (2026-01-11). The diff touches only js/app.js (+9 −2); the signature code is never opened.c1d1207
Why is my DOMContentLoaded event not firing?
Because you attached the listener too late. DOMContentLoaded fires once, when the HTML finishes parsing. If your script runs after that moment (a deferred or module script, or one placed late), the event has already passed and a listener added now is never called. Guard it instead: if document.readyState is 'loading', add the listener; otherwise call your init function immediately.
Why does one feature break when the real problem is global?
Because the first thing a user tries is the first thing they report. If a single init routine wires up everything and that routine never runs, every feature is dead, but the user only notices the one they reached for. The bug arrives labelled 'signature upload does nothing', which points at the signature code, the one place the problem is not. The tell is that nothing else works either, once you check.
What is the safe way to run initialization code on page load?
Check readyState instead of trusting the event: if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }. This runs init once whether the DOM is still parsing or already complete, so a timing change (a script moving, a defer attribute, a bundler reordering) cannot silently skip it.