Most time-tracking applications require an account, store activity in the cloud, or collect more data than users expect.
I wanted a simpler alternative: a Windows application that records activity locally, works without an account, and sends no telemetry.
That idea became Local Time Tracker, a free and open-source desktop application built with Python, Tkinter, and SQLite.
In this article, I will explain what the application measures, the architecture behind it, the privacy trade-offs I had to make, and what I learned while preparing the project for external contributors.
What problem does it solve?
I wanted to answer a basic question: where does my time on Windows actually go?
Manual timers are useful, but they depend on remembering to start and stop them. Cloud-based tracking tools can automate this, but window titles and browser-tab names may reveal document names, searches, email subjects, or client information.
Local Time Tracker takes a deliberately narrow approach:
- it follows the foreground Windows application;
- it records the visible window title;
- it detects keyboard and mouse inactivity;
- it groups samples into continuous activity periods;
- it produces daily and seven-day summaries;
- it stores everything in a local SQLite database;
- it requires no account, server, advertising, or telemetry.
The tracker measures foreground focus, not human productivity. If a window is focused, that does not prove that the user is reading or actively working in it. This distinction is important when interpreting the data.
The architecture
The application is split into a few focused layers:
Windows APIs
|
v
Activity snapshot
|
v
Tracking and state transitions
|
v
Local SQLite database
|
+--> Tkinter dashboard and analysis
|
+--> Standalone offline HTML reports
1. Collecting Windows activity
The Windows provider uses pywin32 and psutil to read the foreground process, its window title, and the time since the latest keyboard or mouse input.
Each observation is only a point-in-time snapshot. It contains the application, title, and idle duration, but it does not yet represent a complete activity period.
2. Turning samples into periods
The tracker compares each new snapshot with the current state. A new period starts when the application, window title, or idle state changes. Otherwise, the current period is extended.
The core idea can be simplified as:
state = IDLE if snapshot.idle_seconds >= idle_threshold else snapshot.window
if state != current_state:
close_current_period()
start_new_period(state)
else:
extend_current_period()
Idle detection has an additional subtlety. If the application checks every few seconds, it discovers inactivity only after the threshold has already been crossed. The tracker compensates for that excess idle time so the transition is recorded closer to when inactivity actually began.
3. Storing data locally with SQLite
SQLite was a natural fit because it is embedded, reliable, and requires no separate database server. Activity periods can be queried by date and aggregated without sending anything outside the computer.
The installed application stores its data under:
%LOCALAPPDATA%\LocalTimeTracker\
|-- data\activity.db
`-- reports\
SQLite also makes the application easy to install and back up, but local does not automatically mean encrypted. The database relies on the security of the Windows account and disk. This limitation is documented clearly because window titles may contain sensitive information.
Why Python, Tkinter, and SQLite?
Python
Python made it possible to keep the activity model, analytics, reporting, and Windows integration understandable in one codebase. Its standard library also covers much of the required data and HTML-processing work.
Tkinter
Tkinter keeps the desktop interface lightweight and avoids introducing a browser runtime or a separate frontend build system. It is not the most visually flexible GUI toolkit, but it matches the project's goal: a focused Windows utility that remains approachable to contributors.
SQLite
SQLite provides transactional local storage with no account, network connection, or service configuration. That directly supports the privacy and offline requirements.
These choices are not universally "best." They are appropriate for the constraints of this project: a small Windows desktop application, local data, straightforward installation, and a codebase that new contributors can understand.
Normalizing browser titles
Browser windows often expose titles such as:
Gmail - Google Chrome
Documentation – Mozilla Firefox
Project — Microsoft Edge
If those suffixes are stored as-is, the analysis becomes noisy. The analytics layer recognizes supported browser executables and removes known browser suffixes, including hyphens, en dashes, and em dashes. Visits to the same tab can then be grouped under a cleaner title such as Gmail or Documentation.
The application currently supports title formats from Chrome, Edge, Firefox, Brave, Opera, and Vivaldi. It does not inspect the page body or send browser activity to an external service.
Offline analysis and reports
The Tkinter interface provides:
- live foreground activity;
- active and idle totals;
- recent activity periods;
- daily and seven-day analysis;
- rankings by category, application, and browser tab;
- hourly and daily charts.
Reports are generated as standalone HTML files with no external scripts, fonts, trackers, or network dependencies. They can be opened later without an internet connection.
Building privacy into the contribution process
Opening an activity tracker to contributors creates an unusual documentation challenge: a normal bug report or screenshot can accidentally expose private window titles.
The contribution and security guides therefore ask people to:
- use synthetic demonstration data;
- never upload a real
activity.db; - avoid screenshots containing personal window titles;
- describe privacy and compatibility impacts in pull requests;
- use private vulnerability reporting for security issues.
This taught me that privacy is not only a runtime feature. It must also influence documentation, testing, issue templates, support, and review practices.
Testing and releases
The project uses Python's unittest framework for tracking transitions, analytics, browser-title normalization, categories, database behavior, and report generation.
GitHub Actions runs the test suite for pull requests and changes to the main branch. Tagged releases run the tests, build the Windows application, create an installer, and publish a SHA-256 checksum beside it.
The current installer may still trigger a Windows SmartScreen warning because it is not yet signed with a trusted Authenticode certificate. I prefer to document that limitation instead of presenting the release as more mature than it is.
What I learned
The most useful lessons from this project have been:
- Privacy promises must become architectural constraints. A "no telemetry" claim should be visible in the code, reports, packaging, and documentation.
- Automatic tracking needs honest language. Foreground focus is useful evidence, but it is not a perfect measure of attention or productivity.
- Edge cases live in ordinary strings. Browser names, capitalization, empty titles, Unicode dashes, and unusual Windows titles all deserve tests.
- Contributor experience is part of the product. Focused issues, acceptance criteria, test commands, and privacy-safe examples make participation easier.
- Limitations build trust. Windows-only support, sampling gaps, unsigned installers, and unencrypted local data should be stated explicitly.
Try it or contribute
Local Time Tracker is available under the MIT License:
If you try it, I would especially value feedback about the first-run experience, tracking accuracy, and privacy controls that should be prioritized next.
If you are interested in contributing, please use synthetic data and start with a focused issue. Questions about the architecture and trade-offs are welcome in the comments.
Top comments (1)
The boring local stack is the right call for this kind of tool. A time tracker is mostly trust and retrieval, not cloud features. SQLite also makes the export story simple because the escape hatch is just a file you can inspect and back up.