Skip to content

About

Local-only per-app screen time for a Windows PC and Android phones, kept on a non-system drive so it survives a Windows reset.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Screen Time Tracker

CI Latest release MIT license Project site

How long each app was actually in the foreground — on a Windows PC and your Android phones — kept on your own machine, and built to survive a Windows reset.

The laptop's Overview: today, the daily average and the range total, above a daily trend line

Screenshots show synthetic data from npm run demo:seed.

See it on the project site: how it works, every chart it draws (live, on invented data) and every feature.

  • Local only. A dashboard on your own PC, reachable from your LAN. No cloud, no account, no telemetry, no outbound requests.
  • Real foreground time, not process uptime. On Windows, a sampler records which window has focus and whether the session is locked. On Android, the phone's own usage events are reconstructed into sessions and screen-on time.
  • Survives a reset. The database lives on a non-system drive and is backed up with SQLite's backup(). A recovery kit and a reset drill prove that a wiped C:\ would cost nothing.
  • Honest numbers. Every figure was checked against something external, such as Android's Digital Wellbeing. The traps found along the way are recorded in docs/DEVLOG.md.

Every page, in full

Each page top to bottom, at 1440px wide, in the default dark theme. A Dark / Light / System menu in the top bar switches every page to a light theme; each image has a light twin beside it (*-light.png), taken in the same run from the same demo, so the two show the same numbers. Retake them all with npm run demo:shots.

The laptop

Overview: today, the daily trend, a six-month heat map, the top apps and the most opened by day, the shape of the day, and where every minute went.

The laptop's Overview, full page

By App: ranked by time and by opens, in each app's brand colour, above the full app table.

The laptop's By App page, full page

App detail: one app's total, opens, typical session and longest session, per day and per hour, and the paths it was merged from.

An app's detail page on the laptop, full page

Activity: the heat map, expanded to every recorded day.

The laptop's Activity page, full page

Sync Status: whether the sampler is running, what is stored, and every ingest run.

The laptop's Sync Status page, full page

A phone

Overview: screen-on time and unlocks, the trend, the heat map, the top apps and the most opened by day, and how much of it any app accounts for.

The phone's Overview, full page

By App: ranked by time and by opens, above every app the phone reported.

The phone's By App page, full page

App detail: one app, per day and per hour.

An app's detail page on the phone, full page

Activity: the phone's heat map, expanded.

The phone's Activity page, full page

Sync Status: how far back Android's history reaches, and every push from the phone.

The phone's Sync Status page, full page

How it works

 Windows PC                                  Android phone(s)
 ----------                                  ----------------
 sampler.ps1  (logon task, unelevated)       Screen Time Reporter (APK)
   polls the foreground window every 2 s       reads UsageStatsManager events
   -> daily JSONL, crash-safe                   -> gzipped JSON over the LAN,
        |                                          every few hours
        v  hourly task / "Sync now"                  |
 ingest-windows.ts ------------------+     +---------+  POST /api/android/ingest
                                     v     v              (bearer token)
                          SQLite on a NON-system drive
                          + a backup() copy after every write
                                     |
                                     v
                  Next.js dashboard on :7844 (shared password)

It is really two different measurements, and the headline figure is computed differently for each. On Windows the sampler produces an exclusive timeline, so per-app time adds up to the total. On Android, per-app time adds up to only about 0.76 of screen-on time, because the lock screen, the launcher and system surfaces claim the rest. So the phone's headline figure comes from screen-on events, never from a sum over apps. docs/DEVLOG.md records why, with the measurements.

Requirements

  • Windows 10 or 11 for the dashboard and the PC collector.
  • Node.js 22.16 or newer. The project uses Node's built-in node:sqlite, so there are no native modules to compile.
  • A drive that is not the system drive, such as D:\, for the database. The code refuses to put it on C:\, since surviving a reset is the point.
  • Optional, for phones: Android 8.1 or newer, and JDK 17 or Android Studio to build the APK. Below Android 9 a phone records no screen-on or unlock events, so its dashboard shows time in apps instead of screen time.

