Renders a small ring icon with the percentage, updates every 10 minutes, and has an optional "live" mode that keeps a Chromium instance open and reads the DOM every 15 seconds.
- **`scraper.ts`** — Playwright + Chromium. On Linux, it uses Xvfb (a virtual display) to run the browser "headed" (required to pass Cloudflare) without showing any windows. The session is persisted to `session/state.json` so you only need to log in once.
- **`tray.py`** — GTK3 + AyatanaAppIndicator3. Draws the icon with Cairo, calls the scraper via `bun`, and renders the result as a circular progress ring (green / yellow / red by percent).
The tray indicator depends on GTK3 + AyatanaAppIndicator3, which is Linux-only. Porting it to Windows or macOS would require swapping in something like [`pystray`](https://github.com/moses-palmer/pystray) (cross-platform tray) or [`rumps`](https://github.com/jaredks/rumps) (macOS).
The scraper itself is portable — it's just Bun + Playwright. The only Linux-specific part is the Xvfb wrapper used to hide the browser window during automated polling.
---
## Linux
### Requirements
- **bun** — installed at `~/.bun/bin/bun` (the path is hard-coded in `tray.py`; edit if yours differs)
- **Python 3** with `PyGObject`, `AyatanaAppIndicator3`, `cairo`
Two pieces in `scraper.ts` reference Linux-only Xvfb. On Windows, replace each `check()` and `live()` Xvfb spawn block with a plain Chromium launch (drop the `Xvfb` spawn and the `env: { ..., DISPLAY: display }` part). The browser window will be visible while it's running — that's the trade-off without Xvfb. If you want it hidden, run Chromium with `headless: true` and accept that Cloudflare may block you intermittently.
A simpler alternative: only ever invoke `--login` and `--json` (one-shot calls), and accept that a browser window flashes up briefly each time.
To poll on a schedule, set up a Task Scheduler job that runs the `--json` command and pipes the output somewhere useful (a log file, a notification, a Streamdeck button — your choice).
A proper Windows tray port would mean rewriting `tray.py` against `pystray` + `Pillow`, which isn't done here.
---
## macOS
Same story as Windows: the scraper works (no Xvfb needed — just remove the Xvfb spawn), the tray does not. Use `rumps` if you want a macOS menubar version.