perf(web): window the schedule task fetch — streaming pages into the cache renders wrong WBS codes and a wrong critical path
Follow-up to #2277 (closed), which capped the schedule's page-fetch burst at 4 concurrent requests but still resolves the whole page set before mapping. The remaining half of #2277 (closed)'s original acceptance — "time-to-first-interactive on the schedule is bounded by the visible window, not total task count" — is not met and is tracked here.
Why streaming pages into the cache is not a small change
The obvious implementation (setQueryData per page as it lands, so the Gantt paints after page 1) does not render an incomplete schedule. It renders a wrong one, in two independent places:
-
computeWbsCodesis positional.packages/web/src/utils/computeWbsCodes.tsnumbers siblings by their index in the array it is handed (`${i + 1}`), andassignCodesrecurses only through parents present in that array. So root tasks A, C, E arriving on page 1 display as WBS 1, 2, 3 and silently renumber to 1, 3, 5 when page 2 brings B and D. A task whose parent has not loaded is unreachable from the root walk entirely and falls back to its stale storedwbs. -
Link criticality is derived from the loaded set.
useScheduleTasksbuildscriticalTaskIdsfrom whichever tasks are currently in the cache, so a dependency arrow between two genuinely critical tasks renders non-critical until both endpoints have arrived.
Both are visible to the user and both resolve themselves a second later, which is worse than a stable wait: the schedule shows a defensible-looking WBS code and critical path that are simply incorrect while it fills.
What a fix has to do first
- Make WBS code assignment stable under a partial set — either derive codes from the server's
wbs_pathrather than array position, or defer code assignment until the set is known complete. - Make link criticality resolve against tasks not yet loaded (server-side
is_criticalon the dependency, or suppress the arrow's critical styling until both endpoints exist) rather than defaulting to non-critical. - Only then window the fetch to the visible WBS range and expand on scroll.
Not in scope here
The WebSocket half of #2277 (closed)'s acceptance — "without a full refetch on every WS event" — is owned by #2341 (splice task_updated instead of refetching) and #1537 (route task_note_* through the scheduleInvalidate debounce). ADR-0091 already delta-splices task_dates_updated.
Acceptance
- Opening a ~5k-task schedule paints an interactive first window without waiting for the full set
- No WBS code and no critical-path styling changes value between the first paint and the fully-loaded state
Constraints (1) and (2) were found by reading the code while fixing #2277 (closed), and were verified against main; they are not hypothetical.