Skip to content

fix(tb-pr): preserve stale columns when GitHub search degrades#23

Merged
ilucin merged 3 commits intomainfrom
feat/tb-pr-stale-fallback
Apr 27, 2026
Merged

fix(tb-pr): preserve stale columns when GitHub search degrades#23
ilucin merged 3 commits intomainfrom
feat/tb-pr-stale-fallback

Conversation

@ilucin
Copy link
Copy Markdown
Contributor

@ilucin ilucin commented Apr 27, 2026

Summary

Today GitHub's /search/issues (status.github.com listed it as a Pull Requests major outage) returned 200 OK with zero results across the board. tb-pr refresh overwrote the cache with the empty results — the first 4 columns went silently empty and the user couldn't tell whether they truly had no PRs or whether GitHub was lying. Refreshing again just re-cleared the cache.

This change makes the cache stale-fallback-aware:

  1. Before fetching, snapshot the prior board (in-memory for the TUI, from cache for refresh / list).
  2. Run fetch_board_state as usual.
  3. For any of the 5 PR columns where the new fetch returned empty but the prior cache had data, restore from prior and record the column on a new BoardState.degraded_columns: Vec<Column> field.
  4. Surface it:
    • TUI — yellow header banner: ⚠ search degraded — using cache for: draft, wait-me
    • tb-pr refresh — stderr warning with the same column list.
    • tb-pr list --jsondegraded_columns field, omitted when empty so the existing skill/JSON consumers are unaffected.

fetched_at and notifications always come from the fresh fetch, so the "refreshed N min ago" header reflects the actual attempt.

What it does NOT do

  • It does not retry or work around the outage — when GitHub is broken there's no good data to show. We just stop destroying the data we already had.
  • The cache eviction window (CacheTtl::Long = 1 hour) is unchanged. If the user hasn't run tb-pr in over an hour during a long outage, the fallback cache is gone — empty columns + the warning is the best we can do.
  • Notifications already had a graceful warning path; left as-is.

Test plan

  • Unit: merge_with_previous restores from prev when new is empty + prev non-empty; passes through unchanged when prev is None; keeps fresh data when both have entries.
  • Unit: degraded_columns is omitted from JSON when empty (backwards-compat for the Claude Code skill).
  • cargo clippy -p tb-pr -- -D warnings clean.
  • cargo test -p tb-pr — 53 tests pass.
  • Manual: ran the new debug binary against the live (still partly degraded) GitHub today; refresh succeeded, populated columns, no false-positive degradation banner.

🤖 Generated with Claude Code

ilucin and others added 3 commits April 27, 2026 21:16
GitHub's `/search/issues` returns 200 OK with zero results during
search-index incidents (status.github.com), which previously wiped the
kanban — `tb-pr refresh` overwrote the cache with empty results, hiding
PRs that very much still existed.

Now: snapshot the prior board before fetching; for any PR column that
comes back empty when the prior cache had data, restore the prior
column and record it in `BoardState.degraded_columns`. The TUI shows a
yellow banner ("⚠ search degraded — using cache for: …"), `tb-pr
refresh` prints a stderr warning, and `tb-pr list --json` exposes the
field (omitted when empty so existing consumers stay
backwards-compatible).

The merge runs at all three call sites: `refresh`, `list::load_or_fetch`,
and the TUI's `spawn_fetch` (which uses the in-memory `app.state` as
prev so manual + auto-refresh both benefit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three pre-existing `sort_by(|a, b| b.x.cmp(&a.x))` patterns now trip the
strengthened `unnecessary_sort_by` lint and break CI on every PR.
Replaced with `sort_by_key(|x| std::cmp::Reverse(x.x))` per clippy's
suggestion. Behaviour identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a single failing search call (rate limit, 5xx, network blip)
propagated `?` and aborted the whole fetch — the user saw a red error
banner with no data. Notification fetch failures were buried in stderr
where TUI users never saw them. And the empty-search degradation flag
was a flat `Vec<Column>` with no room for a reason.

This unifies all three signals into a single `BoardState.column_issues:
Vec<ColumnIssue>` field where each entry carries the column it affects
and a short human reason ("rate limited", "server error 503", "search
returned empty", "network error", …).

- `fetch_board_state` no longer `?`-propagates the four search calls or
  the notifications fetch. Each is independently captured; failures get
  a `ColumnIssue` (the q_author_mine search flags both ReviewMine and
  ReadyToMergeMine since it feeds both).
- `merge_with_previous` still restores empty PR columns from the prior
  cache, but now records the issue with the reason "search returned
  empty" — and skips that label if a more specific issue (rate limited,
  api error, …) was already pushed by the fetch step.
- TUI banner: `⚠ partial fetch — draft (rate limited), wait-me
  (search returned empty), mentions (server error 503)`.
- `tb-pr refresh`: same line on stderr.
- `tb-pr list --json`: structured `column_issues` field, omitted when
  empty.

Renames `BoardState.degraded_columns` → `column_issues`. Backwards
compat is moot since the previous field was added in this same PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ilucin ilucin marked this pull request as ready for review April 27, 2026 19:52
@ilucin ilucin merged commit 62d44b0 into main Apr 27, 2026
1 check passed
@ilucin ilucin deleted the feat/tb-pr-stale-fallback branch April 27, 2026 19:52
@ilucin ilucin mentioned this pull request Apr 27, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant