No description
  • Python 79.7%
  • Go 19.8%
  • PowerShell 0.4%
  • Shell 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-06-09 11:38:15 +00:00
cmd/collector Add device status heartbeat 2026-05-19 07:20:04 +00:00
config Add offline status cache 2026-05-19 10:07:24 +00:00
dist Add collector log rotation 2026-05-19 10:17:52 +00:00
internal/collector Add collector log rotation 2026-05-19 10:17:52 +00:00
scripts Replace collector with native Go service 2026-05-18 13:31:02 +00:00
sink Preserve filters and cache timeline days 2026-06-09 11:38:15 +00:00
.dockerignore Run sink with Docker Compose 2026-05-18 13:38:44 +00:00
.gitignore Add offline status cache 2026-05-19 10:07:24 +00:00
compose.yaml Display visualizations in Perth time 2026-05-26 03:49:19 +00:00
Dockerfile.sink Run sink with Docker Compose 2026-05-18 13:38:44 +00:00
go.mod Replace collector with native Go service 2026-05-18 13:31:02 +00:00
go.sum Replace collector with native Go service 2026-05-18 13:31:02 +00:00
README.md Preserve filters and cache timeline days 2026-06-09 11:38:15 +00:00

ActivityWatch Simple Sync

Tiny ActivityWatch collector and SQLite sink.

This is intentionally smaller than aw-sync-suite: no Prometheus, no Grafana, no auth layer, and no plugin system. It assumes collectors can reach the sink over a private VPN.

Shape

  • cmd/collector builds a standalone Go collector for Windows.
  • The collector can install, start, stop, and remove itself as a native Windows service. No Python and no NSSM are needed on collector PCs.
  • sink/aw_sink.py runs on one VPN-reachable machine and stores events in SQLite.
  • The sink includes a D3.js data visualisation website with analysis, timeline, and classification pages.
  • The collector keeps checkpoints in memory while running. On restart, it asks the sink for the latest stored timestamp for each bucket. If the sink has no checkpoint yet, the collector uses the first-sync lookback window.
  • The collector also sends a status heartbeat so the sink can show whether the computer is online, whether ActivityWatch appears healthy, and basic Windows power/session state.

Sink

Run this on the machine that will hold the SQLite database:

mkdir -p data
docker compose up -d --build

Then open the analysis page:

http://localhost:8720/analysis

The timeline page is available at:

http://localhost:8720/timeline

Classification rules are available at:

http://localhost:8720/classify

Backup and restore controls are available at:

http://localhost:8720/admin

If this server is reachable over a VPN, collectors can post to:

http://<vpn-ip-or-dns-name>:8720

Do not expose the sink directly to the public internet. It has no authentication by design.

The SQLite database is stored on the host at:

data/activitywatch.sqlite3

The admin page includes backup and restore controls. Download creates a consistent SQLite snapshot of the live database. Restore accepts a previously downloaded SQLite backup file and replaces the current sink database after validating the expected tables.

Useful sink commands:

docker compose logs -f sink
docker compose restart sink
docker compose down

If Docker reports sqlite3.OperationalError: unable to open database file, the host data/ folder is not writable by the container. Recreate the folder and restart the sink:

docker compose down
mkdir -p data
docker compose up -d --build

If you do not want Docker, the sink can still run directly with Python:

python3 -m sink.aw_sink --host 0.0.0.0 --port 8720 --db ./data/activitywatch.sqlite3

Build Collector

From Linux, build a Windows collector executable:

./scripts/build-collector.sh

This creates:

dist/aw-simple-sync-collector.exe

Equivalent manual command:

GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o dist/aw-simple-sync-collector.exe ./cmd/collector

Install Collector

Copy aw-simple-sync-collector.exe to the Windows PC, then run PowerShell as Administrator:

.\aw-simple-sync-collector.exe install --sink-url "http://<vpn-ip-or-dns-name>:8720"

The installer copies the executable to:

C:\ProgramData\AwSimpleSync\aw-simple-sync-collector.exe

It writes config and logs under:

C:\ProgramData\AwSimpleSync

The active service log is:

C:\ProgramData\AwSimpleSync\logs\collector.log

The collector rotates this file daily into dated archives like collector-2026-05-19.log and removes archives older than 30 days.

To watch the log:

Get-Content "$env:ProgramData\AwSimpleSync\logs\collector.log" -Tail 100 -Wait

Useful commands:

.\aw-simple-sync-collector.exe status
.\aw-simple-sync-collector.exe stop
.\aw-simple-sync-collector.exe start
.\aw-simple-sync-collector.exe uninstall

Optional wrapper:

.\scripts\install-collector-service.ps1 -SinkUrl "http://<vpn-ip-or-dns-name>:8720" -CollectorExe ".\aw-simple-sync-collector.exe"

Collector Config

The collector config is an INI file:

[collector]
device_id =
activitywatch_url = http://localhost:5600
sink_url = http://127.0.0.1:8720
interval_seconds = 300
lookback_days_first_sync = 90
batch_size = 500
include_buckets =
exclude_buckets =
activitywatch_process_names = aw-server.exe,aw-server-rust.exe,aw-qt.exe,aw-watcher-window.exe,aw-watcher-afk.exe
status_cache_path =
max_status_cache_records = 10000

Leave device_id blank to use the Windows computer name. include_buckets and exclude_buckets are comma-separated ActivityWatch bucket IDs. activitywatch_process_names is the comma-separated process list used for the ActivityWatch process health check. Leave status_cache_path blank to use the default status-cache.jsonl file under C:\ProgramData\AwSimpleSync. max_status_cache_records caps the local offline status cache; at the default five-minute interval, 10,000 records is roughly 34 days of heartbeats.

