8.7 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/missing-task-route-recovery/spec.md | 2026-08-03 | completed |
Implementation Plan: Missing task route recovery
Overview
Make task-detail boot construction aware of whether route-specific data was actually produced. When the requested task cannot be loaded, reuse the existing home Kanban boot builder to hydrate only the selected authorized workspace and its sidebar snapshots; keep successful task routes on their current lean, task-owned hydration path. Then add a workspace-preserving overview action to the unavailable state and prove the full cold-load behavior on desktop and phone.
Root cause
bootInitialState deliberately adds the full workspace/workflow/Kanban state
for Home and unknown routes, but not for RouteTaskDetail. A valid task route
receives that context inside routeData.taskDetail.initialState. When
taskDetailRouteData cannot load the requested task, it returns nil, leaving
the top-level state without workspaces.activeId, workflows, or snapshots.
The client retry then renders the unavailable state, while
useWorkspaceSidebarTasks(null) intentionally scopes the sidebar to an empty
list to avoid leaking stale workspace data.
Backend
Conditional task-detail fallback boot state
- In
apps/backend/internal/backendapp/helpers.go, updatebootPayloadto constructinitialStateandrouteDatabefore assembling the final payload. - When the classified route is
webapp.RouteTaskDetailand route data is unavailable, call the existingbootStateBuilder.addHomeKanbanRouteState(ctx, req, initialState)path. This preserves URL/cookie/settings/default workspace precedence, Office-workflow exclusion, repository hydration, and all-workflow snapshots without adding a second data-loading implementation. - Only run that fallback for auth-disabled instances or requests carrying an authenticated identity. Auth-enabled anonymous bootstrap requests must keep the existing auth-only payload and must never invoke unscoped workspace or task loaders.
- Leave successful task-detail boot unchanged: its top-level initial state
remains lean and
routeData.taskDetail.initialStateremains authoritative for the task's actual workspace. - Preserve the existing not-found/access-denied ambiguity and request identity when listing fallback workspaces and tasks. No unscoped service call or new endpoint is introduced.
- Route-data errors remain intentionally represented by the existing generic unavailable state; the client-side task fetch still retries and determines whether the task can be rendered after the shell loads.
Frontend
Unavailable task recovery action
- In
apps/web/components/task/task-page-content.tsx, giveTaskLoadErrorStatethe resolvedworkspaces.activeIdand render an existingButton/routingLinktolinkToTaskOverview({ workspaceId: activeWorkspaceId ?? undefined }). - Keep the error copy generic and the main error surface unchanged. Add a stable test ID to the overview action for rendered regression coverage.
- Externalize the loading label, unavailable title/description, and new overview
action through
react-i18next; add matching English and pseudo-locale keys inapps/web/src/locales/en/common.jsonandapps/web/src/locales/pseudo/common.json.
Mobile design contract
- Desktop outcome: the global task sidebar remains populated from the fallback workspace, and a valid row can replace the unavailable route.
- Mobile entry point: the unavailable-state task-overview action is the
direct recovery path because
AppSidebaris intentionally hidden below the desktop breakpoint. - Nearest shipped exemplar: the task-overview back link in
apps/web/components/task/mobile/session-mobile-top-bar.tsx; reuse itslinkToTaskOverviewdestination and active-workspace context. The unavailable action uses a normal button/link rather than mounting the full task workbench or stacking a drawer onto an error state. - Hierarchy and presentation: the error remains the single focal surface; the recovery action follows its explanation and navigates directly to Home. No overlay, new scroll owner, or persisted responsive state is introduced.
- Geometry and input: the error container remains viewport-contained, and the action has at least a 44px active height on phone while retaining semantic link and keyboard behavior.
- Shared logic: both viewports use the same resolved workspace ID and link builder. Only responsive sizing differs.
Tests
- Missing task fallback boot: add a focused backend test in
apps/backend/internal/backendapp/helpers_test.gothat creates a visible sibling task, cold-builds/t/<missing-id>with an active-workspace cookie, and asserts that top-level boot state contains the selected workspace, workflows, and a snapshot containing the sibling while route-specific task data remains absent. - Anonymous bootstrap guard: assert that an auth-enabled request without a request identity keeps only auth/features state and does not load workspace, workflow, repository, or Kanban snapshots.
- Successful task boot remains lean: extend the backend coverage to assert
that a valid task route still provides
routeData.taskDetailand does not add fallback workspace/Kanban data to the top-level initial state. - Client route failure: extend
apps/web/src/task-detail-route.test.tsxso a rejected task-data request reaches the null-task shell without a state hydrator; this preserves the boundary between route failure and the unavailable surface. - The new overview action is component markup with no new branching helper; its destination, accessibility, and responsive behavior are covered by the desktop and mobile Playwright scenarios below.
E2E Tests
- Desktop scenario: extend
apps/web/e2e/tests/task/task-loading-state.spec.ts. Seed a sibling task, cold-load a missing/t/:id, asserttask-load-error-state, assert the global sidebar shows the sibling and not No tasks yet, then select the sibling and verify its valid task route opens. - Mobile scenario: extend
apps/web/e2e/tests/task/mobile-task-loading-state.spec.ts. Cold-load a missing task route, assert the unavailable state and overview action fit the Pixel 5 viewport with a touch-sized target and no document horizontal overflow, tap the action, and verify the seeded task appears on mobile Home. - Both scenarios use the production Go-served Vite build through the managed E2E runner so the route-aware boot payload—not only client navigation—is exercised.
Verification Results
- Backend contracts:
cd apps/backend && go test ./internal/backendapp -run '^(TestBootPayloadAnonymousMissingTaskDoesNotLoadHomeState|TestBootPayloadMissingTaskFallsBackToHomeKanbanState|TestBootPayloadValidTaskKeepsRouteSpecificState)$' -count=1— 3 passed. - Anonymous regression:
cd apps/backend && go test ./internal/backendapp -run '^TestBootPayloadAnonymousMissingTaskDoesNotLoadHomeState$' -count=1— 1 passed. - Client route boundary:
cd apps && pnpm --filter @kandev/web exec vitest run src/task-detail-route.test.tsx— 3 passed. - Recovery-link component contract:
cd apps && pnpm --filter @kandev/web exec vitest run components/task/task-page-content.test.tsx— 2 passed. - Frontend:
cd apps/web && pnpm run typecheck— passed;pnpm run i18n:check && pnpm run i18n:ratchet— passed. - Desktop E2E:
cd apps/web && pnpm e2e:run tests/task/task-loading-state.spec.ts -- --grep 'keeps sibling tasks available'— 1 passed. - Mobile E2E:
cd apps/web && pnpm e2e:run --no-build --project mobile-chrome tests/task/mobile-task-loading-state.spec.ts -- --grep 'returns to task overview'— 1 passed. - The managed runner rebuilt the production Go-served Vite bundle and cleaned temporary reports.
Implementation Waves And Parallel Candidates
Wave 1:
Wave 2 (depends on Task 01):
Both tasks are sequential. The rendered regression depends on the backend boot payload change, and no subagent execution is authorized by this plan.
Risks and out of scope
- The fallback intentionally loads the same snapshots as Home only after task route-data failure; accidentally running it for successful routes would add duplicate work and could briefly select the wrong workspace.
- Authorization must remain request-scoped. Tests should assert selected
authorized state, not weaken task
NotFoundbehavior to distinguish missing from inaccessible IDs. - Office task routes, task API semantics, sidebar presentation, and retry policy remain out of scope.