# P5-04b — Table/Board integration Source checkpoint: `6fc8937701a073f89fb680235bce58029a27550b`, branch `codex/ui-redesign-phase4`. This slice implements only the approved [P5-04b plan](../PHASE5_PLAN.md). The [separate readiness checkpoint](../p5-04b-readiness/README.md) established matching-ID parity first; its backend contract, authorization, ordering and complete Board response are unchanged here. ## Structure and compatibility - `/jobs` defaults to Table; `/jobs?view=table` and `/jobs?view=board` explicitly select presentation. Unknown view values safely use Table. No per-account presentation preference or SavedView schema is added. - `/kanban` remains an authenticated route. Its replace redirect targets `/jobs`, sets `view=board` and retains all other query parameters, hash and router state. A conflicting `view=table` on the legacy **Board** URL is overwritten so historical Board links still mean Board. The alias does not add a history entry or redirect externally. All 31 route/compatibility patterns remain. - The existing Jobs sidebar destination now covers both views; the separate Board child is removed. Discover, Reminders, Companies, Offers, Trash and other destinations remain. No provider/auth/permission change or command-catalogue expansion. - App retains the shared heading and Add Job action. The existing `JobTable` is the bounded workspace owner: one responsive filter/saved-view toolbar, a labelled Table/Board link group, matching-result count, and either the original Table/mobile cards or `KanbanBoard`. A new wrapper/framework would duplicate the existing mature filter owner, so none was introduced. - The view links have `aria-current="page"`, text labels, shared theme focus treatment and distinct filled/outlined states. They use native navigation links, support keyboard Enter and remain mounted across switches. Activating the current view is a no-op. New view choices push history; filter edits retain the established replace behavior. Back/Forward restores the recorded query/view. ## State and lifetime contract Search, status (including raw custom values), company, location, follow-up, readiness, include-deleted scope and sort parameters survive switching. The Table's page stays in the URL, is ignored by the Board API parser, and is restored on return. Changing filters or applying a saved view resets the Table page as before. Board grouping/stage ordering still controls visual card placement; Table sort does not redefine Board columns. Table selection is explicitly page/view-local: it clears when leaving or re-entering Table, including history traversal. Hidden selections cannot drive bulk actions. Comfortable/Compact remains local to the mounted workspace and survives a view switch; persisted page-size/column preferences keep their existing owner. Density/columns/pagination/bulk controls are Table-only. Trash always renders Table, even with a crafted `view=board` parameter. Job opening still records the originating row/card on its history entry. Full workspace Back and browser Back restore the originating query and focus. Switching presentation clears only the transient `focusJobId` hint on the new history entry, preventing an old job-return hint from stealing focus from the switch; unrelated route state is retained. ## Loading and performance Board code is lazy-loaded on first selection, with an accessible local fallback. Only the active results representation is mounted. The Table controller/filter owner remains mounted, but its `useViewResource` read is disabled while Board is active; it reloads on return. Table data/preferences can remain in local memory during the switch, not in a new shared/account cache. Board unmounts on leaving and reloads on return. It receives the existing creation refresh token, so Add Job refreshes whichever representation is active. Board query edits debounce for 250 ms using the existing hook, avoiding a full read per keystroke. Initial Board entry reads immediately. One settled Board query uses one complete read; there is no Table-page crawl, arbitrary page size, hidden full-result preload or duplicate Table read. The native regression counts Jobs-result requests across both switch directions. Company-filter metadata still loads through the existing `useCompanies` owner and is now available in Board too; the one-read statement does not claim the entire application makes only one HTTP request. Reload/error behavior and status PATCH → membership reload remain unchanged. **Residual ceiling:** Board still returns and renders the full matching array (O(N)); this integration does not add a result cap or virtualization. Server data can change between separate reads; matching-ID parity is the same-query/stable-data guarantee established by readiness, not a database snapshot across requests. Large-account virtualization/paging requires separate design work. ## Preservation traceability This maps affected contracts, not exhaustive coverage of every branch in the 57-contract catalogue. | Contract | Existing behavior → integration | Automated evidence | Visual/UX evidence | |---|---|---|---| | PRES-J03/J04 | Reviewed creation/capture → existing Add Job action now shared with Board, same modal and post-create refresh | Existing quick-capture/command/create regressions and extension checks; new active-view creation-refresh case | Shared Add Job entry visible in Table and Board screenshots; no new creation flow | | PRES-J05 | Filters, saved views, sort/page/columns, Table/mobile actions → shared query owner and view links | `jobs-view-integration.test.tsx`, retained `job-table-density`, native `board-presentation` membership/history cases and `table-density` | Same filtered population, page-two return, desktop/mobile Table, EN/NB and both themes | | PRES-J06 | Bulk/edit/Trash/restore → selection cleared at view boundary, no Board bulk UI | Existing partial-bulk/Table/Trash tests; new Trash view-parameter and selection-lifetime checks | Retained Table controls; density/browser suite | | PRES-J07 | Full population, custom statuses, pointer/keyboard moves and precise menu → existing Board renderer below shared filters | Retained 15 Board unit cases; native move/failure/custom/filter tests; integration membership/count checks | Keyboard switch/drop/focus, custom query, empty/error/loading, narrow Board | | PRES-W01/W02 | Open job/full workspace/Back and status progression → canonical query preserved | Native Table row Back and Board card Back; unchanged workspace/status tests | Focus returns to initiating row/card | | PRES-E07 | Navigation, mobile shell and active destination → one Jobs item, Board alias remains | Native legacy replace/history/hash/state case; updated shell test now asserts Jobs current + Board switch current | Desktop/rail/mobile shell retained | | PRES-E08 | Locale/theme/focus/error semantics → existing MUI tokens and labels | Full frontend/browser suites; integrated axe/focus checks | Light/dark × English/Bokmål, phone/tablet/desktop | | PRES-E09 | Account/private-state disposal, authenticated reads → unchanged provider/API boundaries | Full B02/account isolation and backend parity/ownership regressions; native fixture ID ownership comparison | No new private storage or identity source | | PRES-S03 | Page-size/column/theme/language preferences → existing mechanisms retained | Existing preference/density tests plus local density switch test | Comfortable/Compact retained in Table; no Board density claim | ## Files and scope Application changes are limited to `src/App.tsx` (alias/sidebar route wiring), `components/JobTable.tsx` (existing shared owner and conditional renderer), `components/KanbanBoard.tsx` (embedded toolbar removal, debounce, active refresh/count), and `i18n/translations.ts` (EN/NB view/count labels and workspace subtitle). No backend, schema, query parser, drag/drop engine, authentication, theme, CV, package or production infrastructure change. Tests: new `jobs-view-integration.test.tsx`; updated Board unit assertions move the old standalone switch responsibility to the integration fixture; `e2e/board-presentation.spec.ts` adds canonical routing/history and both-view evidence without removing movement/failure checks; `shell-readiness.spec.ts` reflects the approved single Jobs destination while retaining rail/mobile/heading-focus coverage. `kanban-theme.spec.ts` now polls the observable no-horizontal-scroll result after resize, allowing the shared responsive toolbar's React update to settle; it records the measured samples and still fails persistent overflow. ## Validation and evidence Inherited baseline: 934 backend, 384 frontend, 76 actual browser passes + P2-D01 expected failure; rate-limit 9, ingress 6, PDF 1, configuration 3. Final validation is complete; no unexpected failures remain. Six new frontend cases cover: both-direction history/page/density/selection and hidden-read behavior; custom-status membership; zero-result membership; shared Board filters; Trash isolation; and saved-query application plus active creation-refresh. The new native browser case proves legacy replacement (including conflicting view, hash and router state) and unknown-view fallback. Existing native Board coverage additionally exercises both-direction ID parity, page-two return, switch focus, one request per selected view, no-results/custom queries and four locale/theme combinations. Existing move/error/Back assertions remain. | Check | Current result | |---|---| | Backend Release regression suite | 934 passed, unchanged | | Frontend Jest suite | Final-code run: 390 passed in 78 suites, 119.9 seconds; 384 + 6 integration cases | | Full native browser suite | 77 actual passes + unchanged P2-D01 expected failure, 9.8 minutes; 76 + 1 legacy-route case. Zero skipped, unexpected or flaky outcomes | | Rate-limit / production-mode ingress / PDF admission | 9 / 6 / 1 passed, 10.3 / 6.4 / 7.7 seconds; unchanged | | TypeScript / ESLint | Passed | | Frontend production export / API Release `-warnaserror` | Passed; API zero warnings/errors | | Ingress configuration | 3 passed | | Supply-chain policy / Ollama evaluation / browser extension | 3 / 5 / 3 passed | | Staged secret scan / whitespace / local Markdown targets | Passed; links check local files, not external URLs or anchor fragments | Commands use the repository-required `rtk`/`rtk proxy` wrapper. UI commands: `npm.cmd test -- --runInBand --silent`, `npm.cmd run typecheck`, `npm.cmd run lint`, `npm.cmd run build`; build uses `NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api`, `CI=false`. Backend: `C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj -c Release --no-restore --logger 'console;verbosity=minimal'` and `build JobTrackerApi/JobTrackerApi.csproj -c Release --no-restore -warnaserror`. Offline checks: `python scripts/test-ingress.py`, `python scripts/test-supply-chain.py`, `python scripts/test-ollama-evaluation.py`, `node --test browser-extension/tests/*.test.mjs`, `python scripts/supply-chain.py scan-secrets`, `git diff --check`, `git diff --cached --check`. Native browser command: `npm.cmd run test:e2e -- --reporter=line,json`, with `DOTNET_ROOT=C:/Users/Connor/.dotnet`, `DOTNET_HOST_PATH=C:/Users/Connor/.dotnet/dotnet.exe`, `PLAYWRIGHT_STATIC_EXPORT=true`, `PLAYWRIGHT_PREBUILT_EXPORT=true`, `PLAYWRIGHT_JSON_OUTPUT_NAME=test-results/integration-accepted.json`. Security commands ran sequentially against the same local API port with the same .NET environment: `npm.cmd run test:rate-limits -- --reporter=line`, `npm.cmd exec playwright test -- --config=playwright.ingress.config.ts --reporter=line`, `npm.cmd run test:pdf-admission -- --reporter=line`. These are local regressions, not production ingress proof. ### Rendered visual/interaction review Evidence is synthetic local data captured through the actual API + exported application, not mocked UI screenshots. Reviewed the actual rendered images for readable hierarchy, status labels, restrained shared surfaces, wrapping, selected links, focus and result context: Capture date: **2026-09-29**, local Playwright Chromium, standard synthetic owner (27 fixture jobs including one custom stage), EN/Bokmål and current test clock; no real user data or provider. The [readiness screenshots](../p5-04b-readiness/README.md) and [P5-04a record](../p5-04a/README.md) are the pre-integration reference. This is reviewed state evidence, not a clock-frozen pixel-diff claim. Existing fixture viewport heights are retained (320×720, 768×900, 1024×900); the broader Phase 5 visual protocol's other heights/1920-wide matrix is not newly certified by this bounded slice. Resolve this implementation's SHA with `git log -1 -- docs/redesign/phase-5/p5-04b/README.md`, avoiding a self-referential hash placeholder. - Desktop 1440×900: [Table light/EN](evidence/integrated-table-desktop-light-en.png), [Board light/EN](evidence/board-desktop-light-en.png), [Table dark/NB](evidence/integrated-table-desktop-dark-nb.png), [Board dark/NB](evidence/board-desktop-dark-nb.png). - Phone 390×844: [Table light/EN](evidence/integrated-table-phone-light-en.png), [Board light/EN](evidence/board-phone-light-en.png), [Table dark/NB](evidence/integrated-table-phone-dark-nb.png), [Board dark/NB](evidence/board-phone-dark-nb.png). All four theme/language combinations are captured in the native test output; this committed set retains representative pairs. - Narrow 320×720: [Board dark/NB](evidence/board-phone-320-dark-nb.png), with wrapped follow-up label and retained filter/switch access. Native overflow assertions also cover 768 and 1024 widths. Desktop Table retains its established internal horizontal scrolling for optional columns/actions; this is not page-level overflow. - Shared query: [24 matching Board jobs with keyboard switch focus](evidence/integrated-filtered-board-keyboard.png), [page two of that same query](evidence/integrated-filtered-table-page-two.png), [custom status](evidence/integrated-custom-status-board.png), [zero results](evidence/board-zero-query-light-en.png). - [Retryable error](evidence/board-error-light-en.png) and [empty state](evidence/board-empty-light-en.png). Lazy loading and actual network loading are both exercised; retry does not fall back to an unfiltered read. Native keyboard automation verifies Enter on the view link keeps focus on that same control after content replacement; the screenshot shows the Phase 4 focus ring. Existing pointer/keyboard move, custom-stage menu, failed-write, row/card Back and shell drawer checks remain active. These are automated browser interactions plus human-style screenshot review by the agent, not a manual screen-reader session. The inherited post-axe missing-glyph screenshot artifact remains an evidence limitation; no typography/icon fix was smuggled into integration. Development checks found and corrected test-only label/matcher/type assumptions. The old loading fixture initially observed the new lazy-code fallback before its network handler existed; it now waits for the actual Board request before releasing its simulated error. No production behavior was weakened to make that fixture pass. The first full browser run (13.2 minutes) had 73 actual passes + P2-D01 expected failure and four unexpected Table-density failures (one per theme/locale). The added result summary duplicated the existing visible loading message: `main.getByText(t.loading, { exact: true })` found two elements at `table-density.spec.ts:235`. The summary now reports only settled results; the established results area owns loading/error feedback. The original four assertions were left unchanged. This was an integration regression, not inherited debt or an expected failure. The [next browser run](evidence/second-browser-summary.json) (10.1 minutes) passed all four unchanged Table-density cases and had 76 actual passes + P2-D01 expected failure, with one unexpected failure: the old immediate post-resize Board assertion read 27px horizontal scroll at 375px. Its failure screenshot showed the settled mobile toolbar fitting the viewport; the same test had passed in the first full run. Integration adds a `useMediaQuery`-driven shared toolbar, so reading the frame immediately after `setViewportSize` is not a stable readiness check. The assertion now uses bounded Playwright polling with the same zero-scroll expectation and records samples. No application overflow-hiding rule or relaxed threshold was added. Three focused repetitions passed after that test correction: [measured resize samples](evidence/resize-samples.json) were **[27, 0], [27, 0], [0]**. This independently confirms the brief pre-reflow measurement and subsequent zero overflow, rather than merely obtaining a lucky passing rerun. The [final full browser result](evidence/final-browser-summary.json) records **77 actual passes plus P2-D01's unchanged expected failure**, with no retries, unexpected failures or skipped cases. Its resize sample is `[0]`. The fixture retains all prior Board movement/failure and Table density assertions. **Accessibility evidence limits:** final Board phone axe checks reported no violations but left `color-contrast` incomplete for eight nodes in each theme. The empty Board check left six contrast nodes and one `aria-prohibited-attr` node incomplete. These are unresolved automated checks, not passes or screen-reader acceptance. Keyboard/focus regressions pass and rendered screenshots were inspected; manual spoken-name/live-state acceptance remains mandatory for P5-15. No existing expected failure or incomplete check was reclassified to obtain this checkpoint. ## Open acceptance items / stop Keep open: P5-02 manual screen-reader spoken names/live-state acceptance for P5-15, native touch-drag verification, P2-D01 expected failure/native package proof, F04 production ingress evidence, Board full-array scaling, and readiness R01/R02 command-focus/CV baseline observations. Automated axe/keyboard checks do not close manual screen-reader/touch requirements. Other inherited debt remains with its existing owner. P5-06, P5-05's unapproved quick preview, CV prerequisites and B07 are outside this slice. Stop after the separate integration checkpoint; no subsequent implementation is authorized by this record. **P5-04b acceptance: complete within the approved scope.** The shared-query prerequisite remains satisfied; native membership, history, alias, active-view request and focus evidence prove the integration. No integration blocker remains. The open items above retain their separate acceptance owners; completion does not certify production ingress, native touch or manual screen-reader behavior.