There is no local checkpoint file. The sink database is the persistent checkpoint source, and the collector only keeps a runtime cache to avoid repeated checkpoint lookups while the service is running.

Device Status

Every sync interval, the collector posts a status heartbeat to:

POST /api/status

The sink stores both the latest status and a status history. The dashboard shows the latest status for each device and the recent status history, including:

  • whether the collector has recently checked in, which means the computer is powered on and able to reach the sink
  • whether the ActivityWatch API is reachable
  • whether common ActivityWatch processes are running
  • Windows lock/unlock state when the collector is running as a service and receives session-change events
  • AC/battery power source, battery percentage, and uptime when available
  • lid state as unknown

If the sink is unavailable, the collector writes that status heartbeat to the local status cache and retries it later. Cached heartbeats are replayed in order before the next live heartbeat after the VPN or sink comes back.

The same data is available as JSON:

GET /api/analysis
GET /api/analysis/events
GET /api/timeline
GET /api/classifications
POST /api/classifications
GET /api/time-filters
POST /api/time-filters
GET /api/chart-highlights
POST /api/chart-highlights
GET /api/events
GET /api/backup
POST /api/restore
GET /api/status
GET /api/status/history?limit=50
GET /api/status/history?device=<device-id>&limit=200

The web visualisations use D3.js in the browser. The pages load D3 from the jsDelivr CDN, so the browser that opens the dashboard needs access to that CDN unless D3 is vendored locally later.

Event timestamps are stored exactly as received from ActivityWatch, normally in UTC. Analysis, timeline, and classification pages group and display events in the configured display timezone. Docker Compose sets this to Australia/Perth with AW_SIMPLE_SYNC_TIMEZONE; change that environment variable if the display timezone needs to move.

The analysis charts follow the ActivityWatch summary model: they aggregate aw-watcher-window activity clipped to aw-watcher-afk not-afk periods. Raw AFK bucket durations are not included in the configurable usage summaries or the timeline barchart.

The analysis page includes date-range presets such as last day, last 7 days, last 30 days, this week, previous week, this month, and previous month. This week and previous week cover full Monday-Sunday weeks. Analysis, timeline, and classification filters are opened from the right-side header summary button and can filter by named hours filters, custom start/end times, days of the week, application, exact window title or URL, category, device, bucket, and broad search text. Hours filters clip event durations to the selected local-time window and weekdays before chart totals are calculated. The admin page lets you edit the names, start and end times, display order, and days of the week for named hours filters such as school hours.

Filter state is carried between analysis, timeline, and classification pages in server-rendered query strings. When a date range is active, the timeline page shows the days loaded for that range and redraws from an in-page cache when a day has already been fetched; it calls /api/timeline only for days missing from that cache.

The /highlights page manages chart-only highlight ranges. Each highlight has a name, start and end time, colour, display order, and days of the week. Hourly charts, including the analysis active-hours grid, analysis hourly histogram, timeline chart, and timeline hourly activity chart, tint matching time spans without changing the underlying totals.

The analysis header has a global aggregation selector for total, category, application, window title, or browser domain. Aggregation-aware analysis charts use that single selection together and redraw in place when it changes. The analysis page loads semi-raw filtered events from /api/analysis/events and uses D3 rollups in the browser to build the chart series. Date ranges are loaded as day-sized requests, with a top progress bar showing loaded days against the total days that need to be requested. Duration axes use shared tick intervals with a minimum 15-minute step, scaling to 30 minutes, 1 hour, 2 hours, and larger steps as totals grow. Category groups use school, games, internet, music, media, and other. The /classify page lets you add case-insensitive app or window-title matching rules that override the built-in fallback patterns. The timeline page shows AFK periods as pale red bands.

Windows does not expose a simple, reliable one-shot lid-open/lid-closed query through the service APIs used here. On a Lenovo Yoga the physical posture can also be ambiguous, so the collector deliberately reports lid state as unknown until a reliable device-specific signal is added.

Run Without Installing

For testing on any platform with Go:

go run ./cmd/collector init-config --config ./collector.ini --sink-url http://127.0.0.1:8720
go run ./cmd/collector once --config ./collector.ini
go run ./cmd/collector run --config ./collector.ini

Offline Behavior

If the collector cannot reach the sink over the VPN, the sync attempt fails, nothing is marked as sent, and the service tries again at the next interval. It does not need public internet access; it only needs local ActivityWatch and the VPN route to the sink.

When connectivity returns, the collector asks the sink for the latest timestamp already stored for each bucket and then fetches newer events from the local ActivityWatch database. If the sink has never received that bucket before, the collector falls back to lookback_days_first_sync. The default is 90 days; set it higher if a machine might be offline before its first successful sync for longer than that.

There is no separate local upload queue. ActivityWatch itself is the local source of truth while offline, and SQLite deduplication on the sink makes repeated uploads safe.

Status heartbeats are different: ActivityWatch does not store collector health, Windows lock state, battery state, or sync errors. Those snapshots are cached in status-cache.jsonl while the sink cannot be reached, then replayed to status_history when connectivity returns. The cache is intentionally bounded by max_status_cache_records; if a machine is offline beyond that window, the newest status samples are kept.

Notes From aw-sync-suite

The reference agent uses the ActivityWatch REST API, tracks checkpoints, chunks uploads, and can install as a service. This project keeps those simple, useful pieces and drops the heavier parts:

  • Prometheus remote-write is replaced with POST /api/events.
  • Grafana is replaced with the built-in SQLite web viewer.
  • Plugins and bearer-token proxying are omitted because VPN access is assumed.
  • The collector is a native Windows service executable.

Development Checks

go test ./...
python3 -m py_compile sink/aw_sink.py