# pr-monitor Single-user dashboard for tracking PRs across multiple GitHub repos: CI checks segmented into lint / specs / e2e / other (every check visible, with per-chip dropdowns), review state derived from review-thread resolution, and a live unified timeline of all comments and reviews with persistent read/unread state. ## How it works - Node/TypeScript server polls the GitHub GraphQL API every 60s using a two-tier query: a cheap `search()` list query whose fields double as change signals (PR `updatedAt` does **not** change when checks or thread resolution change), then batched detail queries only for changed PRs. - State is cached in SQLite (`data/pr-monitor.sqlite`, `node:sqlite`) so the UI loads instantly on restart; timeline events and read/unread survive restarts. - The browser gets a `sync` event over SSE each poll and refetches via TanStack Query. React 19 + Vite SPA, light/dark/system theme, responsive (two-column desktop, tabbed mobile). ## Config `config.json` — repos, the `involves:` filter flag, poll interval, port, and per-repo check-name → category mappings (exact names or `*` globs, e.g. `"Preview E2E *"`). Unmatched checks land in `other`; nothing is ever hidden. Auth: `GITHUB_TOKEN` env var, falling back to `gh auth token`. ## Commands ```sh pnpm dev # server on :4000 (tsx watch) pnpm dev:web # vite dev server on :5173, proxies /api pnpm sync:once # one poll, prints a PR/category table (smoke test) pnpm build # vite build + tsc server build pnpm start # run the production build ``` ## Deploy systemd user unit (survives reboots via linger): ```sh pnpm build cp deploy/pr-monitor.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now pr-monitor loginctl enable-linger $USER ``` Serves the built SPA + API on http://localhost:4000. ## Notes - Own activity appears in the timeline but is never marked unread; the first-ever sync marks the historical backfill as read (`BACKFILL_UNREAD=1` to override). - Events are append-only: comments later deleted on GitHub remain in the timeline. - Polling stretches to 5 min automatically when the GraphQL rate budget dips below 500 points.