Hide the repo Checks tab when no checks exist (reappear on first report) #505

Closed
opened 2026-09-13 23:29:07 +00:00 by crueber · 4 comments
Owner

What's requested

Hide the Checks tab on the repo page when the repository has no checks reported, and make the tab reappear automatically with the first reported check. Static diagnosis from code reading — no local reproduction.

  • A repo with zero reported statuses still shows a Checks tab whose page is just the empty state ("No checks reported yet"). The tab should be hidden in that case, like a tab badge hiding at 0 (tabBadge discipline, issue #319).
  • The tab must reappear as soon as the first check status lands (external CI POST), including for an already-open session.
  • Deep links to /:owner/:name/checks (bookmarks, target_url links from statuses on other instances, old links) must remain sane when the tab is hidden.
  • Reporting-API discoverability must survive the hidden tab: the Checks page's "reporting API" link (web/src/pages/Checks.jsx, toolbar, href /api#checks-ci) is currently the discoverable pointer for external CI authors; hiding the tab on check-less repos removes it exactly where CI hasn't been wired yet.

Evidence

  • Tab bar: web/src/pages/Repo.jsx TABS array includes { id: "checks", … } unconditionally; the <For each={TABS}> renderer (nav repo-tabs) has no per-tab visibility filter (badges render only when count > 0, but tabs themselves always render).
  • Checks page empty state: web/src/pages/Checks.jsx — Empty fallback "No checks reported yet" when getPage().checks is empty; toolbar carries the reporting API link.
  • No checks-existence signal exists on the repo summary: internal/api/summary.go summaryBody has no checks field. The projection precedents to copy are right there in the same handler: CollabCounts (issue #319, open_issues/open_pulls) and ForkSummary (issue #424) — both one exact-key probe behind an Env hook, nil/absent → zero, +0 round trips for non-consumers (law 4: 404s are free).
  • The natural existence source already exists: the CAS'd hot-window index repos/<o>/<r>/checks/index.json (internal/checks/checks.go, IndexKey, created on first report). One exact-key conditional GET on it = "has any checks" (the index is written by every report; the per-sha objects are the backfill truth but the index is the cheap flag).
  • Reappear path: check frames already ride the one repo collaboration stream (web/src/pages/Checks.jsx useCollabStream(… ["check"])) and the summary is fetched by the shell — but note the summary is served no-cache with an ETag (ccMutable, #381), so a new field must be covered by the ETag or a revalidating client 304s and the tab never reappears (the ~d/~m/~c/~v/~f suffix discipline, lines 164–207).

Architecture notes

  • Add a summary projection in the established hook pattern (same shape as CollabCounts/ForkInfo in internal/api/env.go): a ChecksSummary-style Env hook probing IndexKey existence; wire the concrete probe from internal/checks (the package already owns the key) into cmd/walhub/serve.go, exactly as the other feature packages feed their hooks.
  • New field on summaryBody, e.g. has_checks (bool, or checks_total int if a count is trivially available from the index projection — flag-only is enough; pick one and note it). Old clients ignore it (14 §14.12). omitempty vs always-present: follow the #319 discipline (always present, 0/false = none).
  • ETag coverage (pitfall class, #235/#240/#319/#345 precedents): the first report creates the index and changes no ref — without an ETag suffix the summary 304s and the tab stays hidden. Add a suffix (e.g. ~k + version or presence byte) covered by whatever the probe reads. The index is CAS'd, so a version/ETag from the index object is a natural suffix input.
  • Client: filter the Checks entry out of the TABS render when summary.has_checks is false. Consider keeping it a pure helper in web/src/lib/ alongside tabs.js (headless-testable per the settingsNav convention) so the visibility rule is unit-testable without DOM.
  • The internal/checks exposed_test.go pins the discovery list — a new summary field is server-side only and shouldn't touch it, but any route change would; don't add routes.
  • What does NOT exist yet: no per-repo "checks count" field anywhere on the wire today; no Env hook for checks (the Summary(ctx, id) view is git-only). The tab badge machinery (tabBadge, #319) is client-only and consumes summary fields — this ticket supplies the field it would need.

Acceptance criteria

  • Repo summary carries a checks-existence field sourced from a +0-round-trip Env-hook probe (non-consumers pay nothing, law 4/6).
  • Repo with zero checks: Checks tab is not rendered.
  • Repo with ≥1 reported status (hot window or backfilled): Checks tab renders normally.
  • Deep link /:owner/:name/checks on a check-less repo still renders (the empty state is a fine landing — tab hiding must not 404 or redirect-bounce), and the active-tab matcher (web/src/lib/tabs.js) keeps handling checks/check path segments even when the tab is hidden.
  • First report flips the field and the tab appears in already-open sessions without a full reload: ETag covers the new field (revalidation returns 200 with the new body, not 304).
  • Reporting-API discoverability preserved on check-less repos — e.g. the empty state (or repo header) keeps a "reporting API" link to /api#checks-ci even when the tab is hidden; implementer picks the surface and notes it.
  • Summary ETag suffix added for the new field; cached-summary SWR/ETag test coverage updated (the ~-suffix discipline has per-field precedent tests in internal/api).
  • Unit test for the tab-visibility helper (headless, web/src/lib/).
## What's requested Hide the **Checks** tab on the repo page when the repository has no checks reported, and make the tab reappear automatically with the first reported check. Static diagnosis from code reading — no local reproduction. - A repo with zero reported statuses still shows a Checks tab whose page is just the empty state ("No checks reported yet"). The tab should be hidden in that case, like a tab badge hiding at 0 (`tabBadge` discipline, issue #319). - The tab must reappear as soon as the first check status lands (external CI POST), including for an already-open session. - Deep links to `/:owner/:name/checks` (bookmarks, `target_url` links from statuses on other instances, old links) must remain sane when the tab is hidden. - Reporting-API discoverability must survive the hidden tab: the Checks page's "reporting API" link (`web/src/pages/Checks.jsx`, toolbar, href `/api#checks-ci`) is currently the discoverable pointer for external CI authors; hiding the tab on check-less repos removes it exactly where CI hasn't been wired yet. ## Evidence - Tab bar: `web/src/pages/Repo.jsx` `TABS` array includes `{ id: "checks", … }` unconditionally; the `<For each={TABS}>` renderer (nav `repo-tabs`) has no per-tab visibility filter (badges render only when count > 0, but tabs themselves always render). - Checks page empty state: `web/src/pages/Checks.jsx` — `Empty` fallback "No checks reported yet" when `getPage().checks` is empty; toolbar carries the `reporting API` link. - No checks-existence signal exists on the repo summary: `internal/api/summary.go` `summaryBody` has no checks field. The projection precedents to copy are right there in the same handler: `CollabCounts` (issue #319, open_issues/open_pulls) and `ForkSummary` (issue #424) — both one exact-key probe behind an `Env` hook, nil/absent → zero, **+0 round trips** for non-consumers (law 4: 404s are free). - The natural existence source already exists: the CAS'd hot-window index `repos/<o>/<r>/checks/index.json` (`internal/checks/checks.go`, `IndexKey`, created on first report). One exact-key conditional GET on it = "has any checks" (the index is written by every report; the per-sha objects are the backfill truth but the index is the cheap flag). - Reappear path: `check` frames already ride the one repo collaboration stream (`web/src/pages/Checks.jsx` `useCollabStream(… ["check"])`) and the summary is fetched by the shell — but note the summary is served `no-cache` with an ETag (`ccMutable`, #381), so a *new field must be covered by the ETag* or a revalidating client 304s and the tab never reappears (the `~d/~m/~c/~v/~f` suffix discipline, lines 164–207). ## Architecture notes - Add a summary projection in the established hook pattern (same shape as `CollabCounts`/`ForkInfo` in `internal/api/env.go`): a `ChecksSummary`-style Env hook probing `IndexKey` existence; wire the concrete probe from `internal/checks` (the package already owns the key) into `cmd/walhub/serve.go`, exactly as the other feature packages feed their hooks. - New field on `summaryBody`, e.g. `has_checks` (bool, or `checks_total int` if a count is trivially available from the index projection — flag-only is enough; pick one and note it). Old clients ignore it (14 §14.12). `omitempty` vs always-present: follow the #319 discipline (always present, 0/false = none). - **ETag coverage (pitfall class, #235/#240/#319/#345 precedents):** the first report creates the index and changes no ref — without an ETag suffix the summary 304s and the tab stays hidden. Add a suffix (e.g. `~k` + version or presence byte) covered by whatever the probe reads. The index is CAS'd, so a version/ETag from the index object is a natural suffix input. - Client: filter the Checks entry out of the `TABS` render when `summary.has_checks` is false. Consider keeping it a pure helper in `web/src/lib/` alongside `tabs.js` (headless-testable per the settingsNav convention) so the visibility rule is unit-testable without DOM. - **The `internal/checks` `exposed_test.go` pins the discovery list** — a new summary field is server-side only and shouldn't touch it, but any route change would; don't add routes. - What does NOT exist yet: no per-repo "checks count" field anywhere on the wire today; no Env hook for checks (the `Summary(ctx, id)` view is git-only). The tab badge machinery (`tabBadge`, #319) is client-only and consumes summary fields — this ticket supplies the field it would need. ## Acceptance criteria - [ ] Repo summary carries a checks-existence field sourced from a +0-round-trip Env-hook probe (non-consumers pay nothing, law 4/6). - [ ] Repo with zero checks: Checks tab is not rendered. - [ ] Repo with ≥1 reported status (hot window or backfilled): Checks tab renders normally. - [ ] Deep link `/:owner/:name/checks` on a check-less repo still renders (the empty state is a fine landing — tab hiding must not 404 or redirect-bounce), and the active-tab matcher (`web/src/lib/tabs.js`) keeps handling `checks`/`check` path segments even when the tab is hidden. - [ ] First report flips the field and the tab appears in already-open sessions without a full reload: ETag covers the new field (revalidation returns 200 with the new body, not 304). - [ ] Reporting-API discoverability preserved on check-less repos — e.g. the empty state (or repo header) keeps a "reporting API" link to `/api#checks-ci` even when the tab is hidden; implementer picks the surface and notes it. - [ ] Summary ETag suffix added for the new field; cached-summary SWR/ETag test coverage updated (the `~`-suffix discipline has per-field precedent tests in `internal/api`). - [ ] Unit test for the tab-visibility helper (headless, `web/src/lib/`).
crueber added this to the v1 milestone 2026-09-13 23:29:16 +00:00
Author
Owner

Context pairing: #504 documents that the checks subsystem is fully wired but unused (nothing external reports yet) — this ticket hides the tab until the first check arrives, per the owner ruling. The summary has_checks flag prescribed here must invalidate when the first status is POSTed via the #504 write path so the tab reappears without a reload.

Context pairing: #504 documents that the checks subsystem is fully wired but unused (nothing external reports yet) — this ticket hides the tab until the first check arrives, per the owner ruling. The summary has_checks flag prescribed here must invalidate when the first status is POSTed via the #504 write path so the tab reappears without a reload.
Author
Owner

Fix ready for review: #510 (branch fix/issue-505). Summary has_checks + ~k ETag suffix; tab hides when false, reappears live via hidden-state shell stream + check→repo frame mapping; deep-link /checks still renders; header keeps the /api#checks-ci link while hidden.

Fix ready for review: https://git.packden.us/crueber/walhub/pulls/510 (branch fix/issue-505). Summary has_checks + ~k ETag suffix; tab hides when false, reappears live via hidden-state shell stream + check→repo frame mapping; deep-link /checks still renders; header keeps the /api#checks-ci link while hidden.
Author
Owner

REVIEW of PR #510 (fix/issue-505, commit 1779177) — verified in scratch worktree /tmp/pr510 (removed afterward); main worktree left clean (only pre-existing untracked .opencode/). No browser used — tests + reasoning only, per instructions. No docker/compose/system-package changes. Live 127.0.0.1:8080 instance untouched (only the smoke test's own GETs ran against it).

All 8 acceptance criteria PASS. No fixes needed — nothing pushed.

(1) Probe (+0 trips, exact-key, nil→false): PASS. internal/checks/service.go:539 HasChecks does one exact-key GET on IndexKey, nil→(false,0,false,nil); corrupt/store-error→err and cmd/walhub/collab.go:202-211 fails open to absent (nil/absent→has_checks:false, byte-identical ETag). Budget pinned in internal/checks/summary_test.go (1 GET/0 LIST). +1 GET per summary when wired is off the law-6 budgeted paths (push/sync/checkpoint never call summary) — documented in docs/go/07_api.md §9.1. Law 8 holds: internal/api never imports internal/checks (Env.ChecksSummary hook, CollabCounts shape).

(2) ETag ~k suffix: PASS. internal/api/summary.go:226-232 appends ~k only when checksOK; no collision with ~d/~m/~c/~v/~f/~degraded. Flip 200-not-304 proven in internal/api/summary505_test.go TestSummaryChecksRevalidate (bare→200 with ~k1 on first report, version bump→200 again, current→304).

(3) Helper fail-open: PASS. web/src/lib/tabs.js:80-83 showChecksTab returns true for null/undefined/missing-field (loading, deleted, pre-505 servers); false only on explicit has_checks:false. Matrix unit-tested in web/test/unit/checks-tab-505.test.js.

(4) Deep link: PASS. web/src/index.jsx:115-116 /checks and /checks/:sha routes render unconditionally (empty state is the landing); activeTab in tabs.js untouched — checks/check segments still map while hidden (pinned by test).

(5) Live reappear, no reconnect storm: PASS. web/src/lib/collab.js:70 adds repo:{full} to check frames (#319 invalidate-at-minimum precedent); web/src/pages/Repo.jsx:642-643 hidden-state-only shell stream via boolean createMemo (effect deps flip only on hidden↔shown transitions; full() stable across summary refreshes) with null-full early-out, so check-ful/loading/deleted/pre-505 hold no extra stream. Unmount on flip = single cycle, no storm.

(6) Discoverability: PASS. Checks toolbar /api#checks-ci link untouched (Checks.jsx:162-168); repo-header meta line keeps the same href/spelling exactly while hidden, Show when={!showChecksTab(s())} (Repo.jsx:731-733), retiring on first report. Surface noted in 07 Decisions + 12_web_ui Decisions.

(7) exposed_test untouched: PASS — git diff on internal/checks/exposed_test.go is empty; no new routes.

(8) Stale #396 pin update legitimate: PASS — web/test/unit/fetch-rate-guard.test.js repo:{full} 0→1 is the necessary consequence of the new check→repo frame mapping (stale summary refetches once, like every other mapped key).

Checks: internal/api 95.3% + internal/checks 96.3% (≥95% gate holds); go test -race green on both; gofmt clean; go vet clean; node --test 1131 total / 1130 pass / 1 fail = pre-existing smoke.test.js live-server subtest, confirmed failing identically on pristine main (env: something answers /healthz on 127.0.0.1:8080 but 403s /setup); vite build green (chunk-size warning only, pre-existing); go build ./... green; internal/e2e green (56.8s). No new deps (go.mod/package.json untouched). Docs accurate: 07_api §9.1 + suffix lists + Decisions, 06 route table, 12_web_ui Decisions (law 12 satisfied).

Edge noted, not blocking: repos whose index compacted past every sha read has_checks:false (len(SHAs)==0) — consistent with the table page, which reads the same index projection; staleness envelope is stated in the HasChecks doc comment.

MERGE RECOMMENDATION: ready to merge (squash per repo convention; do NOT merge without a second human glance at nothing — no open items).

REVIEW of PR #510 (fix/issue-505, commit 1779177) — verified in scratch worktree /tmp/pr510 (removed afterward); main worktree left clean (only pre-existing untracked .opencode/). No browser used — tests + reasoning only, per instructions. No docker/compose/system-package changes. Live 127.0.0.1:8080 instance untouched (only the smoke test's own GETs ran against it). All 8 acceptance criteria PASS. No fixes needed — nothing pushed. (1) Probe (+0 trips, exact-key, nil→false): PASS. internal/checks/service.go:539 HasChecks does one exact-key GET on IndexKey, nil→(false,0,false,nil); corrupt/store-error→err and cmd/walhub/collab.go:202-211 fails open to absent (nil/absent→has_checks:false, byte-identical ETag). Budget pinned in internal/checks/summary_test.go (1 GET/0 LIST). +1 GET per summary when wired is off the law-6 budgeted paths (push/sync/checkpoint never call summary) — documented in docs/go/07_api.md §9.1. Law 8 holds: internal/api never imports internal/checks (Env.ChecksSummary hook, CollabCounts shape). (2) ETag ~k suffix: PASS. internal/api/summary.go:226-232 appends ~k<index-version> only when checksOK; no collision with ~d/~m/~c/~v/~f/~degraded. Flip 200-not-304 proven in internal/api/summary505_test.go TestSummaryChecksRevalidate (bare→200 with ~k1 on first report, version bump→200 again, current→304). (3) Helper fail-open: PASS. web/src/lib/tabs.js:80-83 showChecksTab returns true for null/undefined/missing-field (loading, deleted, pre-505 servers); false only on explicit has_checks:false. Matrix unit-tested in web/test/unit/checks-tab-505.test.js. (4) Deep link: PASS. web/src/index.jsx:115-116 /checks and /checks/:sha routes render unconditionally (empty state is the landing); activeTab in tabs.js untouched — checks/check segments still map while hidden (pinned by test). (5) Live reappear, no reconnect storm: PASS. web/src/lib/collab.js:70 adds repo:{full} to check frames (#319 invalidate-at-minimum precedent); web/src/pages/Repo.jsx:642-643 hidden-state-only shell stream via boolean createMemo (effect deps flip only on hidden↔shown transitions; full() stable across summary refreshes) with null-full early-out, so check-ful/loading/deleted/pre-505 hold no extra stream. Unmount on flip = single cycle, no storm. (6) Discoverability: PASS. Checks toolbar /api#checks-ci link untouched (Checks.jsx:162-168); repo-header meta line keeps the same href/spelling exactly while hidden, Show when={!showChecksTab(s())} (Repo.jsx:731-733), retiring on first report. Surface noted in 07 Decisions + 12_web_ui Decisions. (7) exposed_test untouched: PASS — git diff on internal/checks/exposed_test.go is empty; no new routes. (8) Stale #396 pin update legitimate: PASS — web/test/unit/fetch-rate-guard.test.js repo:{full} 0→1 is the necessary consequence of the new check→repo frame mapping (stale summary refetches once, like every other mapped key). Checks: internal/api 95.3% + internal/checks 96.3% (≥95% gate holds); go test -race green on both; gofmt clean; go vet clean; node --test 1131 total / 1130 pass / 1 fail = pre-existing smoke.test.js live-server subtest, confirmed failing identically on pristine main (env: something answers /healthz on 127.0.0.1:8080 but 403s /setup); vite build green (chunk-size warning only, pre-existing); go build ./... green; internal/e2e green (56.8s). No new deps (go.mod/package.json untouched). Docs accurate: 07_api §9.1 + suffix lists + Decisions, 06 route table, 12_web_ui Decisions (law 12 satisfied). Edge noted, not blocking: repos whose index compacted past every sha read has_checks:false (len(SHAs)==0) — consistent with the table page, which reads the same index projection; staleness envelope is stated in the HasChecks doc comment. MERGE RECOMMENDATION: ready to merge (squash per repo convention; do NOT merge without a second human glance at nothing — no open items).
Author
Owner

Fixed by PR #510 (review clean — all 8 criteria pass, flip economics proven, live reappear sound), merged. Closing.

Fixed by PR #510 (review clean — all 8 criteria pass, flip economics proven, live reappear sound), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#505
No description provided.