Skip to content

Commit 25bbe14

Browse files
authored
feat(events): replaces notifications gate with Events API polling (#82)
* feat(events): replace notifications gate with Events API polling - Add events.ts with fetchUserEvents(), parseRepoEvents(), ETag management - Remove notifications gate: hasNotificationChanges, skipped field, background tab gating, notification 403 handler, POLL_MANAGED_SOURCES entry - Add createEventsPollCoordinator with 60s setTimeout chain, dual race guards - Add fetchTargetedRepoData with MAX_TARGETED_REPOS=10 cap (SEC-IMPL-003) - Add seedHotSetsFromTargeted for additive hot set population - Wire targeted merge in DashboardPage with ID-based dedup, surfacedBy union - Delete poll-notification-effects.test.ts (all tests were notification gate) * chore: clean up notification references and update docs - Remove notifications OAuth scope from oauth.ts - Remove notifications scope UI from LoginPage.tsx - Replace notifications API source with userEvents in api-usage.ts - Update DEPLOY.md, README.md, USER_GUIDE.md notification references - Update test mocks and assertions for the source rename * fix(poll): addresses review findings from Phase 4 - Captures cache snapshot eagerly before setTimeout (CR-001/PERF-001) - Adds per-repo 2-min cooldown to prevent API amplification (PERF-004) - Adds parts.length guard in fetchTargetedRepoData (SEC-004) - Wires resetEventsState into resetPollState for test isolation (CR-010) - Removes stale notification references from README and USER_GUIDE - Fixes ApiUsageSection test mock for notifications→userEvents rename * fix(poll): adds early return when all repos are on cooldown STRUCT-012: fetchTargetedRepoData now returns empty data immediately when cooldown filtering removes all repos, avoiding unnecessary GraphQL/REST calls and no-op merge pipeline execution. * test(events): adds unit and integration tests for events API polling - events.test.ts: 22 tests for fetchUserEvents (ETag, 304, first-call, numeric ID dedup, empty username guard) and parseRepoEvents (filtering, flags, case-insensitive matching, timestamps) - events-poll.test.ts: 22 tests for fetchTargetedRepoData (scoped repos, workflow activity filter, cooldown, cap, malformed names), seedHotSetsFromTargeted (pending guard, enriched guard, additive only, generation preservation), and config-change effects * test(dashboard): adds targeted merge integration tests - Tracked-user item preservation across targeted refresh - surfacedBy annotation union merging - detectNewItems + dispatchNotifications called after merge - seedHotSetsFromTargeted called (not rebuildHotSets) - MCP relay exclusion (lastRefreshedAt unchanged) * fix(poll): address PR review findings and harden events poll - Extract handleTargetedData to named module-scope function - Fix fetchUserEvents changed flag on empty first call - Reset consecutiveFailures on non-error skip paths - Add isFullRefreshing re-check after fetchTargetedRepoData - Add user login reactive effect to reset events ETag state - Remove redundant lowercase Set copy in parseRepoEvents - Rename trackedRepoNames to getTrackedRepoNames - Fix indentation in pollFetch if/else block - Replace plan-tag comments with descriptive WHY comments - Add EVENTS_POLL_INTERVAL_MS explanatory comment - Strengthen surfacedBy union tests with value assertions - Add 13 new tests covering untested code paths
1 parent 91dcf41 commit 25bbe14

18 files changed

Lines changed: 2061 additions & 968 deletions

DEPLOY.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,12 @@ Cloudflare Worker secrets are set. In CI, the deploy workflow runs
119119

120120
### Scopes
121121

122-
The login flow requests `scope=repo read:org notifications`:
122+
The login flow requests `scope=repo read:org`:
123123

124124
| Scope | Used for |
125125
|-------|----------|
126126
| `repo` | Read issues, PRs, check runs, workflow runs (includes private repos) |
127127
| `read:org` | `GET /user/orgs` — list user's organizations for the org selector |
128-
| `notifications` | `GET /notifications` — polling optimization gate (304 = skip full fetch) |
129128

130129
**Note:** The `repo` scope grants write access to repositories, but this app never performs write operations (POST/PUT/PATCH/DELETE on repo endpoints). It is read-only by design.
131130

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ A second, faster poll loop (default 30s, configurable 10–120s) targets only in
5959

6060
### Desktop Notifications
6161

62-
Browser notifications for new issues, PRs, and failed runs. Per-type toggles in settings. Notification permission requested on first enable. Uses the GitHub Notifications API as a change-detection gate when the `notifications` scope is available.
62+
Browser notifications for new issues, PRs, and failed runs. Per-type toggles in settings. Notification permission requested on first enable. New items are detected via the Events API polling loop and full refresh cycles.
6363

6464
### Repo Pinning and Reordering
6565

@@ -87,7 +87,7 @@ Hide specific items with a persistent ignore list. An "N ignored" badge on the r
8787

8888
### ETag Caching and Auto-Refresh
8989

90-
Conditional requests using `If-None-Match` headers — GitHub doesn't count 304 responses against the rate limit. Background polling keeps data fresh even when the tab is hidden (when the notifications scope is available for efficient change detection).
90+
Conditional requests using `If-None-Match` headers — GitHub doesn't count 304 responses against the rate limit. A 60-second events poll uses ETag requests to detect changes and trigger targeted per-repo refreshes, keeping data fresh even in background tabs at zero rate-limit cost.
9191

9292
## Tech Stack
9393

docs/USER_GUIDE.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ GitHub Tracker is a dashboard that aggregates open issues, pull requests, and Gi
4545

4646
### OAuth Sign-In
4747

48-
OAuth is the recommended sign-in method. Click **Sign in with GitHub** on the login page and authorize the application. GitHub will redirect you back with a token that grants access to `repo`, `read:org`, and `notifications` scopes.
48+
OAuth is the recommended sign-in method. Click **Sign in with GitHub** on the login page and authorize the application. GitHub will redirect you back with a token that grants access to `repo` and `read:org` scopes.
4949

50-
OAuth tokens work across all organizations you belong to and support the notifications optimization that reduces API usage in background tabs.
50+
OAuth tokens work across all organizations you belong to.
5151

5252
### Personal Access Token Sign-In
5353

5454
If you prefer not to use OAuth, you can sign in with a GitHub Personal Access Token (PAT). Click **Use a Personal Access Token** on the login page and paste your token.
5555

5656
Two token formats are accepted:
5757

58-
- **Classic tokens** (starts with `ghp_`) — recommended. Works across all organizations you belong to. Required scopes: `repo`, `read:org` (under admin:org), `notifications`.
59-
- **Fine-grained tokens** (starts with `github_pat_`) — also work, but have limitations: they only access one organization at a time, do not support the `notifications` scope, and therefore cannot use the background-poll optimization. Required permissions: Actions (read), Contents (read), Issues (read), Pull requests (read).
58+
- **Classic tokens** (starts with `ghp_`) — recommended. Works across all organizations you belong to. Required scopes: `repo`, `read:org` (under admin:org).
59+
- **Fine-grained tokens** (starts with `github_pat_`) — also work, but only access one organization at a time. Required permissions: Actions (read), Contents (read), Issues (read), Pull requests (read).
6060

6161
The token is validated against the GitHub API before being stored. It is saved permanently in your browser's `localStorage` — you will not need to re-enter it on revisit.
6262

@@ -316,10 +316,10 @@ Hover the rate limit display in the dashboard footer to see detailed remaining c
316316
When the tab is hidden:
317317

318318
- The **hot poll always pauses** (it provides only visual feedback).
319-
- The **full poll continues in background** when the notifications gate is available (OAuth or classic PAT with `notifications` scope). The gate uses `If-Modified-Since` headers for near-zero-cost 304 checks that do not count against your rate limit.
320-
- When the notifications gate is **unavailable** (fine-grained PAT or classic PAT missing the `notifications` scope), the full poll also pauses in background tabs to conserve API budget.
319+
- The **full refresh pauses** in background tabs — GraphQL requests have no 304 shortcut and every poll consumes real rate-limit budget.
320+
- The **events poll continues in background** — it uses ETag conditional requests (`If-None-Match`) that return 304 when nothing has changed, costing zero rate-limit points. When changes are detected, targeted per-repo refreshes run immediately.
321321

322-
When you return to a tab that has been hidden for more than 2 minutes, a catch-up fetch fires immediately regardless of where the timer is in its cycle.
322+
When you return to a tab that has been hidden for more than 2 minutes, a catch-up full refresh fires immediately regardless of where the timer is in its cycle.
323323

324324
---
325325

@@ -473,19 +473,19 @@ These are UI preferences that persist across sessions but are not included in th
473473

474474
The tracker uses GitHub's GraphQL and REST APIs. Each poll cycle consumes some of your 5,000 request hourly budget. Tracking many repos, tracked users, or having a short refresh interval increases consumption. Increasing the refresh interval or reducing the number of tracked repos will reduce API usage.
475475

476-
OAuth tokens and classic PATs use the notifications gate (304 shortcut), which significantly reduces per-cycle cost when nothing has changed. Fine-grained PATs do not support this optimization.
476+
A 60-second events poll uses ETag conditional requests to detect changes at near-zero cost, triggering targeted per-repo refreshes only when needed.
477477

478478
For detailed per-source API call counts, see Settings > API Usage.
479479

480480
**PAT vs OAuth: what is the difference?**
481481

482-
OAuth tokens (from "Sign in with GitHub") work across all your organizations and support all features including the notifications background-poll optimization. Classic PATs with the correct scopes (`repo`, `read:org`, `notifications`) behave identically to OAuth.
482+
OAuth tokens (from "Sign in with GitHub") work across all your organizations and support all features. Classic PATs with the correct scopes (`repo`, `read:org`) behave identically to OAuth.
483483

484-
Fine-grained PATs are limited to one organization at a time, do not support the `notifications` scope, and therefore cannot use the background-poll optimization — the full poll pauses in hidden tabs, and a warning appears in the notification drawer.
484+
Fine-grained PATs are limited to one organization at a time. Required permissions: Actions (read), Contents (read), Issues (read), Pull requests (read).
485485

486486
**Data looks stale after switching back to the tab.**
487487

488-
When a tab has been hidden for more than 2 minutes, a catch-up fetch fires automatically on return. If the notifications gate is unavailable (fine-grained PAT), polling was paused while the tab was hidden — the catch-up fetch provides a single refresh on return. To ensure continuous background updates, use OAuth or a classic PAT with the `notifications` scope.
488+
When a tab has been hidden for more than 2 minutes, a catch-up fetch fires automatically on return. The events poll continues running in background tabs using ETag conditional requests (zero rate-limit cost), so changes are detected even while the tab is hidden.
489489

490490
**I want to stop tracking a repository.**
491491

0 commit comments

Comments
 (0)