Follow-ups from the fresh-context review of PR #2688 (#2685: the shared board-list read is bounded with timeout: BOARD_REQUEST_TIMEOUT_MS and skipRetry: true, matching the detail read). None blocks that merge; together they are the product cost of the bound, which is now load-bearing on the boards list.
BoardsListView has no Retry control (MEDIUM in the review). src/views/BoardsListView.vue ~134-137 renders boardStore.error in a bare role="alert" block; its only read is onMounted. With skipRetry on the list read, a one-off 503 or a restart of the API during mount (routine in the dotnet run loop) no longer heals itself in the retry layer: the alert stays until the user navigates away and back, and because the read is shared, every unfiltered caller on the page fails on the same blip. Add a Retry affordance the way PaperTriageTable.vue (retryBoardLoad, ~328-330) has one, calling fetchBoards() (the share is released and the throttle stamp is unset on failure, so the retry issues a fresh request).
MetricsView.loadBoards has no catch (LOW). src/views/MetricsView.vue ~33-41 and ~82-89: try { await boardStore.fetchBoards() } finally { ... } with no catch, inside an onMounted async callback, so a failed list read is an unhandled rejection that installWindowErrorListeners forwards to Sentry where present. Pre-existing, but the bound makes the failing path routine. Catch and rely on the store's error surface, as every other caller does.
- A timeout surfaces raw axios copy in a localized alert (LOW).
getErrorMessage (utils/errorMessage.ts ~18-20) prefers err.message, so a 10 s timeout puts "timeout of 10000ms exceeded" into state.error and the toast, rendered verbatim beside $t copy. The detail read has the same shape today. Map ECONNABORTED (and an aborted request, should one ever reach handleApiError) to a translated message.
state.error is never cleared by a successful read (LOW, pre-existing, amplified). boardCrudStore.ts clears error at the start of a read but not on success, so two concurrent list reads (the activity selector's includeArchived read still in flight from the previous route while the boards list mounts an unfiltered one) where the earlier fails and the later succeeds leave error set beside a populated boards, and BoardsListView's v-if loading / v-else-if error / ... / v-else grid chain shows the alert instead of the grid. The bound makes the failing half deterministic at 10 s. Clear error on the success path of a current-generation read.
- Four direct callers of
boardsApi.getBoards outside the store remain unbounded (worker note, not reviewed). ArchiveView.vue, useAutomationChat.ts, useProposalDisplayNames.ts, useReviewProposals.ts call the API directly, so they get neither the share nor the bound. Decide per caller whether they need a bound (they are not joined to the share, so the page-wide stall does not apply to them).
Refs #2685, #1961, PR #2681, PR #2688.
Follow-ups from the fresh-context review of PR #2688 (#2685: the shared board-list read is bounded with
timeout: BOARD_REQUEST_TIMEOUT_MSandskipRetry: true, matching the detail read). None blocks that merge; together they are the product cost of the bound, which is now load-bearing on the boards list.BoardsListViewhas no Retry control (MEDIUM in the review).src/views/BoardsListView.vue~134-137 rendersboardStore.errorin a barerole="alert"block; its only read isonMounted. WithskipRetryon the list read, a one-off 503 or a restart of the API during mount (routine in thedotnet runloop) no longer heals itself in the retry layer: the alert stays until the user navigates away and back, and because the read is shared, every unfiltered caller on the page fails on the same blip. Add a Retry affordance the wayPaperTriageTable.vue(retryBoardLoad, ~328-330) has one, callingfetchBoards()(the share is released and the throttle stamp is unset on failure, so the retry issues a fresh request).MetricsView.loadBoardshas no catch (LOW).src/views/MetricsView.vue~33-41 and ~82-89:try { await boardStore.fetchBoards() } finally { ... }with nocatch, inside anonMountedasync callback, so a failed list read is an unhandled rejection thatinstallWindowErrorListenersforwards to Sentry where present. Pre-existing, but the bound makes the failing path routine. Catch and rely on the store's error surface, as every other caller does.getErrorMessage(utils/errorMessage.ts~18-20) preferserr.message, so a 10 s timeout puts "timeout of 10000ms exceeded" intostate.errorand the toast, rendered verbatim beside$tcopy. The detail read has the same shape today. Map ECONNABORTED (and an aborted request, should one ever reachhandleApiError) to a translated message.state.erroris never cleared by a successful read (LOW, pre-existing, amplified).boardCrudStore.tsclearserrorat the start of a read but not on success, so two concurrent list reads (the activity selector'sincludeArchivedread still in flight from the previous route while the boards list mounts an unfiltered one) where the earlier fails and the later succeeds leaveerrorset beside a populatedboards, andBoardsListView'sv-if loading / v-else-if error / ... / v-else gridchain shows the alert instead of the grid. The bound makes the failing half deterministic at 10 s. Clearerroron the success path of a current-generation read.boardsApi.getBoardsoutside the store remain unbounded (worker note, not reviewed).ArchiveView.vue,useAutomationChat.ts,useProposalDisplayNames.ts,useReviewProposals.tscall the API directly, so they get neither the share nor the bound. Decide per caller whether they need a bound (they are not joined to the share, so the page-wide stall does not apply to them).Refs #2685, #1961, PR #2681, PR #2688.