- NO.
- 010
- DATE
- Updated 2026-07-20
- READ
- ~7 min
- KIND
- Case study
- STATUS
- Reviewed
A Ruler for Time: One Zero-Dependency PWA
A local time logger that does one thing: show where the hours went. Zero dependencies, no build step, and 14 root-cause postmortems.
You become where your time goes
The hardest part of a job search is not sending applications. It is thinking back over the day and feeling busy without being able to say what the day contained. How many hours actually advanced an interview, and how many quietly leaked into "researching tools," "learning methodology," and reading articles?
I did not want another productivity app, or habit streaks, or a dashboard to show anyone. I wanted a ruler: measure once a day, see where the time went, and face it honestly.
So I built Time Logger. It has one use, and deliberately keeps only that one.
How time is stored: points, not intervals
Most timers store intervals: one record with a start and an end. It sounds natural, and the moment you need to backfill, split, or cross midnight, overlaps and gaps between intervals turn into an endless supply of boundary bugs.
Time Logger stores points. Each record carries only a start timestamp; its duration is the gap to the next record, computed at render time and never stored. Today's last record has no right neighbor, so it settles to "now"; a past day's last record settles to 24:00 of that day. The whole model has exactly one hard boundary: 00:00 of the local calendar day.
That choice turns "backfill a past stretch" into a bounded insert: give a start and an end, cut that stretch out and label it, and the tail of the segment automatically returns to the original label while everything else stays untouched. Every write path — create, edit, delete, backfill — converges on the same normalization exit: drop redundant adjacent boundaries, and always leave a trailing placeholder for today. Deleting no longer silently merges a stretch into the previous one; either both sides share a label and heal automatically, or it honestly becomes "unlogged."
The easiest way for a logger to lie to you is rendering "not recorded" as something definite. Time Logger would rather show a grey "unlogged" block than invent your day for you.
Engineering-wise, it is a deliberately unfashionable PWA
No framework, no build step, no bundler, no npm runtime dependency. Just index.html, one styles.css, and a set of native ES modules the browser loads with type="module", published by GitHub Pages straight from the repository root.
That is not nostalgia. It is a constraint in service of discipline:
- Zero dependencies means no supply chain, no weekend spent upgrading 47 packages, and no build artifacts to explain.
- No backend, no account, no cloud sync means the data lives in your own
localStorage; the code and interface can be fully public while the records never leave your machine. - Module boundaries are written into the documentation: time utilities never touch the DOM, statistics never touch
navigator, the UI layer never persists. Each file does one thing.
The whole app is about 195 KB (≈73 KB gzipped). I once did a careful memory and size analysis in the roadmap, and the conclusion was: there is no performance problem to solve, so optimizing now would be textbook gold-plating. Writing that down was the point — so that future me, reaching for "an optimization," gets stopped.
The actual product is the set of red lines
The code is a few thousand lines, and the most important file in the repository is not code. It is CLAUDE.md, a maintenance contract written for future me and for any AI collaborator:
- No runtime dependencies, no build step, native ES modules — iron rules. npm is permitted only for development-time test dependencies.
- Change any runtime asset and you must update the Service Worker cache list, the manifest version, and the expected version in the audit script together — one version ritual, four places aligned.
- Four things run before every commit: the red-line audit script, a confirmation-logic smoke test (real
nodeimporting the real modules), a Playwright responsive UI smoke test, andgit diff --check. - One data-consistency iron rule: any read-then-write must share the result of a single load. Modifying one object graph and saving a different one is forbidden. That rule was paid for in blood (below).
And one meta-rule, blunt enough to sting:
The biggest risk is polishing this tool to avoid advancing the job search. Every time "just one more feature" appears, ask first: did today move an interview forward at a target company?
I even gave it a hard latch: no extensible taxonomy, no reports, no cloud sync until 28 days of real records exist. Before the data accumulates, features are gold plating. The rule actively reminds me — and reminds the AI writing code with me — not to help me hide when the search has made no real progress.
Fourteen postmortems, root causes not patches
The project keeps postmortems.md, fourteen entries so far. Each one requires the symptom, the root cause, the fix, and the guardrail. "Change it and see" is not allowed. Two that stayed with me:
Editing "looked unimplemented." A user reported that edits did not take effect. The root cause was two loads inside commitEdit: the change was applied to graph A while the unmodified graph B was saved back. It looked like a missing feature and was actually a data-consistency incident. The fix was a single load, taking the record from the graph that gets saved — and promoting that into a red line.
A "jump" after saving on iPhone. Editing a long record and saving caused a second reflow of the interface. The root cause: closing the form synchronously while the iOS software keyboard was still present, so the viewport reflowed a frame later and the jump was visible. First fix: detect the keyboard, blur first, wait for visualViewport to settle, then close and re-render in a single frame. That was only the beginning — several variants followed (side-jitter while tracking mid-keyboard-animation, the cancel and overlay close paths never wired in, visualViewport resize events arriving 700ms after the getter on iOS 18…), and with the parameters tuned correctly it still jumped on a real device. By the sixth round the actual error became clear: the parameters were not wrong, the premise was. The form panel should never have been moving with the keyboard's viewport shrink at all. The real fix made the panel fixed and full-screen with a sticky header above where the keyboard can reach, so the keyboard only affects how much scroll room the content area leaves — and about 200 lines of keyboard-timing code were deleted outright. When the third attempt at one symptom still fails, stop and ask whether the approach is wrong instead of tuning further. That lesson is worth more than the postmortem itself.
The value of writing postmortems is not recording bugs. It is forcing yourself down to the "why," then adding the guardrail to the tests so the same hole is not stepped in twice. (Incidentally: the min-width: 0 grid blowout was stepped in twice in this repository, and the second time I finally wrote it into memory.)
A side note on collaborating with AI
This project was largely paired with AI. The interesting lesson: an AI will enthusiastically help you gold-plate. Say "let's polish the scroll feel a bit more" and it will not stop you. So "no gold plating," "the 28-day latch," and "the biggest risk is avoiding the job search" went directly into the instruction file the AI reads, making the constraint part of the context. Discipline cannot rest on willpower; it has to be written into the system.
One more trap: multi-step changes have to run on the main thread, serially. An attempt to fan out subtasks concurrently for speed simply overwhelmed the upstream API and got 429s. So "serial, one at a time" became a red line too.
What it is now
A ruler, not a product. Zero dependencies, offline-capable, data never leaving the device, code fully public (AGPL-3.0). The only thing it does for me is answer, honestly, every day: where did the time go?
If you are also job hunting, or just want to know where your hours went, you may not need another feature-rich app. You may only need a ruler honest enough to trust.
Comments
Comments are powered by GitHub Discussions. Sign in with GitHub to comment. Open the matching Discussion