Skip to content

Commit 4e5666f

Browse files
authored
fix(poll): add adaptive PR backfill splitting, enrichment cache, and 3-tier error surfacing (#173)
## Summary - Add 25-node adaptive binary splitting with a light-query fallback for PR enrichment and dependency/Renovate Dashboard body fetches (including CORS-masked `Failed to fetch` gateway timeouts, with split suppression during broad outages), query `PullRequest.statusCheckRollup` directly with partial-node recovery, carry `checkStatus` forward only when the PR's revision is unchanged, hot-poll in 25-node batches (reading fork head-commit CI for fork-only-CI PRs), and cache settled-PR enrichment for the refresh interval plus one minute. - Harden Octokit requests with per-attempt 15s timeouts (with an `AbortSignal.any` fallback), aborted-signal checks for queued requests, and a 15s inline rate-limit retry cap, plus post-wake/offline poll guards and 3-tier error surfacing (`silentToast` drawer warnings, 3-failure hysteresis, 15m toast cooldown, and a `Partial sync` indicator that clears once no PR is degraded), and require a 7-day `minimumReleaseAge` for pnpm resolution. - Treat unverified CI status (`checkStatusStale`) as unknown in the UI by hiding **Approve**, excluding the PR from Mergeable, and showing a `Checking status` badge; exclude archived repositories from dashboard and MCP searches; and persist parsed Renovate Dependency Dashboard bodies to `localStorage` with Zod validation and cross-identity cleanup.
1 parent bd32e0c commit 4e5666f

38 files changed

Lines changed: 6674 additions & 633 deletions

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ In production, provision `JIRA_CLIENT_ID` and `JIRA_CLIENT_SECRET` as Worker sec
2222

2323
The repo uses a pnpm workspace: the root package is the SolidJS SPA; `mcp/` is a separate package (`github-tracker-mcp`) built with tsup. Running `pnpm install` at the root installs both.
2424

25+
`pnpm-workspace.yaml` sets `minimumReleaseAge: 10080`, so dependency resolution only picks package versions published at least 7 days ago. pnpm applies this to every resolution, including lockfile regeneration (Renovate's lockfile maintenance runs pnpm with this workspace config), which keeps `pnpm-lock.yaml` installable under local supply-chain cooldowns of up to 7 days. If an install reports `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`, the lockfile was produced without this policy — regenerate it rather than relaxing the policy.
26+
2527
To run the MCP server in standalone mode, set `GITHUB_TOKEN` before starting:
2628

2729
```bash

‎docs/USER_GUIDE.md‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ To replace your token later (e.g., after expiration or rotation), go to **Settin
8989

9090
### Repository Selection
9191

92-
After signing in, the onboarding wizard asks you to select repositories to track. Search by name or browse by organization. Forked repositories are displayed with a muted name and a `Fork` badge so you can distinguish them from source repositories at a glance. Archived repositories are automatically excluded from the list (and removed from your selection if a previously tracked repository is archived on GitHub). You can change your selection at any time in **Settings > Repositories**.
92+
After signing in, the onboarding wizard asks you to select repositories to track. Search by name or browse by organization. Forked repositories are displayed with a muted name and a `Fork` badge so you can distinguish them from source repositories at a glance. Archived repositories are automatically excluded from the list. If a tracked repository is later archived on GitHub, its issues and PRs disappear from the dashboard on the next refresh (and are removed from your Tracked items), and the repository is removed from your selection the next time you open the repository list. You can change your selection at any time in **Settings > Repositories**.
9393

9494
### Organization Access
9595

@@ -237,7 +237,7 @@ Draft PRs authored by other users render as a read-only indicator.
237237

238238
#### Inline Approve Action
239239

240-
Open, non-draft pull requests not authored by you show an **Approve** button next to the pull request title on hover when branch protection requires a review (`Review needed`) or when a review has been requested of you or your team (unless you have already approved the PR without a fresh re-request). The **Approve** button is hidden when CI checks are failing (`checkStatus === "failure"`).
240+
Open, non-draft pull requests not authored by you show an **Approve** button next to the pull request title on hover when branch protection requires a review (`Review needed`) or when a review has been requested of you or your team (unless you have already approved the PR without a fresh re-request). The **Approve** button is hidden when CI checks are failing (`checkStatus === "failure"`) or while the app is still re-fetching the PR's CI check status after a partial sync (`checkStatusStale`); it reappears once the next refresh or hot poll restores the check status.
241241

242242
Approval uses a two-step inline confirmation to prevent accidental clicks:
243243
1. Click **Approve** once to arm confirmation (**Confirm?**, or **Confirm (auto)** when auto-merge is enabled on the PR). The confirmation state resets automatically after 4 seconds, on Escape, or when focus leaves the button.
@@ -328,16 +328,18 @@ Unlike the Pull Requests tab (which groups by repo), the Dependencies tab groups
328328
|-------|----------|
329329
| **Mergeable** | CI passing (or no CI configured), not a draft, and no changes requested — ready to review or merge |
330330
| **Pending Rebase** | PR has the configured rebase label — waiting for the bot to rebase |
331-
| **Needs Action** | Failing or running CI checks, merge conflicts, draft PR, or changes requested — rows display explicit reason badges (`Failing checks`, `Merge conflict`, `Checks running`, `Draft`, `Changes`) |
331+
| **Needs Action** | Failing or running CI checks, check status still being fetched by the app, merge conflicts, draft PR, or changes requested — rows display explicit reason badges (`Failing checks`, `Merge conflict`, `Checks running`, `Checking status`, `Draft`, `Changes`) |
332332
| **Stale** | PR has not been updated in over 14 days — may need a rebase or manual review |
333333

334+
Reason badges reflect each PR's current state, so the same badges also appear on rows in the **Pending Rebase** and **Stale** groups. On initial load, and during a refresh in which no PR's details have loaded yet, `Checking status` is withheld on dependency PRs without details, to avoid a flash of the badge on every row.
335+
334336
Within each group, PRs are sorted by repository name, then update category (maintenance, pin, digest, patch, minor, major), then update date.
335337

336338
### Abandoned Dependencies
337339

338340
If a Renovate Dashboard issue is detected in one of your tracked repos, abandoned dependency entries from its "Abandoned" section are shown as pill badges on matching PR rows. Each pill links directly to the Renovate Dashboard issue so you can investigate further.
339341

340-
The parser reads the Renovate Dashboard issue body to extract package names from the abandoned dependencies table. Repos excluded from the Dependencies tab (see Dependencies Settings below) are skipped for abandoned-package detection too — their Renovate Dashboard issue is never checked.
342+
The parser reads the Renovate Dashboard issue body to extract package names from the abandoned dependencies table and caches the parsed entries in `localStorage` (`github-tracker:dashboard-bodies`) keyed by issue `updatedAt`, so page reloads display `Abandoned` badges immediately without re-fetching unchanged Renovate Dependency Dashboard issue bodies. Repos excluded from the Dependencies tab (see Dependencies Settings below) are skipped for abandoned-package detection too — their Renovate Dashboard issue is never checked.
341343

342344
### Dependencies Settings
343345

@@ -401,7 +403,11 @@ Upstream repos are included in Issues and Pull Requests fetches but excluded fro
401403

402404
### Full Refresh
403405

404-
The dashboard polls GitHub at a configurable interval (default: **5 minutes**). Each full refresh fetches all issues, PRs, and workflow runs. You can trigger a manual refresh by clicking the refresh button in the header.
406+
The dashboard polls GitHub at a configurable interval (default: **5 minutes**). Each full refresh fetches all issues, PRs, and workflow runs. You can trigger a manual refresh by clicking the refresh button in the toolbar.
407+
408+
PR enrichment queries (reviewer lists, CI check rollups, merge conflict status, and line-change counts) run in 25-PR batches. If GitHub's GraphQL API is slow, returns a server error, or the request fails outright (`Failed to fetch`, which is how GitHub gateway timeouts appear in the browser), a failing batch is split in half repeatedly until batches are 6 PRs or fewer, then a lightweight fallback query fills in everything except CI check and merge-conflict status. Once a second batch in the same refresh fails before any PR's full details have loaded, later failing batches skip splitting and go straight to the fallback. Dependency PR and Renovate Dashboard body fetches also run in 25-node batches and split failing batches the same way (except after the 45-second safety timeout); any items still failing are retried once a 5-minute cooldown passes. Settled PR enrichments — where CI checks are not pending and the PR's `updatedAt` timestamp is unchanged — are cached in memory for your refresh interval plus one minute (at least 3 minutes), so a settled PR skips the enrichment query on the next automatic poll and is re-fetched on the one after (the cache is also invalidated automatically when repository events or PR actions occur). Changes that don't update a PR's `updatedAt` — such as a CI re-run, or a merge conflict caused by new commits on its base branch — aren't picked up while the cached entry is valid, so such a PR can show its previous check or merge status (including the **Approve** button) for up to about two refresh intervals. Clicking the manual **Refresh** button always bypasses this cache.
409+
410+
A subtle amber **Partial sync** indicator appears next to the Refresh button, while cached PR details are retained, when PR enrichment fails during a refresh and either leaves any PR without details or with unverified CI check status, or is rejected outright by GitHub (for example, a permissions error); otherwise it appears once enrichment failures persist across 3 consecutive polls. PRs whose CI check status couldn't be verified — filled in by the lightweight fallback query, changed (new `headSha` or `updatedAt`) during a degraded backfill, or forks whose CI lookup failed — have their check status cleared (`checkStatusStale`) rather than carried forward: they show `Checking status`, hide **Approve**, and are resolved automatically by the next hot-poll cycle or successful refresh. The indicator clears on the next full refresh whose PR enrichment succeeds, unless a search failure in that refresh leaves a PR still in this unverified state.
405411

406412
Setting the interval to **Off** disables automatic polling; manual refresh still works.
407413

@@ -415,7 +421,7 @@ Each full refresh also checks GitHub's own status page for outages affecting Act
415421

416422
A second, faster poll loop runs alongside the full refresh specifically for in-flight items. It targets:
417423

418-
- **PRs with pending CI checks** — re-checks status until checks resolve
424+
- **PRs with pending or stale (`checkStatusStale`) CI checks** — re-checks status until checks resolve (including PRs whose commit changed during a degraded backfill). For PRs from forks whose CI runs only in the fork, the status is read from the fork's head commit, the same source the full refresh uses.
419425
- **In-progress workflow runs** — re-checks until the run completes
420426

421427
Default interval: **30 seconds** (configurable from 10 to 120 seconds in Settings).
@@ -442,7 +448,7 @@ When the tab is hidden:
442448
- The **full refresh pauses** in background tabs — GraphQL requests have no 304 shortcut and every poll consumes real rate-limit budget.
443449
- The **events poll continues in background** — it is lightweight enough to run even when the tab is hidden, giving you near-real-time change detection without the cost of a full refresh.
444450

445-
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.
451+
When you return to a tab that has been hidden for more than 2 minutes, the dashboard waits a brief 2-second grace period (so the OS network connection can stabilize after waking from sleep) before firing a catch-up full refresh regardless of where the timer is in its cycle. Automatic refreshes also pause while the browser is offline and resume automatically once connectivity returns.
446452

447453
---
448454

@@ -452,6 +458,8 @@ When you return to a tab that has been hidden for more than 2 minutes, a catch-u
452458

453459
The bell icon in the header opens the notification drawer, which shows API errors, rate limit warnings, and other system messages. Notifications are dismissed automatically when the underlying condition clears on the next poll cycle.
454460

461+
Transient single-cycle PR enrichment blips are absorbed silently when cached PR details are available. Under the same conditions that show the **Partial sync** indicator (see [Full Refresh](#full-refresh)), a silent warning entry is logged to the Notification Drawer without popping a disruptive toast. Dependency PR and Renovate Dashboard body fetches similarly use 25-node batches with adaptive binary splitting and log a silent warning to the Notification Drawer only after 3 consecutive failing fetch cycles. For notifications that do trigger a toast, repeated identical toasts for the same source are rate-limited to once per 15 minutes.
462+
455463
### GitHub Outage Notifications
456464

457465
When a GitHub outage affecting a tracked service starts, a toast appears and an entry is added to the notification drawer. A second notification confirms it once the outage resolves. These always appear via the in-app toast and drawer — they are not part of the **Browser Push Notifications** toggles below and cannot be disabled.
@@ -486,7 +494,7 @@ The Tracked tab lets you pin issues and PRs into a personal TODO list that you c
486494

487495
**Reordering:** Use the chevron buttons on the left side of each row to move items up or down. Items slide smoothly into their new position.
488496

489-
**Auto-removal:** When a tracked issue is closed or a tracked PR is merged, it is automatically removed from the list. Closure is detected by absence from the `is:open` poll results. For PRs detected as closed by the hot poll, removal happens within seconds. Auto-removal is suspended when the API returns errors (e.g., rate limiting) to prevent false pruning.
497+
**Auto-removal:** When a tracked issue is closed or a tracked PR is merged, it is automatically removed from the list. Closure is detected by absence from the `is:open` poll results, so items from a selected repository that is archived or deleted on GitHub (or that you lose access to) are also removed. For PRs detected as closed by the hot poll, removal happens within seconds. Auto-removal is suspended when the API returns errors (e.g., rate limiting) to prevent false pruning.
490498

491499
**Relationship to other features:** The Tracked tab bypasses the org/repo filter — it always shows all your pinned items regardless of which repo filter is active. Ignoring an item from the Issues or Pull Requests tab also removes it from the tracked list. The tracked list is preserved when tracking is disabled and restored when re-enabled.
492500

‎e2e/capture-screenshot.spec.ts‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ const heavyPRNodes = [
127127
changedFiles: 8,
128128
comments: { totalCount: 6 },
129129
reviewThreads: { totalCount: 1 },
130-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
130+
statusCheckRollup: { state: "SUCCESS" },
131131
},
132132
{
133133
databaseId: 100002,
@@ -145,7 +145,7 @@ const heavyPRNodes = [
145145
changedFiles: 4,
146146
comments: { totalCount: 12 },
147147
reviewThreads: { totalCount: 3 },
148-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
148+
statusCheckRollup: { state: "SUCCESS" },
149149
},
150150
{
151151
databaseId: 100003,
@@ -160,7 +160,7 @@ const heavyPRNodes = [
160160
changedFiles: 31,
161161
comments: { totalCount: 2 },
162162
reviewThreads: { totalCount: 0 },
163-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "PENDING" } } }] },
163+
statusCheckRollup: { state: "PENDING" },
164164
},
165165
{
166166
databaseId: 100004,
@@ -178,7 +178,7 @@ const heavyPRNodes = [
178178
changedFiles: 3,
179179
comments: { totalCount: 1 },
180180
reviewThreads: { totalCount: 0 },
181-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
181+
statusCheckRollup: { state: "SUCCESS" },
182182
},
183183
{
184184
databaseId: 100005,
@@ -193,7 +193,7 @@ const heavyPRNodes = [
193193
changedFiles: 14,
194194
comments: { totalCount: 8 },
195195
reviewThreads: { totalCount: 2 },
196-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "FAILURE" } } }] },
196+
statusCheckRollup: { state: "FAILURE" },
197197
},
198198
{
199199
databaseId: 100006,
@@ -208,7 +208,7 @@ const heavyPRNodes = [
208208
changedFiles: 6,
209209
comments: { totalCount: 0 },
210210
reviewThreads: { totalCount: 0 },
211-
commits: { nodes: [{ commit: { statusCheckRollup: null } }] },
211+
statusCheckRollup: null,
212212
},
213213
];
214214

@@ -802,7 +802,7 @@ test("capture dependencies screenshot", async ({ page }) => {
802802
latestReviews: { totalCount: 0, nodes: [] },
803803
additions: 12, deletions: 8, changedFiles: 2,
804804
comments: { totalCount: 0 }, reviewThreads: { totalCount: 0 },
805-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
805+
statusCheckRollup: { state: "SUCCESS" },
806806
},
807807
{
808808
// webpack → mergeable (CI success, review required but not approved)
@@ -815,7 +815,7 @@ test("capture dependencies screenshot", async ({ page }) => {
815815
latestReviews: { totalCount: 0, nodes: [] },
816816
additions: 45, deletions: 12, changedFiles: 3,
817817
comments: { totalCount: 0 }, reviewThreads: { totalCount: 0 },
818-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
818+
statusCheckRollup: { state: "SUCCESS" },
819819
},
820820
{
821821
// typescript → needs-action (CI failure)
@@ -828,7 +828,7 @@ test("capture dependencies screenshot", async ({ page }) => {
828828
latestReviews: { totalCount: 0, nodes: [] },
829829
additions: 3, deletions: 3, changedFiles: 1,
830830
comments: { totalCount: 2 }, reviewThreads: { totalCount: 0 },
831-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "FAILURE" } } }] },
831+
statusCheckRollup: { state: "FAILURE" },
832832
},
833833
{
834834
// react monorepo → needs-action (CI pending)
@@ -841,7 +841,7 @@ test("capture dependencies screenshot", async ({ page }) => {
841841
latestReviews: { totalCount: 0, nodes: [] },
842842
additions: 89, deletions: 34, changedFiles: 8,
843843
comments: { totalCount: 0 }, reviewThreads: { totalCount: 0 },
844-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "PENDING" } } }] },
844+
statusCheckRollup: { state: "PENDING" },
845845
},
846846
{
847847
// axios → pending-rebase (has "rebase" label, CI success)
@@ -854,7 +854,7 @@ test("capture dependencies screenshot", async ({ page }) => {
854854
latestReviews: { totalCount: 0, nodes: [] },
855855
additions: 18, deletions: 6, changedFiles: 2,
856856
comments: { totalCount: 1 }, reviewThreads: { totalCount: 0 },
857-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
857+
statusCheckRollup: { state: "SUCCESS" },
858858
},
859859
{
860860
// @types/node → stale (updatedAt 20 days ago)
@@ -867,7 +867,7 @@ test("capture dependencies screenshot", async ({ page }) => {
867867
latestReviews: { totalCount: 0, nodes: [] },
868868
additions: 2, deletions: 2, changedFiles: 1,
869869
comments: { totalCount: 0 }, reviewThreads: { totalCount: 0 },
870-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "PENDING" } } }] },
870+
statusCheckRollup: { state: "PENDING" },
871871
},
872872
{
873873
// eslint → stale (updatedAt 35 days ago, major bump)
@@ -880,7 +880,7 @@ test("capture dependencies screenshot", async ({ page }) => {
880880
latestReviews: { totalCount: 0, nodes: [] },
881881
additions: 312, deletions: 145, changedFiles: 18,
882882
comments: { totalCount: 5 }, reviewThreads: { totalCount: 1 },
883-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "FAILURE" } } }] },
883+
statusCheckRollup: { state: "FAILURE" },
884884
},
885885
];
886886

@@ -932,7 +932,7 @@ test("capture dependencies screenshot", async ({ page }) => {
932932
latestReviews: { totalCount: 1, nodes: [{ author: { login: "msmith" } }] },
933933
additions: 312, deletions: 47, changedFiles: 8,
934934
comments: { totalCount: 6 }, reviewThreads: { totalCount: 1 },
935-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
935+
statusCheckRollup: { state: "SUCCESS" },
936936
},
937937
{
938938
databaseId: 100002,
@@ -943,7 +943,7 @@ test("capture dependencies screenshot", async ({ page }) => {
943943
latestReviews: { totalCount: 1, nodes: [{ author: { login: "jdoe" } }] },
944944
additions: 89, deletions: 23, changedFiles: 4,
945945
comments: { totalCount: 12 }, reviewThreads: { totalCount: 3 },
946-
commits: { nodes: [{ commit: { statusCheckRollup: { state: "SUCCESS" } } }] },
946+
statusCheckRollup: { state: "SUCCESS" },
947947
},
948948
];
949949

0 commit comments

Comments
 (0)