Harrison Samoy
← Projects

App Tracker

personal project · 2026

A Windows tray app that records how long specific applications are actually open, and a local dashboard to read it back. I run it from logon every day - it is the one thing here I built purely because I wanted the answer, and then kept because the answer turned out to be useful.

The dashboard: headline totals, daily activity, and a year of history. Screenshots use seeded demo data.

Guessing badly is the whole problem

Everyone has a story about where their time goes, and the story is usually wrong in a specific direction: the hours you resent feel longer than they were, and the hours that vanish feel shorter. I wanted the boring version - a number, per app, per day, collected without me remembering to do anything.

That constraint shapes the whole design. If the tracker needs starting, it will not get started. So it installs as a logon task into its own directory - deliberately not the build output, so rebuilding the project can never break the copy that is actually running - and lives in the tray from then on.

The log is the truth; sessions are a guess you can redo

The tracker writes immutable events - process started, process stopped, heartbeat, tracker started, tracker stopped - and never edits them. What you actually look at, sessions, is a projection folded from that log.

That split is the decision the rest of the design leans on. Sessionization rules are the part most likely to be wrong at first: what counts as one session, how long a gap breaks it, whether a launcher counts. Because the log is untouched, those rules can change later and every session gets re-derived from history rather than only applying going forward. Rebuild determinism is one of the things the tests pin down.

Making the numbers honest

A poll loop is the obvious way to build this and it quietly lies to you: start late and you lose the beginning, poll every few seconds and every duration rounds to the interval. So the timings do not come from the poll at all. Start times come from the OS process creation time, which backdates a session the tracker only noticed an hour in. End times come from the kernel - a handle is held on each tracked process, so the exact exit time is read rather than rounded up to the next tick. The poll interval affects how fast something is noticed, not what gets recorded.

Crashes are bounded rather than ignored. Heartbeats mean a killed tracker leaves a session that the next run closes at the last known-good moment, instead of dropping it or letting it run forever. And a restart while an app is still open is not a new session: a process is identified by pid plus creation time, so the same instance is recognised and its session reopened - which also stops a recycled pid being mistaken for it.

The most useful rule is about what not to count. The League client sits in the tray for days after you stop playing; counting it would have added phantom hours to exactly the number the whole thing exists to get right. It is on an explicit ignore list, and only the process that means the app is genuinely in use gets recorded.

Local, and staying that way

Everything is a SQLite file and some JSON under the user profile. The dashboard is served on loopback only, needs no admin URL reservation, and only ever reads the database - so it is safe to open while the tracker is mid-session.

Reads are open, but anything that changes state has to carry a header no cross-origin page is allowed to set, which forces a preflight the server never answers. Loopback alone is not a boundary: any page in any other tab can post to localhost.

A year, one square per day. Quiet stretches stay visible instead of being smoothed away.
Weekday-by-hour on the left, per-app totals on the right - the two views that actually changed how I schedule deep work.
Every session keeps how it ended and how confident the record is - a clean exit reads differently from one recovered after a crash.