fix(ui): reconcile shared accessibility and read recovery

This commit is contained in:
cesnimda
2026-10-20 20:26:48 +02:00
parent bba52af4b8
commit d8c0187305
25 changed files with 802 additions and 25 deletions
+4
View File
@@ -2,6 +2,10 @@
## Continuation — 20 October 2026
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.
Independent fixture/focus repair **bba52af4b8724f1a5c74013ce1476a20ca3972c2** normally pushed/directly verified on development18:05:08UTC, with all P7-11 WIP retained. Main's equivalent **3e1057d** plus Qwen adapter cherry-pick **f7dc22e5e7efe1fd1d64f6ace2c04cf2f95bf558** are pushed/directly verified18:05UTC. Release833 tests; no verified deployment/model activation or production GO claim. Run832's16failures are explained in its release report; one flaky Rules case remains debt.
P7-10 [System](p7-10/SYSTEM.md):20unit/46existingbackend/3native plus app/browser TypeScript/lint/build PASS. ExportEAHpn0GPj6rRy3WtHltbf;14axe0violations/100incompletes, two final images inspected after dirty-label correction. Publication follows final staged checks. Audit **f9e1d06bf7fc4081b70ebf461f1645ef219e0a93** pushed/directly verified13:26:54UTC. Separate Qwen adapter preparation **1d34406d1bacdb33d180f0e7e37bb0cb099ece5e** pushed/directly verified13:33:16UTC, full offline sidecar56/56; model activation remains pending. Main d1fcd23 CI832 is independently running; no verified deployment claim.
P7-10 [Audit](p7-10/AUDIT.md) final bounded gate:9 frontend,18 existing backend and3 native cases PASS; app/browser TypeScript, lint/build PASS. Export vZuD4f-46FJekdIR5rhRk; seven scans0violations/110incomplete nodes, two final images inspected. Ordinary REDs and pre-layout candidate remain retained. Users hygiene follow-up **4760fd102c21c684bd139206a12194c6fc8f6104** passed staged/full-phase whitespace and was normally pushed/directly verified13:10:57UTC. Audit publication follows after staged checks; System tests remain separate WIP.
+2
View File
@@ -1,5 +1,7 @@
# Phase 7 — Retained debt and mandatory gates
P7-11 shared/CV continuation corrects the evidenced roleless Checklist/Interview/Undo/Redo tooltip spans, static version-badge name and dangling Quick Search title reference. Final modal26scans0violations/176incomplete observations retain172contrast and4hidden-focus menu-transition contexts; final Documents18scans0/105 are contrast-only. This is bounded source/DOM/browser remediation, not human reader/device or every-pixel conformance. Correspondence/gallery modal-background transitions, existing library wrappers, contrast/physical/output/reliability and production gates remain qualified. No old manual requirement or intermittent Rules incident is closed by rerun.
P7-09's [sender repair](p7-09/evidence/oauth-results.json) confirms/fixes uncorrelated OAuth messages and delayed startup-language feedback. Consumer regression retains89 axe incompletes, including46 roleless-span `aria-label` diagnostics (45 Notes/Tasks,1 Interview) for bounded P7-11 responsible-owner triage. No existing manual/provider/release gate is closed; the Settings presentation findings remain pending.
First-boundary [Preferences/Connections results](p7-09/evidence/preferences-results.json) repair notification draft/reset/recovery/snapshot/disposal and truthful provider loading/error/write-refresh/IMAP-port/focus states. New18 scans retain62 incompletes and existing36 retain222; indeterminate layered contrast, short badges and closing-menu settled focus/AT remain qualified. RD11/RD12/RD13 roots are not closed by successful or controlled recovery. Data lifecycle additionally requires the demonstrated app-export/import casing compatibility repair before its round-trip claim; no restore/transaction/replay capability is invented.
+2
View File
@@ -1,5 +1,7 @@
# Phase 7 — Initial preservation matrix
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.
P7-10 [System](p7-10/SYSTEM.md) adds bounded PRES-A03/E09 proof: four independent diagnostic reads, explicit failed-read availability, denied-refresh disposal, GET-only retry, unchanged SMTP password/clear payloads, retained newer drafts and synchronized/session-guarded operations.20unit/46backend/3native cases pass; administrator browser writes are controlled fixtures, not operational acceptance. All57 groups remain56 PARTIAL/C11 DEFERRED and33 routes. No schema/renderer/provider/AI-safety change. Human/device/provider/output/release qualifications remain.
P7-10 [Audit](p7-10/AUDIT.md) adds PRES-A02/E08/E09 proof: exact Undo allowlist, owned Restore versus administrator Undo, persisted event/readback and ordinary-user denials. GET-only recovery, synchronized writes, current-language errors and disposed-session guards pass. All57 groups retain56 PARTIAL/C11 DEFERRED,33 routes. No server authorization/data contract or PDF/AI safeguard change. System and shared/manual accessibility acceptance remain pending.
+2
View File
@@ -2,6 +2,8 @@
## Current continuation — 20 October 2026
P7-10 is implemented CONDITIONAL GO and published throughd824f59; release repair merge6db6209 is pushed. P7-11 shared integration and paired label foundation have scoped unit/native/type/lint/build proof, with normal publication following; generated-label and error-owner source follow as separate commits. Candidate full frontend898/101 and backend1057 pass, sidecar56/56; final ARIA continuations and P7-12 full browser/source acceptance remain. Main f7dc22e release833 is testing; live/model activation remain unverified. Earlier observations below are historical.
Latest: P7-09 published8af9168; P7-10 Users7ecc0d6/4760fd1 and Auditf9e1d06 are published; System implemented CONDITIONAL GO with20unit/46backend/3native/types/lint/build PASS, final hygiene/publication pending. Current derived inventory884/99frontend,348browser/57files,1057backend; separate full sidecar56/56. These do not replace P7-12's fresh integrated run. Main d1fcd23 is testing in run832 after a narrowly identified Playwright-runtime corruption repair; deployment remains unverified. Qwen3.5 adapter preparation1d34406 is pushed on development, with no live model activation. Earlier observations below remain historical.
P7-00–08 and P7-09 presentation are implemented CONDITIONAL GO. The [P7-09 import prerequisite](p7-09/IMPORT_COMPATIBILITY.md) passes full frontend 837/97, full backend 1057, affected native 25 and type/lint/build gates, published as `3028d81`. [Remaining data presentation](p7-09/DATA_PRIVACY_BACKUP.md) has affected47/6 plus22backendcontracts and final native/type/lint/build evidence, with final consumer/hygiene/publication pending. Derived inventory is855/99frontend,339browser,1057backend; this is not a fresh full integrated suite. P7-10 Administration, P7-11 residual UI and P7-12 acceptance remain. The historical sections below retain their as-observed baselines.
+1 -1
View File
@@ -8,6 +8,6 @@ Follow the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md): exactly th
|---|---|
| [Users](USERS.md) | Implemented CONDITIONAL GO; published7ecc0d6/4760fd1; follow-up passed staged/full-phase whitespace and remote verification13:10:57UTC |
| [Audit](AUDIT.md) | Implemented CONDITIONAL GO; publishedf9e1d06, remote verified13:26:54UTC; exact Undo allowlist and owner-filtered Restore distinction verified |
| [System](SYSTEM.md) | Implemented CONDITIONAL GO;20frontend/46backend/3native plus types/lint/build PASS, final hygiene/publication pending; SMTP drafts and operational scopes preserved |
| [System](SYSTEM.md) | Implemented CONDITIONAL GO;20frontend/46backend/3native plus types/lint/build PASS; publishedd824f59, clean direct remote verification13:40:25UTC; SMTP drafts and operational scopes preserved |
All 57 preservation groups retain **56 PARTIAL / C11 DEFERRED**, with 33 routes. P5-13 is withheld; followMode remains deferred. No Phase 8 implementation. Production readiness remains NO-GO; the independently owner-authorized main release is still unverified after run831 failed with browser crashes/malformed Playwright JavaScript. Do not infer admin recent-auth enforcement: the current Users/Audit/System actions do not implement that separate guard. Preserve actual account-lifecycle/export recent-auth checks and record this gap rather than inventing a security protocol during presentation migration.
+30
View File
@@ -0,0 +1,30 @@
# P7-11 — Residual integration
Entry: P7-10 System `d824f59d2b3e9b680bee5952802aa3c0453aee03`, normally pushed/directly verified with a clean checkout20October13:40:25UTC. CI-only main continuation was merged and pushed as `6db62095c6dc6f7e8275447827c62b09f9b2d2e3`, clean13:40:41UTC. The approved handoff requires responsible-owner repairs, not another migration of completed pages.
## Recorded baseline and bounded owners
| Finding | Impact / responsible boundary |
|---|---|
| Roleless version badge and disabled-control tooltip spans carry unsupported names | Accessibility; shared shell, Checklist and Interview owners |
| Add Job and Strategy headings skip semantic levels | Accessibility; existing form owners |
| Strategy GET failures become an apparent absent plan | Usability; existing Match-tab Strategy read owner; exact `strategy_not_generated` remains intentional emptiness |
| Known template, generated reason/tailoring and history metadata remain English in Bokmål | Localisation; existing gallery/reminder/application/history owners; user and unknown metadata remain verbatim |
| Resumed-draft fallback freezes opening language; singular counts use plural English | Localisation; existing correspondence presentation without draft/save protocol changes |
| CV private-source failures show raw `Not Found`, lack contextual heading and read retry | Usability; existing CV entry owner, private identifiers and server authorization unchanged |
| Unknown route has generic shell title and h4 owner heading | Accessibility/context; existing wildcard route, Home and Back actions retained |
| Documents action splits the last letter at1024px | Responsive usability; existing table and action sizing, metadata/query/download contracts unchanged |
These findings were carried from implemented checkpoints and verified in source, unit regressions and/or browser reproduction. Cosmetic preferences are not additional scope. Analytics already uses the approved DashboardView report owner; it is reused with unchanged periods, cohorts, source links and statistics. Shared loading/feedback and existing confirmations remain; no duplicate framework is introduced.
## Execution boundaries
1. [Shared accessibility and forms](SHARED.md), including the paired translation foundation used by the following owners.
2. Generated-label localisation follows separately, preserving all stored metadata and user text.
3. Documents and error navigation follows separately, including CV history display in its existing owner.
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.
+17
View File
@@ -0,0 +1,17 @@
# Shared accessibility and forms
Final modal continuation **5/5 PASS**,18:23:09–18:24:16UTC; combined with the separate Strategy case,26all-enabled scans have0violations/176incomplete observations:172contrast and4hidden-focus MUI menu-transition contexts. The previous dangling-title and roleless Undo/Redo observations are absent in this fresh vector. No human/device acceptance follows.
**Implemented CONDITIONAL GO; final hygiene/publication pending.** Existing shell/form owners remain; no navigation or mutation protocol replacement.
The static administrator version badge now has a valid image role and localized version/commit accessible name. Checklist and Interview disabled-control tooltips describe their wrappers rather than placing names on roleless spans; item-specific inner-button names and disabled hover help remain. Add Job step headings are h3 under its dialog title, with progress text a paragraph. Strategy has a contextual h2 in the existing Match tab. Raw incomplete triage also proved Quick Search referenced a nonexistent MUI-generated title ID; its existing explicit name now stands without that dangling reference. Two existing command assertions failed before the correction; all10 command cases pass afterward.
Strategy's GET read distinguishes failures from exact404 `strategy_not_generated`. Retry only reads the same plan; denial has no retry, locale changes do not refetch, stale previous-job responses cannot replace the current owner, and plan-read completion cannot unlock controls while the independent initial operation read remains pending. Existing Pro admission, generation/operation endpoints and safeguards remain unchanged. No model calls or qualification run.
Meaningful initial regressions:34passed/11failed across six direct suites; expanded final affected/consumer result **80/80 across11 suites PASS**, TypeScript and targeted lint PASS. An additional English/Bokmål version-label case failed before localization and passed afterward; no net count change is claimed from overlapping runs. The later full run reconciles inventories.
Five native confirmation consumers pass with explicit named-body and both-button focus traversal. The separate Strategy native workflow passes in English desktop and Bokmål320px dark: controlled503 remains distinct from empty; one keyboard GET retry produces exact not-generated emptiness with preserved focus and zero browser mutations. The first native fixture mistakenly selected Overview; source inspection corrected it to the actual Match tab. That fixture failure is retained and is not a product regression. Automated scans and images are recorded separately from human acceptance.
Two Strategy final images were actually inspected:1440px English unavailable and320px Bokmål dark exact-empty. Full-page fixed-shell/skip-link capture artifacts retain their limits; source/advert prose remains document content, not translated UI labels. Other images and incomplete contexts are retained. The final modal rerun after Quick Search correction records updated scan totals in [portable results](evidence/shared-results.json); earlier incomplete vectors remain historical, including the CV-toolbar spans corrected by the error-owner boundary. No axe rule is hidden.
The accompanying59paired translation additions support all three residual boundaries. New labels are presentation-only; unused rows in this first commit do not activate downstream features. All consumers retain existing routes/IDs/actions. Full frontend898/101 and backend1057 pass before final small ARIA continuations; relevant continuation tests pass and P7-12 runs fresh against the committed final source. Shared feedback/confirm infrastructure, renderer geometry, API authorization and P5-13 withholding remain unchanged. Static checks and normal push follow; human/physical/provider/production conditions remain.
@@ -0,0 +1,357 @@
{
"observedUtc": "2026-10-20T18:24:31.227604+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/layout/AppShell.tsx": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
"src/components/QuickCommandDialog.tsx": "27cbbb8b8596fdf4e7474eccfdb6d9a8a219f4baff6a9b8013b825ce5fc2c411",
"src/components/AddJobModal.tsx": "a572ab197526a7b8263ba369b7230db7781176f3e820d255e89d7f71bab94c63",
"src/components/ApplicationChecklist.tsx": "2c58d440669a76ec1882326e1039ca604f5bf7e38fd71ac6800d22e260a1fcc2",
"src/components/InterviewPrep.tsx": "454c97a2cad3a4045fbc301c5da717db79fa41b53a231e9f3f3cc437eb5881d1",
"src/components/ApplicationWorkflowAssist.tsx": "7a9cbb1f38879bb9e9156d19675e9db40c466d06008cc5495dfd99b6b4dc088b",
"src/i18n/translations.ts": "15cc09b7dfcea607b6a597d1379aaad5d3e1c01adcc518846a9047b398ba5458"
},
"native": [
{
"name": "release-modal-contracts",
"stats": {
"startTime": "2026-10-20T18:23:09.847Z",
"duration": 66822.658,
"expected": 5,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"path": ".artifacts/phase7/current/ui/release-modal-contracts.json",
"sha256": "f5352b3ab09d5f4e3c9c45cea369965efbad6d2211d0fad8b7a42502f4560f91",
"command": {
"name": "release-modal-contracts",
"command": [
"rtk",
"proxy",
"node",
"node_modules/@playwright/test/cli.js",
"test",
"e2e/named-controls-modal-state.spec.ts",
"e2e/phase7-discovery.spec.ts",
"--grep",
"named controls, modal keyboard|keyboard native create",
"--retries=0",
"--reporter=list,json",
"--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\release-modal-contracts-results"
],
"startedUtc": "2026-10-20T18:23:09.321036+00:00",
"finishedUtc": "2026-10-20T18:24:16.708620+00:00",
"exitCode": 0
}
},
{
"name": "strategy-residual",
"stats": {
"startTime": "2026-10-20T18:10:59.174Z",
"duration": 16095.752,
"expected": 1,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"path": ".artifacts/phase7/current/ui/strategy-residual.json",
"sha256": "f58413b956da9464f07262d328c5f4712cccdc767e9164e4c7dc7fb801ba8106",
"command": {
"name": "strategy-residual",
"command": [
"rtk",
"proxy",
"node",
"node_modules/@playwright/test/cli.js",
"test",
"e2e/phase7-application-forms.spec.ts",
"--grep",
"Strategy failed reads",
"--retries=0",
"--reporter=list,json",
"--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\strategy-residual-results"
],
"startedUtc": "2026-10-20T18:10:58.660686+00:00",
"finishedUtc": "2026-10-20T18:11:15.305746+00:00",
"exitCode": 0
}
}
],
"images": [
{
"name": "p7-discovery-nb-light-360-native-create-held",
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-create-held-d66c78aa.png",
"sha256": "d66c78aa7e307ba36246428ebe2275c6dc68d5c5069287f32bf5bdee4e7837d2"
},
{
"name": "p7-discovery-nb-light-360-native-delete-held",
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-delete-held-ab570faa.png",
"sha256": "ab570faa611f3074f8ae2ae2b088b40ff7d5b7572c25531384f57b892316775d"
},
{
"name": "p706-forms-en-light-1440-strategy-read-unavailable",
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-read-unavailable-bd863839.png",
"sha256": "bd86383954334e34052a6ca5150a08fb5c03824721b0951276145c075da39a7a"
},
{
"name": "p706-forms-en-light-1440-strategy-exact-not-generated",
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-exact-not-generated-f2c5bfc1.png",
"sha256": "f2c5bfc1b7d4abfba14cf43f9002cc34fec20262d08657d62f6f45ae6b1bd310"
},
{
"name": "p706-forms-nb-dark-320-strategy-read-unavailable",
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-read-unavailable-bdac6183.png",
"sha256": "bdac618366e6869d9f141b939e2b525dff88610fe1bf58d4d0279147efd885cb"
},
{
"name": "p706-forms-nb-dark-320-strategy-exact-not-generated",
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-exact-not-generated-1ad739a1.png",
"sha256": "1ad739a1fe0133e8228cf5fc2f629e82bda121bfc8d9960c58356afea866c0b3"
}
],
"scans": [
{
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-create-held-audit-c82b8641.json",
"sha256": "c82b864175ca44673005263b83b9763ed778629430fabb1bf6697aaf12fb36f1",
"violations": 0,
"incompleteNodes": 1,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-delete-held-audit-2e8a5542.json",
"sha256": "2e8a55425bca9ad5f5c23c865d940b6b108315372dd9002c0653d3dcaf42b834",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-647699d7.json",
"sha256": "647699d71b0d56b61f3af200452834d81210fa67b33696806f02abb7a8442c61",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-c71e3b74.json",
"sha256": "c71e3b74f32943b3a75b85b152e8f6a5c704ef0b65440cd0a93f225cf1b0c57e",
"violations": 0,
"incompleteNodes": 25,
"incompleteRules": [
"aria-hidden-focus",
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-52c71913.json",
"sha256": "52c719132e9830a1736707443438408cf2843f22e508ca1098e567c32bf9f483",
"violations": 0,
"incompleteNodes": 6,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-5a4a000a.json",
"sha256": "5a4a000ad4272cd7ac79c5f5f41c00ca6c6a62658912a2409dc17c134c1fe398",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-5895b5c3.json",
"sha256": "5895b5c3a1046273c59c01c000ff183b0b01c34d1590ccd1814e61c5fc6dc87a",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-db3f7506.json",
"sha256": "db3f7506b91f58b816c29466e7939aa1a1af24e4f5f86a05ffa5782cd0fd02e8",
"violations": 0,
"incompleteNodes": 30,
"incompleteRules": [
"aria-hidden-focus",
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-900a7709.json",
"sha256": "900a77096f5cef09372362420aaa6f9e52ffe94dc55fd6acb234767f2b70cfb6",
"violations": 0,
"incompleteNodes": 6,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-d2e174df.json",
"sha256": "d2e174df033f399a942508acc5e7d046945a228ea022e0408b06945b8b719141",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-c9062adb.json",
"sha256": "c9062adb6dd8206f866ab7af94f0ca38524b7f2478f9d3d0ade0b62405b121a5",
"violations": 0,
"incompleteNodes": 0,
"incompleteRules": []
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-d2dea077.json",
"sha256": "d2dea077614afeec933815d852a1ce72b5c63c41fcf3a3139d757d5519cd6087",
"violations": 0,
"incompleteNodes": 24,
"incompleteRules": [
"aria-hidden-focus",
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-c180f9b6.json",
"sha256": "c180f9b684782328f0a33d32c6fb48705958c9370780bf71c7270f7ff83029b3",
"violations": 0,
"incompleteNodes": 10,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-5a4a000a.json",
"sha256": "5a4a000ad4272cd7ac79c5f5f41c00ca6c6a62658912a2409dc17c134c1fe398",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-c9062adb.json",
"sha256": "c9062adb6dd8206f866ab7af94f0ca38524b7f2478f9d3d0ade0b62405b121a5",
"violations": 0,
"incompleteNodes": 0,
"incompleteRules": []
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-549ca659.json",
"sha256": "549ca65975a2e785420651c37d0be7e9d3c2da3d83857a396c50a2db339bf591",
"violations": 0,
"incompleteNodes": 24,
"incompleteRules": [
"aria-hidden-focus",
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-61e54ade.json",
"sha256": "61e54adebc11daf24dfa3d6c4620c65c61a7867dfe671a6b0d2b62e62088105f",
"violations": 0,
"incompleteNodes": 10,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-f58690b5.json",
"sha256": "f58690b5f684193dbe77aa4227754fb3b6384abda77c1a977b5d8f5c97135b22",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-read-unavailable-audit-09114185.json",
"sha256": "0911418580ebbad2ad3ea66b75195da55852288738efd81ae8000ccebdfb9a77",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-exact-not-generated-audit-c68e7baa.json",
"sha256": "c68e7baae8e9ebf228061b925af86d7932ee3dfe3f56eab0602a887d3ed8fa8f",
"violations": 0,
"incompleteNodes": 2,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-read-unavailable-audit-af80696d.json",
"sha256": "af80696d1ff9ff05489d77f869e4375b135ae89f13f5a9cd8ee029e324571968",
"violations": 0,
"incompleteNodes": 7,
"incompleteRules": [
"color-contrast"
]
},
{
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-exact-not-generated-audit-ad14073a.json",
"sha256": "ad14073a4462a71d5aae93170f6d932bf42cec7241985c78905b181e3dc3e6ce",
"violations": 0,
"incompleteNodes": 7,
"incompleteRules": [
"color-contrast"
]
}
],
"focusedUnit": {
"path": ".artifacts/phase7/p7-11/shared/green.json",
"sha256": "4cdfac6b887315b6734b5eb8e6b9d15fadc0d7279dd31f98193db03d895f2b96",
"numPassedTests": 80,
"numFailedTests": 0,
"numPendingTests": 0,
"numPassedTestSuites": 11,
"qualification": "Overlapping focused vectors; not a new full-suite total. Final integrated run covers subsequent documented corrections."
}
}
@@ -0,0 +1,104 @@
{
"schemaVersion": 1,
"checkpoint": "p7-11",
"status": "PASS",
"checkedUtc": "2026-10-20T18:26:12.103739+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:26:08.411065+00:00",
"finishedUtc": "2026-10-20T18:26:11.161244+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T182608Z/secret-scan.log",
"logSha256Raw": "be5119e6057287d397e87759f855c91e634e3bfac61c8be7584d05592ba2dfcd"
},
{
"name": "working-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--check"
],
"startedUtc": "2026-10-20T18:26:11.162273+00:00",
"finishedUtc": "2026-10-20T18:26:11.270209+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T182608Z/working-whitespace.log",
"logSha256Raw": "85634f1014a6f42ddd6b5982ccec8a9cc597157a051cf7fd824992355be51c5c"
},
{
"name": "staged-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--cached",
"--check"
],
"startedUtc": "2026-10-20T18:26:11.270721+00:00",
"finishedUtc": "2026-10-20T18:26:11.372430+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T182608Z/staged-whitespace.log",
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"name": "phase7-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--check",
"b5319981ad3d1bd635daff861453d23c0de5320b"
],
"startedUtc": "2026-10-20T18:26:11.372430+00:00",
"finishedUtc": "2026-10-20T18:26:11.609452+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261020T182608Z/phase7-whitespace.log",
"logSha256Raw": "4d9b55a18a0e78345259dd2290d41d06f98d6ae3cec9e60a218744e90d41667a"
}
],
"untrackedSecretScan": {
"files": 7,
"findings": 0
},
"documentation": {
"files": 38,
"relativeLinks": 642,
"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."
]
}
+1 -1
View File
@@ -93,7 +93,7 @@ Follow the approved [Phase 7 handoff](../redesign/phase-6/p6-05/PHASE7_HANDOFF.m
- [x] P7-07 [owner-scoped Documents index](../redesign/phase-7/p7-07/README.md): implemented CONDITIONAL GO, final hygiene PASS; UI5a67ec3 and evidence84350b7 published through9dfea12, directly verified21:01:11UTC. Finite86/8+native13/types/lint/build/18images/15axe0/101qualified;33routes/57statuses retained. MariaDB/human/device/production qualifications remain.
- [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 and Auditf9e1d06 published; System20frontend/46backend/3native plus types/lint/build PASS, final hygiene/publication pending. Implemented CONDITIONAL GO; shared/manual accessibility conditions remain.
- [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.
- [ ] P7-12 integrated migration acceptance and Phase 8 handoff.
@@ -220,6 +220,34 @@ async function capture(page: Page, info: TestInfo, sample: Sample, state: string
expect(geometry.rootOverflow).toBe(0); expect(axe.violations).toEqual([]);
}
test('[P7-11 E08] Strategy failed reads remain distinct from exact not-generated state and retry only GET in both locales', async ({ page, session }, info) => {
const job = await createJob(page.request, session.owner, 'Recorded Strategy read recovery');
let unavailable = true;
await page.route(`**/api/jobapplications/${job.id}/focus-plan`, route => {
expect(route.request().method()).toBe('GET');
return route.fulfill(unavailable ? { status: 503, json: { title: 'Controlled unavailable Strategy read' } } : { status: 404, json: { code: 'strategy_not_generated' } });
});
for (const sample of [desktop, { language: 'nb', theme: 'dark', width: 320, height: 844 }] as Sample[]) {
unavailable = true;
await enter(page, session.owner, job, 'answers', sample);
await keyboard(page, page.getByRole('tablist', { name: translations[sample.language].workspaceSections, exact: true }).getByRole('tab', { name: translations[sample.language].workspaceMatch, exact: true }));
const t = translations[sample.language];
const error = page.getByRole('alert').filter({ hasText: t.workspaceStrategyLoadFailed });
await expect(error).toBeVisible();
await expect(page.getByText(t.workspaceStrategyEmpty, { exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: t.workspaceStrategyProRequired, exact: true })).toBeDisabled();
await capture(page, info, sample, 'strategy-read-unavailable');
const before = count(session, 'GET', `/api/jobapplications/${job.id}/focus-plan`).length;
unavailable = false;
await keyboard(page, error.getByRole('button', { name: t.retry, exact: true }));
await expect(page.getByRole('heading', { name: t.workspaceStrategyTitle, level: 2, exact: true })).toBeFocused();
await expect(page.getByText(t.workspaceStrategyEmpty, { exact: true })).toBeVisible();
expect(count(session, 'GET', `/api/jobapplications/${job.id}/focus-plan`).length).toBe(before + 1);
await capture(page, info, sample, 'strategy-exact-not-generated');
}
expect(mutations(session)).toEqual([]);
});
test('[P7-06 forms] held CV read Retry preserves its request and actual attach/replace/detach remain owner scoped', async ({ page, session, playwright }, info) => {
test.setTimeout(120_000); await source(page.request, session.owner);
const job = await createJob(page.request, session.owner); const first = await variant(page.request, session.owner, 'First reusable CV'); const second = await variant(page.request, session.owner, 'Second reusable CV');
@@ -44,7 +44,8 @@ test("notification bell exposes its unread count and keyboard-accessible action"
expect(open.mock.calls[0][0]).toBeInstanceOf(HTMLElement);
});
test("shows build metadata only when the caller supplies the admin-only badge", () => {
test.each([["en", "Application version 2.4.1, commit abc1234"], ["nb", "Applikasjonsversjon 2.4.1, revisjon abc1234"]])("shows localized %s build metadata only when the caller supplies the admin-only badge", (language, label) => {
localStorage.setItem("uiLanguage", language);
const commonProps = {
pageTitle: "Dashboard",
breadcrumbs: ["Home"],
@@ -67,7 +68,8 @@ test("shows build metadata only when the caller supplies the admin-only badge",
);
expect(screen.getByTestId("admin-version-badge")).toHaveTextContent("v2.4.1");
expect(screen.getByLabelText("Application version 2.4.1, commit abc1234")).toBeInTheDocument();
expect(screen.getByLabelText(label)).toBeInTheDocument();
expect(screen.getByRole("img", { name: label })).toBe(screen.getByTestId("admin-version-badge"));
rerender(
<CssVarsProvider theme={getTheme("light") as any} defaultMode="light">
@@ -122,6 +122,20 @@ test("reordering sends the new id order", async () => {
expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/checklist/order", [2, 1, 3]));
});
test("move and remove tooltips describe their wrappers without replacing item-specific button names", async () => {
mockedApi.patch.mockReturnValueOnce(new Promise(() => {}));
renderChecklist();
const down = await screen.findByRole("button", { name: "Move down: Prepare a CV for this role" });
for (const label of ["Move up: Prepare a CV for this role", "Move down: Prepare a CV for this role", "Remove: Prepare a CV for this role"]) {
expect(screen.getByRole("button", { name: label }).parentElement).not.toHaveAttribute("aria-label");
}
fireEvent.click(screen.getByRole("checkbox", { name: "Prepare a CV for this role" }));
expect(down).toBeDisabled();
fireEvent.mouseOver(down.parentElement!);
expect(await screen.findByRole("tooltip")).toHaveTextContent("Move down");
expect(mockedApi.put).not.toHaveBeenCalled();
});
test("removing an item calls delete", async () => {
mockedApi.delete.mockResolvedValue({ data: undefined } as any);
@@ -1,6 +1,6 @@
import React from 'react';
import '@testing-library/jest-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { api } from './api';
import { AccountPlanProvider } from './accountPlan';
@@ -15,7 +15,7 @@ jest.mock('./api', () => ({
const mockedApi = api as jest.Mocked<typeof api>;
beforeEach(() => jest.clearAllMocks());
beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); });
test('recruiter status suggestions require an explicit apply action', async () => {
const applied = jest.fn();
@@ -50,8 +50,56 @@ test('strategy snapshot shows saved output and queues regeneration only on reque
render(<I18nProvider><ToastProvider><AccountPlanProvider value={{ plan: 'pro', canUseAi: true, canUseProThemes: true }}><ApplicationStrategySnapshot jobId={42} /></AccountPlanProvider></ToastProvider></I18nProvider>);
expect(await screen.findByText('Lead with delivery evidence.')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Strategy snapshot', level: 2 })).toBeVisible();
expect(mockedApi.post).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: 'Regenerate' }));
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/42/focus-plan/operations', { attachmentIds: null }));
expect(await screen.findByText('queued')).toBeInTheDocument();
});
function strategy(jobId = 42) {
return <I18nProvider><ToastProvider><AccountPlanProvider value={{ plan: 'free', canUseAi: false, canUseProThemes: false }}><ApplicationStrategySnapshot jobId={jobId} /></AccountPlanProvider></ToastProvider></I18nProvider>;
}
test('failed strategy reads stay distinct from empty, localize without refetching and retry only the read', async () => {
let finishOperation!: (result: any) => void;
mockedApi.get.mockImplementation((url: string) => url.endsWith('/operation') ? new Promise(resolve => { finishOperation = resolve; }) : Promise.reject({ response: { status: 503 } }));
render(strategy());
expect(await screen.findByText('Strategy snapshot could not be loaded.')).toBeVisible();
expect(screen.getByRole('button', { name: 'Retry' })).toBeDisabled();
await act(async () => finishOperation({ data: null }));
expect(screen.queryByText('No strategy snapshot yet. Generate one when you want AI-assisted planning.')).not.toBeInTheDocument();
expect(mockedApi.post).not.toHaveBeenCalled();
const calls = mockedApi.get.mock.calls.length;
act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));
expect(await screen.findByText('Kunne ikke hente strategisammendraget.')).toBeVisible();
expect(mockedApi.get).toHaveBeenCalledTimes(calls);
mockedApi.get.mockResolvedValueOnce({ data: { strategicSummary: 'Saved plan', immediatePriorities: [], proofPointsToLeadWith: [], cvBulletIdeas: [], coverLetterAngles: [], followUpApproach: [] } } as any);
const retry = screen.getByRole('button', { name: 'Prøv igjen' });
act(() => retry.focus()); fireEvent.click(retry);
expect(await screen.findByDisplayValue('Saved plan')).toBeVisible();
expect(mockedApi.get.mock.calls.slice(calls).map(call => call[0])).toEqual(['/jobapplications/42/focus-plan']);
expect(screen.getByRole('heading', { name: 'Strategioversikt', level: 2 })).toHaveFocus();
expect(mockedApi.post).not.toHaveBeenCalled();
expect(screen.getByRole('button', { name: 'Krever Pro' })).toBeDisabled();
});
test('only the explicit not-generated response is an intentional empty strategy', async () => {
mockedApi.get.mockRejectedValue({ response: { status: 404, data: { code: 'strategy_not_generated' } } });
render(strategy());
expect(await screen.findByText('No strategy snapshot yet. Generate one when you want AI-assisted planning.')).toBeVisible();
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
expect(mockedApi.post).not.toHaveBeenCalled();
});
test('a late strategy response cannot replace another job and denial never offers read retry', async () => {
let finish!: (result: any) => void;
mockedApi.get.mockImplementation((url: string) => url === '/jobapplications/42/focus-plan' ? new Promise(resolve => { finish = resolve; }) : Promise.reject({ response: { status: 403 } }));
const view = render(strategy());
view.rerender(strategy(43));
expect(await screen.findByText('Strategy snapshot could not be loaded.')).toBeVisible();
await act(async () => finish({ data: { strategicSummary: 'Do not show old owner plan', immediatePriorities: [], proofPointsToLeadWith: [], cvBulletIdeas: [], coverLetterAngles: [], followUpApproach: [] } }));
expect(screen.queryByDisplayValue('Do not show old owner plan')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
expect(mockedApi.post).not.toHaveBeenCalled();
});
@@ -445,13 +445,13 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
<Stepper activeStep={activeStep} alternativeLabel sx={{ mb: 3, display: { xs: "none", sm: "flex" } }}>
{steps.map((label) => <Step key={label}><StepLabel>{label}</StepLabel></Step>)}
</Stepper>
<Typography variant="subtitle2" sx={{ display: { xs: "block", sm: "none" }, mb: 2 }}>
<Typography component="p" variant="subtitle2" sx={{ display: { xs: "block", sm: "none" }, mb: 2 }}>
{t("addJobModalStepProgress", { current: activeStep + 1, total: steps.length, step: steps[activeStep] })}
</Typography>
{activeStep === 0 ? <Box sx={{ display: "grid", gap: 2 }}>
<Box>
<Typography variant="h6" sx={{ fontWeight: 900 }}>{t("addJobModalStartWithAdvert")}</Typography>
<Typography component="h3" variant="h6" sx={{ fontWeight: 900 }}>{t("addJobModalStartWithAdvert")}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.5 }}>
{t("addJobModalStartWithAdvertBody")}
</Typography>
@@ -609,7 +609,7 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
{activeStep >= 2 ? <Box sx={{ display: "grid", gap: 2 }}>
<Box>
<Typography variant="h6" sx={{ fontWeight: 900 }}>{steps[activeStep]}</Typography>
<Typography component="h3" variant="h6" sx={{ fontWeight: 900 }}>{steps[activeStep]}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.5 }}>
{activeStep === 2 ? t("addJobModalCvStepHelp") : null}
{activeStep === 3 ? t("addJobModalCoverLetterStepHelp") : null}
@@ -150,21 +150,21 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
)}
</Box>
<Stack direction="row" className="checklist-actions" sx={{ opacity: { xs: 1, md: 0 }, transition: "opacity .15s" }}>
<Tooltip title={t("checklistMoveUp")}>
<Tooltip describeChild title={t("checklistMoveUp")}>
<span>
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveUpItem", { title: display.title })} onClick={() => move(item, -1)}>
<ArrowUpwardIcon fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title={t("checklistMoveDown")}>
<Tooltip describeChild title={t("checklistMoveDown")}>
<span>
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveDownItem", { title: display.title })} onClick={() => move(item, 1)}>
<ArrowDownwardIcon fontSize="inherit" />
</IconButton>
</span>
</Tooltip>
<Tooltip title={item.isSystemGenerated ? t("checklistNotRelevant") : t("deleteAction")}>
<Tooltip describeChild title={item.isSystemGenerated ? t("checklistNotRelevant") : t("deleteAction")}>
<span>
<IconButton size="small" disabled={busy} aria-label={t("checklistRemoveItem", { title: display.title })} onClick={() => remove(item)}>
<DeleteOutlineIcon fontSize="inherit" />
@@ -62,19 +62,41 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
const [plan, setPlan] = useState<FocusPlanResponse | null>(null);
const [operation, setOperation] = useState<UserOperation | null>(null);
const [loading, setLoading] = useState(true);
const [readingPlan, setReadingPlan] = useState(false);
const [readError, setReadError] = useState<{ cause: unknown; retryable: boolean } | null>(null);
const announced = useRef<string | null>(null);
const owner = useRef({ mounted: false, jobId, generation: 0 });
const readSequence = useRef(0);
const readPending = useRef(false);
const headingRef = useRef<HTMLHeadingElement>(null);
const loadPlan = useCallback(async () => {
if (!owner.current.mounted || owner.current.jobId !== jobId || readPending.current) return;
const lifetime = owner.current.generation;
const request = ++readSequence.current;
const active = () => owner.current.mounted && owner.current.jobId === jobId && owner.current.generation === lifetime && request === readSequence.current;
readPending.current = true;
setReadingPlan(true);
try {
const { data } = await api.get<FocusPlanResponse>(`/jobapplications/${jobId}/focus-plan`);
if (!active()) return;
setPlan(data);
} catch {
setPlan(null);
setReadError(null);
} catch (cause) {
if (!active()) return;
const response = (cause as { response?: { status?: number; data?: { code?: string } } })?.response;
if (response?.status === 404 && response.data?.code === "strategy_not_generated") { setPlan(null); setReadError(null); }
else setReadError({ cause, retryable: !response?.status || response.status >= 500 });
} finally {
if (active()) { readPending.current = false; setReadingPlan(false); }
}
}, [jobId]);
useEffect(() => {
let active = true;
owner.current = { mounted: true, jobId, generation: owner.current.generation + 1 };
readPending.current = false;
setPlan(null); setReadError(null); setOperation(null);
setLoading(true);
Promise.all([
loadPlan(),
@@ -82,17 +104,18 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
.then(({ data }) => { if (active) setOperation(data); })
.catch(() => { if (active) setOperation(null); }),
]).finally(() => { if (active) setLoading(false); });
return () => { active = false; };
return () => { active = false; owner.current.mounted = false; owner.current.generation += 1; readSequence.current += 1; readPending.current = false; };
}, [jobId, loadPlan]);
useEffect(() => {
if (!operation || terminal(operation.status)) return;
let active = true;
const timer = window.setTimeout(() => {
api.get<UserOperation>(`/operations/${operation.id}`)
.then(({ data }) => setOperation(data))
.then(({ data }) => { if (active) setOperation(data); })
.catch(() => undefined);
}, 1000);
return () => window.clearTimeout(timer);
return () => { active = false; window.clearTimeout(timer); };
}, [operation]);
useEffect(() => {
@@ -133,17 +156,24 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
};
const working = !!operation && !terminal(operation.status);
const reading = loading || readingPlan;
const readErrorTitle = t("workspaceStrategyLoadFailed");
const readErrorDetail = readError ? getApiErrorMessage(readError.cause, readErrorTitle) : "";
return (
<Paper sx={{ p: { xs: 2, sm: 2.5 }, borderRadius: 3 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={1} sx={{ mb: 2 }}>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t("workspaceStrategyTitle")}</Typography>
<Typography ref={headingRef} tabIndex={-1} component="h2" variant="subtitle2" sx={{ fontWeight: 800 }}>{t("workspaceStrategyTitle")}</Typography>
<Typography variant="caption" color="text.secondary">{t("workspaceStrategySubtitle")}</Typography>
</Box>
<Button variant="outlined" size="small" disabled={!canUseAi || loading || working} onClick={() => void generate()}>
<Button variant="outlined" size="small" disabled={!canUseAi || reading || working} onClick={() => void generate()}>
{!canUseAi ? t("workspaceStrategyProRequired") : plan ? t("workspaceStrategyRegenerate") : t("workspaceStrategyGenerate")}
</Button>
</Stack>
{readError ? <Alert severity="error" sx={{ mb: 2, overflowWrap: "anywhere" }} action={readError.retryable ? <Button color="inherit" size="small" disabled={reading} onClick={() => { headingRef.current?.focus(); void loadPlan(); }}>{t("retry")}</Button> : undefined}>
<Typography variant="body2">{readErrorTitle}</Typography>
{readErrorDetail !== readErrorTitle ? <Typography variant="body2">{readErrorDetail}</Typography> : null}
</Alert> : null}
{operation && operation.status !== "succeeded" ? (
<Alert severity={operation.status === "failed" ? "error" : operation.status === "cancelled" ? "warning" : "info"} sx={{ mb: 2 }} action={<>
{operation.canCancel ? <Button size="small" color="inherit" onClick={() => void mutateOperation("cancel")}>{t("cancel")}</Button> : null}
@@ -152,14 +182,14 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
{operation.progressStage || operation.status.replaceAll("_", " ")}{operation.progressPercent != null ? ` · ${operation.progressPercent}%` : ""}
</Alert>
) : null}
{loading && !plan ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : plan ? (
{reading && !plan ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} aria-label={t("loading")} /></Box> : plan ? (
<Stack spacing={2}>
<DraftCard title={t("jobDetailsFocusSummary")} content={plan.strategicSummary} />
<TwoColumnSection leftTitle={t("jobDetailsImmediatePriorities")} leftItems={plan.immediatePriorities} rightTitle={t("jobDetailsProofPoints")} rightItems={plan.proofPointsToLeadWith} />
<TwoColumnSection leftTitle={t("jobDetailsCvBulletIdeas")} leftItems={plan.cvBulletIdeas} rightTitle={t("jobDetailsCoverLetterAngles")} rightItems={plan.coverLetterAngles} />
<ListCard title={t("jobDetailsFollowUpApproach")} items={plan.followUpApproach} />
</Stack>
) : <Typography color="text.secondary">{t("workspaceStrategyEmpty")}</Typography>}
) : !readError ? <Typography color="text.secondary">{t("workspaceStrategyEmpty")}</Typography> : null}
</Paper>
);
}
@@ -381,7 +381,7 @@ function PrepRow({ jobId, item, busy, onChanged, onError }: {
</Stack>
)}
</Box>
<Tooltip title={t("interviewPrepDelete")}>
<Tooltip describeChild title={t("interviewPrepDelete")}>
<span>
<IconButton
size="small"
@@ -147,7 +147,7 @@ export default function QuickCommandDialog({ open, onClose, onNavigate, onOpenAd
};
return (
<Dialog open={open} onClose={onClose} slotProps={{ paper: { "aria-label": t("quickSearch") }, transition: { onEntered: () => searchInputRef.current?.focus() } }} fullWidth maxWidth="sm">
<Dialog open={open} onClose={onClose} slotProps={{ paper: { "aria-label": t("quickSearch"), "aria-labelledby": undefined }, transition: { onEntered: () => searchInputRef.current?.focus() } }} fullWidth maxWidth="sm">
<DialogContent onKeyDown={onKeys} sx={{ p: 1.5, maxHeight: "min(70vh, 620px)" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 1, pt: 0.5 }}>
<SearchIcon fontSize="small" />
@@ -61,6 +61,8 @@ test.each([0, 1, 2, 3])("Add Job attachment bucket %s retains keyboard selection
await screen.findByDisplayValue("Supported role");
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
for (let step = 0; step < bucket; step += 1) fireEvent.click(screen.getByRole("button", { name: "Skip and continue" }));
const headings = screen.getAllByRole("heading").map(heading => heading.tagName);
expect(headings).toEqual(["H2", "H3"]);
const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
expect(input.multiple).toBe(true);
expect(input.accept).toContain(".pdf,.doc,.docx,.txt,.md");
+118
View File
@@ -5,6 +5,7 @@ export const translations = {
appTitle: "Jobbjakt",
appTagline: "Track your hunt",
applicationLanguage: "Application language",
applicationVersionCommit: "Application version {version}, commit {commit}",
dashboard: "Dashboard",
jobApplications: "Job Applications",
reminders: "Reminders",
@@ -130,6 +131,61 @@ export const translations = {
correspondenceInbox: "Job email",
emailInboxSubtitle: "Review linked correspondence and suggested recruitment messages in one place.",
emailInboxItems: "{count} items",
emailInboxItem: "{count} item",
emailInboxAttachmentCount: "{count} attachment",
historySourceManual: "Manual",
historySourceAi: "AI suggestion",
historySourceTemplate: "Template",
historySourceRestore: "Restored version",
historySourceAutosave: "Autosave",
historySourceCreate: "Created",
cvTemplateModernName: "Modern",
cvTemplateModernDescription: "Clean SaaS-style layout with a confident accent header.",
cvTemplateMinimalName: "Minimal",
cvTemplateMinimalDescription: "Quiet, spacious, typography-first. Nothing competes with the content.",
cvTemplateExecutiveName: "Executive",
cvTemplateExecutiveDescription: "High-contrast, serif, leadership-weighted. For senior and client-facing roles.",
cvTemplateTechnicalName: "Technical",
cvTemplateTechnicalDescription: "Dense two-column layout tuned for engineering CVs — skills and projects up front.",
cvTemplateAtsClassicName: "ATS Classic",
cvTemplateAtsClassicDescription: "Single column, no graphics, maximum parser compatibility. The safe default.",
cvTemplateNordicName: "Nordic",
cvTemplateNordicDescription: "Calm cool-blue sidebar, generous whitespace, Scandinavian restraint.",
cvTemplateElegantName: "Elegant",
cvTemplateElegantDescription: "Editorial serif headings over sans body, premium spacing and a plum accent.",
cvTemplateCreativeName: "Creative",
cvTemplateCreativeDescription: "Bold accent sidebar and photo-forward header for design and product roles.",
cvTemplateCodeName: "Code",
cvTemplateCodeDescription: "Developer-focused editorial layout with numbered sections and precise mono details.",
cvTemplateCategoryModern: "Modern Professional",
cvTemplateCategoryMinimal: "Minimal",
cvTemplateCategoryExecutive: "Executive",
cvTemplateCategoryTechnical: "Technical",
cvTemplateCategoryAts: "ATS Professional",
cvTemplateCategoryCreative: "Creative",
assetsTailoringHighlightTitle: "Skills to highlight",
assetsTailoringHighlightDetail: "The advert asks for these and your profile already has them — put them where they are seen first.",
assetsTailoringExperienceTitle: "Experience to prioritise",
assetsTailoringExperienceDetail: "Ordered by how much of the advert each role actually covers.",
assetsTailoringProjectsTitle: "Projects to emphasise",
assetsTailoringProjectsDetail: "These projects demonstrate what the advert is asking for.",
assetsTailoringEvidenceTitle: "Verified evidence to reuse",
assetsTailoringEvidenceDetail: "Facts you previously approved. Adapt the wording, but keep the claim accurate.",
assetsTailoringKeywordsTitle: "Keywords to include",
assetsTailoringKeywordsDetail: "Vocabulary from the advert. Use the ones that are honestly true of you — never pad.",
assetsTailoringGapsTitle: "Gaps to address",
assetsTailoringGapsDetail: "Asked for but not found in your profile. Add them if you have them; otherwise be ready to talk about them.",
reminderReasonInterview: "Interview stage reached but prep notes are still missing.",
reminderReasonCvAndAnswers: "Tailored CV and saved application answers still need work.",
reminderReasonCv: "Tailored CV missing for this role.",
reminderReasonAnswers: "Saved application answers still need work.",
reminderReasonUnscheduled: "No response yet and no follow-up is scheduled.",
reminderReasonDue: "Follow-up is due for this role.",
reminderReasonNoGaps: "No urgent workflow gaps are blocking this job right now.",
reminderReasonNoReplyDays: "No reply after {days}d",
reminderReasonStalledDays: "Stalled after {days}d",
reminderReasonFeedbackDays: "Feedback requested {days}d ago",
reminderReasonWaitingDays: "Waiting {days}d",
emailInboxLinked: "{count} linked",
emailInboxLinkedOnPage: "{count} linked on this page",
emailInboxInboundCount: "{count} inbound",
@@ -869,6 +925,8 @@ export const translations = {
cvEditorShowPhoto: "Show profile photo",
cvEditorContactIcons: "Contact icons",
cvEditorOpenFailed: "Could not open this CV.",
cvEditorSourceUnavailable: "This CV is unavailable or you no longer have access to it.",
cvEditorPermissionDenied: "You do not have permission to open this CV.",
cvEditorDiscardMessage: "This CV has unsaved changes. Leave and discard them?",
cvEditorDiscardTitle: "Discard unsaved CV changes?",
cvEditorDiscardLeave: "Discard and leave",
@@ -2759,6 +2817,7 @@ export const translations = {
workspaceStatusSuggestionApplied: "Application status updated from the latest message.",
workspaceStatusSuggestionFailed: "Could not apply the suggested status.",
workspaceStrategyTitle: "Strategy snapshot",
workspaceStrategyLoadFailed: "Strategy snapshot could not be loaded.",
workspaceStrategySubtitle: "An on-demand plan grounded in this advert and your saved career data.",
workspaceStrategyProRequired: "Pro required",
workspaceStrategyGenerate: "Generate",
@@ -2787,6 +2846,7 @@ export const translations = {
appTitle: "Jobbjakt",
appTagline: "Hold oversikt over jobbsøkingen",
applicationLanguage: "Språk i appen",
applicationVersionCommit: "Applikasjonsversjon {version}, revisjon {commit}",
dashboard: "Dashboard",
jobApplications: "Jobbsøknader",
reminders: "Påminnelser",
@@ -2912,6 +2972,61 @@ export const translations = {
correspondenceInbox: "Jobb-e-post",
emailInboxSubtitle: "Se tilknyttet korrespondanse og foreslåtte rekrutteringsmeldinger på ett sted.",
emailInboxItems: "{count} elementer",
emailInboxItem: "{count} element",
emailInboxAttachmentCount: "{count} vedlegg",
historySourceManual: "Manuell",
historySourceAi: "KI-forslag",
historySourceTemplate: "Mal",
historySourceRestore: "Gjenopprettet versjon",
historySourceAutosave: "Automatisk lagring",
historySourceCreate: "Opprettet",
cvTemplateModernName: "Moderne",
cvTemplateModernDescription: "Ren SaaS-inspirert utforming med en tydelig topptekst i aksentfarge.",
cvTemplateMinimalName: "Minimalistisk",
cvTemplateMinimalDescription: "Rolig og luftig, med typografien i sentrum. Ingenting tar oppmerksomheten fra innholdet.",
cvTemplateExecutiveName: "Leder",
cvTemplateExecutiveDescription: "Høy kontrast og serifftype med vekt på ledelse. For seniorroller og kundevendte roller.",
cvTemplateTechnicalName: "Teknisk",
cvTemplateTechnicalDescription: "Kompakt tokolonneutforming for tekniske CV-er, med ferdigheter og prosjekter først.",
cvTemplateAtsClassicName: "ATS klassisk",
cvTemplateAtsClassicDescription: "Én kolonne uten grafikk, med best mulig støtte for CV-tolkere. Et trygt standardvalg.",
cvTemplateNordicName: "Nordisk",
cvTemplateNordicDescription: "Rolig sidekolonne i kjølig blått, god luft og skandinavisk enkelhet.",
cvTemplateElegantName: "Elegant",
cvTemplateElegantDescription: "Redaksjonelle serifoverskrifter over brødtekst uten seriffer, god luft og en plommefarget aksent.",
cvTemplateCreativeName: "Kreativ",
cvTemplateCreativeDescription: "Tydelig sidekolonne i aksentfarge og topptekst med vekt på bilde, for design- og produktroller.",
cvTemplateCodeName: "Kode",
cvTemplateCodeDescription: "Redaksjonell utforming for utviklere med nummererte seksjoner og presise detaljer i fastbreddeskrift.",
cvTemplateCategoryModern: "Moderne og profesjonell",
cvTemplateCategoryMinimal: "Minimalistisk",
cvTemplateCategoryExecutive: "Leder",
cvTemplateCategoryTechnical: "Teknisk",
cvTemplateCategoryAts: "ATS-profesjonell",
cvTemplateCategoryCreative: "Kreativ",
assetsTailoringHighlightTitle: "Ferdigheter å fremheve",
assetsTailoringHighlightDetail: "Annonsen etterspør disse, og de finnes allerede i profilen din. Plasser dem der de blir sett først.",
assetsTailoringExperienceTitle: "Erfaring å prioritere",
assetsTailoringExperienceDetail: "Sortert etter hvor mye av annonsen hver rolle faktisk dekker.",
assetsTailoringProjectsTitle: "Prosjekter å fremheve",
assetsTailoringProjectsDetail: "Disse prosjektene viser det annonsen etterspør.",
assetsTailoringEvidenceTitle: "Bekreftet dokumentasjon å gjenbruke",
assetsTailoringEvidenceDetail: "Fakta du tidligere har godkjent. Tilpass ordlyden, men behold en korrekt påstand.",
assetsTailoringKeywordsTitle: "Nøkkelord å ta med",
assetsTailoringKeywordsDetail: "Ord fra annonsen. Bruk dem som faktisk stemmer for deg, uten å pynte på innholdet.",
assetsTailoringGapsTitle: "Mangler å ta tak i",
assetsTailoringGapsDetail: "Etterspurt, men ikke funnet i profilen din. Legg dem til hvis du har dem, ellers bør du være forberedt på å snakke om dem.",
reminderReasonInterview: "Du har kommet til intervju, men mangler fortsatt forberedelsesnotater.",
reminderReasonCvAndAnswers: "Tilpasset CV og lagrede søknadssvar trenger fortsatt arbeid.",
reminderReasonCv: "Tilpasset CV mangler for denne rollen.",
reminderReasonAnswers: "Lagrede søknadssvar trenger fortsatt arbeid.",
reminderReasonUnscheduled: "Ingen respons ennå, og ingen oppfølging er planlagt.",
reminderReasonDue: "Det er tid for oppfølging av denne rollen.",
reminderReasonNoGaps: "Ingen akutte mangler i arbeidsflyten blokkerer denne jobben nå.",
reminderReasonNoReplyDays: "Ingen svar etter {days} dager",
reminderReasonStalledDays: "Stoppet opp etter {days} dager",
reminderReasonFeedbackDays: "Tilbakemelding ble forespurt for {days} dager siden",
reminderReasonWaitingDays: "Ventet i {days} dager",
emailInboxLinked: "{count} tilknyttet",
emailInboxLinkedOnPage: "{count} tilknyttet på denne siden",
emailInboxInboundCount: "{count} innkommende",
@@ -3651,6 +3766,8 @@ export const translations = {
cvEditorShowPhoto: "Vis profilbilde",
cvEditorContactIcons: "Kontaktikoner",
cvEditorOpenFailed: "Kunne ikke åpne denne CV-en.",
cvEditorSourceUnavailable: "Denne CV-en er utilgjengelig, eller du har ikke lenger tilgang til den.",
cvEditorPermissionDenied: "Du har ikke tilgang til å åpne denne CV-en.",
cvEditorDiscardMessage: "Denne CV-en har ulagrede endringer. Vil du forlate siden og forkaste dem?",
cvEditorDiscardTitle: "Forkaste ulagrede CV-endringer?",
cvEditorDiscardLeave: "Forkast og forlat",
@@ -5541,6 +5658,7 @@ export const translations = {
workspaceStatusSuggestionApplied: "Søknadsstatusen ble oppdatert fra den siste meldingen.",
workspaceStatusSuggestionFailed: "Kunne ikke bruke den foreslåtte statusen.",
workspaceStrategyTitle: "Strategioversikt",
workspaceStrategyLoadFailed: "Kunne ikke hente strategisammendraget.",
workspaceStrategySubtitle: "En behovsstyrt plan basert på denne annonsen og dine lagrede karrieredata.",
workspaceStrategyProRequired: "Krever Pro",
workspaceStrategyGenerate: "Generer",
@@ -86,6 +86,19 @@ test("prep renders grouped items with progress and marks AI-sourced ones", async
expect(screen.getByText("From AI")).toBeInTheDocument();
});
test("delete tooltip describes its wrapper while the disabled button keeps its item name", async () => {
routeGet();
mockedApi.patch.mockReturnValueOnce(new Promise(() => {}));
render(<ApplicationInterviewPrep jobId={7} />);
const remove = await screen.findByRole("button", { name: "Delete: Funding history" });
expect(remove.parentElement).not.toHaveAttribute("aria-label");
fireEvent.click(screen.getByRole("checkbox", { name: "Ready: Funding history" }));
expect(remove).toBeDisabled();
fireEvent.mouseOver(remove.parentElement!);
expect(await screen.findByRole("tooltip")).toHaveTextContent("Delete");
expect(mockedApi.delete).not.toHaveBeenCalled();
});
test("adding a prep item posts the chosen category and title", async () => {
routeGet();
mockedApi.post.mockResolvedValue({ data: board.groups[0].items[0] } as any);
+3 -2
View File
@@ -340,10 +340,11 @@ export default function AppShell({
const buildVersion = buildMetadata?.version.trim();
const buildCommit = buildMetadata?.commitSha?.trim();
const buildBadge = buildVersion ? (
<Tooltip title={buildCommit ? `Version ${buildVersion} • commit ${buildCommit}` : `Version ${buildVersion}`}>
<Tooltip title={buildCommit ? t("applicationVersionCommit", { version: buildVersion, commit: buildCommit }) : t("applicationVersion", { version: buildVersion })}>
<Chip
data-testid="admin-version-badge"
aria-label={`Application version ${buildVersion}${buildCommit ? `, commit ${buildCommit}` : ""}`}
role="img"
aria-label={buildCommit ? t("applicationVersionCommit", { version: buildVersion, commit: buildCommit }) : t("applicationVersion", { version: buildVersion })}
label={`v${buildVersion}`}
size="small"
variant="outlined"
@@ -74,6 +74,7 @@ test('localizes the job creation workflow in Norwegian Bokmål', async () => {
renderModal();
expect(await screen.findByText('Start med jobbannonsen')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Start med jobbannonsen', level: 3 })).toBeVisible();
expect(screen.getByRole('button', { name: 'Skriv inn detaljer manuelt' })).toBeInTheDocument();
expect(screen.getByText('Trinn 1 av 6: Legg til jobb')).toBeInTheDocument();
});
@@ -24,6 +24,8 @@ test.each([
const onClose = jest.fn();
render(<I18nProvider><QuickCommandDialog open onClose={onClose} onNavigate={onNavigate} onOpenAddJob={jest.fn()} /></I18nProvider>);
expect(screen.getByRole("dialog", { name: "Quick Search" })).toBeVisible();
const labelIds = screen.getByRole("dialog", { name: "Quick Search" }).getAttribute("aria-labelledby");
expect(!labelIds || labelIds.split(/\s+/).every(id => document.getElementById(id))).toBe(true);
fireEvent.change(screen.getByRole("textbox", { name: "Quick Search" }), { target: { value: "Nordlys" } });
fireEvent.click(await screen.findByText(label, { exact: true }));
expect(onNavigate).toHaveBeenCalledWith(destination);