Try it with demo data

npm ci
cp .env.example .env.local   # set DASHBOARD_PASSWORD to anything
npm run demo:seed            # synthetic laptop + phone, written to demo/
npm run build
npm run demo                 # http://localhost:7849

Setting it up for real

1. The dashboard

npm ci
cp config/collector.example.json config/collector.json
cp .env.example .env.local
  • In config/collector.json, point databasePath, backupPath, scratchDir and samplerLogDir at your non-system drive, and set deviceLabel to what you call this PC.

  • In .env.local, set DASHBOARD_PASSWORD and ANDROID_INGEST_TOKEN to two different long random values, for example:

    node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"

Then build it and run it at every logon:

npm run build
npm run autostart -- -RunNow

It serves on http://localhost:7844. The first build takes a minute. After that, the logon task rebuilds whenever the source is newer than the build. start-screen-time-dashboard.bat and stop-dashboard.bat do the same by hand.

It installs as an app. In Edge or Chrome on the PC, open http://localhost:7844 and use the install icon in the address bar, or Apps > Install this site as an app. It opens in its own window with the clock icon. A phone reaching it over the LAN can only add a home-screen shortcut that opens in the browser: installing needs HTTPS or localhost, and the dashboard is plain HTTP on the LAN.

2. The Windows collector

powershell -ExecutionPolicy Bypass -File scripts\install-sampler.ps1 -RunNow

This registers two unelevated tasks: the sampler, at logon, and the ingest, hourly. Windows keeps no usable history of foreground time, so recording starts from the moment the sampler runs.

Or let Screen Time Native record. If the laptop runs Screen Time Native, the standalone Windows app ported from this project's laptop half, run only one sampler: two record every second twice. Register just the ingest with install-sampler.ps1 -IngestOnly (it stops a running sampler cleanly), run npm run ingest once more, then set nativeDatabasePath and nativeFrom in config/collector.json (see the example file). From nativeFrom, a local hour edge, the ingest copies the laptop's screen time from that app's database, and Sync Status reads its sampler's heartbeat. Earlier history stays as this project's sampler recorded it.

3. A phone (optional)

  1. Download the signed APK from the Releases page and install it; Android will ask you to allow installs from your browser or file manager. It is not on the Play Store.

    Or build it yourself:

    cd android
    ./gradlew assembleDebug

    and install app/build/outputs/apk/debug/app-debug.apk, for example with adb install. A self-built copy is signed with a different key, so it cannot install over the released one, or the other way round, without uninstalling first. Uninstalling also resets the phone's device id, so its next sync shows up as a second phone; fold the old entry's history into it with npm run android:merge -- --from <old-slug> --into <new-slug> (it reports first, and writes only with --apply). To sign your own release builds, point android/keystore.properties (gitignored) at your keystore with storeFile, storePassword, keyAlias and keyPassword, then run ./gradlew assembleRelease.

  2. In the app, tap Grant beside Usage access and enable Screen Time Reporter.

  3. Enter the dashboard's address (http://<your-PC's-LAN-IP>:7844) and ANDROID_INGEST_TOKEN, then tap Save and test connection.

The phone syncs in the background. Android keeps about 10 days of event history, so the first sync backfills that much. If the dashboard has not received the phone's history for 168 hours (sooner on a phone that keeps less), the app posts a notification, before the oldest of it is lost; allow notifications when it asks.

Optional: a cloud inbox, so the phone can report from anywhere

On its own the phone posts straight to the dashboard, so it must be on the PC's network, the PC must be on, and the address changes whenever the router hands the PC a new one. A cloud inbox removes all three: the phone posts to a fixed https:// address on any network, and the PC collects whatever is waiting on its hourly ingest and on Sync now. With only ~10 days of history on the phone, that matters more here than for bytes.

The inbox cannot read what it holds. The app encrypts each upload to a key only the PC has (P-256 ECDH, AES-256-GCM), with your ingest token mixed into the key derivation, so the inbox cannot forge one either. It sees an upload's size, its time and the phone's random device id. The format is written out at the top of src/lib/inbox.ts.

