Skip to content

Repository files navigation

Pulse — local-first attention analytics

Pulse

Local-first attention analytics for macOS, Safari, and Chrome.

Turn private ActivityWatch events into an explainable, portfolio-ready dashboard—without publishing raw URLs, window titles, messages, or documents.


Why I built this

Most screen-time tools answer how long was the laptop used? They do not answer the product questions behind digital attention:

  • How often did I switch applications, windows, and browser contexts?
  • Was active time concentrated in focused work or fragmented across many surfaces?
  • What categories were selected browser tabs about?
  • How reliable is the underlying tracking signal?
  • Can the analysis remain useful without sending a detailed behavioral history to the cloud?

Pulse began as a native Swift experiment and evolved into an analytics layer on top of ActivityWatch, an open-source, local-first activity tracker. The project demonstrates a practical build-versus-integrate decision: reuse mature collection infrastructure, then invest engineering effort in reconciliation, privacy, categorization, and product-grade presentation.

What the project adds

ActivityWatch already provides foreground-window and AFK watchers. Pulse adds:

  • AFK-aware interval reconciliation so foreground time is not inflated;
  • context-switch metrics across applications and window contexts;
  • Safari and Chrome enrichment through the ActivityWatch web watcher;
  • explainable browser categorization using deterministic local rules;
  • data-quality checks that prevent browser heartbeats from being double-counted as screen time;
  • privacy-safe export with no raw URLs, page titles, window titles, or message/document names;
  • a responsive React dashboard that runs locally and deploys to GitHub Pages;
  • an optional native macOS prototype for sleep, wake, lock, app-lifecycle, and clamshell research.

Architecture

flowchart LR
    subgraph macOS[macOS collection]
        W[Window watcher]
        A[AFK watcher]
        S[Optional Swift system monitor]
    end

    subgraph browser[Browser enrichment]
        C[Chrome extension]
        F[Safari Web Extension]
    end

    W --> DB[(Local ActivityWatch SQLite)]
    A --> DB
    C --> DB
    F --> DB
    DB --> E[Read-only Python exporter]
    E --> R[Interval reconciliation]
    R --> P[Privacy + categorization]
    P --> J[Aggregate JSON]
    J --> UI[React dashboard]
    S -. product prototype .-> L[(Local JSONL)]
Loading

The React application never receives the ActivityWatch database. It reads a small aggregate JSON contract. Browser category time stays separate from foreground application time unless the underlying sources can be reconciled.

See Architecture and metric contract for calculation details.

Dashboard

Quick demo

If you are reviewing this project, start with the hosted dashboard rather than installing the collector. It opens a sanitized, read-only snapshot of the latest aggregate available in this repository (currently eight days, Aug 15–22). It contains no raw URLs, page titles, window titles, messages, or documents.

The demo is intentionally static: it shows the product experience and metric definitions without asking a visitor to grant macOS permissions or run ActivityWatch. A local installation can replace the demo JSON with a private export from the user's own Mac.

Before publishing changes

Treat a green build as necessary but not sufficient. Before sharing a new deployment, run npm test, open the published URL, confirm the page returns successfully, and verify that the visible dashboard content and sanitized demo dataset load. If the hosted route is blank or returns 404, do not share the URL—check the deployment logs and redeploy only after the page is verified.

The default GitHub build uses a sanitized aggregate snapshot. A local export automatically replaces it during development.

Dashboard sections include:

  • active time, focus share, longest block, and switch rate;
  • daily active-versus-focused trends;
  • foreground application mix;
  • separate Safari and Chrome category distributions;
  • open-tab, observed-URL, and selected-tab-change signals;
  • locked time, AFK transitions, and application switching;
  • data freshness, privacy status, and source-reconciliation warnings.

Browser metrics describe selected-tab signals that overlap non-AFK activity. They are not added to total foreground time, and they are not a complete inventory of untouched background tabs.

Quick start

1. Install dependencies

Requirements: macOS, Python 3.11+, Node.js 20+, and ActivityWatch.

npm --prefix dashboard install

2. Generate a local aggregate

With ActivityWatch running:

npm run collect

This reads:

~/Library/Application Support/activitywatch/aw-server-rust/sqlite.db

and writes:

