CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / Python tests (push) Successful in 45s
CI & Build / Build & push image (push) Successful in 42s
The milestone progress bars and the cards beneath them came from different places. The bar is counted SERVER-SIDE over every task; the kanban rendered whatever a single `limit=100` returned. Project 2 has 166 tasks, so 66 never arrived — and because the route sorts `updated_at desc`, the ones dropped were the least recently touched, which is mostly done tasks in completed milestones. So "v1.0 — 12/12" expanded to two cards, and the auto-collapse rule (100% done starts collapsed) read as arbitrary because the number driving it disagreed with what you saw when you opened it. No benefit was being chased. The limit shipped the day the view was written (012eb1d, March 2), when the project had a couple of dozen tasks. It became wrong as the corpus grew, and nothing was watching: the route returns `total` and the view discarded it. Correct when written, wrong later, silent in between — the same shape as half the coherence survey. Four changes: - **Page until complete.** The board groups by milestone and shows per-milestone progress, so it cannot be right on a partial set. Guards against a page that returns nothing while `total` still claims more, rather than looping forever. - **Stop swallowing the error.** `catch {}` left an empty board, which is indistinguishable from a project with no tasks — the same hidden-with-no- indicator failure one layer up. Styled apart from the empty state deliberately; "no tasks" and "the tasks did not load" must not look alike. - **Clamp long plan bodies** to ~6.5rem with a Show more. A milestone IS the plan, so its body carries the whole design — several hundred words now — and rendered in full one plan pushes every other milestone off screen. max-height rather than line-clamp: the content is rendered markdown with block children, which line-clamp handles unpredictably. Length judged on the source string; a per-milestone scrollHeight measurement is a lot of machinery to decide whether to show one button, and the proxy is only wrong near the threshold. - **Auto-collapse decides ONCE per milestone.** It re-ran on every reload, and `loadMilestones` runs after a task's status changes — so expanding a finished milestone and ticking anything snapped it shut again with no visible cause. That is the other half of why the collapse state looked mixed: it wasn't only deciding at start, it was overriding the reader continuously. Reported by the operator after thefd7097cdeploy. Not caused by it — but restoring `.milestone-header` in #2444 is what made the progress track render again, so the mismatch had been invisible rather than absent.