Privacy-first automatic time tracking for freelancers — runs entirely on your machine. No cloud, no API keys, no subscription. Classifies your work by matching project keywords against window titles, browser URLs, and on-screen text.
Website · Download · Browser extension · Configuration · Contributing
Manual time tracking is broken. You forget to start the timer, you forget to stop it, you forget which project you were on, and at the end of the month you guess at hours and eat unbilled time.
Most "automatic" alternatives fix that by uploading your screen to a cloud service or sending your activity to an LLM. That's a non-starter for anyone working under NDA, on client systems, or simply allergic to handing every keystroke of context to a third party.
Tickwise stays on your laptop. It watches the active window, asks the browser extension for the exact tab URL, runs OCR on the screen if you want richer signal, and decides which project the activity belongs to by matching the project's keywords against that combined text. No network, no inference, no cost — just a fast deterministic match. The dashboard, the database, the classifier, and the matching logic all live inside a single ~80 MB binary that you double-click.
- 🎯 Fuzzy keyword classifier —
"Sceneryenzo"matches"scenery en zo","Scenery-Enzo","sceneryenzo.com"and"SCENERY ENZO". Spacing, punctuation and case are ignored. Multi-word keywords also win on partial matches. - 🖥️ Tray-resident — clock-face system tray icon shows tracking / paused / focus / break states. Right-click for Open Dashboard, Pause, Start Pomodoro, Quit.
- 🪟 Native desktop window — Electron wrapper opens the dashboard in its own application window, not a browser tab. Single installer, single tray icon, single process tree.
- 🔍 Change-detection capture — perceptual-hash diff means OCR runs only when the screen actually changes (~2-4 times/min, not 60).
- 🖥️ Multi-monitor — captures every screen, classifies the focused one, hash-tracks the rest for instant classification on focus switch.
- 💤 Idle detection — auto-pauses after a configurable idle threshold, resumes when input returns.
- 🌐 Browser extension — Chrome MV3 + Firefox bridge that pushes the exact tab URL and title to the local API over WebSocket. Domain blocklist for banking, personal email, etc.
- 🍅 Pomodoro — built-in focus/break state machine. Tags every captured session with the active period. Controls from tray menu, dashboard, browser popup and mobile.
- 📅 Calendar sync — ICS feed (Tuta, Google, Apple), CalDAV (Radicale, Nextcloud), Google Calendar OAuth2.
- 🧾 Invoicing — line-item-grouped PDFs from tracked time, configurable HTML/CSS template, Dutch BTW/KVK/IBAN out of the box.
- 📊 Dashboard — live view, timeline (day/week/month), reports, project & client management, dark mode, keyboard shortcuts (Ctrl+P / Ctrl+,).
- 📱 Mobile PWA — view today, start/stop Pomodoro, see the timeline from your phone. QR-code pairing, optional Cloudflare Tunnel for off-LAN access.
- 🔒 4-level redaction — strips secrets / PII / names / structure before any text is written to disk.
- 🔐 Local-only API — FastAPI bound to
127.0.0.1:19532. Nothing leaves the machine unless you opt-in to Cloudflare Tunnel for the calendar feed or mobile pairing.
| Platform | Installer | Notes |
|---|---|---|
| Windows x64 | Download Tickwise-Setup.exe → | Windows 10 / 11. NSIS installer or portable |
| macOS | coming soon | Apple Silicon + Intel DMG |
| Linux | coming soon | AppImage |
First-launch: the binaries are not yet code-signed. Windows SmartScreen will warn — click More info → Run anyway.
Double-click Tickwise-Setup.exe to install. Launch from the Start Menu —
the dashboard opens in its own native window (no browser tab), the tray
icon appears, and a fresh SQLite database is created at
%APPDATA%\Tickwise\tickwise.db on first run.
There is exactly one user-facing artifact: the Electron-built installer above. The Python backend, the Angular dashboard, the OCR models and the system-tray UX are all bundled inside it. You don't install Python and you don't install Node.js to use Tickwise.
You'll need Python 3.12+ and Node.js 20+. One command builds the
whole stack — npm run build:win from the electron/ directory runs
the Python backend through PyInstaller, then wraps it in the Electron
installer:
git clone https://github.com/code-lodge/tickwise
cd tickwise
# Install backend deps (one-time)
python -m venv .venv
.\.venv\Scripts\pip install -e . pyinstaller
# Build the dashboard (one-time, or after Angular changes)
Push-Location dashboard ; npm install ; .\node_modules\.bin\ng build ; Pop-Location
Copy-Item dashboard\dist\tickwise-dashboard\browser\* tickwise\static -Recurse -Force
# One-shot installer build → dist-electron\Tickwise-Setup-1.0.0.exe
cd electron
npm install
npm run build:winnpm run build:win runs electron/scripts/build-backend.js first
(invokes PyInstaller from the venv) before electron-builder picks up
the result via extraResources. The output is a single installer
that ships the Python service, dashboard, OCR models and Electron
shell as one package.
Cross-platform: npm run build:mac (universal DMG) and
npm run build:linux (AppImage) follow the same pattern from a
matching host. See electron/README.md for the
detailed build pipeline.
For hot-reload while you're hacking on the dashboard:
.\.venv\Scripts\python.exe -m tickwise # backend on :19532, brings up the tray
cd dashboard ; ng serve --proxy-config proxy.conf.json # Angular hot-reload on :4200In dev mode the Python pystray icon shows up directly (no Electron wrapper). Right-clicking Open Dashboard falls back to your default browser if the Electron app isn't installed.
Tickwise has a single classifier — a deterministic keyword matcher that runs in two stages:
Stage 1 — Normalized substring match. Both the keyword and the haystack (window title + browser URL + tab title + OCR text) get lowercased and stripped of every non-alphanumeric character. So "Scenery-Enzo Website!" and "scenery en zo" both become "sceneryenzo"-prefixed strings, and a substring check finds them.
Stage 2 — Token-set fallback. If the whole-keyword pass misses, the keyword is split into significant words (stop-words like website, app, dashboard, the, and are dropped). At least one token must appear in the normalized haystack. This is what makes a project named "Sceneryenzo website" still claim a tab whose only signal is sceneryenzo.
When several projects could match, the project with the highest score wins (score = total characters of matched text, with a 2× bonus for whole-keyword matches). Ties are broken by project id (oldest wins, stable).
You manage keywords directly on the Projects page — one keyword per line. New projects auto-populate with the project's name; you can add as many aliases as you want:
Sceneryenzo
scenery enzo
sceneryenzo.com
scenery-enzo
SE-website
If nothing matches, the activity is stored as unclassified. You can reassign it manually from the timeline at any time.
Window titles in modern browsers are useless for time tracking — Edge will happily report "Tickwise and 5 more pages — Work — Microsoft Edge" while you're on sceneryenzo.com. The browser extension fixes that by pushing the real active-tab URL and title to the local Tickwise API over WebSocket.
Chrome / Edge / Brave / Arc
- Open
chrome://extensions/(oredge://extensions/) - Enable Developer mode (toggle, top-right)
- Load unpacked → select the
browser-extension/folder
Firefox
- Open
about:debugging→ This Firefox - Load Temporary Add-on → select
browser-extension/manifest.firefox.json
The extension talks only to 127.0.0.1:19532 — nothing leaves the browser. A configurable domain blocklist (default: mail.google.com, online.banking) excludes sensitive sites entirely.
Most settings live in the dashboard (Settings page) and are stored in the local SQLite database. Capture interval, idle thresholds, OCR downscaling, redaction levels, Pomodoro durations, dark mode — all editable in the UI, no config file to hand-edit.
| Setting | Default | What it does |
|---|---|---|
capture_interval_ms |
1000 | How often the capture loop ticks |
phash_change_threshold |
5 | Hamming-distance threshold for OCR re-run |
idle_merge_threshold |
120 s | Idle gap that gets merged into the active session |
idle_split_threshold |
300 s | Idle gap that splits the session into two |
min_session_duration |
10 s | Sessions shorter than this are discarded |
privacy_level |
2 | 1 = secrets only, 2 = + PII, 3 = + names, 4 = strip structure |
pomodoro_work_minutes |
25 | Focus duration |
pomodoro_short_break_minutes |
5 | Short break duration |
pomodoro_long_break_minutes |
15 | Long break duration |
pomodoro_cycles_before_long |
4 | Focus cycles before a long break |
dark_mode |
false | Dashboard theme |
| Level | Strips |
|---|---|
| 1 — Minimal | API keys, passwords, private keys, JWTs, connection strings |
| 2 — Standard (default) | + Emails, phone numbers, IPs, IBANs, credit cards, file paths |
| 3 — Aggressive | + Person/organisation names, monetary amounts, chat content, shell commands |
| 4 — Maximum | + Code blocks, all URLs, tabular data, quoted text |
Custom redaction patterns let you blacklist client-specific terms (project codenames, internal domains, etc.). The dashboard has a live preview that shows what each level does to a sample of your text.
┌─────────────────────────────────────────────────────────────────────────┐
│ SYSTEM TRAY (pystray) — clock icon, status text, context menu │
└──────────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ BACKGROUND SERVICE │
│ │
│ Capture loop → pHash diff → OCR (PaddleOCR) → Redaction │
│ (1 s tick) (skip cost) (CPU only) (4 levels) │
│ │
│ → Keyword matcher → Session tracker │
│ (deterministic) (open / extend / close) │
│ │
│ Pomodoro state machine · Idle detector · Browser-bridge WebSocket │
└──────────────────────────────────┬──────────────────────────────────────┘
│ writes
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SQLite (WAL) · %APPDATA%\Tickwise\tickwise.db │
└──────────────────────────────────┬──────────────────────────────────────┘
│ reads
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ FastAPI on 127.0.0.1:19532 │
└──────────┬──────────────────────────────────┬───────────────────────────┘
▼ ▼
Dashboard (Angular 19) Browser extension (Chrome MV3 / Firefox)
Mobile PWA (via Cloudflare Tunnel, optional)
Single process. Multiple threads. No containers, no cloud, no accounts. Just a desktop app that double-clicks.
tickwise/
├── tickwise/
│ ├── capture/ # Screen capture, window info, idle, change detection
│ ├── ocr/ # PaddleOCR wrapper, lazy-loaded
│ ├── redaction/ # 4-level privacy redaction engine
│ ├── classification/
│ │ ├── keyword_matcher.py # Fuzzy normalized + token-set matching
│ │ └── pipeline.py # redact → match → store
│ ├── sessions/ # Open / extend / close session aggregation
│ ├── pomodoro/ # Focus/break state machine
│ ├── calendar/ # CalDAV, ICS feed, Google Calendar
│ ├── cloudflare/ # Optional Tunnel setup (mobile + ICS only)
│ ├── invoices/ # PDF generation via WeasyPrint
│ ├── reports/ # Time / billing / activity aggregation
│ ├── api/ # FastAPI routers, WebSocket, bearer auth
│ ├── db/ # SQLite connection, schema, migrations
│ ├── crypto/ # OS keyring (Windows DPAPI / Keychain / Secret Service)
│ └── platform/ # Cross-platform autostart, notifications, paths
├── dashboard/ # Angular 19 standalone components + signals
├── browser-extension/ # Chrome MV3 + Firefox WebExtensions
├── mobile/ # Angular PWA companion app
├── packaging/ # PyInstaller spec, NSIS installer, build scripts
├── docs/ # Specification, GitHub Pages site
└── tests/ # pytest: unit, integration
| Feature | Status | Platform |
|---|---|---|
| Tray icon + dashboard + capture loop | ✅ Done | Windows |
| Fuzzy keyword classifier (normalized + token-set) | ✅ Done | All |
| pHash change detection | ✅ Done | All |
| Multi-monitor capture | ✅ Done | All |
| Idle detection | ✅ Done | All |
| Pomodoro state machine | ✅ Done | All |
| 4-level redaction engine + custom rules | ✅ Done | All |
| Invoicing (PDF + line-items + Dutch BTW) | ✅ Done | All |
| Calendar sync (ICS feed + CalDAV + Google) | ✅ Done | All |
| Cloudflare Tunnel for mobile + ICS | ✅ Done | All |
| Mobile PWA + bearer auth + QR pairing | ✅ Done | All |
| Browser extension (Chrome MV3 + Firefox) | ✅ Done | All |
| OCR (PaddleOCR, CPU) | 🚧 Optional install | All |
| macOS build | 📋 Planned | macOS |
| Linux AppImage build | 📋 Planned | Linux |
| Code signing | 📋 Planned | All |
| Metric | Target | Why it matters |
|---|---|---|
| CPU (idle screen) | < 1 % | Tickwise should be invisible on a quiet day |
| CPU (active use) | < 5 % | OCR is the only spike — rate-limited by the pHash diff |
| RAM | < 200 MB | Including bundled dashboard + Python runtime |
| VRAM | 0 | Everything is CPU. No GPU dependency, no driver headaches |
| DB growth | ~5–10 MB / month | WAL-mode SQLite, no screenshots stored |
| Classification calls | 0 / month | Local matcher, no network |
| Cost | $0 / month | The only paid component is the (optional) Cloudflare account for the tunnel |
- All data stays local. SQLite database on your machine. No cloud sync, no accounts, no telemetry.
- No keylogging. Input events are counted for idle detection only — keystrokes are never recorded.
- Screenshots are never stored. Captured in memory, OCR'd, then discarded.
- Redaction before persistence. OCR text is stripped of secrets / PII before it touches the database.
- API server is local-only. FastAPI binds to
127.0.0.1— nothing is accessible from the network. - Tunnel is scoped. When enabled, Cloudflare Tunnel exposes only the calendar feed and mobile API. The dashboard is never exposed.
- Credentials in OS keyring. Mobile bearer-token hashes go in SQLite; anything secret (Cloudflare token, Google OAuth refresh token) goes through Windows DPAPI / macOS Keychain / Linux Secret Service.
Contributions of any kind are welcome — bug reports, redaction rules, new calendar providers, platform builds, dashboard polish. To get started:
- Fork the repository and create a feature branch.
- Run the tests with
pytest. Format withblack, lint withruff, type-check withmypy. - New features should ship with tests. Bug fixes should ship with the regression test that would have caught them.
- Open a pull request describing what you changed and why.
Tickwise is licensed under the GNU General Public License v3.0. See LICENSE for the full text.