dashboard/local-data/activity-summary.local.json

The generated file is ignored by Git.

To schedule the privacy-safe export every Monday at 7:00 PM local time on macOS:

npm run schedule:weekly

This installs a per-user launchd job at ~/Library/LaunchAgents/com.pulse.activity-export.plist. It reads ActivityWatch locally and refreshes only the ignored aggregate JSON. Disable it with:

npm run unschedule:weekly

Pulse reads the aggregate when the dashboard page opens; reload the page after a scheduled export to see the new data.

To use another database or output path:

python3 scripts/export_activitywatch.py \
  --db "/path/to/sqlite.db" \
  --output "/path/to/summary.json"

3. Run the dashboard

npm run dev

Open the local URL printed by Vite. If no local aggregate exists, the application falls back to the committed sanitized demo.

4. Build the production site

npm run build

The static output is created in dashboard/dist/ and is compatible with GitHub Pages.

ActivityWatch setup on macOS

Install and start ActivityWatch

  1. Download the current macOS release from the ActivityWatch getting-started guide.
  2. Move the application to /Applications and launch it.
  3. Add ActivityWatch to System Settings → General → Login Items if the release does not configure autostart.
  4. Confirm these local buckets exist in ActivityWatch:
    • aw-watcher-window-*
    • aw-watcher-afk-*

ActivityWatch's bundled watchers track the foreground window and AFK state; its data remains stored locally by default. The upstream project and watcher model are documented in the ActivityWatch repository.

macOS permission

Allow ActivityWatch under System Settings → Privacy & Security → Accessibility if window context is missing. This permission allows the window watcher to read the active application/window metadata; it does not give Pulse page contents or keystrokes.

Chrome setup

Use the official ActivityWatch Web Watcher in the Chrome Web Store.

  1. Install the extension.
  2. Keep ActivityWatch running locally.
  3. Open chrome://extensions, select ActivityWatch Web Watcher, and verify it is enabled.
  4. Leave Allow in Incognito disabled.
  5. Open a normal tab and confirm an aw-watcher-web-chrome-* bucket appears in ActivityWatch.

The watcher records current-tab metadata—not page contents—and sends events to the local ActivityWatch server.

Safari setup

Safari does not currently have the same one-click store path, so this project uses the upstream ActivityWatch browser-watcher source and Apple's Safari Web Extension converter.

Requirements: full Xcode, an Apple development signing identity, Node.js, Git, and Make.

git clone --recurse-submodules https://github.com/activitywatch/aw-watcher-web.git
cd aw-watcher-web
make install
make build-safari
xcrun safari-web-extension-converter ./build

Then:

  1. In Xcode, select the macOS target, sign it, build, and run it.
  2. Open Safari → Settings → Extensions and enable aw-watcher-web.
  3. Keep access to Private Browsing disabled.
  4. Confirm an aw-watcher-web-safari-* bucket appears in ActivityWatch.

Allow the Safari extension's local origin

Safari Web Extensions use a generated safari-web-extension://… origin. If ActivityWatch rejects the request, add the exact extension origin to the Rust server configuration:

# ~/Library/Application Support/activitywatch/aw-server-rust/config.toml
cors_regex = ["^safari-web-extension://<your-extension-uuid>$"]

Restart ActivityWatch after editing the file. Keep this rule narrow: allow the generated Safari extension origin, not arbitrary origins.

Permissions and data access

Component Permission/data Purpose
ActivityWatch window watcher macOS Accessibility Active app and window context
ActivityWatch AFK watcher Local input-idle signal Exclude inactive time
Chrome web watcher tabs, local ActivityWatch host Selected-tab title/URL metadata
Safari web watcher Safari extension access, local ActivityWatch origin Selected-tab title/URL metadata
Pulse exporter Read-only access to local SQLite file Produce aggregate dashboard JSON
React dashboard Aggregate JSON only Visualize sanitized metrics

No component in this repository captures screenshots, keystrokes, message contents, or page bodies.

Privacy by default

Raw behavioral data is highly sensitive. The exporter therefore omits:

  • URLs, domains, search queries, and page titles;
  • window, document, and conversation titles;
  • email addresses and message recipients;
  • browser tab IDs and raw event payloads.

The repository ignores ActivityWatch databases, SQLite sidecars, JSONL logs, local dashboard exports, and the local Safari build checkout. Read the full privacy model before making a fork public.

Metric definitions

Metric Definition
Active time Foreground-window duration intersected with not-afk intervals, excluding loginwindow
Application switch Change in consecutive active foreground application names
Context switch Change in consecutive active (application, window title) contexts
Focused work Active time in the explainable AI/engineering application categories
Browser category share Current-tab event duration overlapping any non-AFK interval, grouped locally
Distinct observed URL Unique URL after fragment removal; not a count of current tabs
Locked time Active-overlap time attributed to loginwindow, shown separately

Engineering decisions and trade-offs

Build versus integrate

The native Swift prototype proved that macOS exposes many useful system signals. ActivityWatch was selected as the primary event backbone because it already offered mature AFK/window watchers, local storage, APIs, and a browser ecosystem. Engineering effort shifted toward differentiated product value rather than rebuilding a watcher platform.

Explainable categorization over black-box classification

Browser categorization uses deterministic URL/title rules. Rules are easy to inspect, test, and customize, and the public export contains only the resulting category. A future local model could handle unmatched sites, but should not require sending browsing history to a hosted API.

Do not force conflicting sources to agree

Browser watchers may keep a current-tab heartbeat alive while another application is foreground. Pulse surfaces that disagreement and keeps browser metrics separate instead of presenting false precision or double-counted totals.

Demo-first publishing

GitHub Pages deploys the sanitized demo. Local development automatically prefers the ignored local aggregate. This creates a safe default while preserving a real working pipeline.

Testing

# Python interval and categorization tests + React production build
npm run test

# Optional native macOS collector tests
npm run test:swift

GitHub Actions validates the Python analytics layer, TypeScript production build, and Swift package. A separate workflow deploys dashboard/dist to GitHub Pages.

Project structure

.
├── dashboard/                  React + TypeScript dashboard
│   ├── public/data/            sanitized public demo
│   ├── local-data/             ignored local aggregate, dev-only
│   └── src/                    components, charts, and data contract
├── scripts/
│   ├── export_activitywatch.py privacy-safe ActivityWatch exporter
│   └── package-app.sh          native macOS app bundler
├── Sources/AttentionTracker/   optional Swift 6 collector prototype
├── BrowserExtension/           optional localhost tab-lifecycle bridge
├── tests/                      Python interval/categorization tests
├── Tests/                      Swift summary tests
└── docs/                       architecture, metrics, and privacy

Product requirements

See docs/PRD.md for the product brief, user problem, goals, metric definitions, privacy requirements, roadmap, risks, and release checklist.

GitHub Pages

After pushing to GitHub:

  1. Open Repository Settings → Pages.
  2. Set the source to GitHub Actions.
  3. Run Deploy dashboard to GitHub Pages, or push a dashboard change to main.

The deployment contains only the sanitized demo unless you intentionally replace it. Do not commit activity-summary.local.json.

Product roadmap

  • configurable category rules and a safe local review UI;
  • weekly trend baselines and change detection;
  • debounced context-switch metrics and focus-session distributions;
  • optional launch-at-login packaging for the native collector;
  • lid/sleep/session events merged into the aggregate contract;
  • local-only tab inventory for untouched-tab cleanup;
  • encrypted local retention controls;
  • one-command release packaging.

Portfolio / resume framing

Suggested resume bullet:

Built a local-first attention analytics product on ActivityWatch using Python, SQLite interval reconciliation, Swift/macOS system signals, Chrome/Safari extensions, and a React dashboard; designed a privacy-safe aggregation layer that categorizes browser behavior without publishing raw URLs or window titles.

The product-management story is broader than the stack: identify an unmet user need, prototype the hard platform constraints, choose where to integrate versus build, define trustworthy metrics, expose data-quality limitations, and ship a usable privacy-first experience.

Attribution

Pulse is an independent project built on top of ActivityWatch and is not affiliated with or endorsed by the ActivityWatch maintainers. ActivityWatch and aw-watcher-web are distributed under their upstream licenses. This repository's original Pulse code is available under the MIT License.

About

A local-first macOS attention analytics dashboard built on ActivityWatch, with Safari and Chrome tab enrichment.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages