diff --git a/docs/redesign/phase-7/COMMITS.md b/docs/redesign/phase-7/COMMITS.md index b93159e..fb710dc 100644 --- a/docs/redesign/phase-7/COMMITS.md +++ b/docs/redesign/phase-7/COMMITS.md @@ -2,6 +2,8 @@ ## Continuation — 20 October 2026 +P7-11 localisation **ab89b6984c4abc4785e9871f93c374f0fd8b8e08** normally pushed/directly verified18:27:48UTC; error-owner WIP retained. [Error/navigation boundary](p7-11/ERRORS.md) final native10/10 PASS18:21:21–18:22:22UTC,18axe0violations/105contrast incompletes; final missing-CV/Documents images re-inspected and byte-identical final NB unknown-route image retained. Shared modal follow-up removes dangling-title/toolbar ARIA observations. Candidate full898/101frontend, full1057backend,56sidecar and finalbuild/type/lint PASS; P7-12 refreshes all lanes against committed source. Final scoped hygiene/publication follows. + P7-11 shared **d8c01873053e59c896e60911b8e5c14b00693c7f** normally pushed/directly verified18:26:54UTC, retaining all remaining owner WIP. [Generated-label localisation](p7-11/LOCALISATION.md) final boundary:76/4 direct plus7/2 compatibility consumer vectors; native9/9 PASS18:15:24–18:17:05UTC,50scans0violations/355incomplete observations, three actual images inspected. Final hygiene/publication follows; no provider/model/save contract changes. P7-10 System **d824f59d2b3e9b680bee5952802aa3c0453aee03** normally pushed/directly verified clean13:40:25UTC. CI-only main repair merge **6db62095c6dc6f7e8275447827c62b09f9b2d2e3** pushed/directly verified clean13:40:41UTC. P7-10 is implemented CONDITIONAL GO and published. P7-11 [shared integration](p7-11/SHARED.md) has80/11 affected/consumer frontend, current7version/10command vectors and native5modal+1Strategy PASS; final hygiene/publication follows.59paired label additions are the shared foundation for subsequent responsible-owner commits. Whole candidate frontend898/101 and backend1057 pass; final ARIA continuations receive focused/native proof and fresh P7-12 regression. diff --git a/docs/redesign/phase-7/PRESERVATION.md b/docs/redesign/phase-7/PRESERVATION.md index 4410389..585edda 100644 --- a/docs/redesign/phase-7/PRESERVATION.md +++ b/docs/redesign/phase-7/PRESERVATION.md @@ -1,5 +1,7 @@ # Phase 7 — Initial preservation matrix +P7-11 [error/navigation integration](p7-11/ERRORS.md) adds bounded PRES-C05/C06/C07/C08/E02/E08/E09: owner404 remains indistinguishable from missing, denied reads do not expose an editor, temporary GET recovery cannot save, version/source metadata and unknown values remain intact, actual document archive/download/query contracts and wildcard/back routes persist. Native10cases and18axe scans0violations/105contrast incompletes remain finite qualifications. All57 groups retain56 PARTIAL/C11 DEFERRED and33 routes; fresh integrated reconciliation is P7-12. + P7-11 [localisation](p7-11/LOCALISATION.md) adds bounded PRES-C08/C10/W04/M04/E09 evidence: original template selection objects/IDs/admission and English/custom metadata retained; known Bokmål generated labels and history enums translate without modifying user text. Resumed-draft fallback derives current language without rereads or dirty-field loss. Native9cases/50scans0violations retain355qualified incomplete observations. All57 groups remain56 PARTIAL/C11 DEFERRED and33 routes; no provider/model/revision/renderer or production acceptance claim. P7-11 [shared integration](p7-11/SHARED.md) adds bounded PRES-E07/E08/W04/W09 evidence: valid badge/command names, retained item-specific checklist/interview actions, contextual form headings and Strategy GET-only error recovery with exact intentional-empty contract. Existing AI/Pro and operations semantics remain. All57 groups stay56 PARTIAL/C11 DEFERRED and33 routes; accessibility incompletes/manual/provider/output/production gates remain qualified. No Phase8 work. diff --git a/docs/redesign/phase-7/p7-11/ERRORS.md b/docs/redesign/phase-7/p7-11/ERRORS.md new file mode 100644 index 0000000..2d070fc --- /dev/null +++ b/docs/redesign/phase-7/p7-11/ERRORS.md @@ -0,0 +1,21 @@ +# Documents and error navigation + +Final1440px missing-CV and1024px Documents images were actually re-inspected after the toolbar correction. The final320px Bokmål unknown-route image is byte-identical to the already inspected image. Final source/hash and scoped hygiene evidence are retained; physical/assistive acceptance remains pending. + +**Implemented CONDITIONAL GO; final hygiene/publication pending.** Existing Documents metadata projection/query/download and CV entry/history owners remain. + +An ordinary browser regression reproduced a single English action word spanning two lines at1024px. Natural table sizing and word-boundary action wrapping retain complete labels without forcing app overflow. Existing metadata wrapping, owner scoping, sort/filter/page/deep-link and immutable archive behaviors remain. + +Private CV opening errors now use shared page/read-state presentation: contextual heading, localized missing/denied context, Back and GET-only Retry for temporary failures. Missing/foreign404 and denied401/403 do not offer retry or expose a private ID. Successful retry focuses the existing editing region without a save; prior-owner lifetime cancellation and renderer/pagination remain unchanged. + +CV history translates only its known source enums, preserving version numbers, restore actions and unknown values. An additional unknown `constructor` regression caught inherited-object lookup; own-key lookup preserves that value verbatim. Current-version metadata uses valid block markup around the existing badge. No revision or content geometry changes. + +Unknown routes retain the wildcard, Home and browser Back actions, shared surface tokens and a contextual shell h1/owner h2. Two bilingual unit cases prove Back retains its previous query. A first test harness attempted data-router navigation without jsdom Request; the corrected MemoryRouter exercises the same navigation without a new runtime dependency. Both results remain retained. + +Initial CV entry44-case vector41passed/3failed; error correction then passed44cases. History added one ordinary failing case and its prototype-name extension also failed before own-key lookup. Affected final CV/NotFound/Documents vector **62/62 across three suites PASS** before that last own-key assertion; its targeted rerun passes and the fresh full run covers the final source. Native **10/10 PASS** includes actual deleted-source404, controlled503 recovery with unchanged version/no save, bilingual unknown-route Back, all seven metadata resource kinds and eight responsive/theme samples. These finite results are not human/device/PDF acceptance. Final source/visual/static/publication evidence follows. + +Raw incomplete triage found the same roleless-label pattern on the existing CV Undo/Redo tooltip wrappers. An assertion in the existing edit/undo/redo regression failed before applying the established describeChild pattern; the complete selected undo/redo workflow passes afterward. Both actual button names, disabled states and data actions remain. No new case or disabled test is introduced. + +Final native **10/10 PASS**, no skips/flaky/retries,18:21:21–18:22:22UTC;20images/18all-enabled axe scans,0violations/105contrast incomplete observations. The earlier two roleless-toolbar incompletes are absent after the correction. [Portable results](evidence/errors-results.json) preserve final source/result/image hashes, while the prior candidate is archived locally. Three representative images were inspected before the toolbar-only correction:320px Bokmål unknown route,1024px English dark Documents and1440px missing CV; final images will be spot-checked separately. Fixed chrome/full-page and overlap/short-badge contrast remain qualified evidence rather than viewport or human acceptance. + +Existing data/backend/PDF/AI safeguards and all57 preservation dispositions remain. Shared modal changes receive their separate all-consumer regression; no new dialog infrastructure or Phase8 work is introduced. Fresh full final-source regression belongs to P7-12. Static checks and normal push follow. diff --git a/docs/redesign/phase-7/p7-11/LOCALISATION.md b/docs/redesign/phase-7/p7-11/LOCALISATION.md index 0f4bd71..25bed86 100644 --- a/docs/redesign/phase-7/p7-11/LOCALISATION.md +++ b/docs/redesign/phase-7/p7-11/LOCALISATION.md @@ -1,5 +1,7 @@ # Generated-label localisation +Publication: **ab89b6984c4abc4785e9871f93c374f0fd8b8e08**, normal push/direct verification20October18:27:48UTC. Error-owner WIP was preserved; earlier pending-publication text is historical. + **Implemented CONDITIONAL GO; final hygiene/publication pending.** Existing gallery, Reminders, application materials and correspondence owners remain. Known built-in template IDs map to Bokmål names/categories/descriptions; English retains server metadata and unknown templates remain verbatim. Selection still passes the original theme object and preserves IDs, availability, Pro admission and renderer settings. A full frontend consumer caught an initial change to English descriptions; limiting the mapping to Bokmål restored the existing English contract, with7affected gallery/document-control cases passing. diff --git a/docs/redesign/phase-7/p7-11/README.md b/docs/redesign/phase-7/p7-11/README.md index 2bfdd54..bfa78f1 100644 --- a/docs/redesign/phase-7/p7-11/README.md +++ b/docs/redesign/phase-7/p7-11/README.md @@ -20,11 +20,13 @@ These findings were carried from implemented checkpoints and verified in source, ## Execution boundaries 1. [Shared accessibility and forms](SHARED.md), including the paired translation foundation used by the following owners. -2. [Generated-label localisation](LOCALISATION.md), preserving all stored metadata and user text; final hygiene/publication follows. -3. Documents and error navigation follows separately, including CV history display in its existing owner. +2. [Generated-label localisation](LOCALISATION.md), publishedab89b69; all stored metadata and user text preserved. +3. [Documents and error navigation](ERRORS.md), including CV history display in its existing owner; final hygiene/publication follows. The independently authorized release repair is separate: development `bba52af4b8724f1a5c74013ce1476a20ca3972c2` was pushed/directly verified18:05:08UTC. Main contains equivalent fixture/focus repair `3e1057d` and Qwen compatibility `f7dc22e`, normally pushed and directly verified20October18:05UTC. Run833 is testing; deployment/model activation are unverified. [Release evidence](../../../infrastructure/2026-10-20-browser-fixture-and-focus-contracts.md) retains run832's314passing/16failed/oneflaky outcome and the two demonstrated causes. No failed assertion was hidden or timeout relaxed. ## Acceptance boundaries -P7-11 implementation and publication are in progress. Full integrated acceptance belongs to P7-12. All57 preservation groups retain56 PARTIAL/C11 DEFERRED and33 routes; no capability or route is removed. Manual reader/device/software-keyboard/zoom, production output/provider, reliability and release requirements remain. P5-13 stays withheld; followMode remains deferred. No Phase8 implementation or new dashboard customization. Production readiness remains NO-GO independently of the owner's authorization to release main. +P7-11 is implemented **CONDITIONAL GO**: shared d8c0187 and localisationab89b69 are published; final error-owner hygiene/publication follows. [Validation](evidence/validation.json) records full candidate frontend898/101, full unchanged backend1057 and offline sidecar56; late ARIA corrections also have direct/native proof. Final committed-source integrated acceptance belongs to P7-12. The14new frontend cases are6shared,3generated-label and5CV-entry/history/not-found; two new suites give101. Three new browser cases derive351/57files. Existing assertion changes and overlapping focused vectors do not inflate totals. + +All57 preservation groups retain56 PARTIAL/C11 DEFERRED and33 routes; no capability or route is removed. Manual reader/device/software-keyboard/zoom, production output/provider, reliability and release requirements remain. P5-13 stays withheld; followMode remains deferred. No Phase8 implementation or new dashboard customization. Production readiness remains NO-GO independently of the owner's authorization to release main. diff --git a/docs/redesign/phase-7/p7-11/evidence/errors-results.json b/docs/redesign/phase-7/p7-11/evidence/errors-results.json new file mode 100644 index 0000000..784ac18 --- /dev/null +++ b/docs/redesign/phase-7/p7-11/evidence/errors-results.json @@ -0,0 +1,318 @@ +{ + "observedUtc": "2026-10-20T18:23:02.369873+00:00", + "sourceScope": "Post-run source identities; no claim of pre-run capture or that later catalogue/comment changes were in earlier exports", + "sources": { + "src/App.tsx": "d82a01fa21d9153bc0a7dbd1be0acbf2182b801eaef570cd2191e0a0440e9138", + "src/views/NotFoundPage.tsx": "7564f69301e84de2b8c88e2099d4667b7d438e459ac5f02009a090b36118207a", + "src/views/DocumentsPage.tsx": "a6158c4df802264df21a15747d235df57b7424222f6fd5fc59f4dc15b298aacf", + "src/views/CvBuilderEditor.tsx": "2ce2ff220e65be23ae46ec68327380733ee4157884db6e6a10450840692d2942", + "src/i18n/translations.ts": "15cc09b7dfcea607b6a597d1379aaad5d3e1c01adcc518846a9047b398ba5458" + }, + "native": [ + { + "name": "documents", + "stats": { + "startTime": "2026-10-20T18:21:21.357Z", + "duration": 61208.676, + "expected": 10, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "path": ".artifacts/phase7/current/ui/documents.json", + "sha256": "aae95e219b6eca208179947bb1f10e087680c6c6ec3771ac00a67267faa34199", + "command": { + "name": "documents", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "e2e/phase7-documents.spec.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\documents-results" + ], + "startedUtc": "2026-10-20T18:21:20.743569+00:00", + "finishedUtc": "2026-10-20T18:22:22.620264+00:00", + "exitCode": 0 + } + } + ], + "images": [ + { + "name": "p707-documents-en-light-1440-seven-honest-metadata-kinds", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-seven-honest-metadata-kinds-58dbe958.png", + "sha256": "58dbe9587c1a70ce3221b6b968b722b5cae88205d915fb812dd5f27bb1e79018" + }, + { + "name": "p707-documents-en-light-1440-filtered-server-page-reloaded", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-filtered-server-page-reloaded-cf36b850.png", + "sha256": "cf36b8505d157f3ec2665a0602c5d855bf5ed9b9b31ed8ed03731c234ab208f3" + }, + { + "name": "p707-documents-en-light-1440-latest-query-after-get-recovery", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-latest-query-after-get-recovery-4fb8302e.png", + "sha256": "4fb8302ef6cbf9161054bf17d0068bc9db5ada5781be705aa87e0479e47b0f05" + }, + { + "name": "p707-documents-en-light-1440-existing-missing-source-editor", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-existing-missing-source-editor-a479b17a.png", + "sha256": "a479b17ae06ec14d50ec11c1553296f901ecfc2cfbbd7458f028e033a9153b14" + }, + { + "name": "p707-documents-en-light-1440-recovered-private-cv-entry", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-recovered-private-cv-entry-6b635896.png", + "sha256": "6b6358963440da5e7cd8ddc2e9a33d3039e2d234d909db2d405cb532ecd0ec3b" + }, + { + "name": "p707-documents-en-light-1440-unknown-route-context", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-unknown-route-context-c141204c.png", + "sha256": "c141204c95347141ad148dfdfd20f5621e5b1ab34723d51a1c9c7edca55ee35e" + }, + { + "name": "p707-documents-nb-dark-320-unknown-route-context", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-dark-320-unknown-route-context-deeec96e.png", + "sha256": "deeec96ef0a4790468fe38184a8ee71ca5ae509825d9f4667c224b84cf4c859f" + }, + { + "name": "p707-documents-en-light-1440-real-deleted-attachment-stale-metadata", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-real-deleted-attachment-stale-metadata-63e75c94.png", + "sha256": "63e75c941099fe587a8c14b5a614fc0f4ed0822aa551787264a9dde43a604a0e" + }, + { + "name": "p707-documents-en-light-1440-second-owner-no-cached-private-rows", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-second-owner-no-cached-private-rows-602ad317.png", + "sha256": "602ad3176148358b9935b9babdb5c737b450a8529f47bda0ac539a64fb1418a8" + }, + { + "name": "p707-documents-en-light-1440-existing-letter-dirty-navigation-cancel", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-existing-letter-dirty-navigation-cancel-6a9d4c32.png", + "sha256": "6a9d4c3290600d9262968cfa4bd3443d53fd1ad8b492fd4b5992ea28dfa61ae1" + }, + { + "name": "p707-documents-en-light-320-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-320-long-metadata-keyboard-363a8f9d.png", + "sha256": "363a8f9dd2a8b536ab696634dfeab196e40d202d4b07876d3900b63a01d9c824" + }, + { + "name": "p707-documents-en-320-focused-viewport", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-320-focused-viewport-4dae32c3.png", + "sha256": "4dae32c3297949523f0b567ee00c2333493a346b965ba81ec1ec538f5338f7ee" + }, + { + "name": "p707-documents-nb-dark-360-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-dark-360-long-metadata-keyboard-c89865a1.png", + "sha256": "c89865a1545364a32d21ba05efbd134b4d009f83f9ebbbd9986720aee02a895e" + }, + { + "name": "p707-documents-en-dark-390-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-dark-390-long-metadata-keyboard-d0c6ac7e.png", + "sha256": "d0c6ac7ec67965cfc0495ab3dcc69ee294299730f515555e43a2957dbc77e0db" + }, + { + "name": "p707-documents-nb-light-768-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-light-768-long-metadata-keyboard-e1c464ab.png", + "sha256": "e1c464ab7de66239d8402dc7b7b17ea7a42d037b71cad61306f2c0332a194710" + }, + { + "name": "p707-documents-en-dark-1024-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-dark-1024-long-metadata-keyboard-ddf40ba8.png", + "sha256": "ddf40ba88b646f4526cef6b106c55d77648e0f94c9309461cefdfeb0fd961783" + }, + { + "name": "p707-documents-nb-light-1280-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-light-1280-long-metadata-keyboard-a91425ef.png", + "sha256": "a91425ef15f7c42df22ee6f53569bcb6a6fd140653a40e357823fa1a018ba69f" + }, + { + "name": "p707-documents-nb-1280-focused-viewport", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-1280-focused-viewport-3badb0ea.png", + "sha256": "3badb0ea5a69911fa93ea3f9cc5073b8a7ec61bd3d4a3a9d65f8dccc790fa6cb" + }, + { + "name": "p707-documents-en-light-1440-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-long-metadata-keyboard-2499d64d.png", + "sha256": "2499d64db08a8f3e54bac6f9cde144528cab60c82bba4ddce87feb866daf841d" + }, + { + "name": "p707-documents-nb-dark-1920-long-metadata-keyboard", + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-dark-1920-long-metadata-keyboard-f540740f.png", + "sha256": "f540740fe60c75cc6bc99f5c2e6fdfcc03ff6a45092ae4c2d80b8b69edeaa8b0" + } + ], + "scans": [ + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-seven-honest-metadata-kinds-audit-461298d6.json", + "sha256": "461298d643f530e809a3bd18f361aae133f9bf3404a7a4eef97137af990b7d66", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-filtered-server-page-reloaded-audit-f18ab53e.json", + "sha256": "f18ab53e1d8b58b3f51b6a26050ce5edc41cf1853e4938d1dd5577a95e626106", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-latest-query-after-get-recovery-audit-97bcc017.json", + "sha256": "97bcc01718f4c8871a228c488e4abe07e1be6b570ecafe554ae3b5da7769e9b6", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-existing-missing-source-editor-audit-356d6a9c.json", + "sha256": "356d6a9c0d6ed87819fb1f4456732175b71264354b49e8c435696d4513bf8c1f", + "violations": 0, + "incompleteNodes": 1, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-recovered-private-cv-entry-audit-b4e46a2f.json", + "sha256": "b4e46a2f71bb33b66cd0ed19830bc1f267124d95827aa95ef2a6dca4de626fb8", + "violations": 0, + "incompleteNodes": 2, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-unknown-route-context-audit-f0e2960a.json", + "sha256": "f0e2960ab385b1034ac006f58773e70a1c01565d6063b5efa7c1f82a14cd0fb5", + "violations": 0, + "incompleteNodes": 1, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-dark-320-unknown-route-context-audit-f618e0bb.json", + "sha256": "f618e0bb7ea781a90353604880de920e0468a46ee3a54bba129a675b8f7402ae", + "violations": 0, + "incompleteNodes": 1, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-real-deleted-attachment-stale-metadata-audit-17f80c12.json", + "sha256": "17f80c124c399d95246f3c8116334fe3eab6dc560d2d04b5aeadc1d1766b40ae", + "violations": 0, + "incompleteNodes": 8, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-second-owner-no-cached-private-rows-audit-29f2d4c5.json", + "sha256": "29f2d4c59be7757381c239e6182a04a5212fd756a4fd55e4d1241bc0b58bfea8", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-existing-letter-dirty-navigation-cancel-audit-fac01749.json", + "sha256": "fac01749688de81fe08163773bfefc725032a470f94f205286efe51a5fee6204", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-320-long-metadata-keyboard-audit-839344d1.json", + "sha256": "839344d1e02a7dc8f1f1262178dfb680b020c461bd8a1eeca35a565fc379a1ab", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-dark-360-long-metadata-keyboard-audit-2de18d00.json", + "sha256": "2de18d00bd329798f804213e178099c37e76e7b9fa45c1d59def0fd2136b2ae2", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-dark-390-long-metadata-keyboard-audit-a3f57bc2.json", + "sha256": "a3f57bc2a217c0a03534ee84fb05f8e056b39ec610ce282a477c56eddc6bdce2", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-light-768-long-metadata-keyboard-audit-a884c3ff.json", + "sha256": "a884c3ff5931f13b4ca8686cdf2bfe4955d1693c9877a9cc616e117fb30d888c", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-dark-1024-long-metadata-keyboard-audit-fb860188.json", + "sha256": "fb860188c02f6f70da417fc5da308a51e663ea9a7728b70d37c94f9299bcf6f8", + "violations": 0, + "incompleteNodes": 8, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-light-1280-long-metadata-keyboard-audit-f02e6dd5.json", + "sha256": "f02e6dd5813d36602ca85326b848eff0d42f9c3a61ed45ebf46beb80731aa510", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-en-light-1440-long-metadata-keyboard-audit-0a4cf49f.json", + "sha256": "0a4cf49f6e20be1c3d80cd2e4ef78d94a704ef6acec5bf32d72922dedc9860ff", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + }, + { + "path": ".artifacts/phase7/p7-11/errors-visuals/p707-documents-nb-dark-1920-long-metadata-keyboard-audit-e8620407.json", + "sha256": "e8620407a7cf06e8d65b15218d07f5b90b5ca9407b8c9682f7395b3bf284decb", + "violations": 0, + "incompleteNodes": 7, + "incompleteRules": [ + "color-contrast" + ] + } + ], + "focusedUnit": { + "path": ".artifacts/phase7/current/unit/residual-cv-final-20261020T180003Z.json", + "sha256": "2ce106be0a01f34ad1ddf7db01386b3d6178e62eac39f046f1a74e3c88afcef6", + "numPassedTests": 62, + "numFailedTests": 0, + "numPendingTests": 0, + "numPassedTestSuites": 3, + "qualification": "Overlapping focused vectors; not a new full-suite total. Final integrated run covers subsequent documented corrections." + } +} diff --git a/docs/redesign/phase-7/p7-11/evidence/errors-static-checks.json b/docs/redesign/phase-7/p7-11/evidence/errors-static-checks.json new file mode 100644 index 0000000..9c3b317 --- /dev/null +++ b/docs/redesign/phase-7/p7-11/evidence/errors-static-checks.json @@ -0,0 +1,104 @@ +{ + "schemaVersion": 1, + "checkpoint": "p7-11", + "status": "PASS", + "checkedUtc": "2026-10-20T18:29:05.012067+00:00", + "commands": [ + { + "name": "secret-scan", + "command": [ + "rtk", + "proxy", + "C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe", + "-X", + "utf8", + "scripts/supply-chain.py", + "scan-secrets" + ], + "startedUtc": "2026-10-20T18:29:01.643188+00:00", + "finishedUtc": "2026-10-20T18:29:04.227001+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261020T182901Z/secret-scan.log", + "logSha256Raw": "be5119e6057287d397e87759f855c91e634e3bfac61c8be7584d05592ba2dfcd" + }, + { + "name": "working-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--check" + ], + "startedUtc": "2026-10-20T18:29:04.227510+00:00", + "finishedUtc": "2026-10-20T18:29:04.314703+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261020T182901Z/working-whitespace.log", + "logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "staged-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--cached", + "--check" + ], + "startedUtc": "2026-10-20T18:29:04.314703+00:00", + "finishedUtc": "2026-10-20T18:29:04.403015+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261020T182901Z/staged-whitespace.log", + "logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "phase7-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--check", + "b5319981ad3d1bd635daff861453d23c0de5320b" + ], + "startedUtc": "2026-10-20T18:29:04.403524+00:00", + "finishedUtc": "2026-10-20T18:29:04.586091+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261020T182901Z/phase7-whitespace.log", + "logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ], + "untrackedSecretScan": { + "files": 0, + "findings": 0 + }, + "documentation": { + "files": 38, + "relativeLinks": 649, + "errors": [] + }, + "preservation": { + "groups": 57, + "statuses": { + "PARTIAL": 56, + "PASS": 0, + "FAIL": 0, + "DEFERRED": 1 + }, + "originalRoutesPreserved": true, + "currentConcreteRoutes": 33 + }, + "production": { + "main": "f7dc22e5e7efe1fd1d64f6ace2c04cf2f95bf558", + "originMain": "f7dc22e5e7efe1fd1d64f6ace2c04cf2f95bf558", + "status": "NO-GO", + "qualification": "Local fetched refs; direct push observation recorded separately." + }, + "hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.", + "limits": [ + "Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.", + "Local links check target existence, not URL/heading anchors.", + "Static evidence is not runtime/manual/device/provider/production acceptance." + ] +} diff --git a/docs/redesign/phase-7/p7-11/evidence/validation.json b/docs/redesign/phase-7/p7-11/evidence/validation.json new file mode 100644 index 0000000..320f99b --- /dev/null +++ b/docs/redesign/phase-7/p7-11/evidence/validation.json @@ -0,0 +1,167 @@ +{ + "observedUtc": "2026-10-20T18:28:35.008151+00:00", + "commands": { + "jest": { + "path": ".artifacts/phase7/current/ui/jest-command.json", + "sha256": "ee68686f64f276210c32a749f32b6f8bcfe56ecd096a0c6733f7c4f3aa424684", + "name": "jest", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "test", + "--", + "--runInBand", + "--watchAll=false", + "--json", + "--outputFile=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\jest.json" + ], + "startedUtc": "2026-10-20T18:21:10.511868+00:00", + "finishedUtc": "2026-10-20T18:26:55.487247+00:00", + "exitCode": 0 + }, + "build": { + "path": ".artifacts/phase7/current/ui/build-command.json", + "sha256": "f46ff88985001de874fd76b6a5656a69a780990622784da99aec16ec285d3ad9", + "name": "build", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "run", + "build" + ], + "startedUtc": "2026-10-20T18:22:53.979066+00:00", + "finishedUtc": "2026-10-20T18:23:01.412353+00:00", + "exitCode": 0 + }, + "lint": { + "path": ".artifacts/phase7/current/ui/lint-command.json", + "sha256": "9ada4bafe999baaa5d628c82bdce4d93557bc47b9e8b8c453d2a59881f1649aa", + "name": "lint", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "run", + "lint" + ], + "startedUtc": "2026-10-20T18:23:19.536275+00:00", + "finishedUtc": "2026-10-20T18:23:49.113610+00:00", + "exitCode": 0 + }, + "typecheck": { + "path": ".artifacts/phase7/current/ui/typecheck-command.json", + "sha256": "7f5e060600d39821481f028800a549f218b382af97fb9b45e5b72148a5380bfa", + "name": "typecheck", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "run", + "typecheck" + ], + "startedUtc": "2026-10-20T18:23:19.539153+00:00", + "finishedUtc": "2026-10-20T18:23:37.316282+00:00", + "exitCode": 0 + }, + "browser-typecheck": { + "path": ".artifacts/phase7/current/ui/browser-typecheck-command.json", + "sha256": "c3cbabe4a0dcbb0dd0b8ee30f3bcd36ce619753057b06b2c43e37456160ec281", + "name": "browser-typecheck", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "exec", + "tsc", + "--", + "--project", + "../.artifacts/phase7/e2e-tsconfig.json", + "--noEmit" + ], + "startedUtc": "2026-10-20T18:17:47.140645+00:00", + "finishedUtc": "2026-10-20T18:17:53.079386+00:00", + "exitCode": 0 + } + }, + "qualification": "Full candidate unit checks and final build/type/lint; focused/native continuations verify the late ARIA corrections. P7-12 fresh committed-source acceptance is pending.", + "frontend": { + "numTotalTests": 898, + "numPassedTests": 898, + "numFailedTests": 0, + "numPendingTests": 0, + "numTotalTestSuites": 101, + "sha256": "2247810dc390ccc51982fc559827ef2cc91bdfe106e9e9cc1290b36e7870a6e5" + }, + "backend": { + "path": ".artifacts/phase7/current/backend-career/p711-unchanged-contracts-20261020T181338Z/command.json", + "sha256": "bb01ea92fcb33505789f09e93e9c3b88ee46c0092f9e7c89e65ffc409ba83756", + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-build", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p711-unchanged-contracts-20261020T181338Z" + ], + "startedUtc": "2026-10-20T18:13:38.997804+00:00", + "finishedUtc": "2026-10-20T18:13:58.699714+00:00", + "exitCode": 0, + "logSha256": "2b13290a9facb98ea4e2413ac24b5130f251ceb15fcd8a344b829987fb88c79c", + "counts": { + "total": 1057, + "passed": 1057, + "failed": 0, + "notExecuted": 0 + }, + "resultSha256": "ffe26174c3e4895b4d6fcc81856fea34f1aa17898ef31e1f72200373a2d1a3ce" + }, + "sidecar": { + "path": ".artifacts/release-20261020/model-compat/green.xml", + "sha256": "e678906577ffb4b68c0147f73f16d83d177d2963cad0084a9c58556e1e792788", + "attributes": { + "name": "pytest", + "errors": "0", + "failures": "0", + "skipped": "0", + "tests": "56", + "time": "8.450", + "timestamp": "2026-10-20T15:32:09.302765+02:00", + "hostname": "Cesnimda" + } + }, + "countReconciliation": { + "p710DerivedFrontend": 884, + "p711AddedFrontend": { + "sharedIncludingVersionLocale": 6, + "generatedLabelOwners": 3, + "cvEntryHistoryAndNotFound": 5 + }, + "frontend": 898, + "suites": 101, + "priorSuites": 99, + "newSuites": [ + "cv-template-gallery", + "not-found-page" + ], + "p710BrowserInventory": 348, + "p711NewBrowserCases": { + "documentsCvRecoveryAndUnknownRoute": 2, + "strategyReadRecovery": 1 + }, + "derivedBrowser": 351, + "browserFiles": 57, + "sidecarPrevious": 49, + "sidecarNewRequestContracts": 7, + "sidecar": 56, + "backend": 1057, + "note": "Existing assertions changed without adding cases; focused overlaps are not summed." + } +} diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md index 1df2625..24a44e2 100644 --- a/docs/roadmap/todo.md +++ b/docs/roadmap/todo.md @@ -94,7 +94,7 @@ Follow the approved [Phase 7 handoff](../redesign/phase-6/p6-05/PHASE7_HANDOFF.m - [x] P7-08 [existing notifications and operations](../redesign/phase-7/p7-08/README.md): bothpresentationboundariesimplementedCONDITIONALGO. UTC9dfea/recent40cfe04 published; Operations77/7+native12+types/lint/build/14Ops+1P2actualimages PASS;12Opsaxe0/3+14notifaxe0/102qualified. FinalOpshygiene/normalpushpending.791/95frontend323browser1054backend derivedonly;57groups33routes/mainunchanged/productionNO-GO. P7-09next. - [x] P7-09 [Settings, Connections and data lifecycle](../redesign/phase-7/p7-09/README.md): implemented CONDITIONAL GO. Sender/Preferences/import/company published; remaining data presentation47/6 affected frontend,22backendcontracts, final native23/types/lint/build PASS. 37axe0violations/141incomplete nodes qualified; six final PNGs inspected. Final hygiene/publication recorded in checkpoint ledger. Inventory855/99frontend,339browser,1057backend; no fresh whole-suite claim. P5-13 withheld, manual/provider/output/release qualifications retained. - [x] P7-10 Users7ecc0d6/4760fd1, Auditf9e1d06 and Systemd824f59 published/directly verified; implemented CONDITIONAL GO. Shared/manual accessibility conditions remain. -- [ ] P7-11 residual Analytics/error/empty/dialog integration. +- [x] P7-11 bounded residual integration implemented CONDITIONAL GO; sharedd8c0187/localisationab89b69 published, error-owner final hygiene/publication follows. Candidate898/101frontend,1057backend,56sidecar; final committed-source acceptance remainsP7-12. - [ ] P7-12 integrated migration acceptance and Phase 8 handoff. ### Final P7-06 embedded-forms observation diff --git a/job-tracker-ui/e2e/phase7-documents.spec.ts b/job-tracker-ui/e2e/phase7-documents.spec.ts index 6fecf72..884f077 100644 --- a/job-tracker-ui/e2e/phase7-documents.spec.ts +++ b/job-tracker-ui/e2e/phase7-documents.spec.ts @@ -99,10 +99,43 @@ test('[P7-07 Documents] initial503 Retry and incompatible held query expose no s test('[P7-07 Documents] stale CV opens the existing missing-source error and Back plus explicit Refresh removes its metadata', async ({ page, session }, info) => { test.setTimeout(120_000); const variant = await cv(page.request, session.owner, 'Removed current CV — æøå'); await enter(page, session.owner); const item = (await readIndex(page.request)).items[0]; await expect(row(page, item)).toBeVisible(); const from = indexPath(page); noImplicitSourceWork(session); expect((await page.request.delete(`${apiUrl}/cv/variants/${variant.id}`, { headers: session.owner.headers })).status()).toBe(204); - const missing = waitResponse(page, 'GET', `/api/cv/variants/${variant.id}`); await keyboard(page, row(page, item).getByRole('link', { name: copy('documentsOpenSource'), exact: true })); expect((await missing).status()).toBe(404); await expect(page).toHaveURL(new RegExp(`/career/builder/${variant.id}$`)); const sourceError = page.getByRole('main').getByRole('alert'); await expect(sourceError).toBeVisible(); await expect(sourceError).toHaveText('Not Found'); await expect(page.getByRole('heading', { level: 2, name: copy('documentsYourDocuments'), exact: true })).toHaveCount(0); await capture(page, info, desktop, 'existing-missing-source-editor'); await page.goBack(); await expect(results(page)).toBeVisible(); expect(indexPath(page)).toBe(from); + const missing = waitResponse(page, 'GET', `/api/cv/variants/${variant.id}`); await keyboard(page, row(page, item).getByRole('link', { name: copy('documentsOpenSource'), exact: true })); expect((await missing).status()).toBe(404); await expect(page).toHaveURL(new RegExp(`/career/builder/${variant.id}$`)); const sourceError = page.getByRole('main').getByRole('alert'); await expect(sourceError).toBeVisible(); await expect(sourceError).toContainText(copy('cvEditorSourceUnavailable')); await expect(sourceError.getByRole('button', { name: copy('retry'), exact: true })).toHaveCount(0); await expect(page.getByRole('heading', { level: 2, name: copy('cvBuilder'), exact: true })).toBeVisible(); await expect(page.getByRole('heading', { level: 2, name: copy('documentsYourDocuments'), exact: true })).toHaveCount(0); await capture(page, info, desktop, 'existing-missing-source-editor'); await page.goBack(); await expect(results(page)).toBeVisible(); expect(indexPath(page)).toBe(from); const refreshed = waitResponse(page, 'GET', '/api/documents'); await keyboard(page, page.getByRole('button', { name: copy('documentsRefresh'), exact: true })); expect(metadata(await json(await refreshed)).total).toBe(0); await expect(row(page, item)).toHaveCount(0); await expect(page.getByText(copy('documentsEmpty'), { exact: true })).toBeVisible(); expect(writes(session)).toEqual([]); }); +test('[P7-11 E08 C05] transient private CV entry recovers GET reads and keyboard focus without saving', async ({ page, session }, info) => { + const variant = await cv(page.request, session.owner, 'Read-only recovery CV'); + await enter(page, session.owner); + let failed = true; + await page.route(`**/api/cv/variants/${variant.id}`, route => failed ? route.fulfill({ status: 503, json: { title: 'Controlled unavailable read' } }) : route.continue()); + await keyboard(page, row(page, { name: variant.name, type: 'cv' }).getByRole('link', { name: copy('documentsOpenSource'), exact: true })); + const alert = page.getByRole('alert').filter({ hasText: copy('cvEditorOpenFailed') }); + await expect(alert).toBeVisible(); + failed = false; + await keyboard(page, alert.getByRole('button', { name: copy('retry'), exact: true })); + const editor = page.getByRole('region', { name: copy('cvEditorEditingRegion'), exact: true }); + await expect(editor).toBeFocused(); + await expect(page.getByLabel(copy('cvEditorName'), { exact: true })).toHaveValue(variant.name); + expect((await json(await page.request.get(`${apiUrl}/cv/variants/${variant.id}`))).version).toBe(variant.version); + expect(writes(session).every(value => value.method === 'POST' && value.path === `/api/cv/variants/${variant.id}/preview`)).toBe(true); + await capture(page, info, desktop, 'recovered-private-cv-entry'); +}); + +test('[P7-11 E08] unknown routes have bilingual contextual headings and preserve browser history', async ({ page, session }, info) => { + for (const sample of [desktop, { language: 'nb', theme: 'dark', width: 320, height: 844 }] as Sample[]) { + await enter(page, session.owner, sample); + const from = indexPath(page); + await page.goto('/phase7-unknown-route?keep=context'); + await expect(page.getByRole('heading', { level: 1, name: copy('notFoundTitle', {}, sample.language), exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { level: 2, name: copy('notFoundTitle', {}, sample.language), exact: true })).toBeVisible(); + await capture(page, info, sample, 'unknown-route-context'); + await keyboard(page, page.getByRole('button', { name: copy('goBack', {}, sample.language), exact: true })); + await expect(results(page, sample.language)).toBeVisible(); + expect(indexPath(page)).toBe(from); + } + expect(writes(session)).toEqual([]); +}); + test('[P7-07 Documents] explicit authenticated attachment GET retries exact bytes and a real404 offers only list Refresh', async ({ page, session }, info) => { test.setTimeout(150_000); const parent = await job(page.request, session.owner); const file = await upload(page.request, session.owner, parent); await enter(page, session.owner); const item = (await readIndex(page.request)).items[0]; await expect(row(page, item)).toBeVisible(); noImplicitSourceWork(session); const path = `/api/attachments/download/${file.attachment.id}`; let attempts = 0; await page.route(`${apiUrl}/attachments/download/${file.attachment.id}`, async route => { attempts += 1; if (attempts === 1) return route.fulfill({ status: 503, json: {} }); return route.fulfill({ response: await route.fetch() }); }); await keyboard(page, row(page, item).getByRole('button', { name: copy('documentsDownloadAttachment'), exact: true })); const failed = page.getByRole('alert').filter({ hasText: copy('documentsDownloadFailed') }); await expect(failed).toBeVisible(); expect(count(session, 'GET', path)).toHaveLength(1); const listReads = count(session, 'GET', '/api/documents').length; const downloaded = page.waitForEvent('download'); const actual = waitResponse(page, 'GET', path); await keyboard(page, failed.getByRole('button', { name: copy('retry'), exact: true })); const response = await actual; expect(response.status()).toBe(200); expect(response.headers()['content-type']).toContain('text/plain'); const download = await downloaded; expect(download.suggestedFilename()).toBe(file.name); const saved = await download.path(); expect(saved).not.toBeNull(); expect(await readFile(saved!)).toEqual(file.bytes); expect(attempts).toBe(2); expect(count(session, 'GET', '/api/documents')).toHaveLength(listReads); await expect(row(page, item)).toBeVisible(); @@ -123,6 +156,25 @@ test('[P7-07 Documents] existing letter source keeps dirty edits on navigation C test('[P7-07 Documents] eight finite bilingual theme widths retain long metadata keyboard actions and complete axe vectors', async ({ page, session }, info) => { test.setTimeout(300_000); const seeded = await seven(page, session.owner, true); const samples: Sample[] = [{ language: 'en', theme: 'light', width: 320, height: 740 }, { language: 'nb', theme: 'dark', width: 360, height: 844 }, { language: 'en', theme: 'dark', width: 390, height: 844 }, { language: 'nb', theme: 'light', width: 768, height: 1024 }, { language: 'en', theme: 'dark', width: 1024, height: 768 }, { language: 'nb', theme: 'light', width: 1280, height: 320 }, { language: 'en', theme: 'light', width: 1440, height: 900 }, { language: 'nb', theme: 'dark', width: 1920, height: 1080 }]; for (const sample of samples) { + if (sample.width === 1024) { + await enter(page, session.owner, sample); + const open = page.getByRole('link', { name: copy('documentsOpenCorrespondence', {}, sample.language), exact: true }).first(); + await expect(open).toBeVisible(); + const wordLines = await open.evaluate(button => { + const walker = document.createTreeWalker(button, NodeFilter.SHOW_TEXT); + let node: Node | null; + while ((node = walker.nextNode())) { + const text = node.textContent?.trim(); + if (!text) continue; + const range = document.createRange(); + range.setStart(node, (node.textContent ?? '').lastIndexOf(' ') + 1); + range.setEnd(node, node.textContent!.length); + return range.getClientRects().length; + } + return 0; + }); + expect(wordLines).toBe(1); + } await enter(page, session.owner, sample); await expect(rows(page, sample.language)).toHaveCount(7); const control = search(page, sample.language); await control.focus(); await expect(control).toBeFocused(); await page.keyboard.press('Tab'); expect(await page.evaluate(() => document.activeElement !== document.body && document.activeElement?.isConnected)).toBe(true); const frozen = seeded.items.find(value => value.type === 'submitted-package')!; const action = row(page, frozen, sample.language).getByRole('link', { name: copy('documentsOpenSubmissionHistory', {}, sample.language), exact: true }); await action.focus(); await action.scrollIntoViewIfNeeded(); await expect(action).toBeFocused(); await expect(action).toHaveAttribute('href', frozen.sourceHref); const beforeCapture = await action.boundingBox(); expect(beforeCapture).not.toBeNull(); expect(beforeCapture!.y).toBeGreaterThanOrEqual(0); expect(beforeCapture!.y + beforeCapture!.height).toBeLessThanOrEqual(sample.height); await capture(page, info, sample, 'long-metadata-keyboard'); await action.scrollIntoViewIfNeeded(); await expect(action).toBeFocused(); const afterCapture = await action.boundingBox(); expect(afterCapture).not.toBeNull(); expect(afterCapture!.y).toBeGreaterThanOrEqual(0); expect(afterCapture!.y + afterCapture!.height).toBeLessThanOrEqual(sample.height); await info.attach(`p707-documents-${sample.language}-${sample.width}-focus-bounds`, { body: JSON.stringify({ beforeFullPageCapture: beforeCapture, afterScrollRestoration: afterCapture, viewport: sample, qualification: 'Full-page screenshot can change scroll. Both actual viewport bounds are asserted after explicit focus/scroll, without relaxing the 0..height requirement.' }), contentType: 'application/json' }); diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index d2efd11..78fd4a1 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -152,7 +152,7 @@ function titleFor(path: string, t: (k: any) => string): string { if (path.startsWith("/admin/audit")) return t("auditLog"); if (path.startsWith("/admin/users")) return t("users"); if (path.startsWith("/admin/system")) return t("systemStatus"); - return t("appTitle"); + return t("notFoundTitle"); } function subtitleFor(path: string, t: (k: any) => string): string | undefined { diff --git a/job-tracker-ui/src/cv-builder-deep-link.test.tsx b/job-tracker-ui/src/cv-builder-deep-link.test.tsx index 56cb8a7..08d7b7e 100644 --- a/job-tracker-ui/src/cv-builder-deep-link.test.tsx +++ b/job-tracker-ui/src/cv-builder-deep-link.test.tsx @@ -203,6 +203,43 @@ test("another user's variant is refused by the server and never rendered", async renderAt(3); expect(await screen.findByText(/Could not open this CV/i)).toBeInTheDocument(); + expect(screen.getByText('This CV is unavailable or you no longer have access to it.')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument(); +}); + +test('a denied CV read has localized permission context without a write or retry', async () => { + routeGet(() => Promise.reject({ response: { status: 403 } })); + renderAt(3); + expect(await screen.findByText('You do not have permission to open this CV.')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'CV Builder', level: 2 })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument(); + expect(mockedApi.post).not.toHaveBeenCalled(); + expect(mockedApi.put).not.toHaveBeenCalled(); +}); + +test('a temporary CV opening failure retries reads and restores focus to the editor without saving', async () => { + let attempt = 0; + routeGet(() => ++attempt === 1 ? Promise.reject({ response: { status: 503 } }) : Promise.resolve({ data: variant })); + renderAt(3); + const retry = await screen.findByRole('button', { name: 'Retry' }); + retry.focus(); fireEvent.click(retry); + await screen.findByDisplayValue('Backend CV'); + await waitFor(() => expect(screen.getByRole('region', { name: 'CV editing controls' })).toHaveFocus()); + expect(attempt).toBe(2); + expect(mockedApi.put).not.toHaveBeenCalled(); + expect(mockedApi.post.mock.calls.every(call => String(call[0]).endsWith('/preview'))).toBe(true); +}); + +test('Bokmål history localizes known sources while preserving unknown metadata and versions', async () => { + localStorage.setItem('uiLanguage', 'nb'); + const sources = ['autosave', 'manual', 'restore', 'create', 'custom-import', 'constructor']; + routeGet(() => Promise.resolve({ data: variant }), { sections: [] }, sources.map((source, index) => ({ version: index + 1, isCurrent: index === 3, source, createdAtUtc: variant.updatedAtUtc }))); + renderAt(3); + await screen.findByDisplayValue('Backend CV'); + fireEvent.click(screen.getByRole('tab', { name: 'Historikk' })); + for (const label of ['Automatisk lagring', 'Manuell', 'Gjenopprettet versjon', 'Opprettet', 'custom-import', 'constructor']) expect(await screen.findByText(new RegExp(` · ${label}$`))).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Gjenopprett versjon 5' })).toBeInTheDocument(); + expect(mockedApi.put).not.toHaveBeenCalled(); }); test('invalid names are explained and are not silently autosaved', async () => { @@ -241,6 +278,8 @@ test('session undo and redo restore content edits before autosave', async () => renderAt(3); await openContentTab(); + expect(screen.getByRole('button', { name: 'Undo' }).parentElement).not.toHaveAttribute('aria-label'); + expect(screen.getByRole('button', { name: 'Redo' }).parentElement).not.toHaveAttribute('aria-label'); const headline = await screen.findByLabelText('Headline override'); fireEvent.change(headline, { target: { value: 'Platform Engineer' } }); expect(screen.getByRole('button', { name: 'Undo' })).toBeEnabled(); diff --git a/job-tracker-ui/src/not-found-page.test.tsx b/job-tracker-ui/src/not-found-page.test.tsx new file mode 100644 index 0000000..c1fe1c8 --- /dev/null +++ b/job-tracker-ui/src/not-found-page.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; +import NotFoundPage from './views/NotFoundPage'; +import { I18nProvider } from './i18n/I18nProvider'; + +afterEach(() => localStorage.removeItem('uiLanguage')); +function Previous() { const location = useLocation(); return
{location.pathname + location.search}
; } + +test.each([['en', 'Page not found', 'Go back'], ['nb', 'Siden ble ikke funnet', 'Gå tilbake']])('unknown %s page has contextual headings and preserves Back', (language, title, back) => { + localStorage.setItem('uiLanguage', language); + render( + } />} /> + ); + expect(screen.getByRole('heading', { name: title, level: 2 })).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: '404' })).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: back })); + expect(screen.getByText('/previous?keep=context')).toBeInTheDocument(); +}); diff --git a/job-tracker-ui/src/views/CvBuilderEditor.tsx b/job-tracker-ui/src/views/CvBuilderEditor.tsx index f661995..bd21d28 100644 --- a/job-tracker-ui/src/views/CvBuilderEditor.tsx +++ b/job-tracker-ui/src/views/CvBuilderEditor.tsx @@ -41,6 +41,8 @@ import { import { previewDocumentHtml } from "../cvPreview"; import { useDialogActions } from "../dialogs"; import { useI18n } from "../i18n/I18nProvider"; +import { PageHeader } from "../layout/PageLayout"; +import ViewStateNotice from "../components/ViewStateNotice"; const MIN_PREVIEW_ZOOM = 0.2; type SaveState = "idle" | "unsaved" | "saving" | "saved" | "error" | "conflict"; @@ -75,7 +77,9 @@ export default function CvBuilderEditor() { const [exporting, setExporting] = useState(false); const [publishing, setPublishing] = useState(false); const [versions, setVersions] = useState([]); - const [loadError, setLoadError] = useState(null); + const [loadError, setLoadError] = useState<{ status?: number } | null>(null); + const [loadAttempt, setLoadAttempt] = useState(0); + const [opening, setOpening] = useState(true); const [mobilePane, setMobilePane] = useState<"edit" | "preview">("edit"); const [historyRevision, setHistoryRevision] = useState(0); @@ -93,6 +97,8 @@ export default function CvBuilderEditor() { const iframeRef = useRef(null); const scrollRef = useRef(null); const editorRef = useRef(null); + const loadRegion = useRef(null); + const retryFocus = useRef(false); const blockerPromptOpen = useRef(false); const undoStack = useRef([]); const redoStack = useRef([]); @@ -103,6 +109,7 @@ export default function CvBuilderEditor() { useEffect(() => { let alive = true; + setOpening(true); (async () => { try { const [variant, themeList, outlineData] = await Promise.all([ @@ -114,16 +121,24 @@ export default function CvBuilderEditor() { applyVariant(variant); setThemes(themeList); setOutline(outlineData); + setLoadError(null); } catch (err) { - if (alive) setLoadError(getApiErrorMessage(err, t("cvEditorOpenFailed"))); + if (alive) setLoadError({ status: (err as { response?: { status?: number } })?.response?.status }); + } finally { + if (alive) setOpening(false); } })(); return () => { alive = false; editorEpoch.current += 1; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [variantId]); + }, [variantId, loadAttempt]); + + useEffect(() => { + if (!retryFocus.current || opening) return; + retryFocus.current = false; + (loadError ? loadRegion.current : editorRef.current)?.focus(); + }, [opening, loadError]); const applyVariant = (variant: CvVariant) => { if (saveTimer.current) clearTimeout(saveTimer.current); @@ -514,10 +529,15 @@ export default function CvBuilderEditor() { }, [compactEditor, editorReady, fitPreview, mobilePane]); if (loadError) { + const denied = loadError.status === 401 || loadError.status === 403; return ( - + + - {loadError} + = 500 || loadError.status === 429 }} + onRetry={() => { retryFocus.current = true; loadRegion.current?.focus(); setLoadAttempt(value => value + 1); }} /> ); } @@ -578,8 +598,8 @@ export default function CvBuilderEditor() { {t("cvEditorEditingRegion")} - - + + { setTab(v); if (v === 4) loadVersions(); }} variant="scrollable" scrollButtons="auto" allowScrollButtonsMobile sx={{ borderBottom: "1px solid", borderColor: "divider" }}> @@ -1360,17 +1380,20 @@ function HistoryTab({ versions, onRestore }: { versions: CvVariantVersionInfo[]; return ( {t("cvEditorVersionCount", { count: versions.length })} - {versions.map((v) => ( + {versions.map((v) => { + const sources = { autosave: "historySourceAutosave", manual: "historySourceManual", restore: "historySourceRestore", create: "historySourceCreate" } as const; + const source = Object.hasOwn(sources, v.source) ? sources[v.source as keyof typeof sources] : undefined; + return ( - + {t("cvEditorVersion", { version: v.version })} {v.isCurrent && } - {relTime(v.createdAtUtc, language)} · {v.source} + {relTime(v.createdAtUtc, language)} · {source ? t(source) : v.source} {!v.isCurrent && } - ))} + ); })} ); } diff --git a/job-tracker-ui/src/views/DocumentsPage.tsx b/job-tracker-ui/src/views/DocumentsPage.tsx index 4e054ad..cb2bd81 100644 --- a/job-tracker-ui/src/views/DocumentsPage.tsx +++ b/job-tracker-ui/src/views/DocumentsPage.tsx @@ -207,7 +207,7 @@ export default function DocumentsPage() { return { id: jobApplicationId, jobTitle: item?.jobTitle ?? t("documentsJobNumber", { id: jobApplicationId }), companyName: item?.companyName ?? "" }; }, [jobApplicationId, jobs, result, t]); const from = { from: `${location.pathname}${location.search}${location.hash}`, fromState: location.state }; - const buttonSx = { whiteSpace: "normal", overflowWrap: "anywhere", maxWidth: "100%", minHeight: 40 } as const; + const buttonSx = { whiteSpace: "normal", overflowWrap: "normal", maxWidth: "100%", minHeight: 40 } as const; const metadataSx = { overflowWrap: "anywhere", minWidth: 0 } as const; const date = (item: DocumentItem) => { @@ -288,7 +288,7 @@ export default function DocumentsPage() { {t(result.total > 0 ? "documentsPageEmptyHelp" : filtered ? "documentsNoMatchesHelp" : "documentsEmptyHelp")} {result.total > 0 ? : null} : desktop ? - +
{t("documentsName")}{t("documentsJobFilter")}{t("documentsState")}{t("documentsRecordedDate")}{t("documentsActions")}{items.map((item) => {details(item)}{job(item)}{badges(item)}{date(item)}{actions(item)})}
: {items.map((item) => {details(item)}{job(item)}{badges(item)}{date(item)}{actions(item)})}} diff --git a/job-tracker-ui/src/views/NotFoundPage.tsx b/job-tracker-ui/src/views/NotFoundPage.tsx index 744bbb4..a38494d 100644 --- a/job-tracker-ui/src/views/NotFoundPage.tsx +++ b/job-tracker-ui/src/views/NotFoundPage.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Box, Button, Paper, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useI18n } from "../i18n/I18nProvider"; +import { PageHeader } from "../layout/PageLayout"; export default function NotFoundPage() { const navigate = useNavigate(); @@ -10,22 +11,16 @@ export default function NotFoundPage() { return ( 404 - - {t("notFoundTitle")} - - - {t("notFoundBody")} - +