Milestone cards show no issues on this milestone despite correct counts; View N issues filter does not apply #564
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#564
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's observed
Two screenshots of the milestones UI contradict each other:
1 open · 1 closedon milestone 1.1,1 open · 0 closedon 1.2), the progress bar renders partially filled — but the card body's issue list says "no issues on this milestone" on both cards, with "View 2 issues" / "View 1 issue" buttons promising the issues exist.So: counts know about the issues, the thread header knows about the milestone, but every surface fed by the milestone-filtered issue list shows nothing — and the "View N issues" link lands on the issues page without showing the issues it promised.
Diagnosis (static — code reading + user evidence, no local repro)
The divergence: counts and the card list come from two independent stores
{m.open_issues} open · {m.closed_issues} closed,web/src/pages/Milestones.jsx:151) come from the milestone object's denormalized counters (internal/issues/model.go:169-170; updated bymoveMilestone/bumpMilestone,internal/issues/service.go:560-583/internal/issues/store.go:486-510).MilestoneIssues,web/src/pages/Milestones.jsx:33-36) comes fromGET …/api/issues?milestone=<id>— the server-sidemilestone=filter (internal/issues/http.go:397-399→filterCards,internal/issues/service.go:1049-1060) which matches against the index cards'Card.Milestonefield.The milestone filter matches
*c.Milestone != strings.ToLower(f.Milestone)— the card's stored id vs the query id. Verified consistent in the current tree: PATCH stores the canonicalm.ID(service.go:494-502, id fromallocMilestoneID= lowercase%06xhex,store.go:47-68), the filter lowercases the query, and the SDK sends the id verbatim (web/sdk/src/issues.jsqs();web/src/lib/milestones.jsmilestoneFilterHref).Root cause: a lost index-card update persists forever because the fast-path gate checks coverage, not freshness
The list is served index-first (
ListIssues,internal/issues/service.go:827-884): whenindexComplete(ix, next)is true, the window is filled from the persisted index cards alone and the header-scan LIST fallback never runs. Two structural gaps combine:updateIndexswallows every failure silently (internal/issues/store.go:227-259): all CAS-retry/store errorsreturnwith no signal. A lost index update — documented as expected ("lost index update, crash between the header CAS and the index CAS",service.go:842-848) — leaves the old card in place and nobody notices.bumpMilestonediscards itscasUpdateresult the same way (store.go:490).indexCompleteverifies only that everynum < counterhas a card (internal/issues/service.go:940-957) — never that the cards carry the current projection. A card written before themilestonefield existed in the Card projection, or one whose update was lost, stays "complete" and wins the fast path indefinitely: the header (truth) is never consulted, the card never repairs, andGET issues?milestone=<id>matches nothing → "no issues on this milestone" on every load, while the counters (separate store) stay right.The thread header keeps the correct milestone id (hence the #3 sidebar), but the card the filter reads is stale/missing the id.
Secondary, in-tree defect: "View N issues" promises more than the landing shows
milestoneFilterHrefemits?milestone=<id>with nostate=param (web/src/lib/milestones.js). Issues.jsx resolves an absentstateto open-only (web/src/pages/Issues.jsx:198-211, the #323 default, noted as "deliberate" there). So even with the filter returning rows, a "View 2 issues" button (1 open + 1 closed) lands on a page showing only the open one — the count promise and the landing disagree by construction. The #416 milestone select bindssearch.milestoneverbatim (resolveMilestoneFilter,web/src/lib/issueFilters.js:50-56), so the dropdown shows the milestone correctly once rows come back.Fix prescription
indexCompletereturns false so the LIST fallback heals the window (header wins). Stop swallowingupdateIndex/bumpMilestonefailures (log + counters/repair sweep). Provide a one-shot backfill that rebuilds any card disagreeing with its thread header — verify the live instance first: compareGET …/api/issues?milestone=<id>against the unfiltered list and the per-thread GET for the affected repo.milestoneFilterHrefcarry the state the count implies (e.g.state=allfor the open+closed total) — or count only open issues in the button label. Pick one and note it; do not leave the half-promise.Acceptance criteria
updateIndex/bumpMilestonefailures are logged (or surfaced), with a repair path backfilled from thread headers.web/test/unit/.References: #314 (milestones UI cards/View button), #416 (milestone dropdown deep-link binding), #484 (milestone icons/chips), #119 (milestone picker +
milestone=filter introduction), #323 (open-only list default), #318 (cross-page cache invalidation).Fixed by #565 (merged): Index carries additive card_version so stale/absent projections fall back to the header scan (self-healing on next filtered read, no operator step); updateIndex/bumpMilestone failures logged+counted with RepairIndex backfill; View-N-issues deep link carries state=all matching the open+closed label. Verified: issues suite -race green (96%+ coverage), web full-minus-smoke green, vite/esbuild green, independent review APPROVE.