A local dashboard that makes per-app network usage history permanent, for a Windows PC and, optionally, Android phones.
Windows already tracks how much data each app uses (Settings → Network & internet → Data usage), but it keeps only about 30 to 60 days, in a database on the OS partition. A Windows reset wipes it. Android keeps about 90 days, and only an app on the device can read it per app.
This project copies both into a SQLite database on a drive that survives a reset, and shows them in a Next.js dashboard that runs on your own machine.
See it on the project site: how it works, every chart it draws (live, on invented data) and every feature.
- Local only. No cloud service, no account, no telemetry, no outbound
requests. The dashboard listens on
127.0.0.1unless you open it to your network. - Windows 10/11. The collector reads SRUM, the database behind Windows' own Data usage page.
- A personal tool, shared as is. It is built and used on one machine, and depends on Windows internals and a third-party parser. Expect to read the scripts before trusting them with your data.
The whole Windows overview: totals, the daily trend, a six-month heat map (outlined days were never collected, which is not the same as a quiet day), each day split by app, the hour of day, and one heavy app set apart from the rest.
Every app, one app in detail, and a phone
Every app, ranked. Each bar is download, then upload in a tint of the app's own colour.
One app in detail: the networks it used, named as the collector sees them and "unnamed" until it has, and the separate programs merged into it.
A phone, in its own accent. Near the bottom, where its traffic went, per Wi-Fi network, with tethering kept apart: the laptop has already counted those bytes.
Every number in these images is invented. They are captured from
npm run demo:data, a synthetic history loaded through the real collector and phone code; see Trying it without your own data. No logos ship, so apps show their colour swatch, as they will on a fresh install until you add some.
Task Scheduler, hourly (not elevated)
│
├─ 1. esentutl /vss → shadow-copy the locked SRUDB.dat
│ (the one elevated step, run by its own task)
├─ 2. esentutl /r → replay its journals so it can be read
├─ 3. SrumECmd → parse the copy to CSV
├─ 4. ingest → append only new rows to SQLite
└─ 5. backup() → a consistent single-file copy beside it
│
▼
D:\DataUsage\ ← any drive but C:, so a reset cannot touch it
live\data-usage.db the working database
data-usage.db the backup: restore from this one
│
▼
Next.js dashboard, http://localhost:7843 ← starts at logon, password-gated
The phone half is optional and independent:
Data Usage Reporter (Android app, every 6 hours, on unmetered networks)
│
├─ NetworkStatsManager.querySummary → per-uid traffic buckets
├─ PackageManager → uid → package → app name
└─ POST /api/android/ingest → bearer token, gzipped
│
▼
the same database, in its own tables
The phone app reads Android's public NetworkStatsManager API rather than
parsing adb output, because the raw dump counts every byte that crossed a VPN
twice. It has no dependencies at all.
The reasoning behind all of this, including the traps that shaped it (SRUM's per-interface total row that doubles every naive sum, the journal replay a shadow copy needs, why the Android dedup rule is the opposite of the Windows one), is in docs/DESIGN.md.
- Windows 10 or 11, and Administrator rights once, to register the scheduled tasks. Only the shadow copy runs elevated, from an admin-owned copy of one small script; the collector and the dashboard run as you.
- Node.js 22.16 or newer (developed on Node 24). The database is Node's
built-in
node:sqlite, so there is no native build step. - SrumECmd, downloaded yourself; it is
not bundled or fetched automatically. It needs a .NET runtime; if it will
not start on the one you have, set
DOTNET_ROLL_FORWARD=Major. - A second drive or partition for the database. It must not be on
C:\; the collector refuses.
For the phone (optional):
- Android 8.0 or newer (
minSdk26,targetSdk36). - A JDK and the Android SDK to build the app, and adb to install it.
npm installEdit config/collector.json:
| Key | What it is |
|---|---|
databasePath |
The working database. Not on C:\. |
backupPath |
The consistent backup, next to it. This is what you restore from. |
scratchDir |
A working folder for the ~99 MB snapshot and CSVs mid-run. Keep it outside any cloud-synced folder, or every run uploads your raw history. |
srumECmdDir |
The folder holding SrumECmd.exe. |
deviceLabel |
What the dashboard calls this machine; also its URL (/windows/<slug>). |
splitApp |
Optional. One app, as the dashboard names it (for example "qBittorrent"), to show against everything else on the Overview, for a machine where one app dwarfs the rest. null for none. |
If you want an offsite copy, point a sync client (OneDrive, Google Drive and so
on) at the folder holding backupPath.
Copy .env.example to .env.local and set a password of at
least 12 characters:
DASHBOARD_PASSWORD=choose-something-longThe dashboard shows which programs run on this machine and when, so it is gated, and it fails closed: with no password, or a short one, it refuses every request. Wrong guesses are throttled. Changing the password signs out every device.
By default the dashboard answers only on 127.0.0.1. To reach it from another
device, which the phone app needs, add DASHBOARD_HOST=0.0.0.0 to .env.local
and allow Node.js through Windows Firewall on Private networks only. It is
plain HTTP behind this password, so keep it off networks you do not trust.
Windows' first prompt for Node.js usually allows Public networks too; after
marking your home network Private, run npm run firewall once from an
Administrator shell to block port 7843 on Public networks.
From an Administrator PowerShell, once:
powershell -ExecutionPolicy Bypass -File scripts\register-task.ps1 -RunNowThis registers two tasks and runs a first collection:
- Data Usage Collector: hourly, catching up if the machine was off. It runs unelevated: it parses, ingests, backs up and cleans up.
- Data Usage Snapshot: elevated, on demand, and started only by the
collector. It takes the shadow copy of the locked SRUM database and nothing
else. It runs a copy of
scripts\srum-snapshot.ps1that registration places in a folder only administrators can write, never the repo's copy. After editing that script, re-run this command.
That folder also holds the ~99 MB snapshot between runs, so it is
DataUsageTracker-snapshot at the root of your database's drive, not on C:
(-DeployRoot picks another; it falls back to %ProgramData%\DataUsageTracker
when that drive is not NTFS). An hourly 99 MB copy is about 2.4 GB a day of
writes, which the system drive, often the smallest and fullest, is better
off without.
It also writes both task definitions to scripts\task\ as a record. They
contain your Windows account ID and paths, so they are git-ignored; keep them
with your backups rather than in a public fork.
After this, a collection needs no elevation, from any shell. It is safe to re-run; a second pass over the same window inserts nothing:
powershell -ExecutionPolicy Bypass -File scripts\collector.ps1powershell -ExecutionPolicy Bypass -File scripts\install-autostart.ps1This registers a logon task that starts the dashboard hidden, so http://localhost:7843 is simply there. No admin rights needed. It also registers a small task that notes which Wi-Fi network you are on every 15 minutes, which is how network profiles get their names. To start the dashboard now:
wscript scripts\dashboard-hidden.vbsBuild after changing anything it serves. The launcher builds only when there
is no build at all; otherwise it serves what is on disk, and
logs\dashboard.log warns when that is older than the source:
npm run buildSet a second secret in .env.local. It is separate from the password, so the
dashboard password never lives on a phone:
ANDROID_INGEST_TOKEN=another-long-random-stringMake sure DASHBOARD_HOST=0.0.0.0 is set (step 2), then install the app. The
simplest way is the signed APK attached to the newest
release that
carries one:
adb install -r data-usage-reporter-1.5.apkOr build it yourself:
cd android && .\gradlew.bat assembleDebugadb install -r android\app\build\outputs\apk\debug\app-debug.apkGradle needs a JDK; Android Studio's bundled one works as JAVA_HOME. On
Xiaomi's MIUI, adb install is refused unless Developer options → Install via
USB is on; without that, adb push the APK to the phone and install it from
the file manager.
Android only lets an APK update an installed copy signed with the same key. So switching between the release APK and your own build means uninstalling first, and so does moving from the debug-signed 1.2 to 1.3 or later. Nothing already uploaded is lost, and the app re-sends the phone's history on its first sync, but the reinstalled app reports under a new device id, so the phone shows up twice. After it has synced once, fold the old entry into the new one:
npm run android:mergelists the phones with their ids and app versions;
npm run android:merge -- --from <old id> --into <new id> shows what would
move, and adding --apply backs the database up beside your backup and merges.
If the install still fails after uninstalling ("package conflicts with an
existing package"), a copy is left in another profile such as Private Space,
Dual Apps or a work profile, or the uninstall kept the app's data. With the
phone on USB, adb uninstall com.naimul.datausage removes it for every user.
To sign release builds with a key of your own, create one with keytool -genkeypair and keep it outside the repo, next to a properties file holding
storeFile (relative to that file), storePassword, keyAlias and
keyPassword. Then point android\local.properties at that file:
signing.properties=D\:/path/to/keystore.properties
.\gradlew.bat assembleRelease then writes a signed app-release.apk. Without
that line a release build comes out unsigned, and with it a missing or
unfinished file fails the build. Back the key up: without it, no future
build can update the installed app.
On the phone, open Data Usage Reporter and:
- Enter the dashboard address and the token, then Save and test
connection. Use this PC's LAN address (
ipconfigshows it, for examplehttp://192.168.1.20:7843), notlocalhost, which on a phone is the phone. - Press Grant usage access and switch it on in Settings. Android never prompts for this, and without it the app silently reports only its own traffic.
- Android 9 and older: press Grant phone permission. Those versions read mobile data per SIM, which needs it; the app will not sync without it rather than silently record no mobile data.
- Press Sync now once, then leave it. It syncs every 6 hours on unmetered networks; both are adjustable in the app.
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 when it next runs. Android keeps ~90 days, so nothing is lost while
the PC is off either way; the inbox only stops the waiting.
The inbox cannot read what it holds. The app encrypts each upload to a
key that only the PC has (P-256 ECDH, AES-256-GCM), and mixes your ingest
token into the key derivation, so the inbox cannot forge one either. It sees
an envelope'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. The same function serves the sibling Screen Time Tracker as a separate mailbox, so one free project covers both:
- Create a Supabase project. Run
supabase/migrations/*.sqlin order in its SQL editor, and deploysupabase/functions/inboxwith JWT verification off (the function checks its own tokens):supabase functions deploy inbox --no-verify-jwt. - In
config/collector.json, set"inbox": { "url": "https://<project>.supabase.co/functions/v1/inbox", "keyPath": "D:\\keys\\inbox-key.pem" }. Keep the key offC:\and out of the repo, and back it up: envelopes still in the inbox cannot be opened without it. - Create the key and the PC's own token, then register both tokens:
Run the SQL that the last one prints in the SQL editor. It holds SHA-256 digests only.
npm run inbox -- keygen npm run inbox -- drain-token npm run inbox -- sql
- Rebuild and restart the dashboard. On the phone, at home, press Set up
cloud inbox 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 -- keyprints the same one.
The hourly collector empties the inbox at the end of each run, or run
npm run inbox -- drain yourself. The Sync page marks an upload that came
this way with when it reached the database. An envelope that cannot be opened
stays in the inbox, marked failed; fix the cause, then
npm run inbox -- drain --retry-failed, or press Full resync on the phone.
A free Supabase project pauses after a week with no requests; the phones'
uploads keep it awake, and the Supabase dashboard restores it if not.
Never add a phone's total to the PC's. Tethering (uid -5) is traffic the
phone relayed for other devices; if this PC tethers off the phone, those bytes
are in both. The dashboard keeps tethering out of the phone's totals.
Per-network phone traffic is not available to apps; npm run android:ssid reads
it over USB when you want it.
| Page | Shows |
|---|---|
| Overview | All / 30 days / 7 days / latest day, the daily trend, a six-month heat map, daily usage by app, hour of day, and where it went: Wi-Fi against wired, per network |
| By App | Every app with enough activity for a detail page, expandable to every app seen; click a Top 10 bar to open that app |
| App detail | One app's daily and hourly usage, networks, and which programs were grouped into it |
| Sync Status | Collector run history, with warnings when it has stopped; jump to any page, or straight to the newest or oldest |
| Phone pages | The same views for each phone, plus per-network (SSID) traffic |
The date range lives in the URL, so every view is linkable, for example
/windows/my-pc/apps?days=90. Sync now in the top bar runs a collection on
demand. Any app can be renamed, or given a colour of your choosing (picked, or
typed as a hex code), with the pencil beside its name; both are stored in the
database with the history.
Light or dark. The sun/moon menu in the top bar switches between a dark theme, a light theme, or following your system. It is remembered per browser.
Totals will not match Windows' page exactly, and that is expected. Windows scopes its page to one network; the dashboard counts all of them. Compare Windows' figure with that network's row under "Where it went" on the Overview.
Watch Sync Status. Surviving a reset depends on the collector actually running; that page is where you notice it has stopped.
The dashboard is an installable web app: its own window, taskbar icon and Start-menu entry, with no browser chrome. In Chrome or Edge, open http://localhost:7843 and choose Install in the address bar (or the menu's Install Data Usage).
Installing needs a secure context: localhost, 127.0.0.1, or HTTPS.
Over plain HTTP on the LAN, a phone's browser offers at most a home-screen
shortcut that opens in a tab. To install on an Android phone anyway, add the
dashboard's LAN address (e.g. http://192.168.0.10:7843) to
chrome://flags/#unsafely-treat-insecure-origin-as-secure on that phone and
relaunch Chrome.
The app caches nothing but a "dashboard unreachable" page, shown when the computer running it is asleep or off. Your usage data is never stored in the browser.
| Report from the command line | npm run stats (add -- --profile <id> to scope) |
| Development server | npm run dev |
| Stop the dashboard | Double-click stop-dashboard.bat, or powershell -ExecutionPolicy Bypass -File scripts\dashboard-stop.ps1 |
| Turn off autostart | powershell -ExecutionPolicy Bypass -File scripts\install-autostart.ps1 -Remove |
| Remove the collector tasks | powershell -ExecutionPolicy Bypass -File scripts\register-task.ps1 -Unregister (Administrator) |
| Self-test (no admin, no real data) | npm run selftest |
| Typecheck | npm run typecheck |
| Rehearse a reset recovery | npm run drill (add -- --full to also test npm ci) |
| Logs | logs\collector-*.log, logs\dashboard.log |
npm run demo:dataWrites a synthetic history to ./demo-data (git-ignored): 100 days of an
invented PC and 90 of an invented phone. It is not a mock. The PC's history is
written as SrumECmd CSVs and loaded by the real ingest.ts, one simulated
collector run at a time, and the phone's goes through the real ingest code, so
every number is computed the way yours would be. It reproduces the awkward
parts of real data on purpose: SRUM's per-interface total rows, apps split
across versioned install paths, overlapping collector runs, networks named by
observation, and tethering that both devices count.
To browse it, start a second dashboard pointed at it. DATA_USAGE_CONFIG is
read by the dashboard only; your own database and collector are untouched.
npm run build
$env:DATA_USAGE_CONFIG = "$PWD\demo-data\collector.json"
$env:NEXT_TELEMETRY_DISABLED = '1'
npx next start -H 127.0.0.1 -p 7899Sign in with your usual password, then open http://127.0.0.1:7899/windows/my-pc.
The screenshots above come from that data, and are regenerated rather than edited by hand:
npm run demo:shotsIt starts its own dashboard on the demo data with a one-off password, captures
four whole pages, top to bottom, in the dark theme and again in the light one
(*-light.webp), in headless Chrome or Edge into docs/screenshots/, and
stops it. Both sets come from one run, so they show the same numbers.
It needs a current build, and it refuses to capture any page that lists a
device other than the demo's two. That way a build that ignored
DATA_USAGE_CONFIG cannot put your own data into an image.
A reset erases C:\: the repo, Node, SrumECmd, SRUM itself and the scheduled
tasks. Your database drive survives. Rehearse the whole recovery at any time,
without resetting anything, with npm run drill.
-
Check your database folder is intact. If you synced it offsite, let it sync back down first.
-
Clone this repo anywhere, then
npm install, and restoreconfig/collector.jsonif you had changed it. -
Restore the database from the backup:
npm run restore
This copies the backup over the working database, never the reverse, moving any existing database aside and clearing stale
-wal/-shmfiles first. -
Recreate
.env.local. If you use the phone app, restoreANDROID_INGEST_TOKENto its old value; the phone has the old one, and otherwise stops syncing without saying so here. -
Re-register the tasks, the collector pair from an Administrator PowerShell, the dashboard from a normal one. Re-run the scripts rather than importing the saved XML: that XML names the old install's account, and the snapshot script has to be redeployed.
powershell -ExecutionPolicy Bypass -File scripts\register-task.ps1 -RunNowpowershell -ExecutionPolicy Bypass -File scripts\install-autostart.ps1 -
Check with
npm run stats, then open http://localhost:7843.
SRUM starts empty on a fresh install. Your history comes from the restored database, and the collector appends to it from there.
- What it exposes: the dashboard serves your per-app usage history. It
listens on
127.0.0.1unless you setDASHBOARD_HOST, and every page and API route is behind the password. There are no default credentials. - Privileges: only the shadow-copy step runs as Administrator, from a folder only administrators can write, and it checks that folder every run. See docs/DESIGN.md.
- Data never leaves the machine, except to your phone's own dashboard
address and any sync client you point at the backup yourself. Next.js
telemetry is disabled. 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
inboxis set in config. - Nothing collected is ever committed:
.gitignoreblocks databases, snapshots and CSVs.
To report a vulnerability, see SECURITY.md.
Issues and pull requests are welcome; see CONTRIBUTING.md. Please do not include your own usage data, device names or network names in reports.