diff --git a/docs/redesign/phase-2/TRACEABILITY.md b/docs/redesign/phase-2/TRACEABILITY.md index c6abe35..5e9a599 100644 --- a/docs/redesign/phase-2/TRACEABILITY.md +++ b/docs/redesign/phase-2/TRACEABILITY.md @@ -1,5 +1,7 @@ # Redesign traceability +Job section migration: [P5-08b implementation trace and evidence](../phase-5/p5-08b/README.md#preservation-impact-and-discovered-owners) maps W01–W12, C/M relationships, E07–E09 and Jobs return entries to seven URL-backed sections, existing domain owners, focused unit/native browser tests and synthetic rendered evidence. Frozen/native package proof is retained, personal Notes stays separate from application answers, and `followMode` remains metadata only. No revision, mail or backend semantics are rewritten; manual screen-reader acceptance remains open for P5-15. + Job workspace readiness: [P5-08b native/navigation/accessibility proof](../phase-5/p5-08b-readiness/README.md#preservation-traceability-and-changed-files) supplements W01/W04–W08, C01/C05/C07/C10 and E07–E09 with real SQLite/ZIP and four native locale/theme navigation cases. Historical P2-D01 rows below retain the original expected-failure evidence; the successor owns the repair/results. Connor explicitly deferred absent `followMode` UI to the mail phase while preserving metadata; manual screen-reader acceptance remains open. Job header/Overview: [P5-08a scoped inventory, preservation and evidence](../phase-5/p5-08a/README.md#preservation-impact-before-implementation) maps PRES-W01/W02/W03 entry, W04/W08/W11 links, J05/J06/J07/J09 entry and E07–E09 to the existing owner, semantic header/summary, progress keyboard access, five new unit cases and two local-browser cases. Deeper W05–W10/W12 owners remain regression dependencies; native package proof, incomplete accordion/contrast checks and manual screen-reader acceptance remain open. diff --git a/docs/redesign/phase-5/OPEN_BLOCKERS.md b/docs/redesign/phase-5/OPEN_BLOCKERS.md index 854a281..c2b3d25 100644 --- a/docs/redesign/phase-5/OPEN_BLOCKERS.md +++ b/docs/redesign/phase-5/OPEN_BLOCKERS.md @@ -15,14 +15,14 @@ Reconciled against [P4-06](../phase-4/p4-06/README.md#opendeferred-issue-reconci | P5-02-D01 utility screen-reader operation | Automated axe and keyboard checks and command/notification semantic regressions pass; spoken names and live/state announcements have not been manually verified | **Open mandatory P5-15 accessibility acceptance item**. It does not block independent P5-03 Jobs work under Connor's explicit scope clarification. Operator steps and evidence request are in [P5-02](p5-02/README.md) | | P3-O02 workspace search replacement loses `followMode` | Addressed by [P5-08b readiness](p5-08b-readiness/README.md): only the section key changes; native history/reload retains query metadata | Query-retention prerequisite met; actual mode activation is the separate contradiction below | | P5-08b-R01 documented follow-up mode has no consumer | Connor explicitly decided on 2026-10-07: preserve `followMode` metadata and defer mode-specific UI; current absent consumer is not presented as implemented | **Deferred to mail phase; does not block P5-08b.** Preserve query compatibility without inventing send/generation; see readiness decision | -| P5-08b readiness rendered-capture limits and deeper presentation debt | Full-page fixed-header stitching and entry/tab transitions limit settled visual claims; existing narrow attachment table/empty-message clipping and backend-generated English text in Bokmål remain | Does not block the scoped native/query/ARIA readiness proof; later workspace presentation/localisation and existing shared-dialog acceptance owners retain the gaps. [Review limits](p5-08b-readiness/README.md#rendered-review-and-evidence-limits) | +| P5-08b readiness rendered-capture limits and deeper presentation debt | Historical stitched/transition captures remain limited. [P5-08b](p5-08b/README.md) adds viewport captures and a named keyboard-scrollable attachment table with an independently readable empty state | Scoped narrow document clipping is addressed; backend-generated English event/content text in Bokmål, shared-dialog timing and human assistive-technology acceptance remain open. Historical [review limits](p5-08b-readiness/README.md#rendered-review-and-evidence-limits) are not erased | | P4-02-D03 mobile progress scroll not focusable | **Closed for the targeted strip in [P5-08a](p5-08a/README.md)**: named keyboard region, native arrow scrolling, visible shared focus and current-step semantics | Scoped G-WORKSPACE 08a gate met; does not certify deeper accordions or full screen-reader acceptance | | P4-02-D04 literal AI context `{name}` | Addressed by P5-08b readiness using the existing interpolation argument; focused AI review regression protects the actual source name | Source/context/API unchanged; live-provider grounding certification remains separate | | P4-02-D02 company-save error outside accessible modal | Open; draft retained, transient toast hidden by modal isolation | **Does not block Phase 5 shell/navigation**. Company form/Phase 7 owner; direct company-form restructuring requires durable in-modal error. P5-00 toast position does not close this | | P4-04-O02 concurrent SQLite notification save | Open, earlier 2/10 native attempts HTTP 500 / database locked | **Separate backend reliability**; no shell/visual blocker with unchanged calls, but any increased polling/write concurrency waits for proof. Sequential passing test does not close it | | P2-O01 isolated rules-save 500 | Historical unconfirmed, cause not established | **Separate reliability triage** if recurrent; keep successful native oracle, no generic retry mask | | P4-05-O01 English status actions/browser-locale dates | **Closed for Jobs/Trash in [P5-03](p5-03/README.md)**: standard status display uses existing translations, custom status text and raw API keys remain stable, and applied/deadline display follows UI language | P5-03 localized table gate met in English/Bokmål; stored date/time semantics are unchanged | -| P2-D01 native SQLite tailoring/capture | Repaired and post-capture assertions reached in [P5-08b readiness](p5-08b-readiness/README.md): frozen source/file readback plus real SQLite/account ZIP and copy-failure/retry proof | Native-package prerequisite addressed; successor final validation owns the expected-to-actual browser transition. CV revision/geometry and production restore remain separate | +| P2-D01 native SQLite tailoring/capture | **Closed/passing** in [P5-08b readiness](p5-08b-readiness/README.md): frozen source/file readback plus real SQLite/account ZIP and copy-failure/retry proof | Ordinary regression retained in P5-08b; no expected-failure branch. CV revision/geometry and production restore remain separate | | F01 / audit D01 stale CV revision (related profile/job revisions distinct) | Open version increments without expected revision guard | **Blocks P5-09–14 broad CV**; separately authorized integrity work with cross-client proof. Profile/job persistence changes likewise gated, but their unchanged page links are safe. No automatic B07 | | Creation-intent idempotency | Open independent contract; not B02 identity or stale revision | **Separate backend/product work**; no Add/import/POST-on-mount expansion in Phase 5. Existing create review/no-write navigation must remain | | CV margins/templates/preview/pagination | Open, user concern reiterated during P4; current preview has extra pagination/cropping logic | **Blocks P5-09–14** via dedicated G-CV-RENDER corpus/repair checkpoint; keyboard passing does not prove geometry. No broad renderer replacement inside UI slice | diff --git a/docs/redesign/phase-5/README.md b/docs/redesign/phase-5/README.md index 6f147f7..71b7b7c 100644 --- a/docs/redesign/phase-5/README.md +++ b/docs/redesign/phase-5/README.md @@ -1,6 +1,6 @@ # Phase 5 — Representative screens / major UI redesign plan -**Current successor: [P5-08b readiness](p5-08b-readiness/README.md).** Native package/ZIP readback and scoped navigation/accessibility regression evidence supplement the unchanged [P5-08a header/Overview](p5-08a/README.md). Five sections, six deeper disclosures, routes and domain owners remain. Connor explicitly reconciled the absent follow-up consumer: preserve compatibility metadata and defer mode-specific UI to the mail phase. Readiness evidence now governs scoped P5-08b GO. [P5-07 Analytics](p5-07/README.md), [P5-06 Dashboard](p5-06/README.md) and [P5-04b Jobs integration](p5-04b/README.md) remain authoritative for their areas. P5-02 manual screen-reader acceptance remains mandatory for P5-15. F04, incomplete accessibility checks, native touch, Board scaling and recorded timing/preference-save observations remain open. P5-08b visual migration, Phase 8 and B07 have not started. +**Current successor: [P5-08b Job sections](p5-08b/README.md).** Seven URL-backed sections and Application/Timeline subsections recompose the existing owners on the Phase 4 foundations. [Native readiness proof](p5-08b-readiness/README.md) and the ordinary passing P2-D01 regression remain authoritative for frozen packages/ZIPs. P5-08a header/Overview and all compatibility routes remain. `followMode` metadata is preserved; mode-specific UI stays deferred to the mail phase by Connor's decision. [P5-07 Analytics](p5-07/README.md), [P5-06 Dashboard](p5-06/README.md) and [P5-04b Jobs integration](p5-04b/README.md) remain authoritative for their areas. P5-02 manual screen-reader acceptance remains mandatory for P5-15. F04, incomplete accessibility checks, native touch, Board scaling and recorded timing/preference-save observations remain open. P5-09 remains blocked by separate revision/rendering gates; no CV slice, Phase 8 or B07 has begun. Historical planning checkpoint: source `2a2f9e48052c562ef599825458d0d3a6f9f9fc61`, inspected on 2026-09-13 on `codex/ui-redesign-phase4`. That checkpoint was documentation-only; the successor record above owns implementation/validation status. diff --git a/docs/redesign/phase-5/p5-08b/README.md b/docs/redesign/phase-5/p5-08b/README.md new file mode 100644 index 0000000..f059ee6 --- /dev/null +++ b/docs/redesign/phase-5/p5-08b/README.md @@ -0,0 +1,146 @@ +# P5-08b — Job sections and preserved domain content + +Implementation from `acd1d94` on `codex/ui-redesign-phase4`. This checkpoint owns deeper workspace presentation and URL composition only; P5-08a header/Overview context and every existing domain/data owner remain intact. No P5-09 or B07 work. + +## Gates and composition + +The [readiness proof](../p5-08b-readiness/README.md) established native SQLite/ZIP contents, immutable readback after live-source mutation/deletion, attachments/ownership, numeric helper compatibility, history/query retention and scoped accessibility. P2-D01 is a genuine passing regression from that checkpoint. + +Seven approved top-level sections: Overview, Match, Application, Documents, Timeline, Notes & tasks, Interview. Application has URL-addressable CV, Letter, Answers and Submission subsections. Timeline has Activity and Correspondence. Existing legacy section URLs reveal their original owner without rewriting the incoming URL. The persisted numeric 0–9 route helper remains unchanged. Navigation preserves unrelated query metadata, including `followMode`, and Jobs return/focus state. The reviewed follow-up composer remains deferred to mail work. + +Presentation reuses MUI/Emotion, provisional primary and semantic surface/text/status tokens, Phase 4 PageSection/typography/focus. No new palette, font, cards framework or provider. Compact section summaries and restrained borders replace the old nested domain accordion collection; only job-detail disclosure remains in Overview. Each visible subsection mounts its existing owner, with existing letter/answer dirty guards covering subsection navigation. No implicit AI generation, freeze or sending. + +## Preservation impact and discovered owners + +| Contracts | Existing owner / invariants | P5-08b change and evidence required | +| --- | --- | --- | +| PRES-W01, E07/E08/E09; J05/J06/J07 entry | Workspace page, URL adapter, jobWorkspaceRoute, protected provider tree | Seven sections; secondary `sub` URL; every legacy/numeric alias, query/history, permissions and Jobs Back-focus regression | +| PRES-W02/W03 | JobDetails, Timeline, Analysis/Match/Strategy/StatusSuggestion | Overview detail owner retained; deterministic Match and stored Timeline events, source/unknown states; no new AI facts | +| PRES-W04/W10/W11 | ApplicationChecklist, FollowUp, ApplicationCalendarActions | Notes & tasks and calendar grouping; human Notes stays Job.Notes and uses existing EditJobDialog; checklist remains its own data type | +| PRES-W05/W06/W07; C01/C04/C05–C12 relationship entry | ApplicationCv/Tailoring, CoverLetter, PackageDrafts, SubmittedPackages | Application subsections; current selected CV/source vs draft vs immutable snapshot; attach/detach, versions, dirty cancel/leave, AI accept/reject and native package regression | +| PRES-W08 | Attachments real upload/metadata/AI consent/preview/download/delete | Documents presentation and narrow local table scroll; preserve filenames, purpose and ownership | +| PRES-W09/W10; M01/M02/M04/M05 entry | InterviewPrep/FollowUp, Correspondence/Gmail manual/provider paths | Scoped section surfaces, existing prep review and logged/imported history; no mail/provider/domain rewrite | +| PRES-W12 adjacent | Existing asynchronous operation progress/notification owners | Entry and current error states retained; no background engine change | + +Data remains account-owned by the same server services. Application drafts are neither an external send nor a frozen package. Frozen readback uses the exact captured CV/version/HTML, letter, material JSON and archived binaries, never current Career Profile/CV data. Documents exposes current attachments separately from frozen package history. Submission owns the explicit Freeze action; viewing and downloading old packages remains read-only. + +## Final section architecture and route compatibility + +| Entry | Visible content / existing data owner | +|---|---| +| `section=overview` | P5-08a identity, summary, progress and actions; existing full job-details disclosure remains | +| `section=match` | Analysis, recorded CV match, strategy snapshot; no generation on mount | +| `section=application&sub=cv` | Current selected CV, exact variant link, tailoring and existing review/save controls | +| `section=application&sub=letter` | Current editable letter and existing AI proposal/accept/reject boundary | +| `section=application&sub=answers` | Application answer and recruiter-message drafts; explicit Save/Discard and navigation guard | +| `section=application&sub=submission` | Explicit Freeze action and immutable historical package readback/download | +| `section=documents` | Current uploaded attachments, CV/letter entry links, read-only frozen history; no Freeze control here | +| `section=timeline&sub=activity` | Stored milestones/events and existing category filters | +| `section=timeline&sub=correspondence` | Existing logged/imported mail, provider status, import/link/unlink/manual entry and retained history | +| `section=tasks` | Human Notes using existing EditJobDialog; independent checklist/progress/reorder/dismiss actions and calendar export/provider actions | +| `section=interview` | Existing preparation, reviewed AI suggestions, manual answers/questions, readiness, debrief and follow-up | + +Legacy `analysis`/`match`, `cv`, `cover-letter`, `checklist`, `notes`, `documents`/`portfolio`, `timeline`, `communication`, `job-details`, `overview` and `interview` remain meaningful. Legacy CV/letter/correspondence keys take precedence over unrelated `sub` metadata. Unknown/prototype-like keys safely fall back to Overview. Canonical Application defaults to CV and Timeline defaults to Activity. The 0–9 helper still emits its original URLs, including the shared correspondence/follow-up destination. `/applications/:id` retains its existing compatibility redirect to `/jobs/:id`; no route/provider tree changes. + +Selecting a subsection writes durable `sub` URL state; selecting a primary section removes obsolete subsection state while retaining other query metadata and `location.state`. Back/Forward/reload and Jobs originating focus context remain protected. Direct deeper entry focuses the named panel only when focus is still on the document body; keyboard tab activation retains the tab focus. Existing overlay props are retained without adding a new host; hosted subsection state remains local because the existing callback only accepts a primary section. + +`followMode` round-trips unchanged and does not select a new compose mode. Connor's approved decision is **preserve compatibility metadata; defer mode-specific UI**. No follow-up send/generation is inferred from that query key. + +## Domain preservation and presentation changes + +- **Timeline:** source event arrays/order/types/IDs are unchanged. Locale-aware day headings replace backend English day labels. Semantic ordered lists and `time` elements expose recorded timestamps with an explicit UTC label, consistent with the server's UTC day grouping. This is not a timezone-model migration; stored event descriptions and user content remain literal. +- **Application/Documents:** selected current CV and editable letter/answer owners remain distinct from frozen history. Freeze is still an explicit existing operation, not proof of external submission. Frozen views show exact captured CV name/theme/version, archived HTML, original letter, material and file downloads. The existing embedded answer encoding is decoded for display into separate Notes/Answer headings without rewriting package JSON or stored data. Native manifests, filenames, byte content, version associations and owner filtering are unchanged. +- **Notes/tasks/calendar:** Notes edits use the original job editor, and a native assertion compares application answers after that write. Checklist tasks remain their own domain, with completion/order/dismiss controls intact. Calendar actions still export/create the existing deadline/follow-up events; no interview appointment, external provider or notification model is invented. +- **Correspondence/interview/AI:** existing manual/provider/import and review owners remain. AI suggestions still require their original explicit accept/reject flow; this slice never adds user facts, silently applies suggestions or sends mail. Provider-dependent behavior is covered by existing mocks where noted, not certified against live services. +- **Loading/errors:** shared PageSection and continuously mounted ViewStateNotice provide consistent headings and loading/error semantics. Genuine initial attachment/mail read failures previously looked empty or escaped error handling; they now show a local error/Retry without hiding the available manual entry. Other owners retain their existing retry/mutation contracts. Partial failure is not converted into a fabricated empty result. + +The six existing component files changed outside the composition adapter are all approved deeper-section owners. `ApplicationAssets`, `ApplicationIntelligence` and `InterviewPrep` consolidate their local wrappers onto PageSection/ViewStateNotice; no new wrapper framework is introduced. `ApplicationChecklist` uses the same sections and responsive add form. `Attachments` and `Correspondence` remove legacy decorative shadows/glass and permit narrow reflow. `translations.ts` contains EN/NB presentation/accessibility/error strings only. Header/Overview business semantics, API endpoints, DTOs, database, auth, billing, renderer, AppShell and CI are unchanged. + +### Scoped issues reproduced and resolved + +1. Attachment rows had eight cells but seven headers. The existing upload date now has its translated header. All existing metadata/AI-use/action controls remain in a named, focusable local scroll region; the empty message is outside the wide table, so it stays readable at 320px. +2. Package View disables its invoking button during asynchronous detail loading, so default dialog focus restoration could miss it. A scoped button ref restores focus on dialog exit; native Escape proves return to that View button. This does not close unrelated shared confirmation timing observations. +3. Expanded populated Notes/tasks axe found completed text at **4.48:1**, below 4.5:1. Completed text now uses `text.secondary`, retaining checked state and strike-through; it is readable content, not a disabled action. The same expanded scan passes after the change. +4. The existing interview answer textarea used only a placeholder below its item title. Its accessible name now includes the recorded question title through EN/NB localisation. The interview loaded-state scan protects this boundary. + +## Regression coverage + +The adapter tests were first run against the old five-section implementation and failed for the intended seven-section architecture/missing subsection resolver before implementation. Existing test assertions were migrated to the approved destinations, never replaced with generic visibility passes. + +Six new Jest cases: + +- `applicationWorkspace.test.ts`: legacy subsection precedence and canonical/unknown subsection handling (+2). +- `application-workspace-overlay.test.tsx`: dirty answer subsection Cancel/Discard plus metadata/no writes; correspondence versus human Notes ownership (+2). +- `application-assets.test.tsx`: read-only frozen history, exact captured content, separate Notes/Answer and no implicit capture/update (+1). +- `attachments.test.tsx`: real read failure versus empty, retry recovery and no mutation (+1); existing metadata case also checks all eight headers. + +Five new native Chromium cases in `job-sections-presentation.spec.ts`: + +- Four EN/NB × light/dark journeys create real account/job/CV/letter/answers/file/task/prep/correspondence data, verify all seven sections and legacy entries, protect dirty answers, explicitly save/freeze, read frozen history, return dialog focus, complete a task, edit human Notes without changing answers, retain `followMode`, and exercise responsive navigation/local document scrolling. +- One local fault-injection case holds then fails an attachment read, shows genuine error/Retry/recovery and independently fails correspondence reads. Only these read responses are mocked; it is not native archive/provider proof. + +Existing ordinary P2-D01 native browser proof still compares frozen CV/letter/material/binaries after live-source changes and reads the native ZIP. Existing readiness cases cover numeric aliases, history/reload and keyboard movement. Inherited AI rejection/edited acceptance, CV attachment/ownership, mail/import, calendar, Jobs Back focus and auth tests remain active. The 57-contract ledger remains the preservation source; mapping a contract here is not a claim of exhaustive coverage. + +## Validation and evidence + +Inherited baseline: backend 936, frontend 412, browser 88 actual passes, rate 9, ingress 6, PDF 1, configuration 3. Final unit run: **418/418, 80 suites** (+6); backend: **936/936**, zero failures/skips. Final complete Chromium run: **93 actual passes** (+5), zero skipped, flaky, expected-failure or unexpected outcomes, 13.5 minutes. P2-D01 remains an ordinary passing case. TypeScript, lint, production export, edited-browser TypeScript and API Release build pass. Security suites remain **9 / 6 / 1**, configuration **3**, unchanged. + +All commands use `rtk proxy`; npm commands run in `job-tracker-ui`. Specialized browser suites share port 5302 and run sequentially. Disposable raw traces/cookies are ignored; only safe summaries, invariant records and synthetic screenshots are retained. + +| Final command | Result | +|---|---| +| `C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj -c Release --no-restore --logger 'console;verbosity=minimal'` | 936 passed, 0 failed/skipped; unchanged count and native SQLite/ZIP proof retained | +| `npm.cmd test -- --runInBand --silent --json --outputFile=../.artifacts/p5-08b-jest.json` | 418 passed / 80 suites, 0 failures; +6 cases described above | +| `npm.cmd run test:e2e -- --reporter=line,json` | 93 actual passes, 0 expected failures/skips/flaky/unexpected; +5 new section cases | +| `npm.cmd run typecheck` / `npm.cmd run lint` | Passed | +| `npm.cmd run build`, `NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api` | Production static export passed | +| `C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi/JobTrackerApi.csproj -c Release --no-restore -warnaserror` | Passed, 0 warnings/errors | +| `npm.cmd exec tsc -- --project ../.artifacts/p5-08b-e2e-tsconfig.json --noEmit --incremental false` | All edited permanent browser fixtures type-check using existing frontend types | +| `npm.cmd run test:rate-limits -- --reporter=line` | 9 passed; same-IP account isolation, forged headers/session context and IP policy behavior | +| `npm.cmd exec playwright test -- --config=playwright.ingress.config.ts --reporter=line` | 6 passed; application-level proxy/host/scheme/CORS regressions, not deployed topology proof | +| `npm.cmd run test:pdf-admission -- --reporter=line` | 1 passed; actual cross-route render admission/recovery | +| `python scripts/test-ingress.py` | 3 configuration/Compose checks passed | +| `python scripts/test-supply-chain.py` / `python scripts/test-ollama-evaluation.py` / `node browser-extension/tests/capture-url.test.mjs` | 3 / 5 / 3 passed; local fixtures, not live provider certification | +| `python scripts/supply-chain.py scan-secrets` | Staged candidate passed; no high-confidence credential patterns | +| `git diff --check` / `git diff --cached --check` | Passed | +| `python .artifacts/p4-04-check-doc-links.py` | 143 current/root Markdown files, zero missing local targets; historical archive excluded | + +Browser runs use `DOTNET_ROOT=C:/Users/Connor/.dotnet`, `DOTNET_HOST_PATH=C:/Users/Connor/.dotnet/dotnet.exe`, `PLAYWRIGHT_STATIC_EXPORT=true`, `PLAYWRIGHT_PREBUILT_EXPORT=true`; main JSON output is `test-results/p5-08b-final.json`. No dependency, CI, package script or production configuration change was needed. Code stayed unchanged throughout final validation and supplemental visual inspection. + +The [safe full-browser summary](evidence/final-browser-summary.json), [native invariant records](evidence/native-invariants.json), [accessibility summary](evidence/accessibility-summary.json) and [screenshot manifest](evidence/visual-manifest.json) contain no authentication storage or raw trace. The main run retains 74 synthetic viewport captures across the five new journeys. + +### Accessibility evidence limits + +Eight new whole-mounted-page scans have **zero reported violations**, using WCAG 2 A/AA and 2.1 A/AA tags. Four scan populated EN/light Documents, Correspondence, Notes/tasks and Interview; four scan 320px Submission in all theme/language combinations. Inactive sections/provider dialogs are not scanned. **Every scan has incomplete checks**: 43 contrast node occurrences and 46 ARIA node occurrences across these captures, with repeated nodes counted per scan rather than unique defects. + +The ARIA incomplete findings identify existing Tooltip-cloned `aria-label` attributes on generic wrapper spans (45 checklist action wrappers, one interview Delete wrapper). The inner action buttons keep their independent accessible names; these findings are not declared passes or silently repaired here. Contrast incompletes include a short notification badge and overlapping field/background samples. Rendered review does not quantify every unresolved pair. Carry both rule types to the relevant shared-control/manual acceptance owners before P5-15. + +Keyboard tab activation, selected/panel naming, reload/history, Cancel/Discard, checklist actions, dialog Escape/return and local table scrolling have native browser evidence. Manual visual review means inspection of actual rendered captures. It is **not** a human screen-reader or physical touch session. P5-02 spoken names/live announcements remain mandatory and open for P5-15; no full assistive-technology acceptance is claimed. + +## Rendered, responsive and localisation review + +The permanent four native journeys exercise 1440×900 desktop, 768×1024 intermediate, 390×844 phone and 320×844 narrow phone, in EN/NB × light/dark. They assert no global horizontal overflow; the existing status strip/tabs and attachment table use deliberate local scrolling instead of squeezing/removing content. Native numeric/history journeys also retain the earlier 1280px/390px coverage. Actions retain their existing permissions and independent accessible names. + +Inspected **21 of 94 retained captures individually**; the manifest marks precisely which were reviewed. The other captures are retained evidence, not asserted to have received individual visual sign-off. Representative review covers all four theme/language combinations: desktop Match, current CV, letter, recorded Timeline, completed tasks and Interview; reopened frozen version; focused section navigation at 320/390/768; long NB title/company; local document-table focus; mobile Notes/Interview/CV/answers; actual loading/error/Retry/empty content. + +Twenty supplemental captures reuse an ignored copy of the same five native fixtures, scroll the relevant content into view and wait for populated controls. They add mobile Notes, Interview, CV and Answers plus four actual read-state frames. The [supplemental summary](evidence/supplemental-visual-summary.json) records **5 passed**, separate from the permanent 93-case baseline. Exact command: `npm.cmd run test:e2e -- --config=../.artifacts/p5-08b-visual-config.ts --reporter=line,json`, with the same browser environment above; its reporter output resolves under `.artifacts/test-results/p5-08b-visual.json`. + +The first supplemental attempt had 4 passes and one **capture-helper** timeout: Playwright waited for the continuously rotating progress SVG to become stable before scrolling. The ignored helper now scrolls its stable parent; all loading/error assertions remain, and the five-case rerun passes without application/permanent-test changes. The failed log/report remain ignored locally; the safe summary preserves the outcome/reason. This failure is not relabelled as an application pass or added to the main baseline. + +Limits remain explicit: some original phone screenshots frame the preserved P5-08a header with the relevant state below the fold; separate `content-*` frames show that state. Long tab sets deliberately scroll locally (including keyboard selection), rather than show all seven labels simultaneously at 320px. Frozen HTML remains a white, scrollable document canvas even in dark mode; the iframe's original geometry/templates are untouched and not certified by this UI review. Stored backend-generated English descriptions/events remain English in NB; authored text/custom status values are not translated. No live provider, physical touch, browser zoom corpus or exhaustive template/output comparison is inferred. + +## Files and rollback boundary + +Application changes: `src/applicationWorkspace.ts`, `src/views/ApplicationWorkspacePage.tsx`, `src/i18n/translations.ts`; existing `components/ApplicationAssets.tsx`, `ApplicationIntelligence.tsx`, `ApplicationChecklist.tsx`, `Attachments.tsx`, `Correspondence.tsx`, `InterviewPrep.tsx`. + +Five existing unit files and four existing browser files are adapted to the approved section composition; one new browser file supplies five cases. Documentation updates this scoped record/evidence, Phase 5 current index/open blockers, Phase 2 traceability and roadmap status. No shared AppShell, route table, dependency, CI, backend, schema or renderer file changed. + +Revert the adapter/composition and child presentation as one UI checkpoint if required; do not rewrite persisted packages/material or restore old data. If later compatibility links use `application/sub`, `timeline/sub` or the new primary sections, retain a route adapter exposing the original child when rolling presentation back. Existing legacy helper URLs must continue functioning in either direction. + +## Open debt and next boundary + +F04 production ingress evidence remains open. Human screen-reader spoken names/live-state acceptance remains open for P5-15, separate from automated keyboard/axe. Native Board touch, Board full-result scaling, Dashboard preference-save 500/SQLite contention, P5-07/shared-confirmation focus timing and incomplete accessibility/manual checks are not silently closed. The pre-existing attachment-preview dialog's Download path still constructs ID 0; the ordinary table/archive download paths are separate and unchanged. Record that existing defect for the later attachment owner rather than repair it in this section migration. `followMode` mode-specific UI is deferred to mail work. P2-D01 is **closed/passing**, not open debt. + +**P5-08b scoped acceptance: complete**, subject to the explicitly retained manual/provider/accessibility limits. Job sections/routes/history, drafts/Notes separation, immutable package/version/file/ownership proof and native entry controls remain green. + +**P5-09: NO-GO until G-CV-REV and G-CV-RENDER are independently satisfied.** Missing proof includes cross-client stale save/restore rejection with unsaved-work recovery, and measured preview versus real PDF geometry/template/page-break fidelity across the agreed corpus. P5-08b does not resolve those contracts or authorise B07, a renderer replacement, schema changes, or CV editing redesign. Stop before P5-09. diff --git a/docs/redesign/phase-5/p5-08b/evidence/accessibility-summary.json b/docs/redesign/phase-5/p5-08b/evidence/accessibility-summary.json new file mode 100644 index 0000000..a414c99 --- /dev/null +++ b/docs/redesign/phase-5/p5-08b/evidence/accessibility-summary.json @@ -0,0 +1,109 @@ +{ + "manualScreenReaderAcceptance": "open for P5-15", + "scans": [ + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "capture": "sections-documents-en-light-axe", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "nodes": 2 + } + ] + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "capture": "sections-correspondence-en-light-axe", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "nodes": 2 + } + ] + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "capture": "sections-notes-tasks-en-light-axe", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "aria-prohibited-attr", + "nodes": 45 + }, + { + "id": "color-contrast", + "nodes": 2 + } + ] + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "capture": "sections-interview-en-light-axe", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "aria-prohibited-attr", + "nodes": 1 + }, + { + "id": "color-contrast", + "nodes": 9 + } + ] + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "capture": "sections-axe-en-light", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "nodes": 7 + } + ] + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, dark)", + "capture": "sections-axe-en-dark", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "nodes": 7 + } + ] + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, light)", + "capture": "sections-axe-nb-light", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "nodes": 7 + } + ] + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, dark)", + "capture": "sections-axe-nb-dark", + "scope": "whole mounted page at capture; excludes inactive sections and provider dialogs", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "nodes": 7 + } + ] + } + ] +} diff --git a/docs/redesign/phase-5/p5-08b/evidence/content-sections-correspondence-error.png b/docs/redesign/phase-5/p5-08b/evidence/content-sections-correspondence-error.png new file mode 100644 index 0000000..62d7f84 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/content-sections-correspondence-error.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-error.png b/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-error.png new file mode 100644 index 0000000..ef4a761 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-error.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-loading.png b/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-loading.png new file mode 100644 index 0000000..327ca0b Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-loading.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-recovered.png b/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-recovered.png new file mode 100644 index 0000000..adbfbc9 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/content-sections-documents-recovered.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/final-browser-summary.json b/docs/redesign/phase-5/p5-08b/evidence/final-browser-summary.json new file mode 100644 index 0000000..15f687b --- /dev/null +++ b/docs/redesign/phase-5/p5-08b/evidence/final-browser-summary.json @@ -0,0 +1,852 @@ +{ + "statistics": { + "startTime": "2026-10-06T23:32:04.072Z", + "duration": 807118.4199999999, + "expected": 93, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "actualPasses": 93, + "expectedFailures": 0, + "unexpected": [], + "cases": [ + { + "file": "accessibility.spec.ts", + "title": "public entry points meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "accessibility.spec.ts", + "title": "primary authenticated workspaces meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "account-isolation.spec.ts", + "title": "retained profile request from A cannot write using B cookies and fresh B CSRF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "account-isolation.spec.ts", + "title": "missing and forged session contexts fail closed while same-account profile saves work", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "account-isolation.spec.ts", + "title": "crafted CV identifiers cannot cross read update delete or job-association ownership", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "account-isolation.spec.ts", + "title": "[P5-00 PRES-E05 E07 E08] account switch disposes open command, account and notification surfaces", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "account-isolation.spec.ts", + "title": "two tabs discard A's private form before B can use it, including Back navigation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "analytics-presentation.spec.ts", + "title": "[PRES-S01 S02 E07 E08 E09 J05 J09] Analytics native reports, direct entry, period, drill-down and owner isolation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "analytics-presentation.spec.ts", + "title": "[PRES-S01 E08] Analytics independent loading and failure states never masquerade as empty reports", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J07 J05 W01 W02 E07 E08 E09] Board query membership matches Table and preserves moves, focus and route", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J05 J07 E07 W01] legacy Board replaces history and preserves query, hash and route state; default Jobs stays Table", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J07 E08] Board loading, retryable error and fully empty columns", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] light/en preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] dark/nb preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] light/nb preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] dark/en preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "csv-export.spec.ts", + "title": "CSV export has safe cells and headers, and browser download preserves its UTF-8 bytes", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsive presentation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 E08] Dashboard loading and independent failed resources retain retry and exact values", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-discovery.spec.ts", + "title": "discovery remains usable across widths and opens the reviewed import", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-overview-presentation.spec.ts", + "title": "[PRES-W01 W02 W03 J05 E08 E09] native Job header/Overview retains actions, custom status, aliases and mobile progress keyboard access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-overview-presentation.spec.ts", + "title": "[PRES-W01 W02 E08] Job Overview loading, failed read, retry and unavailable resource remain explicit", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-sections-presentation.spec.ts", + "title": "[PRES-W01 W08 M01 E08] Job section loading and partial read failures stay explicit and recover locally", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-sections-presentation.spec.ts", + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-sections-presentation.spec.ts", + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, dark)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-sections-presentation.spec.ts", + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, light)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "job-sections-presentation.spec.ts", + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, dark)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "kanban-theme.spec.ts", + "title": "kanban columns use dark theme surfaces", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] light/en/1440 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] dark/nb/1440 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] light/nb/390 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] dark/en/390 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-C01 PRES-C06 PRES-C07 PRES-C10] mobile CV override persists without changing the canonical profile or sibling CV", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-W01 PRES-W04 PRES-W08 PRES-E08 PRES-E09] native workspace aliases, keyboard sections and history retain context (en, light)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-W01 PRES-W04 PRES-W08 PRES-E08 PRES-E09] native workspace aliases, keyboard sections and history retain context (en, dark)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-W01 PRES-W04 PRES-W08 PRES-E08 PRES-E09] native workspace aliases, keyboard sections and history retain context (nb, light)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-W01 PRES-W04 PRES-W08 PRES-E08 PRES-E09] native workspace aliases, keyboard sections and history retain context (nb, dark)", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03 PRES-W11] responsive settings navigation keeps rule edits and persists notification preferences", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03] rule changes persist through the real API", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-E07 PRES-W01 PRES-J04] keyboard command navigation and legacy workspace entry preserve the selected job", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-A01 PRES-S04 PRES-E07] non-admin navigation cannot grant API privileges or unlock CV AI", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J05 PRES-J07] light loaded status semantics, navigation and command focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-C06 PRES-C07 PRES-C08 PRES-C09 PRES-C11 PRES-S03] light loaded CV tabs, selects and settings", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J05 PRES-J07] dark loaded status semantics, navigation and command focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-C06 PRES-C07 PRES-C08 PRES-C09 PRES-C11 PRES-S03] dark loaded CV tabs, selects and settings", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] light/en Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] light/nb Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] dark/en Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] dark/nb Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] light-en command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-en utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] light-en real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] light-en grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E09 C06 S02 S03] light-en dirty editor survives shell utilities and cancel", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] light-nb command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-nb utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] light-nb real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] light-nb grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] dark-en command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-en utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] dark-en real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] dark-en grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] dark-nb command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-nb utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] dark-nb real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] dark-nb grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E09 C06 S02 S03] dark-nb dirty editor survives shell utilities and cancel", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "API liveness and database readiness are separate public probes", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "public plans stay honest, responsive and keyboard accessible", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "login establishes an authenticated session", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "a Free account keeps manual work available while AI actions stay honestly locked", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "a saved job can be created through the reviewed UI flow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "the dedicated application workspace survives deep links, long data and unsaved navigation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "Career Workspace loads from the authenticated application shell", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "a public CV renders anonymously and downloads as PDF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "smoke.spec.ts", + "title": "the Code template exports a long structured CV as searchable multi-page PDF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] light/en table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] light/nb table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] dark/en table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] dark/nb table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + } + ] +} diff --git a/docs/redesign/phase-5/p5-08b/evidence/native-invariants.json b/docs/redesign/phase-5/p5-08b/evidence/native-invariants.json new file mode 100644 index 0000000..9b8673e --- /dev/null +++ b/docs/redesign/phase-5/p5-08b/evidence/native-invariants.json @@ -0,0 +1,69 @@ +[ + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "evidence": { + "packageCapture": true, + "dirtyAnswersKeptAndExplicitlySaved": true, + "currentVsFrozenSeparated": true, + "personalNotesSeparate": true, + "checklistWriteSucceeded": true, + "legacyCvLetterCommunicationNotes": true, + "followModePreserved": true + } + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, dark)", + "evidence": { + "packageCapture": true, + "dirtyAnswersKeptAndExplicitlySaved": true, + "currentVsFrozenSeparated": true, + "personalNotesSeparate": true, + "checklistWriteSucceeded": true, + "legacyCvLetterCommunicationNotes": true, + "followModePreserved": true + } + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, light)", + "evidence": { + "packageCapture": true, + "dirtyAnswersKeptAndExplicitlySaved": true, + "currentVsFrozenSeparated": true, + "personalNotesSeparate": true, + "checklistWriteSucceeded": true, + "legacyCvLetterCommunicationNotes": true, + "followModePreserved": true + } + }, + { + "case": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, dark)", + "evidence": { + "packageCapture": true, + "dirtyAnswersKeptAndExplicitlySaved": true, + "currentVsFrozenSeparated": true, + "personalNotesSeparate": true, + "checklistWriteSucceeded": true, + "legacyCvLetterCommunicationNotes": true, + "followModePreserved": true + } + }, + { + "case": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", + "evidence": { + "packageId": 5, + "version": 1, + "cvVariantId": 20, + "cvVariantVersion": 1, + "attachment": { + "name": "evidence.txt", + "sha256": "4a89d7c5a8430781d00cb8a09550fe69c5dac43f571a8a83843347a5d69d9366", + "bytes": 31 + }, + "immutableReadback": true, + "canonicalAndVariantAndLetterAndAnswerMutated": true, + "liveAttachmentDeleted": true, + "siblingUnchanged": true, + "detachPreservedVariant": true + } + } +] diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-en-dark.png new file mode 100644 index 0000000..81036ae Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-en-light.png new file mode 100644 index 0000000..9a4b260 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-nb-dark.png new file mode 100644 index 0000000..ae8436f Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-nb-light.png new file mode 100644 index 0000000..e5d7460 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-application-320-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-en-dark.png new file mode 100644 index 0000000..64954f4 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-en-light.png new file mode 100644 index 0000000..f1c2f97 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-nb-dark.png new file mode 100644 index 0000000..d23a27c Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-nb-light.png new file mode 100644 index 0000000..c933495 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-communication-390-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-en-dark.png new file mode 100644 index 0000000..4305b05 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-en-light.png new file mode 100644 index 0000000..a196bea Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-error.png b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-error.png new file mode 100644 index 0000000..55f59a8 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-error.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-nb-dark.png new file mode 100644 index 0000000..a0ecf87 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-nb-light.png new file mode 100644 index 0000000..9425fdf Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-correspondence-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-en-dark.png new file mode 100644 index 0000000..3b9a565 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-en-light.png new file mode 100644 index 0000000..db6008a Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-nb-dark.png new file mode 100644 index 0000000..5addebe Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-nb-light.png new file mode 100644 index 0000000..2db4faa Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-current-cv-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-en-dark.png new file mode 100644 index 0000000..1113299 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-en-light.png new file mode 100644 index 0000000..71afc41 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-nb-dark.png new file mode 100644 index 0000000..f4cbcbd Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-nb-light.png new file mode 100644 index 0000000..4734296 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-document-table-320-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-en-dark.png new file mode 100644 index 0000000..c25d403 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-en-light.png new file mode 100644 index 0000000..4f0f4cd Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-nb-dark.png new file mode 100644 index 0000000..e452875 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-nb-light.png new file mode 100644 index 0000000..655fb75 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-320-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-en-dark.png new file mode 100644 index 0000000..f54d2b3 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-en-light.png new file mode 100644 index 0000000..0952354 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-error.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-error.png new file mode 100644 index 0000000..27e38f6 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-error.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-loading.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-loading.png new file mode 100644 index 0000000..27e38f6 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-loading.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-nb-dark.png new file mode 100644 index 0000000..c37e5a9 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-nb-light.png new file mode 100644 index 0000000..de94c94 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-documents-recovered.png b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-recovered.png new file mode 100644 index 0000000..abcb86c Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-documents-recovered.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-en-dark.png new file mode 100644 index 0000000..fd7b3b2 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-en-light.png new file mode 100644 index 0000000..9fd23c3 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-nb-dark.png new file mode 100644 index 0000000..fb8136d Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-nb-light.png new file mode 100644 index 0000000..297c19e Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-answers-320-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-en-dark.png new file mode 100644 index 0000000..869463d Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-en-light.png new file mode 100644 index 0000000..22d2b7c Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-nb-dark.png new file mode 100644 index 0000000..cc75e92 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-nb-light.png new file mode 100644 index 0000000..0e59519 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-application-cv-390-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-en-dark.png new file mode 100644 index 0000000..8138e2d Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-en-light.png new file mode 100644 index 0000000..f8ceb6e Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-nb-dark.png new file mode 100644 index 0000000..84796ac Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-nb-light.png new file mode 100644 index 0000000..e709e15 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-interview-content-390-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-en-dark.png new file mode 100644 index 0000000..1b3dc50 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-en-light.png new file mode 100644 index 0000000..82d7f1b Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-nb-dark.png new file mode 100644 index 0000000..d23133d Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-nb-light.png new file mode 100644 index 0000000..e41ae41 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-extra-tasks-content-390-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-en-dark.png new file mode 100644 index 0000000..6ac429d Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-en-light.png new file mode 100644 index 0000000..ccf7648 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-nb-dark.png new file mode 100644 index 0000000..9a03c29 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-nb-light.png new file mode 100644 index 0000000..0755d00 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-frozen-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-interview-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-en-dark.png new file mode 100644 index 0000000..003d6e2 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-interview-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-en-light.png new file mode 100644 index 0000000..c80ec08 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-interview-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-nb-dark.png new file mode 100644 index 0000000..9c9f76d Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-interview-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-nb-light.png new file mode 100644 index 0000000..0fb25e8 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-interview-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-letter-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-en-dark.png new file mode 100644 index 0000000..0c90e47 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-letter-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-en-light.png new file mode 100644 index 0000000..9012abb Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-letter-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-nb-dark.png new file mode 100644 index 0000000..a3b6802 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-letter-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-nb-light.png new file mode 100644 index 0000000..202d831 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-letter-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-match-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-match-en-dark.png new file mode 100644 index 0000000..3bbeba9 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-match-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-match-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-match-en-light.png new file mode 100644 index 0000000..3f3ec08 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-match-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-match-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-match-nb-dark.png new file mode 100644 index 0000000..1ee9eb2 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-match-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-match-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-match-nb-light.png new file mode 100644 index 0000000..549be6e Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-match-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-en-dark.png new file mode 100644 index 0000000..ff99270 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-en-light.png new file mode 100644 index 0000000..8b36299 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-nb-dark.png new file mode 100644 index 0000000..5cc40c7 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-nb-light.png new file mode 100644 index 0000000..c7a470a Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-notes-tasks-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-overview-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-en-dark.png new file mode 100644 index 0000000..1a48485 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-overview-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-en-light.png new file mode 100644 index 0000000..bb36994 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-overview-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-nb-dark.png new file mode 100644 index 0000000..fa27d21 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-overview-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-nb-light.png new file mode 100644 index 0000000..c1b7d58 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-overview-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-en-dark.png new file mode 100644 index 0000000..94879b6 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-en-light.png new file mode 100644 index 0000000..db9e29b Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-interview.png b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-interview.png new file mode 100644 index 0000000..09c5f52 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-interview.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-nb-dark.png new file mode 100644 index 0000000..45fbc00 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-nb-light.png new file mode 100644 index 0000000..b380450 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-tasks.png b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-tasks.png new file mode 100644 index 0000000..efbdf54 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-sparse-tasks.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-submission-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-en-dark.png new file mode 100644 index 0000000..064fbe3 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-submission-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-en-light.png new file mode 100644 index 0000000..fae848b Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-submission-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-nb-dark.png new file mode 100644 index 0000000..91b6c5a Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-submission-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-nb-light.png new file mode 100644 index 0000000..d5affba Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-submission-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-en-dark.png new file mode 100644 index 0000000..a036b1e Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-en-light.png new file mode 100644 index 0000000..bcdf3d0 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-nb-dark.png new file mode 100644 index 0000000..d3e47e7 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-nb-light.png new file mode 100644 index 0000000..658b0c8 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-768-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-en-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-en-dark.png new file mode 100644 index 0000000..15759ff Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-en-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-en-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-en-light.png new file mode 100644 index 0000000..1c989c3 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-en-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-nb-dark.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-nb-dark.png new file mode 100644 index 0000000..b89f508 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-nb-dark.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-nb-light.png b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-nb-light.png new file mode 100644 index 0000000..8ef36d9 Binary files /dev/null and b/docs/redesign/phase-5/p5-08b/evidence/sections-timeline-nb-light.png differ diff --git a/docs/redesign/phase-5/p5-08b/evidence/supplemental-visual-summary.json b/docs/redesign/phase-5/p5-08b/evidence/supplemental-visual-summary.json new file mode 100644 index 0000000..b350e60 --- /dev/null +++ b/docs/redesign/phase-5/p5-08b/evidence/supplemental-visual-summary.json @@ -0,0 +1,54 @@ +{ + "statistics": { + "startTime": "2026-10-06T23:49:14.245Z", + "duration": 113783.234, + "expected": 5, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "[PRES-W01 W08 M01 E08] Job section loading and partial read failures stay explicit and recover locally", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, dark)", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, light)", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + }, + { + "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, dark)", + "outcome": "expected", + "actualStatuses": [ + "passed" + ] + } + ], + "countedInMainBaseline": false, + "firstAttempt": { + "passed": 4, + "failed": 1, + "cause": "Screenshot helper waited for continuously rotating progress SVG stability; no application failure", + "resolution": "Scroll stable parent without suppressing loading/error assertions" + } +} diff --git a/docs/redesign/phase-5/p5-08b/evidence/visual-manifest.json b/docs/redesign/phase-5/p5-08b/evidence/visual-manifest.json new file mode 100644 index 0000000..1badbae --- /dev/null +++ b/docs/redesign/phase-5/p5-08b/evidence/visual-manifest.json @@ -0,0 +1,1064 @@ +{ + "sourceBase": "acd1d94", + "runtime": "Chromium production export, disposable local SQLite API, 2026-10-07", + "capture": "viewport, animations disabled, fonts ready; no stitched full-page images", + "screenshots": [ + { + "file": "sections-documents-loading.png", + "sha256": "6ce9268f392d6827564316563a39aace908337bf698f8256cbada8480a03da1a", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-documents-error.png", + "sha256": "6ce9268f392d6827564316563a39aace908337bf698f8256cbada8480a03da1a", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": true, + "reviewLimit": "Header-framed viewport; relevant state is below fold. Supplemental content captures/native assertions are separate." + }, + { + "file": "sections-documents-recovered.png", + "sha256": "0b72ab06e36be82615419bb03279829c9c3460f5a1fc2660c2e11ff28fdb0034", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-correspondence-error.png", + "sha256": "bfec1e2fc7fd7db289eb785cf02245c657852fdbe2808f3d67f073eefa4b1b39", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-sparse-tasks.png", + "sha256": "bf1068a1531a7eac42dc554b15120385b68011a39db877258e2766dd6c832d8c", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-sparse-interview.png", + "sha256": "a27145a6982951ec1ad368e3408f2f274aced8f9d745afdf148e5f73337e24b0", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-overview-en-light.png", + "sha256": "fd2aa715c53467a06ca278876006a4f8f6ccd0186da26be22a7a6da40897ea80", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-match-en-light.png", + "sha256": "dc9eaf6f4ec165837a797749e8150cc3361b7a5df10a0a346414babbb821ca4f", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-current-cv-en-light.png", + "sha256": "b36ceddc5fab97f9cac0a62af5ff6b100b08b439b01562591a1ec9794f3d9858", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-letter-en-light.png", + "sha256": "3fea274797205a372a56969d874e887dd6d1bd38ad5d450c8f170798480a0828", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "sections-submission-en-light.png", + "sha256": "69212ed78a2c2ac5792655994b8f8a602aca2525f2c4f97d3661bc51f96cb59d", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-frozen-en-light.png", + "sha256": "f5d854c40b006353c3360212f223f7a403b6f34b28a4151641d2489e26d18bc6", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "sections-documents-en-light.png", + "sha256": "507149fb0c69f1fdec2d0c1821f9b54ee3d529d25d3f6af0e5c923469d15ad77", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-timeline-en-light.png", + "sha256": "fc4d3e531f568c9f950fadb7a03dd32aa9ff07f0fc4765e54f09b8bfdd27b85c", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-correspondence-en-light.png", + "sha256": "af78b286026e279f031072a4e25290a9a1f0c1a5aa19a31f840eaea465880f2e", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-notes-tasks-en-light.png", + "sha256": "7a6e73d33e93474e5324ff85947afdc1aa4c70a96b4735e6bbc5f19d9f255571", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-interview-en-light.png", + "sha256": "d76c5f73860712f783c1fde3f7e6aa3194da9365d2401a98519e97f0c7a2d316", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "sections-timeline-768-en-light.png", + "sha256": "91d9a0a7aea8d26a6fd271f4f73b51661ce605ee4ef0891cfcff9b40f298a1b7", + "viewport": [ + 768, + 1024 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-communication-390-en-light.png", + "sha256": "947098963e3fb8ea18c798fa789a0147c3b97bc744fc0e40418b16e8f334748f", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-documents-320-en-light.png", + "sha256": "affdcb1456099215cb1517ea5fb1d827866d30f470f4500bbead2509b22d4e9d", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-document-table-320-en-light.png", + "sha256": "ef0c3f1c0efc9df2d02584a43201112db9f1ac9cd50d9ebc2b90012b7df75fb2", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-application-320-en-light.png", + "sha256": "780b772821e30d5596780230119af0b9d09214a2215cce06676f7472400f65da", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-sparse-en-light.png", + "sha256": "f15448d5feccedd90b42d5298c15500c8ec509a9bb5f16e3b17f7ed291cc213d", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-overview-en-dark.png", + "sha256": "a43eed69cd1ab899768818db91a38c83b5f38fa71f055aff64ac53f52d516269", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-match-en-dark.png", + "sha256": "5333ec062e2273557c057263d97e51368dac0862be6e1ab961253c12c09cbd6c", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-current-cv-en-dark.png", + "sha256": "ce4751ec6010acfe860be1b372182060e7e49ed19083c271e357e5e184d14923", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": true + }, + { + "file": "sections-letter-en-dark.png", + "sha256": "b8b4740bf8f3928842492c420424c5f1fe20ae81a9fe7e0418bf813bd3e61829", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-submission-en-dark.png", + "sha256": "9974b2bd6162f7f2cf032d34d58be04c74e38b37753ee40d58226b871720f121", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-frozen-en-dark.png", + "sha256": "044771090a443bc87661fab616093d92f6befc9e6036457491421a9083788adf", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-documents-en-dark.png", + "sha256": "8ac5e99815fe76f6ee541f62d5602847f33e1f5429e65e251f85e4ec6a620922", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-timeline-en-dark.png", + "sha256": "150462cf6fad09dcfa9fe350f6e3afb0b088e6145c09dc04434e1f6c23b4059e", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-correspondence-en-dark.png", + "sha256": "5efc4e3ab1ec5450950d1bb0095936b8c4a393a96360251adcddedfee4782275", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-notes-tasks-en-dark.png", + "sha256": "7fba4630cdf8fdb34e7ab56eda0448dbe88e17e8d3b236c47527fc390efd9d45", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-interview-en-dark.png", + "sha256": "2430d565fb041a5fb9205dfd0b0aa2e6d6d3e26d7c6f6347305431d23bfec3da", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-timeline-768-en-dark.png", + "sha256": "de194a6e81e3b5a2956256b90916b41a73a238317b167edb9346e1a3d41a9a87", + "viewport": [ + 768, + 1024 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-communication-390-en-dark.png", + "sha256": "4602ffe1395f5125ea9df5a1fa49b35764a2fabd43e29edee2be8367ce448962", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-documents-320-en-dark.png", + "sha256": "c5053bc4806a382af25767b107723255d3a8b4054b35f69d0261b34b6bc5ccf5", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-document-table-320-en-dark.png", + "sha256": "91eac664a0023aebc96afe9e7bafd51fbbbb9898da00239509e8bf63984cff08", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-application-320-en-dark.png", + "sha256": "ab35d0a949989018b3434fa6938d67c5336dead20edc71d5c852367fc80026af", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "en", + "renderedReview": true + }, + { + "file": "sections-sparse-en-dark.png", + "sha256": "ed008362db7113a34e69e790ec524ed69a903dba6364d87afcf07c398dbacef4", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "en", + "renderedReview": false + }, + { + "file": "sections-overview-nb-light.png", + "sha256": "1b692728bdd0ff7572fbca0bab26b416721c1d6b70de7ed2dfd180a7050c8a08", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-match-nb-light.png", + "sha256": "b8de4fab96dcb129da6975ec3cf13e5bfad1edb197b1517b0db83397129885e0", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": true + }, + { + "file": "sections-current-cv-nb-light.png", + "sha256": "a39d1dd71972291da9df455c767f947380d29293e9241b99a7953b1520afdac1", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-letter-nb-light.png", + "sha256": "d7b821f5f3a4c5a03867b1a4cb97406cc93008c9114b0e57a035436e7fedc605", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-submission-nb-light.png", + "sha256": "b10959bc855bb07f8ac316f31ed50899f139e20f46874cd15d84a98f790b5355", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-frozen-nb-light.png", + "sha256": "5cdfcd0af3316e5a5fd2ace0d3e87c72e2fba768e0dead96b9fda7c56e383e6d", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-documents-nb-light.png", + "sha256": "47908570ee8ef7b2a38742c522df1320a03af56afbdfc773a88185df50413673", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-timeline-nb-light.png", + "sha256": "69ca41edf8e688befaaf634c87578588386ec843967e809c0db57dbc2c6de4c7", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-correspondence-nb-light.png", + "sha256": "6810766de216b6f63b6f2377f51c441e695d5b67a6e50e7ea6145b93ceb16be2", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-notes-tasks-nb-light.png", + "sha256": "c9c2c3420bdb4331451b4178554aced13eda6a93a78302d190dd1b4d92838a1b", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-interview-nb-light.png", + "sha256": "eeb52525de64fcf4183eb0a598ea05e9a32d888694c58ad956de18a44a7eb1cc", + "viewport": [ + 1440, + 900 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-timeline-768-nb-light.png", + "sha256": "51283686da79e1f7b88d61aff740b306d9577d36e5ef5fa858c90f7817806fed", + "viewport": [ + 768, + 1024 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-communication-390-nb-light.png", + "sha256": "6e080fbf2c3a99a0e362f9a0699b9182c6779081e02a89c4a7c0cf9ec528e9f3", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-documents-320-nb-light.png", + "sha256": "325b31938bfcd342ce6c00e563a93796eff5b9054330f8282d38062994f7be6f", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "nb", + "renderedReview": true + }, + { + "file": "sections-document-table-320-nb-light.png", + "sha256": "83fc554ed17c4b0043838b21abc8d64587a6f1f8e19c4a6f4dda236cce178d41", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-application-320-nb-light.png", + "sha256": "9d53b6806d3666c34b8276bcb38eea5aad13f1a4c7f1a90dcc5793348b1aadea", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-sparse-nb-light.png", + "sha256": "ecdf3b59898bf3443c4782de08d7d5872ab4baf6eea157ee68301d24b0400031", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "nb", + "renderedReview": true, + "reviewLimit": "Header-framed viewport; relevant state is below fold. Supplemental content captures/native assertions are separate." + }, + { + "file": "sections-overview-nb-dark.png", + "sha256": "416d855e60bc13a8aac3ab61b00d7f900acff8caf21bb30cc66ec40e98b4edc0", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-match-nb-dark.png", + "sha256": "b611aac8d08928bbbb797c44e52970c4df525da1702820f40c7fe5ffccfb3379", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-current-cv-nb-dark.png", + "sha256": "e53a82a832e084cc5d098a7773f01c5e6b8c1d17574a8a0d9f1d9effad981de9", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-letter-nb-dark.png", + "sha256": "3bdc17fb55b1fa39210fb56edde8250280d9f44f31df87d51a83d7d48aac5228", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-submission-nb-dark.png", + "sha256": "7d6bfb24c6f3047cfda77a4811aae553a6e4c1f7f6852a919e8f3493de7e5137", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-frozen-nb-dark.png", + "sha256": "f79917833a78051b238eb203c1d9c0c10f39348e740d111428003a7d55978803", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-documents-nb-dark.png", + "sha256": "973cbca05426c96279862e9e731392adce43daf9303f370b11685defb4b5f881", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-timeline-nb-dark.png", + "sha256": "d7eb4988e114ef08d4d1b45603cb7c13aa4f40106859b8d4a85712d66cfbac30", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-correspondence-nb-dark.png", + "sha256": "feb4e863b453319208306b910e861c0d169cd3be4e7494c210f660fd5785b6ab", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-notes-tasks-nb-dark.png", + "sha256": "1e6f3906c50380e74c3c996befc0ccda1f2e9f9457c2f205de5e928bbe0f7b27", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "sections-interview-nb-dark.png", + "sha256": "e00e88bb83e9cae7ffec11503287492266fa4488c7b74d30ba5c44b1a6cc9102", + "viewport": [ + 1440, + 900 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-timeline-768-nb-dark.png", + "sha256": "2c1bcdeef11c61ced5262ee296970db9d43c030a30bac39baef65454e06d5c69", + "viewport": [ + 768, + 1024 + ], + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "sections-communication-390-nb-dark.png", + "sha256": "6324ac7ad8f7d15b27151b95d823277704d4121525ef5ccb9a49337a6c79d138", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "sections-documents-320-nb-dark.png", + "sha256": "0617da54ec619449f3c60248d205e7cd8014123239d414533701615f8f3ed797", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-document-table-320-nb-dark.png", + "sha256": "afe3d5b1c52464ff04994967cd731fd0b86df89ff642c211bdfe635c21463353", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "sections-application-320-nb-dark.png", + "sha256": "3cf03491193c76a2981523c42c17a7dc04dc4332ebe9d653b7e2ea2980635dee", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "sections-sparse-nb-dark.png", + "sha256": "70fff9c80f37808d884b3d7743c4208e604081bcb37f1cb01ad0240779426298", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "nb", + "renderedReview": false + }, + { + "file": "content-sections-documents-loading.png", + "sha256": "eb87b4ca2a6d9fe1f0e5c3104a471a139ba2356e206f471631e3fb80399daa3a", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "content-sections-documents-error.png", + "sha256": "c9739d918af9717540720be5a86e0285631ee671ba1d0e487777076f6ccdc591", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "content-sections-documents-recovered.png", + "sha256": "2ed545a6a796e09fadf247e8b4f0642fef83e536fe76005cba9010901544658d", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "content-sections-correspondence-error.png", + "sha256": "e71329fb8cf4cec9251e1af9238662b89a329dbd2ce3a9ab8e8de24e70671b53", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "sections-extra-tasks-content-390-en-light.png", + "sha256": "d2cc6fdfee0ef5d3bb36697909df4fdbbffc738bec5c5d4e4273a09c2c0f8de6", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-interview-content-390-en-light.png", + "sha256": "b0b068509f86b66d466aee50d2832de336156545a6f47ce86ad489deb1791e40", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-application-cv-390-en-light.png", + "sha256": "8e3c2f37119b371a3cb372a951e555716c84edded5cb738007348b552111191b", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "sections-extra-application-answers-320-en-light.png", + "sha256": "f4ca2ea82f85e3c6bbd9bab291d399f66bdf551e35dfa350e9543066cf611c6d", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-tasks-content-390-en-dark.png", + "sha256": "6d0aa99c2131058d2bda4b07e0af74e745846c62feddda7a59bd1d47f723c481", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-interview-content-390-en-dark.png", + "sha256": "3d2be570835c69842ad5d71dd3707289644831e3ce10dabbfcb9ae3cb4429d23", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-application-cv-390-en-dark.png", + "sha256": "39a6e6b8c984965344ecb5e407bba98ed33484e9306fae1eb1e4c1634311344c", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-application-answers-320-en-dark.png", + "sha256": "c9babc45399d5efb66c8f9e737aff7a83cc6dc3e93bf2177d9028e693edf2eae", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "en", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-tasks-content-390-nb-light.png", + "sha256": "b73f9c046be8e4d0bfcecb713d7eb55c9ad31eeeadd3ccea9be4f8e3cc890303", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-interview-content-390-nb-light.png", + "sha256": "dd7a8b002c959063144e9d084b5c981b1185a7dd78fb2fc559cf1c7dfc8a69ee", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-application-cv-390-nb-light.png", + "sha256": "c5027d705aa878ff00ff50a20f4022b1dfecfdd0acb8ed234e4b5f54fac01dde", + "viewport": [ + 390, + 844 + ], + "theme": "light", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-application-answers-320-nb-light.png", + "sha256": "92c0a44af6b7d244d7f97dd26c8833739afd703589872bc24b1354ea08ccdb13", + "viewport": [ + 320, + 844 + ], + "theme": "light", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "sections-extra-tasks-content-390-nb-dark.png", + "sha256": "ece6738bd1c185877141b5b6c87b109afc6458f443bb8d6a3526e04714df8c9f", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "sections-extra-interview-content-390-nb-dark.png", + "sha256": "5d2ac9a350be47b853a90f979a722158e3d31c2e83e833b776a0b810bc870f57", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": true + }, + { + "file": "sections-extra-application-cv-390-nb-dark.png", + "sha256": "a8370dbb3e2974a3e7b8158de1267e95c60b57d3a1eeeccbd3eb29367d6c3ba5", + "viewport": [ + 390, + 844 + ], + "theme": "dark", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + }, + { + "file": "sections-extra-application-answers-320-nb-dark.png", + "sha256": "859e516641e0134c90799fd9efc50bdeb699a86cffc6c33888128bce21f03d3c", + "viewport": [ + 320, + 844 + ], + "theme": "dark", + "language": "nb", + "source": "supplemental content-focused probe using existing five native fixtures; not new CI cases", + "renderedReview": false + } + ], + "supplementalNote": "The first supplemental loading capture timed out waiting for a rotating progress SVG to be stable; scrolling its stable parent fixed the ignored capture helper. No application or permanent test was changed. Final five supplemental cases pass." +} diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md index dbff000..35cbc2c 100644 --- a/docs/roadmap/todo.md +++ b/docs/roadmap/todo.md @@ -4,6 +4,8 @@ Status reconciled: 2026-10-07. ## Current repository work +- **P5-08b — Job sections complete:** [section architecture/preservation record](../redesign/phase-5/p5-08b/README.md) owns seven URL-backed sections, Application and Timeline subsections, current versus frozen documents, readable Timeline/Notes/tasks/correspondence/interview presentation and local read-error handling. Existing native packages, legacy/numeric links, query/history, Jobs Back focus and all domain owners remain. Final baseline: **936 backend / 418 frontend (+6) / 93 actual browser passes (+5)**, zero expected failures; security 9/6/1, configuration 3. Eight scoped mounted-page scans have no violations but retain incomplete findings; human screen-reader acceptance is not passed. Stop before P5-09: revision/rendering gates remain; F04 stays open. Historical checkpoint counts below are not current expected failures. + - **P5-08b readiness complete — GO:** [native package/navigation/accessibility record](../redesign/phase-5/p5-08b-readiness/README.md) owns the provider-compatible capture repair, frozen source/file/account-ZIP proof, existing disclosure destinations, retained query/history and scoped ARIA/keyboard fixes. Backend 936 (+2), frontend 412 (+4), browser 88 actual (+4 new and P2-D01 converted to a real pass), zero expected failures; security 9/6/1 and configuration 3. Connor approved preserving follow-up metadata and deferring absent mode-specific UI to the mail phase. No deeper redesign, CV or B07 work. F04 and manual screen-reader acceptance remain open. Older checkpoint counts below are historical. - **P5-08a — Job header and Overview complete:** shared role/company/status/actions header, recorded date summary, native preparation entries and keyboard-scrollable progress; five section keys/six deeper disclosures and all route/domain owners retained. [Record](../redesign/phase-5/p5-08a/README.md). Baseline: 934 backend, 408 frontend (+5), 83 actual browser passes (+2) plus unchanged P2-D01 expected failure; security 9/6/1, configuration 3. P4-02-D03 closes locally. P5-08b is **NO-GO pending native package proof and its own adapter/draft/AI/deeper-accessibility gates**; no B07 or deeper migration. F04 and manual screen-reader acceptance remain open. @@ -26,8 +28,8 @@ Status reconciled: 2026-10-07. - **Phase 5 plan:** [bounded plan](../redesign/phase-5/README.md) maps all 57 contracts/31 routes. Board integration, native package and CV revision/rendering gates apply only to dependent slices. F04 remains open. - **P2-D01 — successor native proof:** [P5-08b readiness](../redesign/phase-5/p5-08b-readiness/README.md) repairs SQLite ordering and executes successful capture plus immutable-content/file/ZIP assertions. Its final baseline records conversion of the original expected failure into an ordinary regression. Historical reproduction remains in Phase 2. P2-O01's initial rules-500 behavior remains unconfirmed. - **P3-O01/P3-O02 — scoped follow-up:** P5-06/P5-07 address Dashboard/Analytics population/period/sample presentation and independent error retention. Backend metric definitions and other navigation/query surfaces remain separately scoped. Evidence is in the current [redesign decision records](../redesign/phase-3/OPEN_DECISIONS.md). -- **P4-02-D02/D04 — open accessibility/content debt:** company save-error feedback outside the modal's accessible subtree and literal AI context `{name}`. D01 command-list semantics are addressed in P5-00; D03 progress-scroll access closes for the targeted P5-08a strip. D02/D04 remain unchanged. [Original evidence](../redesign/phase-4/p4-02/README.md). Do not disable modal background isolation or hide findings. -- **P5-08a-O01/O03 — deeper workspace entry/accessibility debt:** unsupported next-task aliases and duplicated content IDs in unchanged accordions require P5-08b's own adapter/composition evidence. **O02** retains two failed menu-contrast runs and the explicitly justified settled-state fixture wait; no Dashboard styling change or broader manual certification. [Record](../redesign/phase-5/p5-08a/README.md). +- **P4-02-D02 — open accessibility debt:** company save-error feedback outside the modal's accessible subtree. D01 command-list semantics are addressed in P5-00; D03 progress-scroll access closes for the targeted P5-08a strip; D04's literal AI context was corrected in [P5-08b readiness](../redesign/phase-5/p5-08b-readiness/README.md) and remains regression protected. [Original evidence](../redesign/phase-4/p4-02/README.md). Do not disable modal background isolation or hide findings. +- **P5-08a-O01/O03 — scoped successors:** the readiness checkpoint protects existing next-task aliases and unique labelled content; [P5-08b](../redesign/phase-5/p5-08b/README.md) revalidates the seven-section adapter/composition. **O02** retains two failed menu-contrast runs and the explicitly justified settled-state fixture wait; no Dashboard styling change or broader manual certification. [Record](../redesign/phase-5/p5-08a/README.md). - **P5-00 scoped closures:** P4-03-D01 Tooltip contrast, P4-04-O01 bottom-action toast overlap, P4-06-O01 mobile labels/focus and P4-06-O02 synthetic long admin metadata are addressed with [readiness evidence](../redesign/phase-5/p5-00/README.md). This does not certify all overlays, all accessibility targets or deployed production behavior. - **P4-04-O02 — existing SQLite notification-save contention:** overlapping native preference read/write requests reproduced HTTP 500 with SQLite error 5 in two of ten local runs. The sequential UI test now awaits and requires the save response before readback; server concurrency remains unresolved. Investigate separately from page-layout/density work. [Evidence](../redesign/phase-4/p4-04/README.md). - **P4-05-O01 — closed for Jobs/Trash:** P5-03 localizes standard status display and date labels while retaining raw API status values, custom labels and stored dates. [Evidence](../redesign/phase-5/p5-03/README.md). diff --git a/job-tracker-ui/e2e/job-overview-presentation.spec.ts b/job-tracker-ui/e2e/job-overview-presentation.spec.ts index 0cd2657..5336a54 100644 --- a/job-tracker-ui/e2e/job-overview-presentation.spec.ts +++ b/job-tracker-ui/e2e/job-overview-presentation.spec.ts @@ -40,7 +40,7 @@ test('[PRES-W01 W02 W03 J05 E08 E09] native Job header/Overview retains actions, }); await page.goto(`/jobs/${id}`); await expect(page.getByRole('heading', { name: title, level: 2 })).toBeVisible(); - await expect(page.getByRole('tablist', { name: translations.en.workspaceSections }).getByRole('tab')).toHaveCount(5); + await expect(page.getByRole('tablist', { name: translations.en.workspaceSections }).getByRole('tab')).toHaveCount(7); await expect(page.getByRole('link', { name: translations.en.workspaceOpenAdvert })).toHaveAttribute('href', jobData.jobUrl); await expect(page.getByRole('link', { name: translations.en.workspaceOpenAdvert })).toHaveAttribute('rel', 'noopener noreferrer'); expect(writes).toEqual([]); diff --git a/job-tracker-ui/e2e/job-sections-presentation.spec.ts b/job-tracker-ui/e2e/job-sections-presentation.spec.ts new file mode 100644 index 0000000..6dc346f --- /dev/null +++ b/job-tracker-ui/e2e/job-sections-presentation.spec.ts @@ -0,0 +1,175 @@ +import { expect, test, type APIRequestContext, type Page, type TestInfo } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import { translations } from '../src/i18n/translations'; + +const apiUrl = 'http://localhost:5302/api'; +const password = 'E2ePassword123!'; +async function login(api: APIRequestContext, email: string) { + expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); + const me = await (await api.get(`${apiUrl}/auth/me`)).json(); + return { 'X-CSRF-TOKEN': (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value, 'X-Session-Context': me.sessionContext as string }; +} + +test('[PRES-W01 W08 M01 E08] Job section loading and partial read failures stay explicit and recover locally', async ({ page }, info) => { + const headers = await login(page.request, 'e2e@example.test'); + const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Section state fixture' } }); + expect(company.status()).toBe(201); + const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: (await company.json()).id, jobTitle: 'Section state fixture', status: 'Saved', notes: 'Retained personal notes' } }); + expect(created.status()).toBe(201); const id = (await created.json()).id; + await page.setViewportSize({ width: 390, height: 844 }); + let release: (() => void) | undefined; let fail = true; + const hold = new Promise(resolve => { release = resolve; }); + await page.route(`${apiUrl}/attachments/${id}`, async route => { + await hold; + return fail ? route.fulfill({ status: 503, json: { message: 'Temporary attachment read failure' } }) : route.continue(); + }); + await page.goto(`/jobs/${id}?section=documents`); + await expect(page.getByRole('region', { name: translations.en.workspaceCurrentDocuments })).toBeVisible(); + await expect(page.getByRole('progressbar').first()).toBeVisible(); + await capture(page, info, 'sections-documents-loading'); release!(); + await expect(page.getByRole('alert').filter({ hasText: 'Temporary attachment read failure' })).toBeVisible(); + await expect(page.getByText(translations.en.attachmentsEmpty, { exact: true })).toHaveCount(0); + await capture(page, info, 'sections-documents-error'); fail = false; + await page.getByRole('button', { name: 'Retry', exact: true }).click(); + await expect(page.getByText(translations.en.attachmentsEmpty, { exact: true })).toBeVisible(); + await capture(page, info, 'sections-documents-recovered'); + await page.route(`${apiUrl}/correspondence/${id}`, route => route.fulfill({ status: 503, json: { message: 'Temporary correspondence read failure' } })); + await page.goto(`/jobs/${id}?section=communication&followMode=waiting-update`); + await expect(page.getByRole('alert').filter({ hasText: 'Temporary correspondence read failure' })).toBeVisible(); + await expect(page.getByText(translations.en.correspondenceNoMessages, { exact: true })).toHaveCount(0); + await expect(page.getByRole('textbox', { name: translations.en.correspondenceLogNoteOrMessage })).toBeVisible(); + await capture(page, info, 'sections-correspondence-error'); + await page.goto(`/jobs/${id}?section=tasks`); + await expect(page.getByRole('region', { name: translations.en.workspaceNotes, exact: true })).toContainText('Retained personal notes'); + await capture(page, info, 'sections-sparse-tasks'); + await page.goto(`/jobs/${id}?section=interview`); + await expect(page.getByText(translations.en.interviewPrepEarly, { exact: true })).toBeVisible(); + await capture(page, info, 'sections-sparse-interview'); +}); +async function capture(page: Page, info: TestInfo, name: string) { + await page.evaluate(() => document.fonts.ready); + const path = info.outputPath(`${name}.png`); + await page.screenshot({ path, fullPage: false, animations: 'disabled' }); + await info.attach(name, { path, contentType: 'image/png' }); + if (name.endsWith('en-light') && page.viewportSize()?.width === 1440 && ['sections-documents-', 'sections-correspondence-', 'sections-notes-tasks-', 'sections-interview-'].some(prefix => name.startsWith(prefix))) { + const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); + await info.attach(`${name}-axe`, { body: JSON.stringify({ violations: scan.violations, incomplete: scan.incomplete }), contentType: 'application/json' }); + expect(scan.violations).toEqual([]); + } +} + +for (const language of ['en', 'nb'] as const) for (const mode of ['light', 'dark'] as const) { + test(`[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (${language}, ${mode})`, async ({ page, playwright }, info) => { + test.setTimeout(240_000); + const t = translations[language]; + const admin = await playwright.request.newContext(); + const adminHeaders = await login(admin, 'e2e@example.test'); + const email = `job-sections-${language}-${mode}-${Date.now()}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); + await admin.dispose(); + const headers = await login(page.request, email); + await page.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) || ['blob:', 'data:', 'about:'].includes(new URL(route.request().url()).protocol) ? route.continue() : route.abort()); + await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); + expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204); + expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: { version: '1', contact: { fullName: 'Åse Eksempel', headline: 'Recorded engineer' }, skills: ['C#', 'SQL'] } } })).status()).toBe(200); + const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys samfunnskritiske plattformer og inkluderende digitale tjenester æøå' } }); + expect(company.status()).toBe(201); const companyId = (await company.json()).id; + const title = 'Senior utvikler og rådgiver for langsiktig plattformutvikling'; + const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId, jobTitle: title, status: 'Interview', description: 'Build C# and SQL services.', notes: 'Human notes — æøå', deadline: '2026-12-14T12:00:00Z', followUpAt: '2026-12-10T12:00:00Z' } }); + expect(created.status()).toBe(201); const id = (await created.json()).id; + const jobUrl = `${apiUrl}/jobapplications/${id}`; + const cv = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: 'Application CV — Åse', jobApplicationId: id } }); + expect(cv.status()).toBe(200); const variant = await cv.json(); + expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: variant.id } })).status()).toBe(200); + expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: 'Reviewed letter — æøå', source: 'manual' } })).status()).toBe(200); + expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { applicationAnswerDraft: 'Reviewed answer — æøå', recruiterMessageDraft: 'Message draft; never sent' } })).status()).toBe(204); + expect((await page.request.post(`${jobUrl}/checklist`, { headers, data: { title: 'Review employer questions — æøå', category: 'custom' } })).status()).toBe(200); + expect((await page.request.post(`${jobUrl}/interview-prep`, { headers, data: { title: 'Ask about sustainable delivery', category: 'question', content: 'My recorded question', source: 'manual' } })).status()).toBe(200); + expect((await page.request.post(`${apiUrl}/correspondence`, { headers, data: { jobApplicationId: id, from: 'Company', subject: 'Interview invitation — æøå', content: 'Recorded invitation; no inferred company facts', channel: 'Email' } })).ok()).toBe(true); + expect((await page.request.post(`${apiUrl}/attachments`, { headers, multipart: { jobId: String(id), files: { name: 'Employment evidence — æøå.txt', mimeType: 'text/plain', buffer: Buffer.from('Recorded evidence — æøå') } } })).ok()).toBe(true); + await page.setViewportSize({ width: 1440, height: 900 }); + const entry = async (section: string, sub?: string) => { + await page.goto(`/jobs/${id}?section=${section}${sub ? `&sub=${sub}` : ''}&followMode=waiting-update`); + await expect(page.getByRole('heading', { name: title, exact: true })).toBeVisible(); + await expect(page.getByRole('tablist', { name: t.workspaceSections }).getByRole('tab')).toHaveCount(7); + expect(new URL(page.url()).searchParams.get('followMode')).toBe('waiting-update'); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); + }; + await entry('overview'); await capture(page, info, `sections-overview-${language}-${mode}`); + await entry('analysis'); await expect(page.getByRole('heading', { name: t.workspaceCvMatch, exact: true })).toBeVisible(); + await capture(page, info, `sections-match-${language}-${mode}`); + await entry('cv'); await expect(page.getByRole('combobox', { name: new RegExp(t.assetsAttachedCv) })).toBeVisible(); + await expect(page.getByRole('link', { name: t.assetsEditPreviewExport })).toHaveAttribute('href', `/career/builder/${variant.id}`); + await expect(page.getByRole('button', { name: t.assetsPackageCapture, exact: true })).toHaveCount(0); + await capture(page, info, `sections-current-cv-${language}-${mode}`); + await entry('cover-letter'); await expect(page.getByRole('textbox', { name: t.jobDetailsCoverLetter, exact: true })).toHaveValue('Reviewed letter — æøå'); + await capture(page, info, `sections-letter-${language}-${mode}`); + await entry('application', 'answers'); + const answer = page.getByRole('textbox', { name: t.assetsApplicationAnswer, exact: true }); + await expect(answer).toHaveValue('Reviewed answer — æøå'); + await answer.fill('Edited answer, pending explicit save — æøå'); + await page.getByRole('tab', { name: t.workspaceSubmission, exact: true }).click(); + await expect(page.getByRole('dialog', { name: t.workspaceUnsavedTitle })).toBeVisible(); + await page.getByRole('button', { name: t.workspaceKeepEditing, exact: true }).click(); + await expect(answer).toHaveValue('Edited answer, pending explicit save — æøå'); + const save = page.waitForResponse(r => r.url() === `${jobUrl}/application-drafts` && r.request().method() === 'PUT'); + await page.getByRole('button', { name: t.assetsSaveDrafts, exact: true }).click(); expect((await save).status()).toBe(204); + await page.getByRole('tab', { name: t.workspaceSubmission, exact: true }).click(); + const freeze = page.waitForResponse(r => r.url() === `${jobUrl}/submitted-packages` && r.request().method() === 'POST'); + await page.getByRole('button', { name: t.assetsPackageCapture, exact: true }).click(); expect((await freeze).status()).toBe(200); + await expect(page.getByText(t.assetsSubmittedPackageVersion.replace('{version}', '1'), { exact: true })).toBeVisible(); + await capture(page, info, `sections-submission-${language}-${mode}`); + await page.getByRole('button', { name: t.assetsPackageView, exact: true }).click(); + await expect(page.getByRole('dialog').getByText(t.workspaceFrozenHelp, { exact: true })).toBeVisible(); + await expect(page.getByRole('dialog').getByText('Reviewed letter — æøå', { exact: true })).toBeVisible(); + await capture(page, info, `sections-frozen-${language}-${mode}`); + await page.keyboard.press('Escape'); await expect(page.getByRole('button', { name: t.assetsPackageView, exact: true })).toBeFocused(); + await entry('documents'); await expect(page.getByRole('switch', { name: `${t.attachmentsAiUse}: Employment evidence — æøå.txt` })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: t.attachmentsUploaded, exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: t.assetsPackageCapture, exact: true })).toHaveCount(0); + await capture(page, info, `sections-documents-${language}-${mode}`); + await entry('timeline'); await expect(page.getByRole('heading', { name: t.intelligenceTimeline, exact: true })).toBeVisible(); + await expect(page.locator('time').first()).toBeVisible(); await capture(page, info, `sections-timeline-${language}-${mode}`); + await entry('communication'); await expect(page.getByText('Interview invitation — æøå', { exact: true })).toBeVisible(); + await capture(page, info, `sections-correspondence-${language}-${mode}`); + await entry('notes'); + await expect(page.getByRole('region', { name: t.workspaceNotes, exact: true })).toContainText('Human notes — æøå'); + await expect(page.getByRole('region', { name: t.workspaceNotes, exact: true })).not.toContainText('Edited answer'); + const complete = page.waitForResponse(r => r.url().includes('/checklist/') && r.request().method() === 'PATCH'); + const task = page.getByRole('checkbox', { name: 'Review employer questions — æøå', exact: true }); + await task.click(); expect((await complete).status()).toBe(200); + await expect(task).toBeChecked(); // Existing owner re-reads after the server write; no optimistic tick. + await capture(page, info, `sections-notes-tasks-${language}-${mode}`); + const notes = page.getByRole('region', { name: t.workspaceNotes, exact: true }); + await notes.getByRole('button', { name: t.workspaceEdit, exact: true }).click(); + await page.getByRole('dialog').getByRole('textbox', { name: t.editJobNotes, exact: true }).fill('Edited human notes — æøå'); + const notesSave = page.waitForResponse(r => r.url() === jobUrl && r.request().method() === 'PUT'); + await page.getByRole('dialog').getByRole('button', { name: t.save, exact: true }).click(); expect((await notesSave).status()).toBe(204); + await expect(notes).toContainText('Edited human notes — æøå'); + expect((await (await page.request.get(`${jobUrl}/workspace`)).json()).applicationAnswerDraft).toBe('Edited answer, pending explicit save — æøå'); + await entry('interview'); await expect(page.getByText('Ask about sustainable delivery', { exact: true })).toBeVisible(); + await capture(page, info, `sections-interview-${language}-${mode}`); + for (const [width, height, section] of [[768, 1024, 'timeline'], [390, 844, 'communication'], [320, 844, 'documents'], [320, 844, 'application']] as const) { + await page.setViewportSize({ width, height }); await entry(section, section === 'application' ? 'submission' : undefined); + const selected = page.getByRole('tablist', { name: t.workspaceSections }).locator('[aria-selected=true]'); await selected.focus(); + await expect(selected).toBeFocused(); + await capture(page, info, `sections-${section}-${width}-${language}-${mode}`); + if (section === 'documents') { + const files = page.getByRole('region', { name: t.attachmentsTitle.replace('{count}', '1'), exact: true }); + await files.focus(); await page.keyboard.press('ArrowRight'); + await expect.poll(() => files.evaluate(node => node.scrollLeft)).toBeGreaterThan(0); + await capture(page, info, `sections-document-table-${width}-${language}-${mode}`); + } + } + const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); + await info.attach(`sections-axe-${language}-${mode}`, { body: JSON.stringify({ violations: scan.violations, incomplete: scan.incomplete }), contentType: 'application/json' }); + expect(scan.violations).toEqual([]); + const sparse = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId, jobTitle: 'Sparse sections', status: 'Saved' } }); + expect(sparse.status()).toBe(201); + await page.goto(`/jobs/${(await sparse.json()).id}?section=documents`); + await expect(page.getByText(t.attachmentsEmpty, { exact: true })).toBeVisible(); + await expect(page.getByText(t.assetsNoSubmittedPackages, { exact: true })).toBeVisible(); + await capture(page, info, `sections-sparse-${language}-${mode}`); + await info.attach(`sections-native-invariants-${language}-${mode}`, { body: JSON.stringify({ packageCapture: true, dirtyAnswersKeptAndExplicitlySaved: true, currentVsFrozenSeparated: true, personalNotesSeparate: true, checklistWriteSucceeded: true, legacyCvLetterCommunicationNotes: true, followModePreserved: true }), contentType: 'application/json' }); + }); +} diff --git a/job-tracker-ui/e2e/named-controls-modal-state.spec.ts b/job-tracker-ui/e2e/named-controls-modal-state.spec.ts index 29ac25d..76edbcc 100644 --- a/job-tracker-ui/e2e/named-controls-modal-state.spec.ts +++ b/job-tracker-ui/e2e/named-controls-modal-state.spec.ts @@ -133,7 +133,7 @@ for (const [mode, language, width] of [["light", "en", 1440], ["dark", "nb", 144 await page.getByRole("combobox", { name: new RegExp(t.coverAiAction) }).scrollIntoViewIfNeeded(); await capture(page, info, `cover-controls-${suffix}`); await page.goto(`/jobs/${owner.jobId}?section=overview`); - await page.getByRole("button", { name: t.workspaceDocuments, exact: true }).click(); + await page.getByRole("tab", { name: t.workspaceDocuments, exact: true }).click(); const purpose = page.getByRole("combobox", { name: new RegExp(t.attachmentsPurpose) }); await purpose.focus(); await purpose.press("Enter"); diff --git a/job-tracker-ui/e2e/redesign-preservation.spec.ts b/job-tracker-ui/e2e/redesign-preservation.spec.ts index 9a13428..398c88c 100644 --- a/job-tracker-ui/e2e/redesign-preservation.spec.ts +++ b/job-tracker-ui/e2e/redesign-preservation.spec.ts @@ -95,6 +95,7 @@ test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves const [attachment] = await (await page.request.get(`${apiUrl}/attachments/${job.id}`)).json(); await page.goto(`/applications/${job.id}?section=cv`); await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=cv$`)); + await page.getByRole("tab", { name: "Submission", exact: true }).click(); const captured = page.waitForResponse(response => response.url() === `${jobUrl}/submitted-packages` && response.request().method() === 'POST'); await page.getByRole("button", { name: "Freeze current package", exact: true }).click(); const captureResponse = await captured; @@ -168,11 +169,10 @@ for (const language of ["en", "nb"] as const) { await expect(page.getByRole("heading", { name: "Readiness workspace", exact: true })).toBeVisible(); expect(new URL(page.url()).searchParams.get("followMode")).toBe("waiting-update"); const section = expectedSections[tab]; - if (["communication", "documents", "checklist", "timeline"].includes(section)) { - const names = language === "en" ? { communication: "Communication", documents: "Documents", checklist: "Next actions and checklist", timeline: "Activity history" } : { communication: "Kommunikasjon", documents: "Dokumenter", checklist: "Neste handlinger og sjekkliste", timeline: "Aktivitetshistorikk" }; - await expect(page.getByRole("button", { name: names[section as keyof typeof names], exact: true })).toHaveAttribute("aria-expanded", "true"); - } - await expect(page.getByRole("tabpanel")).toBeVisible(); + const names = language === "en" ? { communication: "Timeline", documents: "Documents", checklist: "Notes & tasks", timeline: "Timeline", cv: "Application", match: "Match", analysis: "Match", overview: "Overview", interview: "Interview Prep" } : { communication: "Tidslinje", documents: "Dokumenter", checklist: "Notater og oppgaver", timeline: "Tidslinje", cv: "Søknad", match: "Samsvar", analysis: "Samsvar", overview: "Oversikt", interview: "Intervjuforberedelse" }; + await expect(page.getByRole("tablist", { name: language === "en" ? "Workspace sections" : "Deler av arbeidsområdet" }).getByRole("tab", { name: names[section as keyof typeof names], exact: true })).toHaveAttribute("aria-selected", "true"); + if (section === "communication") await expect(page.getByRole("tabpanel", { name: language === "en" ? "Communication" : "Kommunikasjon", exact: true })).toBeVisible(); + await expect(page.getByRole("tabpanel").first()).toBeVisible(); } await page.goto(`/jobs/${job.id}?section=checklist&followMode=waiting-update`); const move = page.getByRole("button", { name: language === "en" ? /^Move up:/ : /^Flytt opp:/ }).first(); @@ -191,13 +191,13 @@ for (const language of ["en", "nb"] as const) { const overviewTab = page.getByRole("tab", { name: language === "en" ? "Overview" : "Oversikt", exact: true }); await overviewTab.focus(); await page.keyboard.press("ArrowRight"); - const analysisTab = page.getByRole("tab", { name: language === "en" ? "Analysis" : "Analyse", exact: true }); + const analysisTab = page.getByRole("tab", { name: language === "en" ? "Match" : "Samsvar", exact: true }); await expect(analysisTab).toBeFocused(); await page.keyboard.press("Enter"); await expect(analysisTab).toHaveAttribute("aria-selected", "true"); expect(new URL(page.url()).searchParams.get("followMode")).toBe("waiting-update"); await page.goBack(); - await expect(overviewTab).toHaveAttribute("aria-selected", "true"); + await expect(page.getByRole("tab", { name: language === "en" ? "Documents" : "Dokumenter", exact: true })).toHaveAttribute("aria-selected", "true"); expect(new URL(page.url()).searchParams.get("section")).toBe("documents"); await page.goForward(); await expect(analysisTab).toHaveAttribute("aria-selected", "true"); @@ -256,7 +256,7 @@ test("[PRES-E07 PRES-W01 PRES-J04] keyboard command navigation and legacy worksp await expect(page.getByRole("heading", { name: "Command engineer", exact: true })).toBeVisible(); await page.goto(`/applications/${job.id}?section=match`); await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=match$`)); - await expect(page.getByRole("tab", { name: "Analysis", exact: true })).toHaveAttribute("aria-selected", "true"); + await expect(page.getByRole("tab", { name: "Match", exact: true })).toHaveAttribute("aria-selected", "true"); await page.keyboard.press("Control+k"); await page.getByRole("dialog").getByText("Add new job", { exact: true }).click(); await expect(page.getByRole("dialog")).toBeVisible(); diff --git a/job-tracker-ui/e2e/smoke.spec.ts b/job-tracker-ui/e2e/smoke.spec.ts index 4b4f669..bce6c21 100644 --- a/job-tracker-ui/e2e/smoke.spec.ts +++ b/job-tracker-ui/e2e/smoke.spec.ts @@ -260,10 +260,11 @@ test("the dedicated application workspace survives deep links, long data and uns await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}$`)); const workspaceNav = page.getByRole("tablist", { name: "Workspace sections" }); - await workspaceNav.getByRole("tab", { name: "Cover Letter", exact: true }).click(); - await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=cover-letter$`)); + await workspaceNav.getByRole("tab", { name: "Application", exact: true }).click(); + await page.getByRole("tab", { name: "Answers & message draft", exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=application&sub=answers$`)); await page.getByLabel("Application answer", { exact: true }).fill("A reviewed answer that must not be lost."); - await workspaceNav.getByRole("tab", { name: "Analysis", exact: true }).click(); + await workspaceNav.getByRole("tab", { name: "Match", exact: true }).click(); await expect(page.getByRole("dialog", { name: "Unsaved application changes" })).toBeVisible(); await page.getByRole("button", { name: "Keep editing" }).click(); await expect(page.getByLabel("Application answer", { exact: true })).toHaveValue("A reviewed answer that must not be lost."); diff --git a/job-tracker-ui/src/application-assets.test.tsx b/job-tracker-ui/src/application-assets.test.tsx index 754b293..b65e8bd 100644 --- a/job-tracker-ui/src/application-assets.test.tsx +++ b/job-tracker-ui/src/application-assets.test.tsx @@ -3,7 +3,7 @@ import "@testing-library/jest-dom"; import { fireEvent, render as rtlRender, screen, waitFor } from "@testing-library/react"; import { - ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, + ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, SubmittedPackagesSection, } from "./components/ApplicationAssets"; import { api } from "./api"; import { I18nProvider } from "./i18n/I18nProvider"; @@ -74,6 +74,22 @@ beforeEach(() => { afterEach(() => window.localStorage.removeItem("uiLanguage")); +test("document package history is read-only and separates captured notes from answers", async () => { + const summary = { id: 91, version: 1, cvVariantId: 3, cvVariantName: "Original CV", cvVariantVersion: 2, cvThemeId: "nordic", hasCv: false, hasCoverLetter: true, createdAtUtc: "2026-07-19T10:00:00Z", attachments: [] }; + mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url.endsWith('/91') ? { + summary, renderedCvHtml: null, coverLetterText: "Frozen letter", applicationMaterialJson: JSON.stringify({ Notes: "Human notes\n\n<<>>\nFrozen answer\n<<>>" }), + } : [summary] } as any)); + render(); + fireEvent.click(await screen.findByRole("button", { name: "View preserved package" })); + expect(await screen.findByRole("dialog")).toHaveTextContent("immutable submission snapshot"); + expect(screen.getByText("Human notes", { exact: true })).toBeInTheDocument(); + expect(screen.getByText("Frozen answer", { exact: true })).toBeInTheDocument(); + expect(screen.queryByText(/<< { @@ -161,7 +177,7 @@ test("editing marks the draft dirty and saving sends the new text", async () => const onDirtyChange = jest.fn(); render(); - fireEvent.change(await screen.findByLabelText("Cover letter"), { target: { value: "Dear hiring team" } }); + fireEvent.change(await screen.findByRole("textbox", { name: "Cover letter" }), { target: { value: "Dear hiring team" } }); expect(screen.getByText("Unsaved changes")).toBeInTheDocument(); expect(onDirtyChange).toHaveBeenLastCalledWith(true); @@ -177,7 +193,7 @@ test("discarding returns to the saved text", async () => { routeGet(); render(); - fireEvent.change(await screen.findByLabelText("Cover letter"), { target: { value: "scratch" } }); + fireEvent.change(await screen.findByRole("textbox", { name: "Cover letter" }), { target: { value: "scratch" } }); fireEvent.click(screen.getByRole("button", { name: /Discard changes/i })); expect(await screen.findByDisplayValue("Dear team")).toBeInTheDocument(); @@ -202,7 +218,7 @@ test("an empty cover letter offers the template and an empty history", async () expect(await screen.findByText(/No versions yet/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /Start from template/i })); - expect((screen.getByLabelText("Cover letter") as HTMLTextAreaElement).value) + expect((screen.getByRole("textbox", { name: "Cover letter" }) as HTMLTextAreaElement).value) .toContain("Dear Hiring Manager"); }); @@ -213,7 +229,7 @@ test("the empty cover-letter template and rich-text controls follow Bokmål UI l render(); fireEvent.click(await screen.findByRole("button", { name: /start fra mal/i })); - expect((screen.getByLabelText("Søknadsbrev") as HTMLTextAreaElement).value) + expect((screen.getByRole("textbox", { name: "Søknadsbrev" }) as HTMLTextAreaElement).value) .toContain("Kjære rekrutteringsansvarlig"); expect(screen.getByRole("button", { name: "Fet" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Lenke" })).toBeInTheDocument(); @@ -250,9 +266,9 @@ test("AI cover letter suggestions use the linked CV and require explicit apply", action: "generate", }), )); - expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team"); + expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Dear team"); fireEvent.click(await screen.findByRole("button", { name: /Apply to editor/i })); - expect(screen.getByLabelText("Cover letter")).toHaveValue("A tailored, truthful suggestion."); + expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("A tailored, truthful suggestion."); mockedApi.put.mockResolvedValue({ data: { ...coverLetter, text: "A tailored, truthful suggestion." } } as any); fireEvent.click(screen.getByRole("button", { name: "Save" })); @@ -272,10 +288,10 @@ test("[PRES-W06] rejecting an AI cover letter preserves the saved draft without expect(screen.queryByText(/\{name\}/)).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Generate" })); await screen.findByText("Unaccepted letter"); - expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team"); + expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Dear team"); fireEvent.click(screen.getByRole("button", { name: "Reject" })); expect(screen.queryByText("Unaccepted letter")).not.toBeInTheDocument(); - expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team"); + expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Dear team"); expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); expect(onDirtyChange).not.toHaveBeenCalledWith(true); expect(mockedApi.put).not.toHaveBeenCalled(); diff --git a/job-tracker-ui/src/application-intelligence.test.tsx b/job-tracker-ui/src/application-intelligence.test.tsx index 4593178..19a1a0e 100644 --- a/job-tracker-ui/src/application-intelligence.test.tsx +++ b/job-tracker-ui/src/application-intelligence.test.tsx @@ -86,7 +86,8 @@ test("timeline renders grouped days, milestones and readable summaries", async ( render(); expect(await screen.findByText("Milestones")).toBeInTheDocument(); - expect(screen.getByText("Today")).toBeInTheDocument(); + expect(screen.getByText(new Date(`${timeline.days[0].date}T12:00:00`).toLocaleDateString("en", { dateStyle: "long" }))).toBeInTheDocument(); + expect(screen.getByRole("list")).toHaveTextContent("AI suggestions refreshed"); expect(screen.getAllByText("Moved from Applied to Interview").length).toBeGreaterThan(0); expect(screen.getByText("AI suggestions refreshed")).toBeInTheDocument(); }); @@ -146,9 +147,9 @@ test("analysis prompts for the advert when there is none", async () => { test("analysis shows a loading state before the data arrives", () => { mockedApi.get.mockReturnValue(new Promise(() => {}) as any); - const { container } = render(); + render(); - expect(container.querySelectorAll(".MuiSkeleton-root").length).toBeGreaterThan(0); + expect(screen.getByRole("progressbar", { name: "Loading..." })).toBeInTheDocument(); }); // ---------- Match ---------- diff --git a/job-tracker-ui/src/application-workspace-overlay.test.tsx b/job-tracker-ui/src/application-workspace-overlay.test.tsx index 134c02a..04097fb 100644 --- a/job-tracker-ui/src/application-workspace-overlay.test.tsx +++ b/job-tracker-ui/src/application-workspace-overlay.test.tsx @@ -27,7 +27,8 @@ jest.mock("./components/ApplicationAssets", () => ({ ), ApplicationCvSection: () =>
CV section
, - ApplicationPackageDraftsSection: () =>
Application drafts section
, + ApplicationPackageDraftsSection: ({ onDirtyChange }: { onDirtyChange?: (dirty: boolean) => void }) =>
Application drafts section
, + SubmittedPackagesSection: () =>
Frozen packages section
, })); jest.mock("./components/InterviewPrep", () => ({ ApplicationInterviewPrep: () =>
Interview section
})); jest.mock("./components/ApplicationWorkflowAssist", () => ({ @@ -174,10 +175,10 @@ test("opens the dedicated workspace from the whole row and preserves list state expect(await screen.findByText("Backend Developer")).toBeInTheDocument(); expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42"); - fireEvent.click(screen.getByRole("tab", { name: "Analysis" })); + fireEvent.click(screen.getByRole("tab", { name: "Match" })); expect(await screen.findByText("Analysis section")).toBeInTheDocument(); expect(await screen.findByText("Match section")).toBeInTheDocument(); - expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=analysis"); + expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match"); fireEvent.click(screen.getByRole("button", { name: /back to applications/i })); await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs?q=backend")); @@ -190,7 +191,7 @@ test("opens a direct workspace URL and returns to applications", async () => { expect(await screen.findByText("Analysis section")).toBeInTheDocument(); expect(await screen.findByText("Match section")).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Analysis" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("tab", { name: "Match" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByLabelText(/application progress: waiting/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /back to applications/i })); @@ -219,16 +220,16 @@ test("warns before section navigation would discard application edits", async () renderTable("/jobs/42?section=cover-letter"); fireEvent.click(await screen.findByRole("button", { name: "Make cover letter dirty" })); - fireEvent.click(screen.getByRole("tab", { name: "Analysis" })); + fireEvent.click(screen.getByRole("tab", { name: "Match" })); expect(await screen.findByRole("dialog", { name: /Unsaved application changes/i })).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Keep editing" })); expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=cover-letter"); await waitFor(() => expect(screen.queryByRole("dialog", { name: /Unsaved application changes/i })).not.toBeInTheDocument()); - fireEvent.click(screen.getByRole("tab", { name: "Analysis" })); + fireEvent.click(screen.getByRole("tab", { name: "Match" })); fireEvent.click(await screen.findByRole("button", { name: "Discard and leave" })); - await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=analysis")); + await waitFor(() => expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match")); }); test("renders shared workspace navigation and progress in Norwegian Bokmål", async () => { @@ -248,8 +249,8 @@ test("renders shared workspace navigation and progress in Norwegian Bokmål", as renderTable("/jobs/42"); expect(await screen.findByRole("tab", { name: "Oversikt" })).toHaveAttribute("aria-selected", "true"); - expect(screen.getByRole("tab", { name: "Analyse" })).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Søknadsbrev" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Samsvar" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Søknad" })).toBeInTheDocument(); expect(await screen.findByLabelText(/søknadsprogresjon/i)).toBeInTheDocument(); expect(screen.getByRole("button", { name: /tilbake til søknader/i })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Forbered en CV for denne stillingen" })).toBeInTheDocument(); @@ -323,17 +324,17 @@ test("uses recorded dates in the summary and keeps absent dates unknown", async test("section changes retain follow-up query context and expose the selected labelled panel", async () => { renderTable("/jobs/42?section=communication&followMode=waiting-update"); await screen.findByRole("heading", { name: "Backend Developer" }); - fireEvent.click(screen.getByRole("tab", { name: "Analysis" })); - expect(await screen.findByRole("tabpanel", { name: "Analysis" })).toHaveTextContent("Match section"); - expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=analysis&followMode=waiting-update"); - const tab = screen.getByRole("tab", { name: "Analysis" }); - expect(document.getElementById(tab.getAttribute("aria-controls")!)).toBe(screen.getByRole("tabpanel", { name: "Analysis" })); + fireEvent.click(screen.getByRole("tab", { name: "Match" })); + expect(await screen.findByRole("tabpanel", { name: "Match" })).toHaveTextContent("Match section"); + expect(screen.getByTestId("location")).toHaveTextContent("/jobs/42?section=match&followMode=waiting-update"); + const tab = screen.getByRole("tab", { name: "Match" }); + expect(document.getElementById(tab.getAttribute("aria-controls")!)).toBe(screen.getByRole("tabpanel", { name: "Match" })); }); test("accordion controls identify exactly one labelled content region", async () => { renderTable("/jobs/42"); await screen.findByRole("heading", { name: "Backend Developer" }); - const trigger = screen.getByRole("button", { name: "Documents" }); + const trigger = screen.getByRole("button", { name: "Job details" }); fireEvent.click(trigger); const target = trigger.getAttribute("aria-controls")!; expect(document.querySelectorAll(`[id='${target}']`)).toHaveLength(1); @@ -341,12 +342,11 @@ test("accordion controls identify exactly one labelled content region", async () expect(document.getElementById(target)).toHaveAttribute("aria-labelledby", trigger.id); }); -test("legacy document links reveal their existing content and focus the disclosure", async () => { +test("legacy document links reveal current attachments and frozen history in the named section", async () => { renderTable("/jobs/42?section=documents"); - const trigger = await screen.findByRole("button", { name: "Documents" }); - await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "true")); - expect(trigger).toHaveFocus(); - expect(screen.getByRole("region", { name: "Documents" })).toHaveTextContent("Documents section"); + expect(await screen.findByRole("tab", { name: "Documents" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("tabpanel", { name: "Documents" })).toHaveTextContent("Documents section"); + expect(screen.getByRole("tabpanel", { name: "Documents" })).toHaveTextContent("Frozen packages section"); }); test("the missing-profile next task opens the existing canonical profile entry", async () => { @@ -357,3 +357,29 @@ test("the missing-profile next task opens the existing canonical profile entry", expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.put).not.toHaveBeenCalled(); }); + +test("application subsection navigation retains metadata and protects unsaved answers", async () => { + renderTable("/jobs/42?section=application&sub=answers&followMode=waiting-update"); + fireEvent.click(await screen.findByRole("button", { name: "Make answers dirty" })); + fireEvent.click(screen.getByRole("tab", { name: "Submission" })); + fireEvent.click(await screen.findByRole("button", { name: "Keep editing" })); + await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument()); + expect(screen.getByRole("tabpanel", { name: "Answers & message draft" })).toHaveTextContent("Application drafts section"); + fireEvent.click(screen.getByRole("tab", { name: "Submission" })); + fireEvent.click(await screen.findByRole("button", { name: "Discard and leave" })); + expect(await screen.findByRole("tabpanel", { name: "Submission" })).toHaveTextContent("Frozen packages section"); + expect(screen.getByTestId("location")).toHaveTextContent("section=application&sub=submission&followMode=waiting-update"); + expect(mockedApi.post).not.toHaveBeenCalled(); + expect(mockedApi.put).not.toHaveBeenCalled(); +}); + +test("legacy correspondence and notes reveal distinct domain content", async () => { + mockedApi.get.mockResolvedValue({ data: { ...overview, notes: "Human notes, not application answers" } } as any); + renderTable("/jobs/42?section=communication&followMode=waiting-update"); + expect(await screen.findByRole("tabpanel", { name: "Communication" })).toHaveTextContent("Communication section"); + fireEvent.click(screen.getByRole("tab", { name: "Notes & tasks" })); + expect(await screen.findByRole("region", { name: "Notes" })).toHaveTextContent("Human notes, not application answers"); + expect(screen.getByRole("tabpanel", { name: "Notes & tasks" })).toHaveTextContent("Checklist section"); + expect(screen.queryByText("Application drafts section")).not.toBeInTheDocument(); + expect(screen.getByTestId("location")).toHaveTextContent("followMode=waiting-update"); +}); diff --git a/job-tracker-ui/src/applicationWorkspace.test.ts b/job-tracker-ui/src/applicationWorkspace.test.ts index beb1a1e..49ccc22 100644 --- a/job-tracker-ui/src/applicationWorkspace.test.ts +++ b/job-tracker-ui/src/applicationWorkspace.test.ts @@ -1,15 +1,34 @@ -import { WORKSPACE_SECTIONS, workspaceSection } from "./applicationWorkspace"; +import { WORKSPACE_SECTIONS, workspaceSection, workspaceSubsection } from "./applicationWorkspace"; -test("workspace navigation exposes five purposes and preserves legacy links", () => { +test("workspace navigation exposes seven purposes and preserves legacy links", () => { expect(WORKSPACE_SECTIONS.map((section) => section.key)).toEqual([ - "overview", "analysis", "cv", "cover-letter", "interview", + "overview", "match", "application", "documents", "timeline", "tasks", "interview", ]); - expect(workspaceSection("match")).toBe("analysis"); - expect(workspaceSection("timeline")).toBe("overview"); - expect(workspaceSection("portfolio")).toBe("overview"); - expect(workspaceSection("notes")).toBe("cover-letter"); - for (const alias of ["job-details", "checklist", "documents", "communication"]) expect(workspaceSection(alias)).toBe("overview"); + expect(workspaceSection("analysis")).toBe("match"); + expect(workspaceSection("portfolio")).toBe("documents"); + expect(workspaceSection("notes")).toBe("tasks"); + expect(workspaceSection("checklist")).toBe("tasks"); + expect(workspaceSection("communication")).toBe("timeline"); + expect(workspaceSection("cv")).toBe("application"); + expect(workspaceSection("cover-letter")).toBe("application"); + expect(workspaceSection("job-details")).toBe("overview"); expect(workspaceSection("unknown-section")).toBe("overview"); expect(workspaceSection("constructor")).toBe("overview"); expect(workspaceSection("__proto__")).toBe("overview"); }); + +test("legacy material and correspondence links reveal their original owner without rewriting URLs", () => { + expect(workspaceSubsection("cv", "submission")).toBe("cv"); + expect(workspaceSubsection("cover-letter", null)).toBe("letter"); + expect(workspaceSubsection("communication", "activity")).toBe("correspondence"); + expect(workspaceSubsection("timeline", null)).toBe("activity"); +}); + +test("canonical subsection URLs are durable and unknown inputs have safe defaults", () => { + for (const sub of ["cv", "letter", "answers", "submission"]) expect(workspaceSubsection("application", sub)).toBe(sub); + expect(workspaceSubsection("timeline", "correspondence")).toBe("correspondence"); + for (const sub of [null, "unknown", "constructor", "__proto__"]) { + expect(workspaceSubsection("application", sub)).toBe("cv"); + expect(workspaceSubsection("timeline", sub)).toBe("activity"); + } +}); diff --git a/job-tracker-ui/src/applicationWorkspace.ts b/job-tracker-ui/src/applicationWorkspace.ts index 6b86f7a..bd93b78 100644 --- a/job-tracker-ui/src/applicationWorkspace.ts +++ b/job-tracker-ui/src/applicationWorkspace.ts @@ -77,15 +77,16 @@ export const CHECKLIST_CATEGORIES: { key: string; label: string }[] = [ { key: "custom", label: "Your own tasks" }, ]; -// Workspace navigation. Sections map to the Phase 5 milestones; each is added as its milestone lands -// so the workspace is always usable rather than a shell of placeholders. -export type WorkspaceSectionKey = "overview" | "analysis" | "cv" | "cover-letter" | "interview"; +// URL-addressable presentation; domain components remain the data owners. +export type WorkspaceSectionKey = "overview" | "match" | "application" | "documents" | "timeline" | "tasks" | "interview"; export const WORKSPACE_SECTIONS: { key: WorkspaceSectionKey; label: string }[] = [ { key: "overview", label: "Overview" }, - { key: "analysis", label: "Analysis" }, - { key: "cv", label: "CV" }, - { key: "cover-letter", label: "Cover Letter" }, + { key: "match", label: "Match" }, + { key: "application", label: "Application" }, + { key: "documents", label: "Documents" }, + { key: "timeline", label: "Timeline" }, + { key: "tasks", label: "Notes & tasks" }, { key: "interview", label: "Interview Prep" }, ]; @@ -95,14 +96,14 @@ export function checklistSystemTranslationToken(systemKey?: string | null): stri } const LEGACY_WORKSPACE_SECTIONS: Record = { - match: "analysis", + analysis: "match", + cv: "application", + "cover-letter": "application", "job-details": "overview", - checklist: "overview", - documents: "overview", - timeline: "overview", - communication: "overview", - portfolio: "overview", - notes: "cover-letter", + checklist: "tasks", + communication: "timeline", + portfolio: "documents", + notes: "tasks", }; /** Preserve old shared/bookmarked workspace links while keeping one clear primary navigation. */ @@ -111,6 +112,15 @@ export const workspaceSection = (value: string | null): WorkspaceSectionKey => { return WORKSPACE_SECTIONS.some((section) => section.key === value) ? value as WorkspaceSectionKey : "overview"; }; +/** Legacy links select their original content, even when unrelated sub metadata is present. */ +export function workspaceSubsection(section: string | null, sub: string | null): string { + if (section === "cv") return "cv"; + if (section === "cover-letter") return "letter"; + if (section === "communication") return "correspondence"; + if (workspaceSection(section) === "application") return ["cv", "letter", "answers", "submission"].includes(sub ?? "") ? sub! : "cv"; + return sub === "correspondence" ? "correspondence" : "activity"; +} + export const applicationWorkspaceApi = { overview: (jobId: number) => api.get(`/jobapplications/${jobId}/workspace`).then((r) => r.data), diff --git a/job-tracker-ui/src/attachments.test.tsx b/job-tracker-ui/src/attachments.test.tsx index da361b1..598b69f 100644 --- a/job-tracker-ui/src/attachments.test.tsx +++ b/job-tracker-ui/src/attachments.test.tsx @@ -9,6 +9,7 @@ import { I18nProvider } from './i18n/I18nProvider'; import { api } from './api'; jest.mock('./api', () => ({ + getApiErrorMessage: (_error: unknown, fallback: string) => fallback, api: { get: jest.fn(), post: jest.fn(), @@ -19,6 +20,8 @@ jest.mock('./api', () => ({ const mockedApi = api as jest.Mocked; +beforeEach(() => jest.clearAllMocks()); + test('attachments empty state renders drag and drop guidance', async () => { mockedApi.get.mockResolvedValueOnce({ data: [] } as any); @@ -35,7 +38,7 @@ test('attachments empty state renders drag and drop guidance', async () => { ); expect(await screen.findByText(/drag and drop files here/i)).toBeInTheDocument(); - expect(screen.getByText(/no attachments yet/i)).toBeInTheDocument(); + expect(await screen.findByText(/no attachments yet/i)).toBeInTheDocument(); expect(screen.getByText(/max 10 mb each/i)).toBeInTheDocument(); }); @@ -72,6 +75,8 @@ test('attachments metadata controls update purpose and ai usage', async () => { expect(screen.getByRole('button', { name: 'Download: resume.pdf' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Rename: resume.pdf' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Delete: resume.pdf' })).toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(8); + expect(screen.getByRole('columnheader', { name: 'Uploaded' })).toBeInTheDocument(); fireEvent.mouseDown(screen.getByRole('combobox', { name: /purpose/i })); fireEvent.click(await screen.findByRole('option', { name: /portfolio/i })); @@ -86,3 +91,14 @@ test('attachments metadata controls update purpose and ai usage', async () => { expect(mockedApi.patch).toHaveBeenCalledWith('/attachments/7', expect.objectContaining({ purpose: 'portfolio', useForAi: false })); }); }); + +test('attachment failures remain explicit and retry restores the real empty state', async () => { + mockedApi.get.mockRejectedValueOnce({ response: { status: 503 } }).mockResolvedValueOnce({ data: [] } as any); + render(); + expect(await screen.findByRole('alert')).toHaveTextContent("Could not load this application's attachments."); + expect(screen.queryByText(/no attachments yet/i)).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Retry' })); + expect(await screen.findByText(/no attachments yet/i)).toBeInTheDocument(); + expect(mockedApi.get).toHaveBeenCalledTimes(2); + expect(mockedApi.post).not.toHaveBeenCalled(); +}); diff --git a/job-tracker-ui/src/components/ApplicationAssets.tsx b/job-tracker-ui/src/components/ApplicationAssets.tsx index 9a906b1..e4e8c67 100644 --- a/job-tracker-ui/src/components/ApplicationAssets.tsx +++ b/job-tracker-ui/src/components/ApplicationAssets.tsx @@ -1,8 +1,8 @@ -import React, { useCallback, useEffect, useId, useState } from "react"; +import React, { useCallback, useEffect, useId, useRef, useState } from "react"; import { - Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, IconButton, InputLabel, MenuItem, Paper, Select, - Skeleton, Stack, TextField, Tooltip, Typography, + Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, IconButton, InputLabel, MenuItem, Paper, Select, + Stack, TextField, Tooltip, Typography, } from "@mui/material"; import RichTextField from "./RichTextField"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; @@ -19,6 +19,9 @@ import { cvBuilderApi } from "../cvBuilder"; import { aiWorkspaceApi } from "../aiWorkspace"; import { useAccountPlan } from "../accountPlan"; import { useI18n } from "../i18n/I18nProvider"; +import { PageSection } from "../layout/PageLayout"; +import ViewStateNotice from "./ViewStateNotice"; +import { extractApplicationAnswerDraft, removeApplicationAnswerDraft } from "../applicationDrafts"; // Phase 5.4 — Application Assets sections for the workspace. // @@ -70,24 +73,16 @@ function Shell({ title, subtitle, loading, error, children }: { children: React.ReactNode; }) { return ( - - {title} - {subtitle && {subtitle}} - - {loading ? ( - {[0, 1, 2].map((i) => )} - ) : error ? ( - {error} - ) : ( - children - )} - + + + {!loading && !error && children} + ); } // ---------- CV ---------- -export function ApplicationCvSection({ jobId }: { jobId: number }) { +export function ApplicationCvSection({ jobId, includePackages = true }: { jobId: number; includePackages?: boolean }) { const { t } = useI18n(); const { data, error, loading, setData, setError } = useAsset( () => applicationAssetsApi.cv(jobId), @@ -166,7 +161,7 @@ export function ApplicationCvSection({ jobId }: { jobId: number }) { {data.attachedIsPublic ? ` · ${t("assetsPublic")}` : ""} - + + } {(data?.length ?? 0) === 0 ? ( {t("assetsNoSubmittedPackages")} ) : data?.map((item) => ( - - + + {t("assetsSubmittedPackageVersion", { version: item.version })} {new Date(item.createdAtUtc).toLocaleString(language === "nb" ? "nb-NO" : "en")} - + - {item.attachments.map((attachment) => ( - ))} @@ -292,15 +289,17 @@ function SubmittedPackagesSection({ jobId }: { jobId: number }) { ))} - setDetail(null)} fullWidth maxWidth="lg" aria-labelledby="submitted-package-title"> - + setDetail(null)} fullWidth maxWidth="lg" aria-labelledby={titleId} slotProps={{ transition: { onExited: () => viewTrigger.current?.focus() } }}> + {detail ? t("assetsSubmittedPackageVersion", { version: detail.summary.version }) : t("assetsSubmittedPackages")} + {t("workspaceFrozenHelp")} {detail?.renderedCvHtml ? ( {t("workspaceCv")} + {detail.summary.cvVariantName} · {t("assetsThemeVersion", { theme: detail.summary.cvThemeId ?? "—", version: detail.summary.cvVariantVersion ?? "—" })} ) : {t("assetsPackageNoCv")}} @@ -310,12 +309,19 @@ function SubmittedPackagesSection({ jobId }: { jobId: number }) { {detail?.coverLetterText || t("assetsPackageNoCoverLetter")} - {material && ["TailoredCvText", "RecruiterMessageDraft", "Notes"].map((key) => typeof material[key] === "string" && material[key] ? ( + {material && ["TailoredCvText", "RecruiterMessageDraft"].map((key) => typeof material[key] === "string" && material[key] ? ( {t(`assetsPackage${key}` as "assetsPackageTailoredCvText")} {String(material[key])} ) : null)} + {material && typeof material.Notes === "string" && [ + [t("workspaceNotes"), removeApplicationAnswerDraft(material.Notes)], + [t("assetsApplicationAnswer"), extractApplicationAnswerDraft(material.Notes)], + ].map(([label, content]) => content ? + {label} + {content} + : null)} diff --git a/job-tracker-ui/src/components/ApplicationChecklist.tsx b/job-tracker-ui/src/components/ApplicationChecklist.tsx index 0f42c2d..f6526a1 100644 --- a/job-tracker-ui/src/components/ApplicationChecklist.tsx +++ b/job-tracker-ui/src/components/ApplicationChecklist.tsx @@ -10,6 +10,7 @@ import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import { getApiErrorMessage } from "../api"; import { useI18n } from "../i18n/I18nProvider"; +import { PageSection } from "../layout/PageLayout"; import { CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi, checklistSystemTranslationToken, } from "../applicationWorkspace"; @@ -98,25 +99,18 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb {error && setError(null)}>{error}} {progress && ( - - - {t("checklistTitle")} - - {t("checklistProgress", { completed: progress.completed, total: progress.total })} - - + - + )} {grouped.map((group) => ( - - {checklistCategoryLabel(t, group.key)} + {group.items.map((item) => { const display = checklistItemDisplay(t, item); @@ -143,7 +137,7 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb sx={{ fontWeight: 600, textDecoration: item.status === "done" ? "line-through" : "none", - color: item.status === "done" ? "text.disabled" : "text.primary", + color: item.status === "done" ? "text.secondary" : "text.primary", }} > {display.title} @@ -182,11 +176,11 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb ); })} - + ))} - - + + setDraft(e.target.value)} /> - + diff --git a/job-tracker-ui/src/components/ApplicationIntelligence.tsx b/job-tracker-ui/src/components/ApplicationIntelligence.tsx index 42aa6e5..4a37699 100644 --- a/job-tracker-ui/src/components/ApplicationIntelligence.tsx +++ b/job-tracker-ui/src/components/ApplicationIntelligence.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { - Alert, Box, Chip, Divider, LinearProgress, Paper, Skeleton, Stack, ToggleButton, + Alert, Box, Chip, LinearProgress, Stack, ToggleButton, ToggleButtonGroup, Typography, } from "@mui/material"; @@ -10,6 +10,8 @@ import { CareerMatch, JobAnalysis, Timeline, applicationIntelligenceApi, } from "../applicationWorkspace"; import { useI18n } from "../i18n/I18nProvider"; +import { PageSection } from "../layout/PageLayout"; +import ViewStateNotice from "./ViewStateNotice"; // Phase 5.3 — Application Intelligence sections for the workspace. // @@ -62,20 +64,14 @@ function SectionShell({ title, subtitle, loading, error, empty, emptyText, child children: React.ReactNode; }) { return ( - - {title} - {subtitle && {subtitle}} - - {loading ? ( - {[0, 1, 2].map((i) => )} - ) : error ? ( - {error} - ) : empty ? ( + + + {!loading && !error && (empty ? ( {emptyText} ) : ( children - )} - + ))} + ); } @@ -123,11 +119,10 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) { return ( {hasEvents && (data?.milestones.length ?? 0) > 0 && ( - - {t("intelligenceMilestones")} + {(data?.milestones ?? []).map((m) => ( - + {m.summary} {new Date(m.at).toLocaleDateString(language === "nb" ? "nb-NO" : "en")} @@ -135,7 +130,7 @@ export function ApplicationTimeline({ jobId }: { jobId: number }) { ))} - + )} ( - {day.label} + {new Date(`${day.date.slice(0, 10)}T12:00:00`).toLocaleDateString(language === "nb" ? "nb-NO" : "en", { dateStyle: "long" })} - + {day.events.map((e) => ( - + + {new Date(e.at).toLocaleTimeString(language === "nb" ? "nb-NO" : "en", { hour: "2-digit", minute: "2-digit", timeZone: "UTC", timeZoneName: "short" })} · {timelineCategoryLabel(t, e.category)} {e.summary} diff --git a/job-tracker-ui/src/components/Attachments.tsx b/job-tracker-ui/src/components/Attachments.tsx index 17ae448..90d351e 100644 --- a/job-tracker-ui/src/components/Attachments.tsx +++ b/job-tracker-ui/src/components/Attachments.tsx @@ -35,6 +35,7 @@ import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; import { useDialogActions } from "../dialogs"; +import ViewStateNotice from "./ViewStateNotice"; interface AttachmentItem { id: number; @@ -85,9 +86,11 @@ function purposeLabel(purpose: string | null | undefined, t: (key: any) => strin export default function Attachments({ jobId }: { jobId: number }) { const labelId = useId(); const { toast } = useToast(); - const { t } = useI18n(); + const { language, t } = useI18n(); const { confirmAction, promptForValue } = useDialogActions(); const [items, setItems] = useState([]); + const [loadError, setLoadError] = useState(null); + const [loading, setLoading] = useState(true); const [previewOpen, setPreviewOpen] = useState(false); const [preview, setPreview] = useState<{ url: string; type: string; name: string } | null>(null); const [dragActive, setDragActive] = useState(false); @@ -95,13 +98,17 @@ export default function Attachments({ jobId }: { jobId: number }) { const fileInputRef = useRef(null); const load = useCallback(async () => { + setLoading(true); try { const res = await api.get(`/attachments/${jobId}`); setItems(res.data); - } catch { - // non-fatal + setLoadError(null); + } catch (error) { + setLoadError(getApiErrorMessage(error, t("attachmentsLoadFailed"))); + } finally { + setLoading(false); } - }, [jobId]); + }, [jobId, t]); useEffect(() => { void load(); @@ -253,11 +260,10 @@ export default function Attachments({ jobId }: { jobId: number }) { sx={{ mb: 2, p: 2, - borderRadius: 3, + borderRadius: 1, border: "1px dashed", borderColor: dragActive ? "primary.main" : "divider", backgroundColor: dragActive ? "action.hover" : "background.paper", - transition: "all 0.2s ease", textAlign: "center", }} > @@ -268,14 +274,17 @@ export default function Attachments({ jobId }: { jobId: number }) { {uploading ? : null} - - + + {!loading && !loadError && items.length === 0 && {t("attachmentsEmpty")}} + {items.length > 0 && +
{t("attachmentsName")} {t("attachmentsKind")} {t("attachmentsType")} {t("attachmentsSize")} + {t("attachmentsUploaded")} {t("attachmentsPurpose")} {t("attachmentsAiUse")} {t("attachmentsActions")} @@ -288,14 +297,14 @@ export default function Attachments({ jobId }: { jobId: number }) { return ( - {kind} {a.fileType ? a.fileType.replace("application/", "") : ""} {a.fileSize ? fmtSize(a.fileSize) : ""} - {a.uploadDate ? new Date(a.uploadDate).toLocaleString() : ""} + {a.uploadDate ? new Date(a.uploadDate).toLocaleString(language === "nb" ? "nb-NO" : "en") : ""} {t("attachmentsPurpose")} @@ -337,18 +346,9 @@ export default function Attachments({ jobId }: { jobId: number }) { ); })} - {items.length === 0 ? ( - - - - {t("attachmentsEmpty")} - - - - ) : null}
-
+ } setPreviewOpen(false)} fullWidth maxWidth="md"> {preview?.name ? t("attachmentsPreviewTitle", { name: preview.name }) : t("attachmentsPreview")} diff --git a/job-tracker-ui/src/components/Correspondence.tsx b/job-tracker-ui/src/components/Correspondence.tsx index 61341b2..a9d2bb7 100644 --- a/job-tracker-ui/src/components/Correspondence.tsx +++ b/job-tracker-ui/src/components/Correspondence.tsx @@ -43,6 +43,7 @@ import { } from "../types"; import { useDialogActions } from "../dialogs"; import { useI18n } from "../i18n/I18nProvider"; +import ViewStateNotice from "./ViewStateNotice"; function parseRawEmail(raw: string): { subject?: string; date?: string; from?: string; to?: string; body: string } { const lines = raw.replace(/\r\n/g, "\n").split("\n"); @@ -127,6 +128,8 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j return label; }; const [messages, setMessages] = useState([]); + const [messagesLoading, setMessagesLoading] = useState(true); + const [messagesError, setMessagesError] = useState(null); const [from, setFrom] = useState<"Me" | "Company">("Me"); const [text, setText] = useState(""); const scrollRef = useRef(null); @@ -154,9 +157,18 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j const autoRefreshKeyRef = useRef(null); const load = useCallback(async () => { - const res = await api.get(`/correspondence/${jobId}`); - setMessages(res.data); - }, [jobId]); + setMessagesLoading(true); + try { + const res = await api.get(`/correspondence/${jobId}`); + setMessages(res.data); + setMessagesError(null); + } catch (error) { + setMessagesError(getApiErrorMessage(error, t("correspondenceLoadFailed"))); + throw error; // Existing mutation callers still handle their own operation failure. + } finally { + setMessagesLoading(false); + } + }, [jobId, t]); const loadGmailStatus = useCallback(async () => { try { @@ -242,7 +254,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j }, [gmailQuery, gmailStatus?.connected, importOpen, importTab, jobId, linkedThreadIds.length, load, loadGmailMatches, loadGmailStatus, t, toast]); useEffect(() => { - void load(); + void load().catch(() => undefined); }, [load]); useEffect(() => { @@ -460,8 +472,9 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j return ( - - {messages.length === 0 ? ( + load().catch(() => undefined)} compact /> + + {messages.length === 0 && !messagesLoading && !messagesError ? ( {t("correspondenceNoMessages")} ) : ( @@ -470,7 +483,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j const accent = isMe ? theme.palette.primary.main : theme.palette.warning.main; return ( - + {m.subject ? {m.subject} : null} {m.content} {(m.externalThreadId || m.externalFrom || m.externalTo || m.externalLabelsJson || m.attachmentMetadataJson) ? ( @@ -498,7 +511,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j )} - + {t("correspondenceLinkedContinuity")} {t("correspondenceLinkedContinuityHelp")} @@ -548,7 +561,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j - setText(e.target.value)} multiline minRows={3} sx={{ flex: "1 1 320px" }} helperText={t("correspondenceCharacters", { count: text.length })} /> + setText(e.target.value)} multiline minRows={3} sx={{ flex: { xs: "1 1 100%", sm: "1 1 320px" }, minWidth: 0 }} helperText={t("correspondenceCharacters", { count: text.length })} /> diff --git a/job-tracker-ui/src/components/InterviewPrep.tsx b/job-tracker-ui/src/components/InterviewPrep.tsx index f99c60c..3a53b8c 100644 --- a/job-tracker-ui/src/components/InterviewPrep.tsx +++ b/job-tracker-ui/src/components/InterviewPrep.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useState } from "react"; import { - Alert, Box, Button, Checkbox, Chip, Divider, IconButton, LinearProgress, MenuItem, Paper, - Skeleton, Stack, TextField, Tooltip, Typography, + Alert, Box, Button, Checkbox, Chip, IconButton, LinearProgress, MenuItem, Paper, + Stack, TextField, Tooltip, Typography, } from "@mui/material"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh"; @@ -16,6 +16,8 @@ import { aiWorkspaceApi } from "../aiWorkspace"; import { useAccountPlan } from "../accountPlan"; import { useI18n } from "../i18n/I18nProvider"; import { useConfirm } from "../confirm"; +import { PageSection } from "../layout/PageLayout"; +import ViewStateNotice from "./ViewStateNotice"; // Phase 5.5 — Interview preparation and follow-up. // @@ -31,18 +33,10 @@ function Shell({ title, subtitle, loading, error, children }: { children: React.ReactNode; }) { return ( - - {title} - {subtitle && {subtitle}} - - {loading ? ( - {[0, 1, 2].map((i) => )} - ) : error ? ( - {error} - ) : ( - children - )} - + + + {!loading && !error && children} + ); } @@ -367,6 +361,7 @@ function PrepRow({ jobId, item, busy, onChanged, onError }: { fullWidth size="small" placeholder={t("interviewPrepAnswerPlaceholder")} + inputProps={{ "aria-label": t("interviewPrepAnswerFor", { title: item.title }) }} value={content} disabled={busy || saving} onChange={(e) => setDraft(e.target.value)} diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index aa31532..160d91c 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -2408,6 +2408,21 @@ export const translations = { workspaceBack: "Back to applications", workspaceOpenFull: "Open full-page workspace", workspaceSections: "Workspace sections", + workspaceMatch: "Match", + workspaceApplication: "Application", + workspaceTimeline: "Timeline", + workspaceTasks: "Notes & tasks", + workspaceApplicationSections: "Application materials", + workspaceTimelineSections: "Timeline sections", + workspaceAnswers: "Answers & message draft", + workspaceSubmission: "Submission", + workspaceCurrentDocuments: "Current documents", + workspaceCurrentDocumentsHelp: "Manage the files attached to this application. Submitted snapshots below keep their original contents independently of these current files.", + workspaceFrozenHelp: "This is an immutable submission snapshot. Its captured CV version, letter and files are not replaced when current documents change. Freezing a package does not send an application.", + workspaceNoNotes: "No personal notes yet. Application answers have their own draft in Application.", + attachmentsLoadFailed: "Could not load this application's attachments.", + correspondenceLoadFailed: "Could not load this application's correspondence.", + interviewPrepAnswerFor: "Preparation answer: {title}", workspaceOverview: "Overview", workspaceAnalysis: "Analysis", workspaceCv: "CV", @@ -4924,6 +4939,21 @@ export const translations = { workspaceBack: "Tilbake til søknader", workspaceOpenFull: "Åpne arbeidsområdet på helside", workspaceSections: "Deler av arbeidsområdet", + workspaceMatch: "Samsvar", + workspaceApplication: "Søknad", + workspaceTimeline: "Tidslinje", + workspaceTasks: "Notater og oppgaver", + workspaceApplicationSections: "Søknadsmateriale", + workspaceTimelineSections: "Deler av tidslinjen", + workspaceAnswers: "Svar og meldingsutkast", + workspaceSubmission: "Innsending", + workspaceCurrentDocuments: "Gjeldende dokumenter", + workspaceCurrentDocumentsHelp: "Administrer filene knyttet til denne søknaden. Innsendte øyeblikksbilder nedenfor beholder det opprinnelige innholdet uavhengig av disse filene.", + workspaceFrozenHelp: "Dette er et uforanderlig øyeblikksbilde av innsendingen. Den lagrede CV-versjonen, brevet og filene erstattes ikke når gjeldende dokumenter endres. Å fryse en pakke sender ikke en søknad.", + workspaceNoNotes: "Ingen personlige notater ennå. Søknadssvar har et eget utkast under Søknad.", + attachmentsLoadFailed: "Kunne ikke laste vedleggene til denne søknaden.", + correspondenceLoadFailed: "Kunne ikke laste korrespondansen til denne søknaden.", + interviewPrepAnswerFor: "Forberedelsessvar: {title}", workspaceOverview: "Oversikt", workspaceAnalysis: "Analyse", workspaceCv: "CV", diff --git a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx index dca424c..fb03a35 100644 --- a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx +++ b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx @@ -27,7 +27,7 @@ import { ApplicationAnalysis, ApplicationMatch, ApplicationTimeline, } from "../components/ApplicationIntelligence"; import { - ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, + ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, SubmittedPackagesSection, } from "../components/ApplicationAssets"; import { ApplicationInterviewPrep } from "../components/InterviewPrep"; import EditJobDialog from "../components/EditJobDialog"; @@ -40,7 +40,7 @@ import { useI18n } from "../i18n/I18nProvider"; import { useConfirm } from "../confirm"; import { WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi, - checklistSystemTranslationToken, workspaceSection, + checklistSystemTranslationToken, workspaceSection, workspaceSubsection, } from "../applicationWorkspace"; // Phase 5 Milestone 1 — the dedicated Application Workspace. @@ -54,7 +54,7 @@ export default function ApplicationWorkspacePage() { type ApplicationWorkspaceProps = { jobIdOverride?: number; - sectionOverride?: WorkspaceSectionKey; + sectionOverride?: WorkspaceSectionKey | "analysis" | "cv" | "cover-letter"; onSectionChange?: (section: WorkspaceSectionKey) => void; onClose?: () => void; fullPageHref?: string; @@ -74,8 +74,13 @@ export function ApplicationWorkspace({ const { t } = useI18n(); const { confirm } = useConfirm(); const [params, setParams] = useSearchParams(); - const section = sectionOverride ?? workspaceSection(params.get("section")); + const section = workspaceSection(sectionOverride ?? params.get("section")); + const [overlaySubsection, setOverlaySubsection] = useState<{ section: WorkspaceSectionKey; value: string } | null>(null); + const subsection = onSectionChange && overlaySubsection?.section === section + ? overlaySubsection.value : workspaceSubsection(sectionOverride ?? params.get("section"), params.get("sub")); const sectionId = useId(); + const secondaryId = useId(); + const panelRef = useRef(null); const [overview, setOverview] = useState(null); const [error, setError] = useState(null); @@ -140,15 +145,25 @@ export function ApplicationWorkspace({ load(); }, [load]); - const go = (next: string) => { + useEffect(() => { + // Direct entry gets a named target; keyboard tab activation keeps tab focus. + if (!loading && section !== "overview" && document.activeElement === document.body) panelRef.current?.focus({ preventScroll: true }); + }, [loading, section, subsection]); + + const go = (next: string, sub?: string) => { if (next === "career-profile") { navigate("/career?section=profile"); return; } - if (onSectionChange) onSectionChange(workspaceSection(next)); + if (onSectionChange) { + setOverlaySubsection({ section: workspaceSection(next), value: workspaceSubsection(next, sub ?? null) }); + onSectionChange(workspaceSection(next)); + } else setParams((current) => { const updated = new URLSearchParams(current); updated.set("section", next); + if (sub) updated.set("sub", sub); + else updated.delete("sub"); return updated; }, { state: location.state }); }; @@ -161,6 +176,15 @@ export function ApplicationWorkspace({ { replace: true, state: focusJobId ? { focusJobId } : undefined }, ); }); + const subsections = section === "application" ? [ + { key: "cv", label: t("workspaceCv") }, + { key: "letter", label: t("workspaceCoverLetter") }, + { key: "answers", label: t("workspaceAnswers") }, + { key: "submission", label: t("workspaceSubmission") }, + ] : [ + { key: "activity", label: t("workspaceActivityHistory") }, + { key: "correspondence", label: t("workspaceCommunication") }, + ]; if (error) { return ( @@ -213,10 +237,10 @@ export function ApplicationWorkspace({ - + {section === "overview" && jobId > 0 && } {section === "overview" && setEditOpen(true)} />} - {section === "analysis" && jobId > 0 && ( + {section === "match" && jobId > 0 && ( @@ -224,19 +248,39 @@ export function ApplicationWorkspace({ )} {section === "interview" && jobId > 0 && } - {section === "cv" && jobId > 0 && } - {section === "cover-letter" && jobId > 0 && ( - <> - - + go(section, sub)} variant="scrollable" scrollButtons="auto" aria-label={t(section === "application" ? "workspaceApplicationSections" : "workspaceTimelineSections")}> + {subsections.map((sub) => )} + + + {section === "application" && subsection === "cv" && jobId > 0 && } + {section === "application" && subsection === "letter" && jobId > 0 && } + {section === "application" && subsection === "answers" && jobId > 0 && - - )} + />} + {section === "application" && subsection === "submission" && jobId > 0 && } + {section === "timeline" && subsection === "activity" && jobId > 0 && } + {section === "timeline" && subsection === "correspondence" && jobId > 0 && } + + } + {section === "documents" && jobId > 0 && <> + + + + }> + + } + {section === "tasks" && jobId > 0 && <> + } onClick={() => setEditOpen(true)}>{t("workspaceEdit")}}> + {overview?.notes || t("workspaceNoNotes")} + + + + } - +
); } -function OverviewDetails({ jobId, overview, onReload, onEdit }: { jobId: number; overview: WorkspaceOverview; onReload: () => void; onEdit: () => void }) { +function OverviewDetails({ overview, onEdit }: { overview: WorkspaceOverview; onEdit: () => void }) { const { t } = useI18n(); const [params] = useSearchParams(); const prefix = useId(); const triggers = useRef>({}); - const targets: Record = { "job-details": "details", checklist: "tasks", documents: "documents", portfolio: "documents", timeline: "timeline", communication: "communication" }; + const targets: Record = { "job-details": "details" }; const key = params.get("section") ?? ""; const target = Object.prototype.hasOwnProperty.call(targets, key) ? targets[key] : undefined; const [expanded, setExpanded] = useState>({}); @@ -412,11 +456,6 @@ function OverviewDetails({ jobId, overview, onReload, onEdit }: { jobId: number; }, [target]); const panels = [ { id: "details", title: t("workspaceJobDetails"), content: }, - { id: "tasks", title: t("workspaceChecklist"), content: }, - { id: "calendar", title: t("calendarTitle"), content: }, - { id: "timeline", title: t("workspaceActivityHistory"), content: }, - { id: "documents", title: t("workspaceDocuments"), content: }, - { id: "communication", title: t("workspaceCommunication"), content: }, ]; return ( @@ -435,9 +474,11 @@ function OverviewDetails({ jobId, overview, onReload, onEdit }: { jobId: number; function workspaceSectionLabel(t: (key: any) => string, section: WorkspaceSectionKey): string { const keys: Record = { overview: "workspaceOverview", - analysis: "workspaceAnalysis", - cv: "workspaceCv", - "cover-letter": "workspaceCoverLetter", + match: "workspaceMatch", + application: "workspaceApplication", + documents: "workspaceDocuments", + timeline: "workspaceTimeline", + tasks: "workspaceTasks", interview: "workspaceInterviewPrep", }; return t(keys[section]);