It runs on Supabase's free tier as one Edge Function and two tables, in supabase/. The function also serves the sibling Data Usage Tracker, each app as its own mailbox, so one free project covers both.

  1. Create a Supabase project (or reuse the sibling's). Run supabase/migrations/*.sql in order in its SQL editor, and deploy supabase/functions/inbox with JWT verification off (the function checks its own tokens): supabase functions deploy inbox --no-verify-jwt.
  2. In config/collector.json, set "inbox": { "url": "https://<project>.supabase.co/functions/v1/inbox/screen-time", "keyPath": "D:\\keys\\inbox-key.pem" }. Keep the key off C:\ and out of the repo, and back it up: envelopes still in the inbox cannot be opened without it.
  3. Create the key and the PC's own token, then register both tokens:
    npm run inbox -- keygen
    npm run inbox -- drain-token
    npm run inbox -- sql
    Run the SQL that the last one prints in the SQL editor. It holds SHA-256 digests only.
  4. Rebuild and restart the dashboard. On the phone, at home, tap Set up from the dashboard. The app fetches the inbox address and the PC's key over the LAN (never from the inbox, which could swap the key) and shows the key's fingerprint; npm run inbox -- key prints the same one.

An upload that came this way shows on the phone's Sync page with when it reached the database. One that cannot be opened stays in the inbox, marked failed; fix the cause, then npm run inbox -- drain --retry-failed, or tap Full resync on the phone.

4. Backups

npm run backup:kit   # the code, the secrets and the local-only files, off C:\
npm run drill        # restore into TEMP, verify, and list anything a reset would lose
npm run restore      # after a reset: bring the database back from the backup

Re-run backup:kit after changing config, logos or secrets. The drill fails if the copy is out of date.

5. App logos (optional)

No logos ship with the repository: they are other companies' trademarks. Drop your own into public/apps_logo/, and brand colours into config/app-colours.json. Both are gitignored. Until then, every app shows its initial. See public/apps_logo/README.md.

Security

The dashboard is single-user, over plain HTTP on your LAN. Every page sits behind one password, which fails closed, and wrong guesses are throttled. Sessions are signed with a key derived from the password, and the phone uses its own token. The pages send a strict Content-Security-Policy.

Because it listens on every network interface, run this once from an Administrator shell to block the dashboard on Public networks. Mark your home network Private first, or the phone will be cut off.

npm run firewall

The optional cloud inbox is the one outbound connection. It carries only the phones' uploads, encrypted on the phone to a key the inbox never sees, and is off unless inbox is set in config.

Read SECURITY.md for the threat model and its limits, and for how to report a vulnerability.

Accessibility

The pages are built to WCAG 2.1 AA. Contrast is measured, every page has its own title, every chart has a text summary, and there's a skip link and screen-reader navigation state. Two deliberate exceptions:

  • Phones get the desktop layout, zoomed to fit, rather than a reflowed narrow one (WCAG 1.4.10). Pinch-zoom still works.
  • The heat map's quietest shades sit close to the background. The heat map has a text summary, and every cell has a title.

Development

npm run dev          # dev server on :7844. Stop the logon-task server first:
                     # the two share .next.
npm run typecheck
npm run selftest     # no device or database needed
npm run drill        # the reset drill, against your real backup

CI runs the typecheck, the self-test, a production build, a PowerShell 5.1 check and the APK build on every push.

Before changing anything, read docs/DEVLOG.md. It is the day-by-day design record: every measurement behind a decision, and every trap that was found by running things rather than by reasoning about them. scripts/research/ holds the measurements that shaped the design. Nothing there is needed to run the tracker.

This is a personal project. Issues are welcome; see CONTRIBUTING.md. For pull requests, please open an issue first, since the design is deliberately narrow.

Licence

MIT © 2026 Naimul Nashid. App names and logos belong to their owners. None are distributed here.

About

Local-only per-app screen time for a Windows PC and Android phones, kept on a non-system drive so it survives a Windows reset.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages