Skip to content

Repository files navigation

WhatsApp Task Tracker

Reads one WhatsApp Web thread on a timer, extracts action items with a local Ollama model, and files them as tasks in a Notion database. Runs unattended as a Windows Scheduled Task. No cloud services beyond Notion, no API keys for WhatsApp, nothing leaves the machine except the finished task.

Setup

  1. Create the virtual environment and install dependencies:

    & "C:\Users\Arin\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe" -m venv ".venv"
    .\.venv\Scripts\python.exe -m pip install -r requirements.txt
    .\.venv\Scripts\python.exe -m playwright install chromium
  2. Copy config.example.json to config.json and fill it in.

  3. Provide the Notion token. It is resolved in this order:

    1. NOTION_TOKEN environment variable
    2. a file, by default %LOCALAPPDATA%\whatsapp_task_tracker\notion_token.txt (override with notion.token_file)
    3. notion.token in config.json

    Use one of the first two. This folder sits inside a cloud-synced vault, so a token written into config.json is replicated to the cloud (see Security).

  4. Log in to WhatsApp Web once. This writes a persistent browser profile that subsequent headless runs reuse:

    .\.venv\Scripts\python.exe tracker.py login
  5. Verify without writing to Notion:

    .\.venv\Scripts\python.exe tracker.py run-once --headed --dry-run
  6. Run the persistent loop (this is what the Scheduled Task invokes):

    .\.venv\Scripts\pythonw.exe tracker.py loop

Configuration

Key Default What it does
whatsapp.phone — Thread to watch, digits only, country code included
whatsapp.poll_minutes 25 Gap between read cycles
whatsapp.active_hours 08:00–23:00 Cycles only run inside this window. A window that wraps past midnight (22:00–06:00) is supported
whatsapp.first_read_timeout_ms 900000 Budget for the first read after opening a browser. WhatsApp Web resyncs chat history on every fresh connection and that takes minutes, not seconds
whatsapp.read_timeout_ms 60000 Budget for steady-state reads once the session is warm
whatsapp.reload_after_minutes 60 Reload the page if the process heartbeat is older than this, i.e. the machine slept
whatsapp.reopen_after_failures 3 Consecutive unclassified failures before forcing a fresh browser
whatsapp.alert_after_hours 6 Hours without a successful read before the tracker files a Notion task about its own outage
whatsapp.profile_dir %LOCALAPPDATA%\whatsapp_task_tracker\browser-profile Chromium profile location. Keep it off any cloud-synced path
ollama.timeout_seconds 180 Extraction request timeout

How it survives things going wrong

  • Cold-start resync. A fresh browser cannot render any message until WhatsApp finishes downloading history. That state is detected explicitly and reported as syncing, not as an error, and it keeps the same browser and the long timeout rather than restarting the sync from zero.
  • Reload loops. Page reloads key off a heartbeat written every cycle, not off the last successful run. Keying it off success meant a run of failures forced a reload every cycle, restarting the multi-minute resync before it could ever complete — a failure that sustained itself indefinitely.
  • Dead browser. Playwright reports a dead page through several different message shapes. Any of them raise BrowserGone, which reopens the browser instead of polling a page that no longer exists. A consecutive-failure counter forces the same reopen for anything that could not be classified.
  • Duplicate tasks. Seen messages are tracked as a bounded set of message keys, not a single checkpoint pointer. WhatsApp virtualises the message list, so a single checkpoint message is regularly absent from the DOM; the old fallback re-read the last 20 messages and re-filed tasks it had already filed.
  • Lost tasks. A failed Notion write is queued to state/pending_tasks.json and retried at the start of every later cycle, because by the time it fails the source messages are already marked as processed.
  • Two instances at once. An OS-level file lock stops a second process sharing the Chromium profile directory, which otherwise corrupts the profile and takes the WhatsApp session with it. The lock releases automatically if the process is killed, so there is no stale lock to clear by hand.
  • Corrupt state. Every state file is written to a temp file and then replaced, so a crash mid-write leaves the previous version intact. Unreadable state files fall back to a default instead of raising.
  • Console encoding. Message text contains emoji and Devanagari; Windows consoles default to cp1252 and reject both. All console output goes through a helper that cannot raise, and stdout is reconfigured to UTF-8 at startup.
  • Silent death. This is the one failure the code cannot fix by itself, so it reports instead: after alert_after_hours without a successful read, it files a Notion task saying it is down. It already holds Notion credentials, and that board is somewhere actually read, unlike a local log. One task per outage, cleared on the next success. Added because the tracker failed silently for four days and nothing surfaced it.
  • A model that misfires. Extraction output is capped at 20 tasks per cycle, so a confused or prompt-injected response cannot flood the database.
  • A model that was never pulled. Checked against /api/tags and reported by name, rather than surfacing as a bare HTTP 404 from Ollama.
  • Disk growth. tracker.log rotates at 2 MB, keeping one backup. History is capped at 60 entries.

Operations

# status at a glance
.\.venv\Scripts\python.exe dashboard.py     # http://127.0.0.1:8765

# tests (no framework, no network)
.\.venv\Scripts\python.exe test_tracker.py
.\.venv\Scripts\python.exe test_dashboard_escaping.py

# scheduled task
Get-ScheduledTask -TaskName 'WhatsApp Task Tracker'
Start-ScheduledTask -TaskName 'WhatsApp Task Tracker'
Stop-ScheduledTask  -TaskName 'WhatsApp Task Tracker'

Stop the Scheduled Task before running login or run-once, or the instance lock will refuse to start a second browser against the same profile.

Security

This repository is public. config.json and state/ are gitignored and have never been committed; no token appears anywhere in git history.

Two things are deliberately kept outside this folder, because it sits inside a cloud-synced Obsidian vault and everything in it replicates to the cloud:

  • The Chromium profile (%LOCALAPPDATA%\whatsapp_task_tracker\browser-profile). It holds an authenticated WhatsApp session, which is a credential. A sync client writing to a live browser profile also corrupts it, so this is a reliability fix as much as a security one.
  • The Notion token (%LOCALAPPDATA%\whatsapp_task_tracker\notion_token.txt, or NOTION_TOKEN in the environment). notion.token in config.json still works but should be left empty.

state/ does still live in the synced folder and pending_tasks.json can hold message-derived text, so it is chat content going to the cloud. Lower stakes than a credential, but worth knowing. Moving it would break the companion check-in assistant that reads state/last_run.txt.

  • Message text is treated strictly as data. The extraction prompt says so explicitly, model output is validated before use, and the dashboard escapes stored log content before rendering it.

Files

Path Purpose
tracker.py Read, extract, file. All three commands live here
dashboard.py Read-only status page bound to localhost
config.json Local secrets and runtime config, gitignored
state/processed.json Bounded set of already-handled message keys
state/pending_tasks.json Notion writes awaiting retry
state/heartbeat.txt Written every cycle; drives reload decisions
state/last_run.txt Written on a successful read only
state/status.json Current status snapshot for the dashboard
state/outage_alert.txt Present while an outage has already been reported to Notion
state/tracker.log Rotating run log
test_tracker.py Self-checks for the logic whose failures are silent

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages