feat(ui): add A1 dashboard and resumable onboarding

This commit is contained in:
cesnimda
2026-11-03 09:45:57 +01:00
parent 5e8a07d13e
commit c7d26d354c
61 changed files with 1522 additions and 23 deletions
@@ -28,6 +28,18 @@ public sealed class DashboardPreferencesTests
private const string Route = "/api/auth/preferences/dashboard";
private const string Layout = "{\"version\":1,\"revision\":0,\"widgets\":[{\"id\":\"cards\",\"visible\":true,\"size\":\"full\"}]}";
[Fact]
public void Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks()
{
const string json = "{\"version\":2,\"revision\":0,\"widgets\":[{\"id\":\"agenda\",\"visible\":true,\"size\":\"standard\",\"x\":0,\"y\":0,\"w\":8,\"h\":8},{\"id\":\"funnel\",\"visible\":true,\"size\":\"standard\",\"x\":8,\"y\":0,\"w\":4,\"h\":8}]}";
using var valid = JsonDocument.Parse(json);
Assert.NotNull(DashboardLayout.Parse(valid.RootElement));
using var unknown = JsonDocument.Parse(json.Replace("agenda", "unrecognised"));
Assert.Null(DashboardLayout.Parse(unknown.RootElement));
using var overlap = JsonDocument.Parse(json.Replace("\"x\":8", "\"x\":4"));
Assert.Null(DashboardLayout.Parse(overlap.RootElement));
}
[Fact]
public async Task Geometry_roundtrip_preserves_hidden_positions_and_blocks_legacy_downgrade()
{
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed record DashboardLayout(int Version, long Revision, IReadOnlyList<D
// Responses cross a JavaScript client; the incremented revision must remain an exact integer there.
private const long MaxRevision = 9_007_199_254_740_991L;
private static readonly HashSet<string> KnownIds = new(StringComparer.Ordinal)
{ "cards", "attention", "funnel", "activity", "stageAge", "sources", "salary", "companies", "skills" };
{ "cards", "attention", "agenda", "funnel", "activity", "stageAge", "sources", "salary", "companies", "skills" };
public static DashboardLayout? Parse(JsonElement root, bool reading = false, long revision = 0)
{
@@ -0,0 +1,39 @@
# A3-IM03 dashboard and onboarding qualification
3 November 2026. Owner accepted navigation992608c7 and explicitly answered **"Approve navigation and A3-IM03"**. This authorises this increment; the next milestone still requires Gate F review under the approved A3 implementation proposal. Publication receipt follows after committing.
## Implemented scope
New/reset defaults put Today first, Your week left and pipeline right, then metrics and retained analytics. Saved layouts retain order, hidden positions and geometry. All nine original widget IDs remain; one additive `agenda` ID is accepted by the existing schema2 server validator. Existing saved layouts append it hidden. No migration, endpoint or dependency change. Personal greetings use recorded first/display names only; unnamed accounts retain Dashboard.
Your week uses strictly parsed, recorded deadlines and follow-ups within seven local calendar days, from the existing attention feed. Date-only deadlines retain their calendar date and raw datetime attribute. Missing/free-text/invalid dates are omitted. It displays at most three entries and links to Reminders; it does not claim complete calendar or interview coverage. Full calendar remains A3-IM07 work.
Setup moves below the task dashboard. Existing dismissal keys still work; dismissal exposes a resume/walkthrough entry. Individual skips persist per owner and never increase completion. Completion remains based on the existing account/Career/mail/job reads. Walkthrough steps link to existing destinations, explain optional import/mail consent, support previous/next/close and focus return. No guidance action writes source data, grants access, or blocks manual tracking. Existing loading/error/retry remains.
## Fresh evidence
| Check | Result and boundary |
|---|---|
| Baseline | 71 frontend tests /9 suites PASSED before changes |
| Test-first new behavior | Six frontend failures reproduced before implementation; backend agenda acceptance failed before whitelist addition |
| Final selected frontend | 95 tests /12 suites PASSED, zero failed; dashboard/grid/CAS hook/customisation/presentation/onboarding/dates/control-loop/navigation/metadata/catalogue/provider. Baseline, red runs and reviewer45 overlap; do not add totals |
| Backend layout | 36 DashboardPreferencesTests PASSED, zero failed/skipped; isolated SQLite HTTP host, real owner/CAS/bounds/collision/legacy tests. New agenda test rejects unknown IDs and overlap. Focused green1 overlaps36 |
| Typecheck / full lint / build | PASSED on combined UI03 + localisation5e8a07d1 tree; zero-warning lint, fresh static export |
| Actual React browser | 16 configurations: widths320/390/768/1440 × EN/NB × light/dark; 32 whole-page axe scans, zero returned violations;32 PNGs |
| Connected journeys | All16: week dates/order, no overflow, desktop week/pipeline row, skip/reload/resume, dismiss/reload/resume, guide next/close/focus, hide agenda/save/reload |
| Write boundary | 16 explicit layout PUTs fulfilled by in-memory fixtures; zero unexpected writes/page errors. Guidance causes zero writes. Every external request aborted; Google Fonts attempts are not provider validation |
| Independent review | No actionable findings. Reviewer45 tests /3 suites PASSED, overlapping95; did not independently rerun backend/browser/build |
Native JSON/TRX, captures and source-bound hashes are in [evidence/dashboard](evidence/dashboard). Browser harness: `rtk proxy node scripts/verify-a3-dashboard.mjs` from job-tracker-ui against the static export on localhost4338. Browser persistence is controlled in-memory API behavior, separate from real backend SQLite tests; this is not an integrated database E2E pass. Screenshots use synthetic nurse, teacher and engineer data; English job descriptions in Bokmål UI are source content, not translated UI labels. Fixed-header placement in full-page scroll captures is a browser capture artifact, not a native-device result.
New catalogue keys are paired EN/NB and covered by catalogue QA. The existing widget-title dynamic domain now has ten IDs, including agenda, each backed by both catalogues. All188 historical dynamic reviews remain accounted for; full European localisation is still open. The independent mobile build-metadata fix is now combined and included in selected tests. Prior navigation/foundation evidence remains historical source-bound evidence, not a fresh full rerun.
## Preservation, recovery and open gates
Schema2 bounds, collisions, CAS and ownership remain in their existing owners. No route declarations or permission flags changed. Career Profile, CV/letter revisions, null-as-inherit overrides and RD21 owners are untouched. No physical document renderer changes warrant reopening accepted PDF metrics/visuals.
Rollback: revert this increment on the feature branch, retaining localisation5e8a07d1. No source job/profile data or database schema is changed. An old build may ignore/omit the new agenda preference on a subsequent layout save; all nine old widgets remain compatible. Production rollout/rollback qualification is still required before deployment. Existing layouts are never automatically reset.
NOT RUN: MariaDB, real backend/browser integrated journeys, hosted CI, native device/touch/screen reader/actual zoom, real provider/mail/model workflows, final integrated security, live recovery or release acceptance. No production services were accessed. Phase10 final acceptance and the wider roadmap remain open. The broader calendar and remaining A3 screens are not complete.
Next protected dependency: owner Gate F review of this dashboard/onboarding increment before A3-IM04 B1 application table/Kanban/detail implementation. Other independently eligible work remains authorised.
+20
View File
@@ -0,0 +1,20 @@
# A3-IM03 — A1 dashboard and optional onboarding
Status: IMPLEMENTED / VALIDATED; publication and owner Gate F review tracked in DASHBOARD_VALIDATION.md. Owner explicitly answered the navigation Gate F question: **"Approve navigation and A3-IM03"** on 3 November 2026. Approved navigation checkpoint992608c7 remains immutable. Branch codex/a3-ui-03 starts there, with the independently validated small localisation patch d4b938fa applied as5e8a07d1. No wholesale merge of the older i18n/theme tree. Combined checks qualify this integration only for the stated scope.
## Scope / task ledger
- [x] Read existing DashboardView, OnboardingChecklist, DashboardGrid/layout/hook/editor, source-backed completion tests, backend DashboardLayout reader/validation and current A3 contracts.
- [x] Baseline dashboard/grid/layout/onboarding and combined navigation/metadata checks.
- [x] Task-first A1 defaults for new/reset layouts; existing geometry, schema2/CAS and controls preserved. Additive agenda ID is the only server contract change (ruling below); no migration.
- [x] Quiet hierarchy, personal greeting when a recorded name exists, movable week glance beside pipeline; reporting retained. Week coverage limited to recorded attention-feed dates.
- [x] Optional owner-scoped setup, skip/resume and on-demand walkthrough; skips never count as completed data. Guidance controls do not mutate source data or grant consent.
- [x] Targeted regression, types/lint/build and fixture-backed browser journeys across languages/themes/viewports; physical renderer unchanged.
- [x] Independent read-only review: no actionable findings; reviewer45 tests overlap final95.
- [ ] Commit/publish/read-back and next Gate F owner review. Do not begin A3-IM04 before approval; continue unrelated eligible work.
Preservation:57 groups/34 routes remain; all nine existing widget IDs remain supported. Career Profile stays canonical, account identity remains separate, old dismissal keys remain compatible, owner disposal prevents cross-account state. Existing layout persistence and rendering logic stay in their owners. Legacy version1 preferences keep their recorded order; schema2 coordinates are not repacked on load. API/provider/model/backend/production qualification remains distinct from controlled browser evidence.
Ruling: the week panel must be separately movable/hideable to reproduce A1's week-left/progress-right row. Add one `agenda` widget ID to the existing catalogue and server JSON whitelist; no schema migration or new endpoint. Keep all nine old widgets and their saved coordinates. Existing saved layouts append the new widget hidden; only new/reset defaults show it. Backend parser/CAS/bounds/collision protections remain unchanged and require targeted regression. The earlier plan's "no backend contract change" assumption is superseded by this small additive ID; no new calendar/provider service is introduced.
Next dependency after review: A3-IM04 / approved B1 application tracking/detail work. No automatic advance through a future protected owner gate.
@@ -0,0 +1,11 @@
JobTrackerApi -> C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi\bin\Debug\net9.0\JobTrackerApi.dll
PdfBrowserStub -> C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\PdfBrowserStub\bin\Debug\net9.0\PdfBrowserStub.dll
JobTrackerApi.Tests -> C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll
Test run for C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll (.NETCoreApp,Version=v9.0)
VSTest version 17.14.1 (x64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
Results File: C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\docs\ui-modernisation\evidence\dashboard\backend\a3-dashboard-preferences.trx
Passed! - Failed: 0, Passed: 36, Skipped: 0, Total: 36, Duration: 36 s - JobTrackerApi.Tests.dll (net9.0)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
{
"date": "2026-11-03",
"branch": "codex/a3-ui-03",
"base": "5e8a07d1",
"frontend": {"report": "FINAL_TESTS.json", "passed": 95, "failed": 0, "suites": 12},
"backend": {"report": "backend/a3-dashboard-preferences.trx", "passed": 36, "failed": 0, "skipped": 0, "environment": "isolated temporary SQLite HTTP test host"},
"commands": [
{"command": "rtk proxy npm run typecheck", "exitCode": 0},
{"command": "rtk proxy npm run lint", "exitCode": 0},
{"command": "rtk proxy npm run build", "exitCode": 0},
{"command": "rtk proxy node scripts/verify-a3-dashboard.mjs", "exitCode": 0},
{"command": "rtk proxy git diff --check", "exitCode": 0},
{"command": "rtk proxy graphify update .", "exitCode": 0, "qualification": "AST only, SQL extractor unavailable; generated untracked graph not acceptance evidence"}
],
"review": {"result": "no actionable findings", "reviewerTests": 45, "overlapsFrontendTotal": true},
"notRun": ["MariaDB", "real backend browser integration", "native devices and screen readers", "hosted CI", "provider or model services", "final integrated security", "production release qualification"]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
> job-tracker-ui@0.1.0 test
> jest --runInBand --runTestsByPath src/dashboardLayout.test.ts src/hooks/useDashboardLayout.test.tsx src/components/DashboardGrid.test.tsx src/dashboard-customisation.test.tsx src/dashboard-presentation.test.tsx src/onboarding-checklist.test.tsx src/daily-control-loop.test.tsx src/dashboard-week.test.tsx src/a3-navigation.test.tsx src/build-metadata-localisation.test.tsx src/translation-catalogue.test.ts src/i18n-provider.test.tsx --json --outputFile=../a3-dashboard-tests.json
Test Suites: 12 passed, 12 total
Tests: 95 passed, 95 total
Snapshots: 0 total
Time: 23.394 s
Ran all test suites within paths "src/dashboardLayout.test.ts", "src/hooks/useDashboardLayout.test.tsx", "src/components/DashboardGrid.test.tsx", "src/dashboard-customisation.test.tsx", "src/dashboard-presentation.test.tsx", "src/onboarding-checklist.test.tsx", "src/daily-control-loop.test.tsx", "src/dashboard-week.test.tsx", "src/a3-navigation.test.tsx", "src/build-metadata-localisation.test.tsx", "src/translation-catalogue.test.ts", "src/i18n-provider.test.tsx".
Test results written to: ..\a3-dashboard-tests.json
File diff suppressed because one or more lines are too long
@@ -0,0 +1,156 @@
{
"recordedAtUtc": "2026-11-03T08:45:39.150316+00:00",
"base": "5e8a07d1",
"policy": "Text LF-normalised SHA-256; PNG bytes raw",
"sources": {
"JobTrackerApi.Tests/DashboardPreferencesTests.cs": "b004e3048b59de954915b1a051f34adcb934e87296a9ef8527e4107b8d5cc842",
"JobTrackerApi/Models/DashboardLayout.cs": "33d66ad0988c3d132f909108a98b8bbde067606b3acd591dc84947f97bab7cac",
"docs/pre-phase10-ux/preservation.json": "a4c2a5a0244ea89381008218de32daee1f615a250eb3808b60e0ee103c674d4d",
"job-tracker-ui/package-lock.json": "bf31e90d7af03e8af059db2aaf893925b9e8881a30a1688034ed88ef45d4fcab",
"job-tracker-ui/package.json": "3745394e406cefa40d30a87e840bccb359be147bac6ffba736e2ae90898a8626",
"job-tracker-ui/scripts/verify-a3-dashboard.mjs": "30abaf916ad7dd2d66b9068ea469bfc1b8ac2895ea1b9893c4fdb25ae2908a7a",
"job-tracker-ui/src/App.tsx": "608b4bed6c7e2881dfd65b7541ea211d193a636dd3f9cdf9a6d172acd12d137d",
"job-tracker-ui/src/build-metadata-localisation.test.tsx": "065c5550a18f7b3c7dca9bbb26c5564a53a53eaf389e7c9695596fcbb2c06f6e",
"job-tracker-ui/src/components/DashboardGrid.test.tsx": "423ba80f06cc92ace630a731a5536fc74256f921e2caa29d6b0dffd6d62513bc",
"job-tracker-ui/src/components/DashboardGrid.tsx": "1f9dfb5dc17b7804c00cca823e7045ec1804d3bcf162bb56590bcd3cf85358d8",
"job-tracker-ui/src/components/DashboardView.tsx": "21be089f43dfaec5ac5469c36b4610559421f958f52613b4deadbdaa12eadf4f",
"job-tracker-ui/src/components/DashboardWeek.tsx": "12ba41a7df41da9ea9e964c594ebb7f2c5364b4a7c523966912f31a048407340",
"job-tracker-ui/src/components/OnboardingChecklist.tsx": "78b3112465fb62c381c48e32253763f22b6f2034fbcd23c27faeaa0dddf380e6",
"job-tracker-ui/src/dashboard-customisation.test.tsx": "9fbdf02f6f4815b7d98e32c8bb295a72dafc552118567eebbd885cd1b1d5be94",
"job-tracker-ui/src/dashboard-presentation.test.tsx": "23743259e498bc525cb6e228d71ddb9a16e95868b1ddf93fdb19ec5a295ed91b",
"job-tracker-ui/src/dashboard-week.test.tsx": "a6d6fd1931057428695be595a4dc344e70ae4ac8dc95a0551e7dccb0201e4cdc",
"job-tracker-ui/src/dashboardLayout.test.ts": "4ec561778ee9cedd5fb578d18822124d38caa2357eef749eb32486a2922dddb3",
"job-tracker-ui/src/dashboardLayout.ts": "eead1fb296d50d914cd6ff6bea6c1411dd906ec4cbd96fd8ec378c2a1b9645f3",
"job-tracker-ui/src/hooks/useDashboardLayout.test.tsx": "4937c40959487ab39f98036d9c674fbb149bb26d2728fcf4a80096e7fdaf95a3",
"job-tracker-ui/src/hooks/useDashboardLayout.ts": "fa47ca4fb8aaacbc62641f5adca4120fa057e78290791cd62fad1844b2f46314",
"job-tracker-ui/src/i18n/translations.ts": "5e2b85153e329d2e6f30deaa572774248fa5d784e7b9f922e46590b41a235360",
"job-tracker-ui/src/layout/AppShell.tsx": "33dc40054ce390cc532e3bc51d6301795359f4105bac38b2dcdf2d17b3f4b917",
"job-tracker-ui/src/onboarding-checklist.test.tsx": "614d1be16911f87e12ac1ecd47da2aa3e914446f533dbafeb6cf48ea8873ac28"
},
"evidence": {
"backend/a3-agenda-green.trx": "52d94de21ca4281761f40f554f0cec2db1641c6f4c7a06228e96ae5d4f006424",
"backend/a3-agenda-red.trx": "d3e24c227961a11d093627432b199edcaf3cc3916564b06c0e7350a67dff74a6",
"backend/a3-dashboard-preferences.trx": "ee82d831e6964085b6724bc963f9cc8302828e88a2c8998adbe6e72eab749e66",
"BACKEND.log": "82b2fb95acd3df170cba7ca1dd22b1a0d02f33da9a9dce53150a38d447c0fc91",
"BASELINE.json": "6d377505929caa1c20f42db8943cadf37b6c434ebdd5593282f943a45622787c",
"browser/BROWSER.json": "0512d68cf0a905fe47914be79130e4618cb094886d3a9274580a1ec0e1cd1bbf",
"browser/dashboard-en-dark-1440.png": "26f801f6f3d3026967cf7880718fbd899777b1ae3ca58458493375423921fdf1",
"browser/dashboard-en-dark-320.png": "107d019772aad51e6f167c4214ccbb34e67b1b870e51033a5bd1029579d25197",
"browser/dashboard-en-dark-390.png": "9bfceda63388d510198ef3c9756821579fa762d679e949c9684fd2ee9b080184",
"browser/dashboard-en-dark-768.png": "31cd55d7624b197379fe4aff64a216bb3642626ef9910e75ecfc0d71dfcc7634",
"browser/dashboard-en-light-1440.png": "e9fd3bd714a041a28b08174a58eaf24655a089847a369589ef1e30b3e8ed7a51",
"browser/dashboard-en-light-320.png": "70453cc4bec711728b485d8137278afe83075ae7356c276e32e09dbeacf65112",
"browser/dashboard-en-light-390.png": "8e241fca5fc25a985ce4c7301421203a1e11057eeb7c250615fc99557d8c1e6f",
"browser/dashboard-en-light-768.png": "e4e1c3e607c271cfb696e905f567cab40b3f48724dcc41ad3f85d92172b17aa8",
"browser/dashboard-nb-dark-1440.png": "8fcaf9ca2c381386b74f1c09510021c3fae504681e0902f5f9c0d67fdd2767e4",
"browser/dashboard-nb-dark-320.png": "391485db740a2b4fd2804976de2e783b799f3d07e04944b04ab301287b75b95d",
"browser/dashboard-nb-dark-390.png": "d674e5f1659176f1a6848fd2157ae924bd46e4848cb76a2e5be633b5cf58f452",
"browser/dashboard-nb-dark-768.png": "5441d3ae7f259acec39d43bd667eeb40fa43fbed9c6696b159466e11f7ae4dfd",
"browser/dashboard-nb-light-1440.png": "7efe3c549ffeda656b96f35e2f0a130123016072426d4123279104f0eb6bc5a3",
"browser/dashboard-nb-light-320.png": "f1c6e184b98ae78e254a9dc862814b542e98fe3da4211d1eaf842d622b7b33fc",
"browser/dashboard-nb-light-390.png": "ddc3aa03ec7246e5f6fdabfabe14006cb0ab43e972a2b396b2525111cd54c69c",
"browser/dashboard-nb-light-768.png": "7ae34f94766577fa157239743f06e74532dfb2f2c6b65824536797bb7e8936f3",
"browser/walkthrough-en-dark-1440.png": "f97eb34cf0591806945f6cd80e2019108ae3ed5bac1705183b759c183ea3af0d",
"browser/walkthrough-en-dark-320.png": "198c32c54248d707fb1be5c69a97c0ba9ecaaa6aa75a1f852952429a8a9b0d2a",
"browser/walkthrough-en-dark-390.png": "9b3c139868d5ddf9109ec90de11df11d92d45febf2f7adfdf0787e8bc242e18c",
"browser/walkthrough-en-dark-768.png": "177f5dbda30660cfa3df91e743c33f6fa711c2095893ebbff84bcf9ae0bd4435",
"browser/walkthrough-en-light-1440.png": "29f2ddbfc63a972dbd820dc98b4fc3d96ca4028775194cd114be3ea6ff65f345",
"browser/walkthrough-en-light-320.png": "f93ce4c54ca7d47115fd165894a973416e2be5bfd6eef139a89726ba347d8616",
"browser/walkthrough-en-light-390.png": "1ac2a2ab2cbbddde25650a774cfd2676a75fd622982bb0ed6e52c1209ae0cce1",
"browser/walkthrough-en-light-768.png": "87255e8aaaca5c86905bd2e0630b471fc08d06f09048840c6b1cee894809c222",
"browser/walkthrough-nb-dark-1440.png": "e63cb7c6a0a1e52a36a9b3ee6c72e0bed357d3fa08d0f941e389003aa20a3180",
"browser/walkthrough-nb-dark-320.png": "fc76e2a08c43d7f00a1ee0abea50ca293f17a21fea1c4dc649ab23f410ee808b",
"browser/walkthrough-nb-dark-390.png": "899de4dd2b459a6a531fab9273ef31f22270fd5c85bfb1bd01def2f69bbc6432",
"browser/walkthrough-nb-dark-768.png": "5450dbf4ec37d9ab87f46960aac64fbe22ef6025e936fe1e8554da77c51eb7cb",
"browser/walkthrough-nb-light-1440.png": "c8f0e8d865a625b777de51f6aacdfea74a5574af12c3ad3ee17815f6cd502fd7",
"browser/walkthrough-nb-light-320.png": "f99975a21c78784903fe4617c35dd652b50cd213a657d285eb2c51ea10097396",
"browser/walkthrough-nb-light-390.png": "0fc7b535dedc1377bde7752da29ce964748c38d62132b854f68c7625325a09e6",
"browser/walkthrough-nb-light-768.png": "f33c8360bf48f7eaf433290a197fb5e33389c64bb156b0786e908242c9ed4e30",
"CHECKS.json": "e4caec33a024c9e48aa21f8d321a86c300a9b63d03a3a1558a33fbd2c689e2cd",
"FINAL_TESTS.json": "6a4c174751d1d7576e598c3e74e2610746ed2ec626c742cd47e2ec76b6dfca03",
"FRONTEND.log": "cdc842bfcdd36b05e02b252a4e56f9030b3734b152f3b2ef5704891fd3d5e787",
"RED_TESTS.json": "30c09b48f503ae331ed6fcce2949d5d794bbde7cbd8d9470481240642c2a8e2a"
},
"export": {
"404.html": "9967148f8c10546f2e6995f66ec20f8a42691c6f2a48ea9edd5ac5054332ed7a",
"__next.__PAGE__.txt": "51c3cd8bc84d9a73c34a8b03cec6a258a0597424a835b731f653e00f520d634f",
"__next._full.txt": "43eaf678423ad1afc243b9f65ee264868b38e86080c68acfb49ddb6a82aeed94",
"__next._tree.txt": "829be44f5ac7f37509dd83f0a1fa7f055e7250bd8ec2f81ab3ee0197727b1d04",
"_next/static/AOeRuze6s4O9UcClb3rJx/_buildManifest.js": "9476e0afd3f9da68a64e6ec58631191fd3eaca62c9f5a377e7517fe9a89e59ea",
"_next/static/AOeRuze6s4O9UcClb3rJx/_clientMiddlewareManifest.js": "5483e3295dc35dfe29ea932efc85341c916f10ff78ff5b0e965e003710893a4a",
"_next/static/AOeRuze6s4O9UcClb3rJx/_ssgManifest.js": "678f6ce2cb80b1fe72fc67e7412be6e2ab6ada083111b64f7c40d35e3cba5e00",
"_next/static/chunks/0-_aq6ipzcshj.js": "39f90c215bf52aef6274ed3dcc0f8af32e21a37fe8845b3bca0d6ea60da6a8c2",
"_next/static/chunks/07vdc-duql714.js": "5a4840d1ba6b65782459968940a2f4cf973c99f0f77890f4171c4f420f339353",
"_next/static/chunks/0bdt7ln111t48.js": "6184b1a61f22e2a640c17941634fdc75678ce93d142848d9f07f163e0d1fd201",
"_next/static/chunks/0bwvt3mysnclc.js": "7705667a1b75812b7833d9c9dff47fe023015e58e382c40f4507d9948c973af4",
"_next/static/chunks/0cz1d0mv5g_q7.js": "0973c1d64c88adc8e3c950410cb58b288f72118d5965b78049438deb8f2f9683",
"_next/static/chunks/0enhs8x_8oeah.js": "44c76f6178d407e1f147fdf70015fd05c21e58ed220fc922d23dfd1e0f8631bd",
"_next/static/chunks/0id-zhx_m2eg0.js": "3f1fd8f0e52a8b68a022f53fb9546eb1c06700950485b2932a36d93bec3629a9",
"_next/static/chunks/0jcvc3qm6-lyk.js": "7c27e1fe373b822adc68c63096a5bf38444ad605b9743f0920271652989c56f1",
"_next/static/chunks/0q20g9o8x-k8z.js": "3d4d9086985419aa8866b032158196c774acb8bf318f3c8245508c8daa9c5148",
"_next/static/chunks/0q_pwu9jxv4-d.js": "42a43b3d7245a413c7c29b4cbe87823037a6dbc637e2e282c92405470e0f575d",
"_next/static/chunks/0vlr_s464rzwd.js": "b404d85296c25066c63fd7abf499ebf22c0021bede6f1d0f459c6bb8fbedcea1",
"_next/static/chunks/11-48p8kygxgp.js": "57eb9059599b5a5a30598d0a334912d58af571c0ffe28448ca76ad77c8ea8df9",
"_next/static/chunks/14-lfugawba2w.js": "6a5780ca3c84d7f130f3c0a14431c85c7a1e799e2645b8c171161dab40a64360",
"_next/static/chunks/1435in_k_-dhc.js": "4757476512d5c4dd25f9264a18b304ed4824b23aff1c09e134fcbff21d9c36b6",
"_next/static/chunks/145oicobmjvm7.js": "d6bbbdeed1381652dc7412e5ce673a077579ecd5cee8934cb6b533c814098764",
"_next/static/chunks/14e7ovj0s3elm.js": "070567dd4d23fb9fa0b9ba132185a697af6578045d43b2b1c7bbb2a1594d935d",
"_next/static/chunks/154b5a6du9olf.js": "49e4df3a3e10119ddc34f84767883b9228ff6798a07fd759c2fb8e2be62fe9fb",
"_next/static/chunks/16lfece6cnlln.js": "716363ba044f30b0e70440756b4e5eafa2d25f2e1628e855e3bc4ab9eabee3d3",
"_next/static/chunks/1d0xrdyyzw7qc.js": "c11668ff1e152008e0d7b4454af12eb36c97df46a5860938e2eb5d8429b374a8",
"_next/static/chunks/1h7j7ef9e6i41.js": "cb6cd497566dc188b67d09b9c031ef9bbf36f2b85d0aac02621f56daa44faa34",
"_next/static/chunks/1ke8pzecwofrg.css": "32e9ab963ebadebcbe2c68dffb24cfbfc8cc24255ad91b48b7941fc777bf7439",
"_next/static/chunks/1m3dt58dmprbw.js": "3a80b00707e6a884a03fa636260511585f1760f37c24fd4861ead110e6b3e985",
"_next/static/chunks/1req80b9jj38r.js": "eee4d2e3789e1215195dba99ad0f09d34f8c83da848322b76054722233e9dae3",
"_next/static/chunks/1w3q8yrcjn2jk.js": "365200e241ee9290e93d7c2fdba169f2b0a8e774541e3cbf3f6f2781110fa644",
"_next/static/chunks/1xn023_28rgzj.js": "4f7f2108d030a48d039930736c92a55710975dddf419fe673f84d92899b1f575",
"_next/static/chunks/1xwc-b795syag.css": "f73c002233f931ca14d0ae58815c47ca9f2659f25b1547a6d1fdca12a2a00b3f",
"_next/static/chunks/1zi8i6dnxiac_.js": "8aea28bbe6a138e5d4b619429ca71570883a7aee974b86d5a4a746d90f704dbc",
"_next/static/chunks/22uh2tvaw80j2.js": "dafe6b4e23dc5c1d32f8b4a60c561238d2d5e7545f23ebc5b3f7304b48626dbf",
"_next/static/chunks/22wx8--1b9tnq.js": "3f7d8e762371262c6a740b4dac21725b424847319afac46055e97074a99ff5f5",
"_next/static/chunks/245ibf0f31ne8.js": "8b27ab5d720bd278b5d73a7e824bfcccf4cc893ced9fa6f1937bb2bc7a65f967",
"_next/static/chunks/280cq9ekejv1f.js": "01de8c1c3555cdd8d711763065ea9d8aec2968c3053f61254e43e26c999027bf",
"_next/static/chunks/2br3ad_-bc7k9.js": "80fa55f7a32db4d743fda36747c36994fde00a0f53f2eb25b2a6d07c6fb97cd7",
"_next/static/chunks/2gd9s_n-0nl1p.js": "31e2811be51a3c4d0ff2d1e3cea756e7fd8d57ef89141b5de69f638c1301d039",
"_next/static/chunks/2ghgf2s9b2iou.js": "23d8ff31e2128d0541c1887785fb75885323fea7c5d4f35826513b75c12c2ae3",
"_next/static/chunks/2j9f-6w2x4auf.js": "a23068fc68514dcaa1375de8802bf7f9bdfc561835e4fe47b9e6fe8f7372f85c",
"_next/static/chunks/2s2gjr6ro25dj.js": "7489e2ac285a05b563c2bcb4eaf9274bf930f2a7385f17d6d5e6850064b88b10",
"_next/static/chunks/2sjyagpqg_e_b.js": "17b0602a3b95b7776a76a48dc33803d45785586344ad28e480d21cc3acdf0318",
"_next/static/chunks/2u5qve004opku.js": "be388d9a6aa423082bafb4584c3facc65ddfed71bcfa5c1d06b097b408c5ff97",
"_next/static/chunks/2wae6urahmel1.js": "ae86575883781981cc373601a07009101d81eba801f40d4607e6f3f168777a5b",
"_next/static/chunks/2xlg4_1fj8fxh.js": "8ab4ba521db0167f3dbf684787ebf643cbc66fa7ffff7cdc65a8d8076a926155",
"_next/static/chunks/2y9p90vb01dn0.js": "d87e69ba315676b2c478b5c69d6716eff5b533cb346cf07e8e462e721a08e2b1",
"_next/static/chunks/2z42el0b2mp8_.js": "86ff990ef32c90c4839583e4f1d1bcbddb165dad3141b9fcd2ecec67f4ff24b8",
"_next/static/chunks/334vtqp3-g1j6.js": "c33d42da97f3af7388e29273297e9f42c67f54b7f5ec77a5d0637e2da06d7f7e",
"_next/static/chunks/3720519r84mh6.js": "54358db392a13914e3363046d594e824b1a3ef987913d95002bf48267dded4aa",
"_next/static/chunks/3bqn9b34na0b9.js": "f1b8ebcc1b22251c33c9f3a5d11c1f893cad2da2e8744a441e790f7347aa9e73",
"_next/static/chunks/3ckk_j5ril9ag.js": "87f631dae7b648d32171e9e05a3e000753ecb54f8d3b86a4401c0e4ab4d73553",
"_next/static/chunks/3f96r6vscobou.js": "d4b452ff3f1433f8fe1df705a8f592326f51b5a8846e6c2790d28df5a977aed3",
"_next/static/chunks/3gkylew_qn7cw.js": "7e6c74c81be0c41ab433e36d65b914f62c48f332922e27f7cc953476f4dd78cb",
"_next/static/chunks/3mowdlh-jq9ii.js": "2c6ccd1500d28ec0c7312cb08ee221f0e4184ba21465ee166a054aa9a6c7f3ed",
"_next/static/chunks/3os11ovm65tyl.js": "c5a908bd61f53f0d75927efb71e53aef8fdd9b2e4fef412a167990dbc702c166",
"_next/static/chunks/3q4v10rrqy40n.js": "d87ff7b7a0f07d0d92eab3876481eb686a12d9c43d8b92cbe406aefa227a11c1",
"_next/static/chunks/3q80eur194l-9.js": "7b52d29843c23a0ed74056eeb1704de14488f36e9d05f2f8271434c61051dce0",
"_next/static/chunks/3u61o8t7m_jza.js": "86231c12e0f8cacc8638bed0bc4b34f645fc35e60de342dc154d3cce0c9a5afa",
"_next/static/chunks/40y_aqqilokr2.js": "1f5a5260316134d6bffe949400752ee6b42addf908ae714ef4ae4ebde916dddd",
"_next/static/chunks/43b5x8qjjh3ax.js": "6ee7261cf9a63e0e6ea181bbb09125e09948588b6e10b5c02727c686a6062a3a",
"_next/static/chunks/442d7ylb3po0z.js": "d95cedfbdcd00c674fd718cbbf391ec01c0728018b0db2461a06df7074049132",
"_next/static/chunks/turbopack-25fzg9ay_1ss-.js": "c40f39e993a30c96d6247bb3a7b581538bb046efe8ef8ec07bea3ba32291750f",
"_not-found/__next._full.txt": "f4aebb067159d7ee927c964a4864a19495a4b91405ac39b70510a0c5bb6b8bbb",
"_not-found/__next._not-found/__PAGE__.txt": "803da6910a8f775def309668e8f924404853a0c473d74866b006fbe1f91905b3",
"_not-found/__next._tree.txt": "33cb524cf6cbd29a5e6a5063bce32695613363d733e01c69b7738703a532d66b",
"_not-found.html": "9967148f8c10546f2e6995f66ec20f8a42691c6f2a48ea9edd5ac5054332ed7a",
"_not-found.txt": "f4aebb067159d7ee927c964a4864a19495a4b91405ac39b70510a0c5bb6b8bbb",
"favicon.ico": "8d4923e2c487805ea9f852a685b2cf0fb10512ded8290d2a5c37d1cc8fd0702a",
"favicon.svg": "63ecf8f37f8bb146ecfae3917187c6b692d4e801641e8a314486a4007912f3a3",
"index.html": "bf511f325131b975ac8ccb33eb927584226460ad3508701a52a0bf3c05b5d9a7",
"index.txt": "43eaf678423ad1afc243b9f65ee264868b38e86080c68acfb49ddb6a82aeed94",
"logo192.png": "bc652031597fabd19b9f14e22a71f809490d218037521b3b64d013418c1390b4",
"logo512.png": "5129e9bc1ba812b711fd9a646d3a38a906d365a26ee685df183b8752458944d4",
"manifest.json": "2667ce63c2e0835b7007239816a5597639321d333da396a78638dfa9e8a7c935",
"mockups/dashboard.svg": "318be3adb1324c26eb30f05572a29528a3e38331c6a604652eef75b54f8c29dc",
"mockups/pipeline.svg": "db2ef41faabeb90e7ce85413e168581686c54535d970fc7d8deda3888a479de4",
"mockups/workspace.svg": "9bbab22349147c7b01d6e18c93c622a6f8aed6dedaf23715189ab54d60e1ddbd",
"robots.txt": "e5ab0d231eeb01b4a982d1c79a6729cac9797ad15a69247e4f28ba6afc149b4c"
}
}
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="25d77d1c-09c4-41d2-aaeb-e67ef49fa65d" name="Connor@CESNIMDA 2026-11-03 09:20:27" runUser="Cesnimda\Connor" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-11-03T09:20:27.6909121+01:00" queuing="2026-11-03T09:20:27.6909130+01:00" start="2026-11-03T09:20:26.8289020+01:00" finish="2026-11-03T09:20:27.6951073+01:00" />
<TestSettings name="default" id="4656ef06-a585-4a77-96c0-c222bcba5415">
<Deployment runDeploymentRoot="Connor_CESNIMDA_2026-11-03_09_20_27" />
</TestSettings>
<Results>
<UnitTestResult executionId="c06ca50a-061a-41ab-a6ea-f4ba5cb5697c" testId="6b6417b8-7d21-953a-f7e4-4bd7b527d518" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" computerName="CESNIMDA" duration="00:00:00.0055674" startTime="2026-11-03T09:20:27.6140667+01:00" endTime="2026-11-03T09:20:27.6140936+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="c06ca50a-061a-41ab-a6ea-f4ba5cb5697c" />
</Results>
<TestDefinitions>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="6b6417b8-7d21-953a-f7e4-4bd7b527d518">
<Execution id="c06ca50a-061a-41ab-a6ea-f4ba5cb5697c" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="6b6417b8-7d21-953a-f7e4-4bd7b527d518" executionId="c06ca50a-061a-41ab-a6ea-f4ba5cb5697c" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="1" executed="1" passed="1" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.19)&#xD;
[xUnit.net 00:00:00.21] Discovering: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.37] Discovered: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.37] Starting: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.41] Finished: JobTrackerApi.Tests&#xD;
</StdOut>
</Output>
</ResultSummary>
</TestRun>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="5b5e3c19-194f-4074-9590-245c67af754c" name="Connor@CESNIMDA 2026-11-03 09:13:49" runUser="Cesnimda\Connor" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-11-03T09:13:49.3650324+01:00" queuing="2026-11-03T09:13:49.3650326+01:00" start="2026-11-03T09:13:47.9232035+01:00" finish="2026-11-03T09:13:49.3699613+01:00" />
<TestSettings name="default" id="2d88e1a8-28ee-472f-be0d-ff31c210ebef">
<Deployment runDeploymentRoot="Connor_CESNIMDA_2026-11-03_09_13_49" />
</TestSettings>
<Results>
<UnitTestResult executionId="2cfaf2b5-6d40-439f-b5a0-a4e697bfc166" testId="6b6417b8-7d21-953a-f7e4-4bd7b527d518" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" computerName="CESNIMDA" duration="00:00:00.0910233" startTime="2026-11-03T09:13:49.2860882+01:00" endTime="2026-11-03T09:13:49.2861030+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Failed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="2cfaf2b5-6d40-439f-b5a0-a4e697bfc166">
<Output>
<ErrorInfo>
<Message>Assert.NotNull() Failure: Value is null</Message>
<StackTrace> at JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks() in C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\DashboardPreferencesTests.cs:line 36&#xD;
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)&#xD;
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)</StackTrace>
</ErrorInfo>
</Output>
</UnitTestResult>
</Results>
<TestDefinitions>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="6b6417b8-7d21-953a-f7e4-4bd7b527d518">
<Execution id="2cfaf2b5-6d40-439f-b5a0-a4e697bfc166" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="6b6417b8-7d21-953a-f7e4-4bd7b527d518" executionId="2cfaf2b5-6d40-439f-b5a0-a4e697bfc166" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Failed">
<Counters total="1" executed="1" passed="0" failed="1" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.19)&#xD;
[xUnit.net 00:00:00.50] Discovering: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.64] Discovered: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.64] Starting: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.77] Assert.NotNull() Failure: Value is null&#xD;
[xUnit.net 00:00:00.77] Stack Trace:&#xD;
[xUnit.net 00:00:00.77] C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\DashboardPreferencesTests.cs(36,0): at JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks()&#xD;
[xUnit.net 00:00:00.77] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)&#xD;
[xUnit.net 00:00:00.77] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)&#xD;
[xUnit.net 00:00:00.78] Finished: JobTrackerApi.Tests&#xD;
</StdOut>
</Output>
<RunInfos>
<RunInfo computerName="CESNIMDA" outcome="Error" timestamp="2026-11-03T09:13:49.2846664+01:00">
<Text>[xUnit.net 00:00:00.77] JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks [FAIL]</Text>
</RunInfo>
</RunInfos>
</ResultSummary>
</TestRun>
@@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="c3d6f9a6-c555-44f6-af19-ed2cc2b1e9b3" name="Connor@CESNIMDA 2026-11-03 09:23:44" runUser="Cesnimda\Connor" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-11-03T09:23:44.6082031+01:00" queuing="2026-11-03T09:23:44.6082034+01:00" start="2026-11-03T09:23:40.2007540+01:00" finish="2026-11-03T09:24:17.3885142+01:00" />
<TestSettings name="default" id="e8c6d2c3-b90f-4d87-ac26-f81c05df57b2">
<Deployment runDeploymentRoot="Connor_CESNIMDA_2026-11-03_09_23_44" />
</TestSettings>
<Results>
<UnitTestResult executionId="ed092692-4a3e-4986-ae59-bbb7914cc10e" testId="bd5a296a-74ef-c319-d7e7-fb0710af79f2" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9164533" startTime="2026-11-03T09:23:56.0718134+01:00" endTime="2026-11-03T09:23:56.0718135+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="ed092692-4a3e-4986-ae59-bbb7914cc10e" />
<UnitTestResult executionId="edc9c56b-1f69-4bba-9d8f-3f8b19a01c98" testId="c477a02b-b877-7311-61ea-c4937451a4b9" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0.5, y: 0, w: 6, h: 8)" computerName="CESNIMDA" duration="00:00:01.2416443" startTime="2026-11-03T09:23:48.2703086+01:00" endTime="2026-11-03T09:23:48.2703088+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="edc9c56b-1f69-4bba-9d8f-3f8b19a01c98" />
<UnitTestResult executionId="49390f31-5933-46e1-b91c-f087828f920e" testId="8fcff7c5-2448-5834-9446-b26e9212b07d" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Simultaneous_first_writes_have_exactly_one_winner" computerName="CESNIMDA" duration="00:00:00.8942167" startTime="2026-11-03T09:24:13.8969503+01:00" endTime="2026-11-03T09:24:13.8969506+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="49390f31-5933-46e1-b91c-f087828f920e" />
<UnitTestResult executionId="f3402972-3872-4951-9a9b-9f4e1e0e10aa" testId="651f4bc4-1228-9b6e-3d1c-9770735e172d" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Layout_write_succeeds_while_WAL_reader_remains_open" computerName="CESNIMDA" duration="00:00:00.9042811" startTime="2026-11-03T09:24:16.4219776+01:00" endTime="2026-11-03T09:24:16.4219777+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="f3402972-3872-4951-9a9b-9f4e1e0e10aa" />
<UnitTestResult executionId="0071886f-8184-4dc4-82f9-df325e4d6008" testId="74880f91-9f13-5fc4-6c6a-0aa2c05c5724" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 250, w: 6, h: 8)" computerName="CESNIMDA" duration="00:00:00.9174251" startTime="2026-11-03T09:23:51.4022912+01:00" endTime="2026-11-03T09:23:51.4022913+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="0071886f-8184-4dc4-82f9-df325e4d6008" />
<UnitTestResult executionId="e0f3bf43-15fc-4bf0-9fe1-1075da969e58" testId="ae759567-21dc-6a9c-ec7d-6e91226c9c31" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9168425" startTime="2026-11-03T09:23:57.9044872+01:00" endTime="2026-11-03T09:23:57.9044873+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="e0f3bf43-15fc-4bf0-9fe1-1075da969e58" />
<UnitTestResult executionId="427ce37e-e9e3-4997-abb2-a6895833fafa" testId="00cbe2b7-7503-73ab-7a39-97abd779c5fa" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported(visible: True, x: 6, expected: OK)" computerName="CESNIMDA" duration="00:00:01.0014396" startTime="2026-11-03T09:24:09.9875159+01:00" endTime="2026-11-03T09:24:09.9875162+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="427ce37e-e9e3-4997-abb2-a6895833fafa" />
<UnitTestResult executionId="5fec0fcc-d345-4fc5-81c6-dc6ad42dda28" testId="25b5a522-0c02-04f4-6920-a5fa106346a5" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported(visible: False, x: 4, expected: OK)" computerName="CESNIMDA" duration="00:00:00.9219636" startTime="2026-11-03T09:24:08.9856475+01:00" endTime="2026-11-03T09:24:08.9856477+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="5fec0fcc-d345-4fc5-81c6-dc6ad42dda28" />
<UnitTestResult executionId="e0888327-0c8f-42c8-a923-643d2c98e51e" testId="e64caa47-6626-6353-5cd9-53d089044833" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:\&quot;1\&quot;,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9068983" startTime="2026-11-03T09:24:01.5785202+01:00" endTime="2026-11-03T09:24:01.5785204+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="e0888327-0c8f-42c8-a923-643d2c98e51e" />
<UnitTestResult executionId="2103137b-4aca-4911-8164-bb9f5d46c9d0" testId="3ac3bd70-5981-dd61-1b42-b9305dad9c28" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9574193" startTime="2026-11-03T09:23:59.7710462+01:00" endTime="2026-11-03T09:23:59.7710463+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="2103137b-4aca-4911-8164-bb9f5d46c9d0" />
<UnitTestResult executionId="77de097d-9102-4943-ae02-cb4a2278a2b7" testId="2e164cee-cade-cfff-881d-6f67ddae8fd7" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{&quot;, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9484730" startTime="2026-11-03T09:23:53.2955959+01:00" endTime="2026-11-03T09:23:53.2955962+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="77de097d-9102-4943-ae02-cb4a2278a2b7" />
<UnitTestResult executionId="a5a82e9e-d946-4c08-9a9b-03a1a7233337" testId="d3b39df1-aa3a-6b70-6223-0dc76d6a0a68" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 0, w: 6, h: 25)" computerName="CESNIMDA" duration="00:00:01.1769496" startTime="2026-11-03T09:23:49.4475868+01:00" endTime="2026-11-03T09:23:49.4475869+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="a5a82e9e-d946-4c08-9a9b-03a1a7233337" />
<UnitTestResult executionId="6a8af080-2a85-4b66-84a3-c6cee2216a9e" testId="a9dddd4b-1f26-5de9-61c5-3130b9b05240" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 0, w: 3, h: 8)" computerName="CESNIMDA" duration="00:00:01.2703772" startTime="2026-11-03T09:23:45.8584245+01:00" endTime="2026-11-03T09:23:45.8584249+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6a8af080-2a85-4b66-84a3-c6cee2216a9e" />
<UnitTestResult executionId="5288714c-a555-43cd-9ea6-e1ffd5ad3273" testId="ac228776-c03d-fa70-87f3-0fe90f214aad" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:\&quot;0\&quot;,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9264793" startTime="2026-11-03T09:23:55.1549889+01:00" endTime="2026-11-03T09:23:55.1549890+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="5288714c-a555-43cd-9ea6-e1ffd5ad3273" />
<UnitTestResult executionId="1fd40d68-0a6a-4930-b32f-aa25406b56ca" testId="a9e9b367-f126-7214-ef9c-196367984c69" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:9007199254740991,\&quot;wid&quot;···, expected: 400)" computerName="CESNIMDA" duration="00:00:00.8997442" startTime="2026-11-03T09:24:00.6711300+01:00" endTime="2026-11-03T09:24:00.6711302+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="1fd40d68-0a6a-4930-b32f-aa25406b56ca" />
<UnitTestResult executionId="7ea62853-9895-4dc2-87f2-bd324d6aae8e" testId="c2688c31-3622-0572-6338-a6dedbccf5db" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Future_schema_is_exposed_and_cannot_be_overwritten_by_version_one" computerName="CESNIMDA" duration="00:00:00.8953324" startTime="2026-11-03T09:24:06.1842067+01:00" endTime="2026-11-03T09:24:06.1842069+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="7ea62853-9895-4dc2-87f2-bd324d6aae8e" />
<UnitTestResult executionId="ec8af7fb-bc04-4072-b7ad-acb71f14bd21" testId="7427eff0-13cb-cec7-85ff-c1a23528ec6b" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Future_schema_installed_while_body_is_paused_cannot_be_overwritten_with_guessed_revision" computerName="CESNIMDA" duration="00:00:00.9721049" startTime="2026-11-03T09:24:05.2885055+01:00" endTime="2026-11-03T09:24:05.2885056+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="ec8af7fb-bc04-4072-b7ad-acb71f14bd21" />
<UnitTestResult executionId="49d6af01-4bfb-405d-959f-942d9e38ec02" testId="4f5f26a7-2c02-6ff7-5252-f52cf67a51cd" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 0, w: 6, h: 3)" computerName="CESNIMDA" duration="00:00:01.1697005" startTime="2026-11-03T09:23:47.0282055+01:00" endTime="2026-11-03T09:23:47.0282056+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="49d6af01-4bfb-405d-959f-942d9e38ec02" />
<UnitTestResult executionId="9529d0cc-5c55-4abe-9c55-766dc8194f5b" testId="2e66217b-e82f-b769-599a-14d793b1e550" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unknown_authenticated_subject_cannot_access_or_write_preferences" computerName="CESNIMDA" duration="00:00:00.9079006" startTime="2026-11-03T09:24:04.3160701+01:00" endTime="2026-11-03T09:24:04.3160703+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="9529d0cc-5c55-4abe-9c55-766dc8194f5b" />
<UnitTestResult executionId="0a625357-338b-4270-a8a8-440d5eca1fe5" testId="5587abc1-b12b-04c1-16c3-f42b7b42f93b" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Stale_identity_profile_update_cannot_overwrite_new_layout" computerName="CESNIMDA" duration="00:00:00.8944210" startTime="2026-11-03T09:24:17.3175263+01:00" endTime="2026-11-03T09:24:17.3175263+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="0a625357-338b-4270-a8a8-440d5eca1fe5" />
<UnitTestResult executionId="3699c58e-1aeb-4f72-8e6c-43efed80ad08" testId="8fb0a999-0023-97cf-e9ba-aaecb40b56b4" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Read_normalises_corrupt_unknown_and_invalid_stored_widgets_without_resetting_revision" computerName="CESNIMDA" duration="00:00:01.0838722" startTime="2026-11-03T09:24:13.0023261+01:00" endTime="2026-11-03T09:24:13.0023264+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="3699c58e-1aeb-4f72-8e6c-43efed80ad08" />
<UnitTestResult executionId="09a12c49-abd0-4d80-b89e-b966bd74e732" testId="535719c6-8737-84cc-9553-9d7cd09a45aa" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:null}&quot;, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9223603" startTime="2026-11-03T09:24:03.4076299+01:00" endTime="2026-11-03T09:24:03.4076302+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="09a12c49-abd0-4d80-b89e-b966bd74e732" />
<UnitTestResult executionId="19799517-3488-472a-b558-c141b625e52b" testId="bc9754d5-9113-1a17-ce7e-adebc507ba06" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:-1,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9433131" startTime="2026-11-03T09:23:52.3468430+01:00" endTime="2026-11-03T09:23:52.3468431+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="19799517-3488-472a-b558-c141b625e52b" />
<UnitTestResult executionId="7908d212-0cde-481a-a28d-446871fa2cbe" testId="67714637-5bb0-2141-95ec-0ee23e55b8ef" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Geometry_roundtrip_preserves_hidden_positions_and_blocks_legacy_downgrade" computerName="CESNIMDA" duration="00:00:00.9630900" startTime="2026-11-03T09:24:14.8604682+01:00" endTime="2026-11-03T09:24:14.8604685+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="7908d212-0cde-481a-a28d-446871fa2cbe" />
<UnitTestResult executionId="ae42c28b-cfa8-455f-a1e5-f4a4e8aa503c" testId="6716b850-0161-eeda-b230-791735b7b3d3" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:3,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9321097" startTime="2026-11-03T09:23:54.2282029+01:00" endTime="2026-11-03T09:23:54.2282029+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="ae42c28b-cfa8-455f-a1e5-f4a4e8aa503c" />
<UnitTestResult executionId="21d9d5a0-044e-416c-afca-074fc8a1c05b" testId="d0a0ff5e-949e-84b7-1ec0-47ec1fe04c1a" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: -1, y: 0, w: 6, h: 8)" computerName="CESNIMDA" duration="00:00:01.0362937" startTime="2026-11-03T09:23:50.4844452+01:00" endTime="2026-11-03T09:23:50.4844456+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="21d9d5a0-044e-416c-afca-074fc8a1c05b" />
<UnitTestResult executionId="efe2cf5a-f75d-4cbb-9234-3523a04d5d8d" testId="e1656e26-67d3-91bd-d658-e5f876ad8ce9" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:9223372036854775807,\&quot;&quot;···, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9058150" startTime="2026-11-03T09:24:02.4847614+01:00" endTime="2026-11-03T09:24:02.4847616+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="efe2cf5a-f75d-4cbb-9234-3523a04d5d8d" />
<UnitTestResult executionId="38348ac2-87ea-4040-a1c7-e06aaeab3819" testId="61c52afe-7a15-a74e-32de-6947ab0d66cd" testName="JobTrackerApi.Tests.DashboardPreferencesTests.All_known_widgets_and_empty_reset_save_roundtrip_in_order" computerName="CESNIMDA" duration="00:00:00.9775379" startTime="2026-11-03T09:24:07.1620783+01:00" endTime="2026-11-03T09:24:07.1620783+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="38348ac2-87ea-4040-a1c7-e06aaeab3819" />
<UnitTestResult executionId="293b1af4-1aae-4d5f-a776-4e34dbe86387" testId="6b6417b8-7d21-953a-f7e4-4bd7b527d518" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" computerName="CESNIMDA" duration="00:00:00.0007066" startTime="2026-11-03T09:24:16.4227765+01:00" endTime="2026-11-03T09:24:16.4227766+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="293b1af4-1aae-4d5f-a776-4e34dbe86387" />
<UnitTestResult executionId="1abca55f-7ebf-42e3-9527-dc692420c195" testId="3839c63e-6069-9b14-673c-8db9c2136e7a" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Http_roundtrip_is_owner_scoped_and_preserves_other_preferences" computerName="CESNIMDA" duration="00:00:01.0018509" startTime="2026-11-03T09:24:10.9898354+01:00" endTime="2026-11-03T09:24:10.9898355+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="1abca55f-7ebf-42e3-9527-dc692420c195" />
<UnitTestResult executionId="d344f3a2-0a77-40b4-a8bb-62574361c793" testId="26bac2d0-8a68-a227-625e-58d2b2caabc5" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9151566" startTime="2026-11-03T09:23:56.9873170+01:00" endTime="2026-11-03T09:23:56.9873171+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="d344f3a2-0a77-40b4-a8bb-62574361c793" />
<UnitTestResult executionId="9f1fa279-51a5-4120-87ac-2f3574e5dab9" testId="06f41ffa-cc39-1438-e40d-11cb6a278410" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Additive_migration_preserves_legacy_account_and_defaults_and_generates_MariaDb_SQL" computerName="CESNIMDA" duration="00:00:00.6564367" startTime="2026-11-03T09:24:15.5171962+01:00" endTime="2026-11-03T09:24:15.5171966+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="9f1fa279-51a5-4120-87ac-2f3574e5dab9" />
<UnitTestResult executionId="079642bd-dbdb-464f-accb-dd499a33b87f" testId="b72d41c3-766f-2dd0-8bbb-7d2a55e3b0ab" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Duplicate_excessive_and_oversized_payloads_are_bounded" computerName="CESNIMDA" duration="00:00:00.9277497" startTime="2026-11-03T09:24:11.9180643+01:00" endTime="2026-11-03T09:24:11.9180646+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="079642bd-dbdb-464f-accb-dd499a33b87f" />
<UnitTestResult executionId="511ff458-bba1-4c92-9dfc-ab390920ec21" testId="7e277599-bf0e-a485-97b6-f50c42699d22" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported(visible: True, x: 4, expected: BadRequest)" computerName="CESNIMDA" duration="00:00:00.9005173" startTime="2026-11-03T09:24:08.0631728+01:00" endTime="2026-11-03T09:24:08.0631729+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="511ff458-bba1-4c92-9dfc-ab390920ec21" />
<UnitTestResult executionId="74db57c9-7375-4f97-a074-7d21aed76198" testId="55f8e066-bd5b-b475-3bf4-c27965a1f99c" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 8, y: 0, w: 6, h: 8)" computerName="CESNIMDA" duration="00:00:03.7448732" startTime="2026-11-03T09:23:44.5873326+01:00" endTime="2026-11-03T09:23:44.5873442+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="74db57c9-7375-4f97-a074-7d21aed76198" />
<UnitTestResult executionId="4773a4a8-3921-4bc9-b152-8f9a922e5672" testId="7b891091-5c11-6aac-881a-9013f5bc3328" testName="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" computerName="CESNIMDA" duration="00:00:00.9078460" startTime="2026-11-03T09:23:58.8130260+01:00" endTime="2026-11-03T09:23:58.8130263+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="4773a4a8-3921-4bc9-b152-8f9a922e5672" />
</Results>
<TestDefinitions>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Stale_identity_profile_update_cannot_overwrite_new_layout" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="5587abc1-b12b-04c1-16c3-f42b7b42f93b">
<Execution id="0a625357-338b-4270-a8a8-440d5eca1fe5" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Stale_identity_profile_update_cannot_overwrite_new_layout" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported(visible: True, x: 4, expected: BadRequest)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="7e277599-bf0e-a485-97b6-f50c42699d22">
<Execution id="511ff458-bba1-4c92-9dfc-ab390920ec21" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="bd5a296a-74ef-c319-d7e7-fb0710af79f2">
<Execution id="ed092692-4a3e-4986-ae59-bbb7914cc10e" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:\&quot;1\&quot;,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="e64caa47-6626-6353-5cd9-53d089044833">
<Execution id="e0888327-0c8f-42c8-a923-643d2c98e51e" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 0, w: 6, h: 3)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="4f5f26a7-2c02-6ff7-5252-f52cf67a51cd">
<Execution id="49d6af01-4bfb-405d-959f-942d9e38ec02" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unbounded_or_fractional_geometry_is_rejected_before_persistence" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Geometry_roundtrip_preserves_hidden_positions_and_blocks_legacy_downgrade" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="67714637-5bb0-2141-95ec-0ee23e55b8ef">
<Execution id="7908d212-0cde-481a-a28d-446871fa2cbe" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Geometry_roundtrip_preserves_hidden_positions_and_blocks_legacy_downgrade" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported(visible: True, x: 6, expected: OK)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="00cbe2b7-7503-73ab-7a39-97abd779c5fa">
<Execution id="427ce37e-e9e3-4997-abb2-a6895833fafa" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:9223372036854775807,\&quot;&quot;···, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="e1656e26-67d3-91bd-d658-e5f876ad8ce9">
<Execution id="efe2cf5a-f75d-4cbb-9234-3523a04d5d8d" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported(visible: False, x: 4, expected: OK)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="25b5a522-0c02-04f4-6920-a5fa106346a5">
<Execution id="5fec0fcc-d345-4fc5-81c6-dc6ad42dda28" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Future_schema_installed_while_body_is_paused_cannot_be_overwritten_with_guessed_revision" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="7427eff0-13cb-cec7-85ff-c1a23528ec6b">
<Execution id="ec8af7fb-bc04-4072-b7ad-acb71f14bd21" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Future_schema_installed_while_body_is_paused_cannot_be_overwritten_with_guessed_revision" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Read_normalises_corrupt_unknown_and_invalid_stored_widgets_without_resetting_revision" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="8fb0a999-0023-97cf-e9ba-aaecb40b56b4">
<Execution id="3699c58e-1aeb-4f72-8e6c-43efed80ad08" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Read_normalises_corrupt_unknown_and_invalid_stored_widgets_without_resetting_revision" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Layout_write_succeeds_while_WAL_reader_remains_open" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="651f4bc4-1228-9b6e-3d1c-9770735e172d">
<Execution id="f3402972-3872-4951-9a9b-9f4e1e0e10aa" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Layout_write_succeeds_while_WAL_reader_remains_open" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 0, w: 6, h: 25)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="d3b39df1-aa3a-6b70-6223-0dc76d6a0a68">
<Execution id="a5a82e9e-d946-4c08-9a9b-03a1a7233337" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unbounded_or_fractional_geometry_is_rejected_before_persistence" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Duplicate_excessive_and_oversized_payloads_are_bounded" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="b72d41c3-766f-2dd0-8bbb-7d2a55e3b0ab">
<Execution id="079642bd-dbdb-464f-accb-dd499a33b87f" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Duplicate_excessive_and_oversized_payloads_are_bounded" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 0, w: 3, h: 8)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="a9dddd4b-1f26-5de9-61c5-3130b9b05240">
<Execution id="6a8af080-2a85-4b66-84a3-c6cee2216a9e" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unbounded_or_fractional_geometry_is_rejected_before_persistence" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:null}&quot;, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="535719c6-8737-84cc-9553-9d7cd09a45aa">
<Execution id="09a12c49-abd0-4d80-b89e-b966bd74e732" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="ae759567-21dc-6a9c-ec7d-6e91226c9c31">
<Execution id="e0f3bf43-15fc-4bf0-9fe1-1075da969e58" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Simultaneous_first_writes_have_exactly_one_winner" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="8fcff7c5-2448-5834-9446-b26e9212b07d">
<Execution id="49390f31-5933-46e1-b91c-f087828f920e" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Simultaneous_first_writes_have_exactly_one_winner" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Http_roundtrip_is_owner_scoped_and_preserves_other_preferences" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="3839c63e-6069-9b14-673c-8db9c2136e7a">
<Execution id="1abca55f-7ebf-42e3-9527-dc692420c195" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Http_roundtrip_is_owner_scoped_and_preserves_other_preferences" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="26bac2d0-8a68-a227-625e-58d2b2caabc5">
<Execution id="d344f3a2-0a77-40b4-a8bb-62574361c793" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="6b6417b8-7d21-953a-f7e4-4bd7b527d518">
<Execution id="293b1af4-1aae-4d5f-a776-4e34dbe86387" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Agenda_widget_uses_existing_schema_two_without_weakening_unknown_id_or_overlap_checks" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:-1,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="bc9754d5-9113-1a17-ce7e-adebc507ba06">
<Execution id="19799517-3488-472a-b558-c141b625e52b" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="7b891091-5c11-6aac-881a-9013f5bc3328">
<Execution id="4773a4a8-3921-4bc9-b152-8f9a922e5672" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Future_schema_is_exposed_and_cannot_be_overwritten_by_version_one" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="c2688c31-3622-0572-6338-a6dedbccf5db">
<Execution id="7ea62853-9895-4dc2-87f2-bd324d6aae8e" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Future_schema_is_exposed_and_cannot_be_overwritten_by_version_one" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0, y: 250, w: 6, h: 8)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="74880f91-9f13-5fc4-6c6a-0aa2c05c5724">
<Execution id="0071886f-8184-4dc4-82f9-df325e4d6008" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unbounded_or_fractional_geometry_is_rejected_before_persistence" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: -1, y: 0, w: 6, h: 8)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="d0a0ff5e-949e-84b7-1ec0-47ec1fe04c1a">
<Execution id="21d9d5a0-044e-416c-afca-074fc8a1c05b" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unbounded_or_fractional_geometry_is_rejected_before_persistence" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:3,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="6716b850-0161-eeda-b230-791735b7b3d3">
<Execution id="ae42c28b-cfa8-455f-a1e5-f4a4e8aa503c" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{&quot;, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="2e164cee-cade-cfff-881d-6f67ddae8fd7">
<Execution id="77de097d-9102-4943-ae02-cb4a2278a2b7" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:0,\&quot;widgets\&quot;:[{\&quot;id\&quot;&quot;···, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="3ac3bd70-5981-dd61-1b42-b9305dad9c28">
<Execution id="2103137b-4aca-4911-8164-bb9f5d46c9d0" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:\&quot;0\&quot;,\&quot;widgets\&quot;:[]}&quot;, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="ac228776-c03d-fa70-87f3-0fe90f214aad">
<Execution id="5288714c-a555-43cd-9ea6-e1ffd5ad3273" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unknown_authenticated_subject_cannot_access_or_write_preferences" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="2e66217b-e82f-b769-599a-14d793b1e550">
<Execution id="9529d0cc-5c55-4abe-9c55-766dc8194f5b" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unknown_authenticated_subject_cannot_access_or_write_preferences" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Additive_migration_preserves_legacy_account_and_defaults_and_generates_MariaDb_SQL" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="06f41ffa-cc39-1438-e40d-11cb6a278410">
<Execution id="9f1fa279-51a5-4120-87ac-2f3574e5dab9" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Additive_migration_preserves_legacy_account_and_defaults_and_generates_MariaDb_SQL" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.All_known_widgets_and_empty_reset_save_roundtrip_in_order" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="61c52afe-7a15-a74e-32de-6947ab0d66cd">
<Execution id="38348ac2-87ea-4040-a1c7-e06aaeab3819" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="All_known_widgets_and_empty_reset_save_roundtrip_in_order" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 0.5, y: 0, w: 6, h: 8)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="c477a02b-b877-7311-61ea-c4937451a4b9">
<Execution id="edc9c56b-1f69-4bba-9d8f-3f8b19a01c98" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unbounded_or_fractional_geometry_is_rejected_before_persistence" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Invalid_payload_does_not_save(payload: &quot;{\&quot;version\&quot;:1,\&quot;revision\&quot;:9007199254740991,\&quot;wid&quot;···, expected: 400)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="a9e9b367-f126-7214-ef9c-196367984c69">
<Execution id="1fd40d68-0a6a-4930-b32f-aa25406b56ca" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Invalid_payload_does_not_save" />
</UnitTest>
<UnitTest name="JobTrackerApi.Tests.DashboardPreferencesTests.Unbounded_or_fractional_geometry_is_rejected_before_persistence(x: 8, y: 0, w: 6, h: 8)" storage="c:\users\connor\.codex\worktrees\direct-roadmap-integration\job tracker\jobtrackerapi.tests\bin\debug\net9.0\jobtrackerapi.tests.dll" id="55f8e066-bd5b-b475-3bf4-c27965a1f99c">
<Execution id="74db57c9-7375-4f97-a074-7d21aed76198" />
<TestMethod codeBase="C:\Users\Connor\.codex\worktrees\direct-roadmap-integration\Job tracker\JobTrackerApi.Tests\bin\Debug\net9.0\JobTrackerApi.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="JobTrackerApi.Tests.DashboardPreferencesTests" name="Unbounded_or_fractional_geometry_is_rejected_before_persistence" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="bd5a296a-74ef-c319-d7e7-fb0710af79f2" executionId="ed092692-4a3e-4986-ae59-bbb7914cc10e" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="c477a02b-b877-7311-61ea-c4937451a4b9" executionId="edc9c56b-1f69-4bba-9d8f-3f8b19a01c98" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="8fcff7c5-2448-5834-9446-b26e9212b07d" executionId="49390f31-5933-46e1-b91c-f087828f920e" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="651f4bc4-1228-9b6e-3d1c-9770735e172d" executionId="f3402972-3872-4951-9a9b-9f4e1e0e10aa" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="74880f91-9f13-5fc4-6c6a-0aa2c05c5724" executionId="0071886f-8184-4dc4-82f9-df325e4d6008" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="ae759567-21dc-6a9c-ec7d-6e91226c9c31" executionId="e0f3bf43-15fc-4bf0-9fe1-1075da969e58" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="00cbe2b7-7503-73ab-7a39-97abd779c5fa" executionId="427ce37e-e9e3-4997-abb2-a6895833fafa" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="25b5a522-0c02-04f4-6920-a5fa106346a5" executionId="5fec0fcc-d345-4fc5-81c6-dc6ad42dda28" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="e64caa47-6626-6353-5cd9-53d089044833" executionId="e0888327-0c8f-42c8-a923-643d2c98e51e" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="3ac3bd70-5981-dd61-1b42-b9305dad9c28" executionId="2103137b-4aca-4911-8164-bb9f5d46c9d0" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="2e164cee-cade-cfff-881d-6f67ddae8fd7" executionId="77de097d-9102-4943-ae02-cb4a2278a2b7" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="d3b39df1-aa3a-6b70-6223-0dc76d6a0a68" executionId="a5a82e9e-d946-4c08-9a9b-03a1a7233337" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="a9dddd4b-1f26-5de9-61c5-3130b9b05240" executionId="6a8af080-2a85-4b66-84a3-c6cee2216a9e" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="ac228776-c03d-fa70-87f3-0fe90f214aad" executionId="5288714c-a555-43cd-9ea6-e1ffd5ad3273" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="a9e9b367-f126-7214-ef9c-196367984c69" executionId="1fd40d68-0a6a-4930-b32f-aa25406b56ca" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="c2688c31-3622-0572-6338-a6dedbccf5db" executionId="7ea62853-9895-4dc2-87f2-bd324d6aae8e" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="7427eff0-13cb-cec7-85ff-c1a23528ec6b" executionId="ec8af7fb-bc04-4072-b7ad-acb71f14bd21" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="4f5f26a7-2c02-6ff7-5252-f52cf67a51cd" executionId="49d6af01-4bfb-405d-959f-942d9e38ec02" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="2e66217b-e82f-b769-599a-14d793b1e550" executionId="9529d0cc-5c55-4abe-9c55-766dc8194f5b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="5587abc1-b12b-04c1-16c3-f42b7b42f93b" executionId="0a625357-338b-4270-a8a8-440d5eca1fe5" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="8fb0a999-0023-97cf-e9ba-aaecb40b56b4" executionId="3699c58e-1aeb-4f72-8e6c-43efed80ad08" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="535719c6-8737-84cc-9553-9d7cd09a45aa" executionId="09a12c49-abd0-4d80-b89e-b966bd74e732" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="bc9754d5-9113-1a17-ce7e-adebc507ba06" executionId="19799517-3488-472a-b558-c141b625e52b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="67714637-5bb0-2141-95ec-0ee23e55b8ef" executionId="7908d212-0cde-481a-a28d-446871fa2cbe" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="6716b850-0161-eeda-b230-791735b7b3d3" executionId="ae42c28b-cfa8-455f-a1e5-f4a4e8aa503c" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="d0a0ff5e-949e-84b7-1ec0-47ec1fe04c1a" executionId="21d9d5a0-044e-416c-afca-074fc8a1c05b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="e1656e26-67d3-91bd-d658-e5f876ad8ce9" executionId="efe2cf5a-f75d-4cbb-9234-3523a04d5d8d" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="61c52afe-7a15-a74e-32de-6947ab0d66cd" executionId="38348ac2-87ea-4040-a1c7-e06aaeab3819" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="6b6417b8-7d21-953a-f7e4-4bd7b527d518" executionId="293b1af4-1aae-4d5f-a776-4e34dbe86387" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="3839c63e-6069-9b14-673c-8db9c2136e7a" executionId="1abca55f-7ebf-42e3-9527-dc692420c195" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="26bac2d0-8a68-a227-625e-58d2b2caabc5" executionId="d344f3a2-0a77-40b4-a8bb-62574361c793" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="06f41ffa-cc39-1438-e40d-11cb6a278410" executionId="9f1fa279-51a5-4120-87ac-2f3574e5dab9" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="b72d41c3-766f-2dd0-8bbb-7d2a55e3b0ab" executionId="079642bd-dbdb-464f-accb-dd499a33b87f" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="7e277599-bf0e-a485-97b6-f50c42699d22" executionId="511ff458-bba1-4c92-9dfc-ab390920ec21" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="55f8e066-bd5b-b475-3bf4-c27965a1f99c" executionId="74db57c9-7375-4f97-a074-7d21aed76198" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="7b891091-5c11-6aac-881a-9013f5bc3328" executionId="4773a4a8-3921-4bc9-b152-8f9a922e5672" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="36" executed="36" passed="36" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.19)&#xD;
[xUnit.net 00:00:00.11] Discovering: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.24] Discovered: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:00.24] Starting: JobTrackerApi.Tests&#xD;
[xUnit.net 00:00:36.75] Finished: JobTrackerApi.Tests&#xD;
</StdOut>
</Output>
</ResultSummary>
</TestRun>
@@ -0,0 +1,512 @@
{
"screens": [
{
"width": 320,
"language": "en",
"mode": "light",
"violations": []
},
{
"width": 320,
"language": "en",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 320,
"language": "en",
"mode": "dark",
"violations": []
},
{
"width": 320,
"language": "en",
"mode": "dark",
"state": "walkthrough",
"violations": []
},
{
"width": 320,
"language": "nb",
"mode": "light",
"violations": []
},
{
"width": 320,
"language": "nb",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 320,
"language": "nb",
"mode": "dark",
"violations": []
},
{
"width": 320,
"language": "nb",
"mode": "dark",
"state": "walkthrough",
"violations": []
},
{
"width": 390,
"language": "en",
"mode": "light",
"violations": []
},
{
"width": 390,
"language": "en",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 390,
"language": "en",
"mode": "dark",
"violations": []
},
{
"width": 390,
"language": "en",
"mode": "dark",
"state": "walkthrough",
"violations": []
},
{
"width": 390,
"language": "nb",
"mode": "light",
"violations": []
},
{
"width": 390,
"language": "nb",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 390,
"language": "nb",
"mode": "dark",
"violations": []
},
{
"width": 390,
"language": "nb",
"mode": "dark",
"state": "walkthrough",
"violations": []
},
{
"width": 768,
"language": "en",
"mode": "light",
"violations": []
},
{
"width": 768,
"language": "en",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 768,
"language": "en",
"mode": "dark",
"violations": []
},
{
"width": 768,
"language": "en",
"mode": "dark",
"state": "walkthrough",
"violations": []
},
{
"width": 768,
"language": "nb",
"mode": "light",
"violations": []
},
{
"width": 768,
"language": "nb",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 768,
"language": "nb",
"mode": "dark",
"violations": []
},
{
"width": 768,
"language": "nb",
"mode": "dark",
"state": "walkthrough",
"violations": []
},
{
"width": 1440,
"language": "en",
"mode": "light",
"violations": []
},
{
"width": 1440,
"language": "en",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 1440,
"language": "en",
"mode": "dark",
"violations": []
},
{
"width": 1440,
"language": "en",
"mode": "dark",
"state": "walkthrough",
"violations": []
},
{
"width": 1440,
"language": "nb",
"mode": "light",
"violations": []
},
{
"width": 1440,
"language": "nb",
"mode": "light",
"state": "walkthrough",
"violations": []
},
{
"width": 1440,
"language": "nb",
"mode": "dark",
"violations": []
},
{
"width": 1440,
"language": "nb",
"mode": "dark",
"state": "walkthrough",
"violations": []
}
],
"journeys": [
{
"width": 320,
"language": "en",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 320,
"language": "en",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 320,
"language": "nb",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 320,
"language": "nb",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 390,
"language": "en",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 390,
"language": "en",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 390,
"language": "nb",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 390,
"language": "nb",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 768,
"language": "en",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 768,
"language": "en",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 768,
"language": "nb",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 768,
"language": "nb",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 1440,
"language": "en",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 1440,
"language": "en",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 1440,
"language": "nb",
"mode": "light",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
},
{
"width": 1440,
"language": "nb",
"mode": "dark",
"skipResumeDismissWalkthrough": "PASS",
"noGuidanceWrites": "PASS",
"explicitLayoutSaveReload": "PASS"
}
],
"errors": [],
"unexpectedWrites": [],
"mockedWrites": [
{
"width": 320,
"language": "en",
"mode": "light",
"revision": 1
},
{
"width": 320,
"language": "en",
"mode": "dark",
"revision": 1
},
{
"width": 320,
"language": "nb",
"mode": "light",
"revision": 1
},
{
"width": 320,
"language": "nb",
"mode": "dark",
"revision": 1
},
{
"width": 390,
"language": "en",
"mode": "light",
"revision": 1
},
{
"width": 390,
"language": "en",
"mode": "dark",
"revision": 1
},
{
"width": 390,
"language": "nb",
"mode": "light",
"revision": 1
},
{
"width": 390,
"language": "nb",
"mode": "dark",
"revision": 1
},
{
"width": 768,
"language": "en",
"mode": "light",
"revision": 1
},
{
"width": 768,
"language": "en",
"mode": "dark",
"revision": 1
},
{
"width": 768,
"language": "nb",
"mode": "light",
"revision": 1
},
{
"width": 768,
"language": "nb",
"mode": "dark",
"revision": 1
},
{
"width": 1440,
"language": "en",
"mode": "light",
"revision": 1
},
{
"width": 1440,
"language": "en",
"mode": "dark",
"revision": 1
},
{
"width": 1440,
"language": "nb",
"mode": "light",
"revision": 1
},
{
"width": 1440,
"language": "nb",
"mode": "dark",
"revision": 1
}
],
"blockedExternal": [
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com"
],
"limits": [
"synthetic API fixtures, not real backend end-to-end",
"no native device/screen reader/model/provider/security/release qualification"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

@@ -0,0 +1,102 @@
// Actual exported React UI with synthetic, in-memory API fixtures; no live backend.
import { chromium } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/evidence/dashboard/browser');
fs.mkdirSync(out, { recursive: true });
const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], mockedWrites: [], blockedExternal: [] };
const browser = await chromium.launch();
try {
for (const width of [320, 390, 768, 1440]) for (const language of ['en', 'nb']) for (const mode of ['light', 'dark']) {
const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' });
const page = await context.newPage(); page.on('pageerror', error => report.errors.push(error.message));
await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
let layout = { version: 2, revision: 0, widgets: [] };
const now = new Date(), due = new Date(now); due.setDate(due.getDate() + 2);
const deadline = `${due.getFullYear()}-${String(due.getMonth() + 1).padStart(2, '0')}-${String(due.getDate()).padStart(2, '0')}`;
const jobs = [
{ id: 31, version: 1, jobTitle: 'Community nurse', company: { id: 1, name: 'Fjord Care' }, status: 'Interview', workflowSignal: { actionKey: 'interview-prep', workspaceTab: 'interview-prep', needsAttention: true, needsInterviewPrep: true, reason: 'Review your recorded preparation' } },
{ id: 32, version: 1, jobTitle: 'Primary teacher', company: { id: 2, name: 'Linden School' }, status: 'Preparing', deadline, workflowSignal: { actionKey: 'package-work', workspaceTab: 'tailored-cv', needsAttention: true, hasPackageGap: true, reason: 'Review application documents' } },
{ id: 33, version: 1, jobTitle: 'Electrical engineer', company: { id: 3, name: 'Northline Energy' }, status: 'Applied', followUpAt: due.toISOString(), workflowSignal: { actionKey: 'follow-up', workspaceTab: 'follow-up', needsAttention: true, needsFollowUpAction: true, reason: 'Review a follow-up' } },
];
await page.route('**/*', route => {
const req = route.request(), url = new URL(req.url());
if (!['localhost', '127.0.0.1'].includes(url.hostname)) { report.blockedExternal.push(url.origin); return route.abort(); }
if (!url.pathname.startsWith('/api/')) return route.continue();
if (req.method() === 'PUT' && url.pathname === '/api/auth/preferences/dashboard') {
const value = req.postDataJSON(); assert.equal(value.version, 2); assert.equal(value.revision, layout.revision);
assert.equal(value.widgets.length, 10); layout = { ...value, revision: layout.revision + 1 };
report.mockedWrites.push({ width, language, mode, revision: layout.revision });
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(layout) });
}
if (req.method() !== 'GET') { report.unexpectedWrites.push(url.pathname); return route.abort(); }
const fixtures = {
'/api/auth/config': { requireAuth: true },
'/api/auth/me': { id: 'a3-dashboard-owner', sessionContext: 'a3-dashboard-session', firstName: 'Alex', displayName: 'Alex Sample', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en' },
'/api/auth/preferences/dashboard': layout,
'/api/jobapplications/stats': { total: 12, active: 10, deleted: 2, byStatus: { Preparing: 3, Applied: 5, Interview: 2 }, appliedLast30Days: 4, averageDaysSinceApplied: 5 },
'/api/jobapplications/analytics-overview': { funnel: [{ label: 'Preparing', count: 3 }, { label: 'Applied', count: 5 }, { label: 'Interview', count: 2 }], responseRateBySource: [], topCompanies: [], timeInStage: [], salaryInsights: [], totalResponses: 2, totalActive: 10 },
'/api/jobapplications/reminders': jobs,
'/api/jobapplications/analytics': [{ month: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`, applied: 4, responses: 1 }],
'/api/jobapplications/tags': [{ tag: 'Communication', count: 3 }],
'/api/jobapplications/tag-trends': { months: [], series: [] },
'/api/notifications/unread-count': { count: 0 },
'/api/career/profile': { revision: 1, cvText: '', profile: { version: '1', skills: ['Teaching', 'Care coordination'] } },
'/api/gmail/status': { connected: false }, '/api/microsoft-graph/status': { connected: false }, '/api/imap/status': { connected: false },
};
return route.fulfill({ status: Object.hasOwn(fixtures, url.pathname) ? 200 : 404, contentType: 'application/json', body: JSON.stringify(fixtures[url.pathname] ?? {}) });
});
await page.goto('http://localhost:4338/dashboard');
const heading = page.locator('main h1'); await heading.waitFor();
assert.match(await heading.innerText(), language === 'en' ? /^Good (morning|afternoon|evening), Alex$/ : /^God (morgen|ettermiddag|kveld), Alex$/);
const weekName = language === 'en' ? 'Your week' : 'Uken din';
const week = page.getByRole('region', { name: weekName, exact: true }); await week.waitFor();
await page.getByText('3 / 4', { exact: true }).waitFor();
assert.deepEqual(await page.locator('[data-dashboard-widget]').evaluateAll(nodes => nodes.slice(0, 4).map(node => node.getAttribute('data-dashboard-widget'))), ['attention', 'agenda', 'funnel', 'cards']);
assert.equal(await week.locator('time').first().getAttribute('datetime'), deadline);
assert.equal(await week.getByRole('link', { name: 'Community nurse' }).count(), 0);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false);
if (width === 1440) await page.waitForFunction(() => {
const a = document.querySelector('[data-dashboard-widget=agenda]').getBoundingClientRect(), b = document.querySelector('[data-dashboard-widget=funnel]').getBoundingClientRect();
return Math.abs(a.top - b.top) < 2 && a.left < b.left;
});
const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze();
report.screens.push({ width, language, mode, violations: scan.violations }); assert.deepEqual(scan.violations, []);
await page.screenshot({ path: path.join(out, `dashboard-${language}-${mode}-${width}.png`), fullPage: true });
await page.getByRole('button', { name: language === 'en' ? /^Skip for now:/ : /^Hopp over foreløpig:/ }).click();
await page.reload(); await page.getByRole('button', { name: language === 'en' ? /^Resume step:/ : /^Fortsett steg:/ }).waitFor();
await page.getByRole('button', { name: language === 'en' ? /^Resume step:/ : /^Fortsett steg:/ }).click();
await page.getByRole('button', { name: language === 'en' ? 'Dismiss setup checklist' : 'Skjul oppstartslisten', exact: true }).click();
await page.reload();
await page.getByRole('button', { name: language === 'en' ? 'Continue setup' : 'Fortsett oppsett', exact: true }).click();
await page.getByText('3 / 4', { exact: true }).waitFor();
await page.getByRole('button', { name: language === 'en' ? 'Show walkthrough' : 'Vis veiledning', exact: true }).click();
assert.equal(await page.evaluate(() => document.activeElement.tagName), 'H3');
await page.getByRole('button', { name: language === 'en' ? 'Next step' : 'Neste steg', exact: true }).click();
assert.equal(await page.evaluate(() => document.activeElement.tagName), 'H3');
const guideScan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze();
report.screens.push({ width, language, mode, state: 'walkthrough', violations: guideScan.violations }); assert.deepEqual(guideScan.violations, []);
await page.screenshot({ path: path.join(out, `walkthrough-${language}-${mode}-${width}.png`), fullPage: true });
await page.getByRole('button', { name: language === 'en' ? 'Close walkthrough' : 'Lukk veiledning', exact: true }).click();
assert.equal(await page.getByRole('button', { name: language === 'en' ? 'Show walkthrough' : 'Vis veiledning', exact: true }).evaluate(node => node === document.activeElement), true);
assert.equal(report.mockedWrites.filter(row => row.width === width && row.language === language && row.mode === mode).length, 0);
await page.getByRole('button', { name: language === 'en' ? 'Customize dashboard' : 'Tilpass dashboard', exact: true }).click();
const editor = page.getByRole('region', { name: language === 'en' ? 'Editing dashboard' : 'Tilpasser dashboard', exact: true });
await editor.locator('summary').click(); await editor.getByRole('checkbox', { name: weekName, exact: true }).uncheck();
await editor.getByRole('button', { name: language === 'en' ? 'Save layout' : 'Lagre oppsett', exact: true }).click();
await editor.waitFor({ state: 'hidden' }); assert.equal(layout.revision, 1); assert.equal(layout.widgets.find(widget => widget.id === 'agenda').visible, false);
await page.reload(); await page.getByRole('button', { name: language === 'en' ? 'Customize dashboard' : 'Tilpass dashboard', exact: true }).waitFor();
assert.equal(await week.count(), 0);
report.journeys.push({ width, language, mode, skipResumeDismissWalkthrough: 'PASS', noGuidanceWrites: 'PASS', explicitLayoutSaveReload: 'PASS' });
await context.close();
}
assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []);
assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com'));
assert.equal(report.mockedWrites.length, 16);
console.log('PASS: 16 dashboard journeys,32 scoped axe sweeps,32 captures; controlled API persistence only.');
} finally {
fs.writeFileSync(path.join(out, 'BROWSER.json'), JSON.stringify({ ...report, limits: ['synthetic API fixtures, not real backend end-to-end','no native device/screen reader/model/provider/security/release qualification'] }, null, 2));
await browser.close();
}
+6 -1
View File
@@ -321,7 +321,12 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
if (requireAuth === null || !authResolved) return <Box sx={{ p: 4 }}><Typography variant="h6">{t("loading")}</Typography></Box>;
if (requireAuth && !me) return <Navigate to="/" replace state={{ from: `${path}${location.search}${location.hash}`, fromState: location.state }} />;
const pageTitle = titleFor(path, t);
const greetingName = me?.firstName?.trim() || me?.displayName?.trim();
const hour = new Date().getHours();
const pageTitle = path === "/dashboard" && greetingName
? hour < 12 ? t("dashboardGoodMorning", { name: greetingName })
: hour < 18 ? t("dashboardGoodAfternoon", { name: greetingName }) : t("dashboardGoodEvening", { name: greetingName })
: titleFor(path, t);
const pageSubtitle = subtitleFor(path, t);
const breadcrumbs = breadcrumbsFor(path, t);
const setAndPersistPageSize = (n: 15 | 20 | 25) => { setJobPageSize(n); window.localStorage.setItem("jobPageSize", String(n)); };
@@ -42,7 +42,7 @@ test('mount and compaction do not save; completed resize keeps hidden geometry a
expect(change).toHaveBeenCalledTimes(1);
const saved = change.mock.calls[0][0];
expect(saved.revision).toBe(8);
expect(saved.widgets[1]).toMatchObject({ w: 8, h: 12, size: 'standard' });
expect(saved.widgets.find((widget: { id: string }) => widget.id === 'attention')).toMatchObject({ w: 8, h: 12, size: 'standard' });
expect(saved.widgets[2]).toEqual(layout.widgets[2]);
});
@@ -58,7 +58,7 @@ test('narrow viewport retains desktop geometry and keyboard height changes are b
fireEvent.change(screen.getByLabelText('Height for Today'), { target: { value: '25' } });
expect(change).not.toHaveBeenCalled();
fireEvent.change(screen.getByLabelText('Height for Today'), { target: { value: '12' } });
expect(change.mock.calls[0][0].widgets[1].h).toBe(12);
expect(change.mock.calls[0][0].widgets.find((widget: { id: string }) => widget.id === 'attention').h).toBe(12);
mockMeasuredWidth = 1200;
view.rerender(<I18nProvider><DashboardGrid layout={layout} editing disabled={false} onChange={change} childrenFor={widget => <p>{widget.id} content</p>} /></I18nProvider>);
expect(change).toHaveBeenCalledTimes(1);
@@ -20,6 +20,7 @@ import { WIDGETS, defaultDashboardLayout, WidgetId } from "../dashboardLayout";
import { useDashboardLayout } from "../hooks/useDashboardLayout";
import DashboardLayoutEditor, { DashboardNavigationGuard } from "./DashboardLayoutEditor";
import DashboardGrid from "./DashboardGrid";
import DashboardWeek from "./DashboardWeek";
import { useOptionalToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
import { statusLabel, statusTone } from "../pipeline";
@@ -177,7 +178,7 @@ export default function DashboardView({ mode = "dashboard" }: { mode?: "dashboar
</Box>;
const detailsSx = { mt: 2, "& summary": { cursor: "pointer", color: "text.primary", fontWeight: 600, py: 1 }, "& th, & td": { px: 1, py: 0.75, overflowWrap: "anywhere" } };
const prefsButton = !layoutState.editing ? <Button ref={customizeButton} variant="outlined" startIcon={<TuneIcon />} disabled={layoutState.loading || Boolean(layoutState.loadError)} onClick={() => { setSavedMessage(false); layoutState.startEditing(); }}>{t("dashboardCustomize")}</Button> : null;
const pipelineReport = <PageSection surface title={t("dashboardCurrentPipeline")} description={t("dashboardPipelineBody")}>
const pipelineReport = <PageSection surface={reporting} title={t("dashboardCurrentPipeline")} description={t("dashboardPipelineBody")}>
<Box sx={reporting ? grid : undefined}>
<Stack spacing={1.5}>
{funnel.map(item => <Box key={item.label}>
@@ -199,6 +200,7 @@ export default function DashboardView({ mode = "dashboard" }: { mode?: "dashboar
</PageSection>;
const reports: Record<WidgetId, React.ReactNode> = {
agenda: summaryReady && hasJobs ? <DashboardWeek jobs={reminderJobs} /> : null,
cards: summaryReady && hasJobs ? <Box component="dl" aria-label={t("dashboardSummaryCards")} sx={{ m: 0, display: "grid", gridTemplateColumns: { xs: "1fr", sm: "repeat(2, minmax(0, 1fr))", lg: "repeat(4, minmax(0, 1fr))" }, borderBlock: "1px solid", borderColor: "divider", py: 1, gap: 1 }}>
{[
{ label: t("dashboardTrackedJobs"), value: stats!.active, caption: t("dashboardTrackedBody"), to: "/jobs" },
@@ -324,7 +326,6 @@ export default function DashboardView({ mode = "dashboard" }: { mode?: "dashboar
return (
<Box sx={{ display: "grid", gap: 3, minWidth: 0, "& p, & h2, & h3": { overflowWrap: "anywhere" } }}>
{!reporting ? <OnboardingChecklist hasJobs={summaryReady ? hasJobs : null} /> : null}
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
<Typography variant="body2" color="text.secondary">{t(reporting ? "analyticsScope" : "dashboardSnapshotScope")}</Typography>
{reporting ? <Button component={RouterLink} to="/dashboard">{t("goToDashboard")}</Button> : prefsButton}
@@ -371,6 +372,7 @@ export default function DashboardView({ mode = "dashboard" }: { mode?: "dashboar
if (!content && !(ready && (hasJobs || layoutState.editing))) return null;
return content ?? <PageSection title={t(WIDGETS.find(item => item.id === widget.id)!.titleKey)}><Typography color="text.secondary">{t("dashboardNoSample")}</Typography></PageSection>;
}} />}
{!reporting ? <OnboardingChecklist hasJobs={summaryReady ? hasJobs : null} /> : null}
</Box>
);
}
@@ -0,0 +1,46 @@
import React from 'react';
import { Box, Link, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { useI18n } from '../i18n/I18nProvider';
import { PageSection } from '../layout/PageLayout';
import type { JobApplication } from '../types';
// Date-only deadlines remain local calendar dates; never infer a time from free text.
function storedDate(value: string | undefined): Date | null {
if (typeof value !== 'string') return null;
const parts = /^(\d{4})-(\d{2})-(\d{2})(?:$|T)/.exec(value);
if (!parts) return null;
const [year, month, day] = parts.slice(1).map(Number);
const calendar = new Date(0); calendar.setFullYear(year, month - 1, day); calendar.setHours(0, 0, 0, 0);
if (calendar.getFullYear() !== year || calendar.getMonth() !== month - 1 || calendar.getDate() !== day) return null;
if (value.length === 10) return calendar;
const time = /T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,7})?)?(?:Z|[+-]\d{2}:\d{2})?$/.exec(value);
if (!time || Number(time[1]) > 23 || Number(time[2]) > 59 || Number(time[3] ?? 0) > 59) return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
export default function DashboardWeek({ jobs }: { jobs: JobApplication[] }) {
const { t, language } = useI18n();
const start = new Date(); start.setHours(0, 0, 0, 0);
const end = new Date(start); end.setDate(end.getDate() + 7);
const events = jobs.flatMap(job => ([
{ kind: 'deadline', raw: job.deadline, label: t('dashboardWeekDeadline') },
{ kind: 'followUp', raw: job.followUpAt, label: t('dashboardWeekFollowUp') },
]).flatMap(event => {
const date = storedDate(event.raw);
return date && date >= start && date < end ? [{ ...event, raw: event.raw!, date, job }] : [];
})).sort((a, b) => a.date.getTime() - b.date.getTime());
const format = new Intl.DateTimeFormat(language === 'nb' ? 'nb-NO' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' });
return <PageSection title={t('dashboardYourWeek')} description={t('dashboardWeekScope')}
actions={<Link component={RouterLink} to="/reminders">{t('reminders')}</Link>}>
{events.length ? <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, minmax(0, 1fr))' }, gap: 2 }}>
{events.slice(0, 3).map(event => <Box key={`${event.job.id}:${event.kind}`} sx={{ borderInlineStart: '3px solid', borderColor: 'primary.main', pl: 1.5, minWidth: 0 }}>
<Typography variant="caption" component="time" dateTime={event.raw}>{format.format(event.date)}</Typography>
<Typography variant="body2" sx={{ fontWeight: 700 }}>{event.label}</Typography>
<Link component={RouterLink} to={`/jobs/${event.job.id}`} sx={{ display: 'inline-block', py: 1, overflowWrap: 'anywhere' }}>{event.job.jobTitle}</Link>
<Typography variant="body2" color="text.secondary">{event.job.company?.name}</Typography>
</Box>)}
</Box> : <Typography color="text.secondary">{t('dashboardWeekEmpty')}</Typography>}
</PageSection>;
}
@@ -42,12 +42,54 @@ export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean | nu
try { return window.localStorage.getItem(dismissalKey) === "true"; }
catch { return false; }
});
const skippedKey = dismissalKey.replace('onboardingDismissed:', 'onboardingSkipped:');
const [skipped, setSkipped] = useState<string[]>(() => {
try {
const value: unknown = JSON.parse(localStorage.getItem(skippedKey) ?? '[]');
return Array.isArray(value) ? ['/profile', '/career', '/settings/connected-accounts', '/jobs'].filter(path => value.includes(path)) : [];
} catch { return []; }
});
const [guideStep, setGuideStep] = useState<number | null>(null);
const guideTitle = useRef<HTMLHeadingElement>(null);
const guideEntry = useRef<HTMLButtonElement>(null);
const resumeEntry = useRef<HTMLButtonElement>(null);
const returnToEntry = useRef(false);
const stepActions = useRef<Record<string, HTMLButtonElement | null>>({});
const pendingStepFocus = useRef<string | null>(null);
const [status, setStatus] = useState<{ profile: boolean; cv: boolean; email: boolean } | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<{ cause: unknown } | null>(null);
const [readAttempt, setReadAttempt] = useState(0);
const dismissRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (guideStep !== null) guideTitle.current?.focus();
else if (returnToEntry.current) {
returnToEntry.current = false;
(resumeEntry.current ?? guideEntry.current)?.focus();
}
}, [guideStep, dismissed]);
useEffect(() => {
if (!pendingStepFocus.current) return;
(stepActions.current[pendingStepFocus.current] ?? guideEntry.current)?.focus();
pendingStepFocus.current = null;
}, [skipped]);
const resume = (walkthrough: boolean) => {
try { localStorage.setItem(dismissalKey, 'false'); } catch { /* Session-only continuation remains available. */ }
if (!walkthrough) returnToEntry.current = true;
setDismissed(false); setGuideStep(walkthrough ? 0 : null);
};
const skip = (path: string, value: boolean) => {
const next = skipped.filter(item => item !== path);
if (value) next.push(path);
try { localStorage.setItem(skippedKey, JSON.stringify(next)); } catch { /* Keep this session's choice. */ }
pendingStepFocus.current = path;
setSkipped(next);
};
const closeGuide = () => { returnToEntry.current = true; setGuideStep(null); };
useEffect(() => {
if (dismissed) return;
let active = true;
@@ -87,8 +129,6 @@ export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean | nu
return () => { active = false; };
}, [dismissed, readAttempt]);
if (dismissed) return null;
// These four recorded steps are guidance, not a completeness score or proof of
// verified email, an imported CV, a rendered PDF, or a live mailbox grant.
const steps = [
@@ -98,7 +138,17 @@ export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean | nu
{ done: hasJobs, label: t("onboardingStepJob"), path: "/jobs", actionLabel: t("onboardingStepJobAction") },
];
const completed = steps.filter((step) => step.done === true).length;
if (status && hasJobs !== null && completed === steps.length) return null;
const allDone = Boolean(status && hasJobs !== null && completed === steps.length);
if (dismissed || (allDone && guideStep === null)) return <Box component="section" aria-label={t('onboardingResumeTitle')}
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 2, borderTop: '1px solid', borderColor: 'divider', pt: 2 }}>
<Box sx={{ minWidth: 0 }}><Typography sx={{ fontWeight: 700 }}>{t('onboardingResumeTitle')}</Typography>
<Typography variant="body2" color="text.secondary">{t('onboardingResumeBody')}</Typography></Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{!allDone ? <Button ref={resumeEntry} onClick={() => resume(false)}>{t('onboardingResume')}</Button> : null}
<Button ref={guideEntry} onClick={() => resume(true)}>{t('onboardingWalkthrough')}</Button>
</Box>
</Box>;
const guideHelp = [t('onboardingGuideProfile'), t('onboardingGuideCareer'), t('onboardingGuideEmail'), t('onboardingGuideJob')];
return (
<PageSection surface title={t("onboardingTitle")} description={t("onboardingBody")} actions={
@@ -109,12 +159,14 @@ export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean | nu
onClick={() => {
try { window.localStorage.setItem(dismissalKey, "true"); }
catch { /* This dismissal still applies while the page remains open. */ }
setDismissed(true);
returnToEntry.current = true;
setGuideStep(null); setDismissed(true);
}}
>
<CloseIcon fontSize="small" />
</IconButton>
}>
<Button ref={guideEntry} onClick={() => setGuideStep(0)} sx={{ mb: 1 }}>{t('onboardingWalkthrough')}</Button>
{loading ? <>
<Typography role="status" variant="body2" color="text.secondary">{t("loading")}</Typography>
<LinearProgress aria-label={t("loading")} sx={{ mt: 1.5 }} />
@@ -145,11 +197,31 @@ export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean | nu
{step.label}
</Typography>
</Box>
{step.done === false ? <Button size="small" variant="text" sx={{ maxWidth: "100%", whiteSpace: "normal", overflowWrap: "anywhere" }} onClick={() => navigate(step.path)}>{step.actionLabel}</Button> : null}
{step.done === false ? <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{skipped.includes(step.path) ? <>
<Typography variant="caption" sx={{ alignSelf: 'center' }}>{t('onboardingSkipped')}</Typography>
<Button ref={node => { stepActions.current[step.path] = node; }} aria-label={t('onboardingResumeStep', { name: step.label })} onClick={() => skip(step.path, false)}>{t('onboardingResumeStepAction')}</Button>
</> : <>
<Button ref={node => { stepActions.current[step.path] = node; }} size="small" variant="text" sx={{ maxWidth: "100%", whiteSpace: "normal", overflowWrap: "anywhere" }} onClick={() => navigate(step.path)}>{step.actionLabel}</Button>
<Button aria-label={t('onboardingSkipStep', { name: step.label })} onClick={() => skip(step.path, true)}>{t('onboardingSkipAction')}</Button>
</>}
</Box> : null}
</Box>
))}
</Box>
{skipped.length ? <Typography variant="caption" sx={{ display: 'block', mt: 1 }}>{t('onboardingSkipHelp')}</Typography> : null}
</> : null}
{guideStep !== null ? <Box sx={{ mt: 3, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<PageSection title={steps[guideStep].label} titleRef={guideTitle} level="h3" description={guideHelp[guideStep]}>
<Typography role="status" variant="body2">{t('onboardingGuideCount', { current: guideStep + 1, total: steps.length })}</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
<Button onClick={() => navigate(steps[guideStep].path)}>{steps[guideStep].actionLabel}</Button>
<Button disabled={guideStep === 0} onClick={() => setGuideStep(guideStep - 1)}>{t('onboardingGuidePrevious')}</Button>
<Button onClick={() => guideStep === steps.length - 1 ? closeGuide() : setGuideStep(guideStep + 1)}>{guideStep === steps.length - 1 ? t('onboardingGuideFinish') : t('onboardingGuideNext')}</Button>
<Button onClick={closeGuide}>{t('onboardingGuideClose')}</Button>
</Box>
</PageSection>
</Box> : null}
</PageSection>
);
}
@@ -80,9 +80,13 @@ test('all hidden widgets leave an accessible recovery and reset remains cancella
test('keyboard ordering keeps focus and announces the resulting position without refetching reports', async () => {
open(); const editor = await edit(); const before = mocked.get.mock.calls.length;
const preview = widgetOptions('Today');
const move = within(preview).getByRole('button', { name: 'Move Today earlier' }); move.focus(); fireEvent.click(move);
const move = within(preview).getByRole('button', { name: 'Move Today later' }); act(() => move.focus()); fireEvent.click(move);
const earlier = within(preview).getByRole('button', { name: 'Move Today earlier' });
await waitFor(() => expect(earlier).toHaveFocus());
expect(screen.getByText('Today moved to position 2 of 10.')).toBeInTheDocument();
fireEvent.click(earlier);
await waitFor(() => expect(within(preview).getByRole('button', { name: 'Move Today later' })).toHaveFocus());
expect(screen.getByText('Today moved to position 1 of 9.')).toBeInTheDocument();
expect(screen.getByText('Today moved to position 1 of 10.')).toBeInTheDocument();
expect(mocked.get.mock.calls).toHaveLength(before);
});
@@ -30,6 +30,21 @@ beforeEach(() => {
mockedApi.put.mockImplementation((_url: string, body: any) => { layout = { ...body, revision: body.revision + 1 }; return Promise.resolve({ data: layout }) as any; });
});
test('A1 week glance shows recorded upcoming dates from attention data without inventing missing dates', async () => {
const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1);
const deadline = `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, '0')}-${String(tomorrow.getDate()).padStart(2, '0')}`;
const jobs = [
{ id: 91, jobTitle: 'School vacancy', company: { name: 'Linden School' }, deadline },
{ id: 92, jobTitle: 'No recorded date', company: { name: 'Care centre' }, deadline: 'as soon as possible' },
];
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url.endsWith('/reminders') ? jobs : data(url) }) as any);
open();
const week = await screen.findByRole('region', { name: 'Your week' });
expect(within(week).getByText('School vacancy')).toBeVisible();
expect(within(week).queryByText('No recorded date')).not.toBeInTheDocument();
expect(within(week).getByRole('link', { name: /School vacancy/ })).toHaveAttribute('href', '/jobs/91');
});
test('[PRES-S01] exact monthly values, sample, custom statuses and separate salary group remain readable', async () => {
open();
expect(await screen.findByText('Tracked jobs')).toBeVisible();
@@ -41,7 +56,7 @@ test('[PRES-S01] exact monthly values, sample, custom statuses and separate sala
expect(within(table).getByRole('row', { name: 'September 2026 2 1' })).toBeVisible();
expect(screen.getByText('Faglig vurdering')).toBeInTheDocument();
expect(screen.getByText(/NOK.*2 jobs/)).toBeInTheDocument();
expect(screen.queryByText(/upcoming|conversion funnel|active applications/i)).not.toBeInTheDocument();
expect(screen.queryByText(/upcoming interviews|conversion funnel|active applications/i)).not.toBeInTheDocument();
});
test('[PRES-S02] all widget visibility choices save and survive remount', async () => {
@@ -0,0 +1,35 @@
import React from 'react';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import DashboardWeek from './components/DashboardWeek';
import { I18nProvider } from './i18n/I18nProvider';
import type { JobApplication } from './types';
beforeEach(() => { localStorage.clear(); jest.useFakeTimers(); jest.setSystemTime(new Date(2026, 10, 3, 12)); });
afterEach(() => { jest.useRealTimers(); localStorage.clear(); });
function open(deadline?: string, followUpAt?: string) {
const jobs = [{ id: 3, jobTitle: 'Recorded role', company: { name: 'Example employer' }, deadline, followUpAt }] as JobApplication[];
const original = JSON.stringify(jobs);
const view = render(<MemoryRouter><I18nProvider><DashboardWeek jobs={jobs} /></I18nProvider></MemoryRouter>);
expect(JSON.stringify(jobs)).toBe(original);
return view;
}
test.each([
['2026-11-03', true], ['2026-11-09', true], ['2026-11-10', false], ['2026-11-02', false],
['as soon as possible', false], ['2026-11-04T24:00:00Z', false], ['2026-11-04T10:99:00Z', false],
])('deadline %s respects the local seven-day boundary and recorded precision', (value, visible) => {
const view = open(value);
expect(screen.queryByRole('link', { name: 'Recorded role' }) !== null).toBe(visible);
if (visible) expect(view.container.querySelector('time')).toHaveAttribute('datetime', value);
});
test('invalid calendar dates do not silently roll into a real upcoming day', () => {
jest.setSystemTime(new Date(2026, 2, 1, 12)); open('2026-02-30');
expect(screen.queryByRole('link', { name: 'Recorded role' })).not.toBeInTheDocument();
});
test('recorded follow-up timestamps and date-only deadlines stay distinct and untranslated', () => {
localStorage.setItem('uiLanguage', 'nb'); const view = open('2026-11-04', '2026-11-05T10:30:00Z');
expect(screen.getByRole('heading', { name: 'Uken din' })).toBeInTheDocument();
expect(screen.getByText('Søknadsfrist')).toBeInTheDocument(); expect(screen.getByText('Oppfølging')).toBeInTheDocument();
expect([...view.container.querySelectorAll('time')].map(node => node.getAttribute('datetime'))).toEqual(['2026-11-04', '2026-11-05T10:30:00Z']);
});
+12 -2
View File
@@ -31,13 +31,23 @@ test('schema two preserves chosen hidden geometry without resetting the owner re
test('default catalogue preserves every section and supported widths', () => {
const layout = defaultDashboardLayout();
expect(layout.widgets.map(w => w.id)).toEqual(['cards', 'attention', 'funnel', 'activity', 'stageAge', 'sources', 'salary', 'companies', 'skills']);
expect(new Set(layout.widgets.map(w => w.id))).toEqual(new Set(WIDGETS.map(w => w.id)));
expect(layout.widgets.every(w => w.visible)).toBe(true);
expect(WIDGETS[0].sizes).toEqual(['full']);
layout.widgets[0].visible = false;
expect(defaultDashboardLayout().widgets[0].visible).toBe(true);
});
test.each([1, 2])('new schema %s preferences use task-first A1 defaults without omitting widgets', version => {
const layout = normalizeDashboardLayout({ version, revision: 0, widgets: [] });
expect(layout.widgets[0]).toMatchObject({ id: 'attention', visible: true, x: 0, y: 0, w: 12 });
expect(layout.widgets.findIndex(widget => widget.id === 'cards')).toBeGreaterThan(layout.widgets.findIndex(widget => widget.id === 'funnel'));
expect(layout.widgets).toHaveLength(WIDGETS.length);
const visible = layout.widgets.filter(widget => widget.visible);
expect(visible.every(widget => widget.x >= 0 && widget.x + widget.w <= 12 && widget.h <= 24 && widget.y + widget.h <= 256)).toBe(true);
expect(visible.some((a, index) => visible.slice(index + 1).some(b => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y))).toBe(false);
});
test('saved layouts retain known order, reject duplicates and append missing sections hidden', () => {
const result = normalizeDashboardLayout({ version: 1, revision: 7, widgets: [
{ id: 'skills', visible: false, size: 'full' },
@@ -49,7 +59,7 @@ test('saved layouts retain known order, reject duplicates and append missing sec
expect(result.widgets[0]).toMatchObject({ id: 'skills', visible: false, size: 'full' });
expect(result.widgets[1]).toMatchObject({ id: 'cards', visible: false, size: 'full' });
expect(result.widgets.slice(2).every(w => !w.visible)).toBe(true);
expect(result.widgets).toHaveLength(9);
expect(result.widgets).toHaveLength(WIDGETS.length);
});
test.each([null, {}, { version: 3, revision: 8, widgets: [] }, { version: 1, revision: -1, widgets: [] }])('invalid input has safe defaults: %j', input => {
+9 -4
View File
@@ -3,6 +3,7 @@ export type WidgetSize = 'standard' | 'full';
export const WIDGETS = [
{ id: 'cards', titleKey: 'dashboardSummaryCards', defaultSize: 'full', sizes: ['full'] },
{ id: 'attention', titleKey: 'dashboardTodayTitle', defaultSize: 'standard', sizes: ['standard', 'full'] },
{ id: 'agenda', titleKey: 'dashboardYourWeek', defaultSize: 'standard', sizes: ['standard', 'full'] },
{ id: 'funnel', titleKey: 'dashboardCurrentPipeline', defaultSize: 'standard', sizes: ['standard', 'full'] },
{ id: 'activity', titleKey: 'dashboardApplicationActivity', defaultSize: 'full', sizes: ['standard', 'full'] },
{ id: 'stageAge', titleKey: 'dashboardStageAge', defaultSize: 'standard', sizes: ['standard', 'full'] },
@@ -29,8 +30,12 @@ export function arrangeDashboardWidgets(widgets: DashboardWidget[]): DashboardWi
}
export function defaultDashboardLayout(): DashboardLayout {
return { version: 2, revision: 0, widgets: arrangeDashboardWidgets(WIDGETS.map(w => ({ id: w.id, visible: true, size: w.defaultSize,
x: 0, y: 0, w: w.defaultSize === 'full' ? 12 : 6, h: w.id === 'cards' ? 5 : 8 }))) };
const order: WidgetId[] = ['attention', 'agenda', 'funnel', 'cards', 'activity', 'stageAge', 'sources', 'salary', 'companies', 'skills'];
return { version: 2, revision: 0, widgets: arrangeDashboardWidgets(order.map(id => {
const definition = WIDGETS.find(widget => widget.id === id)!;
const w = id === 'attention' ? 12 : id === 'agenda' ? 8 : id === 'funnel' ? 4 : definition.defaultSize === 'full' ? 12 : 6;
return { id, visible: true, size: w === 12 ? 'full' : 'standard', x: 0, y: 0, w, h: id === 'cards' ? 5 : id === 'attention' ? 9 : 8 };
})) };
}
export function normalizeDashboardLayout(input: unknown): DashboardLayout {
@@ -54,8 +59,8 @@ export function normalizeDashboardLayout(input: unknown): DashboardLayout {
size: w === 12 ? 'full' : 'standard', x: valid ? item.x : 0, y: valid ? item.y : 0, w,
h: valid ? item.h : definition.id === 'cards' ? 5 : 8 });
}
for (const definition of WIDGETS) {
if (!seen.has(definition.id)) widgets.push({ ...defaultDashboardLayout().widgets.find(w => w.id === definition.id)!, visible: revision === 0 });
for (const widget of defaultDashboardLayout().widgets) {
if (!seen.has(widget.id)) widgets.push({ ...widget, visible: revision === 0 });
}
return { version: 2, revision, widgets: value.version === 1 ? arrangeDashboardWidgets(widgets) : widgets };
}
@@ -19,8 +19,8 @@ test('loads once, migrates only owner legacy flags, and cancels without writing'
localStorage.setItem('dashboardPrefs:a', JSON.stringify({ cards: false, activity: false }));
const { result, rerender } = renderHook(() => useDashboardLayout(true));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.layout.widgets[0].visible).toBe(false);
expect(result.current.layout.widgets[1].visible).toBe(true);
expect(result.current.layout.widgets.find(widget => widget.id === 'cards')?.visible).toBe(false);
expect(result.current.layout.widgets.find(widget => widget.id === 'attention')?.visible).toBe(true);
act(() => result.current.startEditing());
act(() => result.current.setDraft(d => d && ({ ...d, widgets: [...d.widgets].reverse() })));
rerender();
+54
View File
@@ -1960,6 +1960,25 @@ export const translations = {
dashboardOverviewTitle: "Dashboard overview",
dashboardHeroLabel: "Job search overview",
onboardingTitle: "Get set up",
onboardingResumeTitle: "Make Jobbjakt yours",
onboardingResumeBody: "Optional setup. Pick up whenever you are ready.",
onboardingResume: "Continue setup",
onboardingWalkthrough: "Show walkthrough",
onboardingSkipAction: "Skip for now",
onboardingSkipStep: "Skip for now: {name}",
onboardingResumeStep: "Resume step: {name}",
onboardingResumeStepAction: "Resume",
onboardingSkipped: "Skipped for now",
onboardingSkipHelp: "Skipped steps stay optional and do not count as completed.",
onboardingGuideProfile: "Review your account details and security. Your account identity is separate from your Career Profile.",
onboardingGuideCareer: "Record experience you want to reuse. Import is optional; review changes before applying them to your Career Profile.",
onboardingGuideEmail: "Connect a mailbox only if you want to. Sign-in identity and mailbox access are separate permissions.",
onboardingGuideJob: "Add a job manually or discover an opportunity. Setup is never required before you start tracking.",
onboardingGuidePrevious: "Previous step",
onboardingGuideNext: "Next step",
onboardingGuideFinish: "Finish walkthrough",
onboardingGuideClose: "Close walkthrough",
onboardingGuideCount: "Walkthrough step {current} of {total}",
onboardingBody: "Setup guidance based on your saved account details, Career information, mailbox connections and tracked jobs.",
onboardingLoadFailed: "Could not load setup status.",
onboardingJobStatusUnknown: "Tracked-job status is unavailable. Retry the dashboard summary to check your jobs.",
@@ -1976,6 +1995,14 @@ export const translations = {
onboardingStepEmailAction: "Connect email",
dashboardResponseRate: "{rate}% response rate",
dashboardTodayTitle: "Today",
dashboardYourWeek: "Your week",
dashboardGoodMorning: "Good morning, {name}",
dashboardGoodAfternoon: "Good afternoon, {name}",
dashboardGoodEvening: "Good evening, {name}",
dashboardWeekScope: "Upcoming deadlines and follow-ups recorded in your current attention list. More dates may exist outside this list.",
dashboardWeekDeadline: "Application deadline",
dashboardWeekFollowUp: "Follow-up",
dashboardWeekEmpty: "No recorded dates in these priorities for the next seven days.",
dashboardTodayBody: "Your highest-priority application actions, ordered by urgency.",
dashboardSummaryUnavailableTitle: "Unable to load dashboard summary",
dashboardSummaryUnavailableBody: "The dashboard summary is unavailable right now.",
@@ -5085,6 +5112,25 @@ export const translations = {
dashboardOverviewTitle: "Dashboard-oversikt",
dashboardHeroLabel: "Oversikt over jobbsøket",
onboardingTitle: "Kom i gang",
onboardingResumeTitle: "Gjør Jobbjakt til ditt",
onboardingResumeBody: "Valgfritt oppsett. Fortsett når du er klar.",
onboardingResume: "Fortsett oppsett",
onboardingWalkthrough: "Vis veiledning",
onboardingSkipAction: "Hopp over foreløpig",
onboardingSkipStep: "Hopp over foreløpig: {name}",
onboardingResumeStep: "Fortsett steg: {name}",
onboardingResumeStepAction: "Fortsett",
onboardingSkipped: "Hoppet over foreløpig",
onboardingSkipHelp: "Steg du hopper over, er fortsatt valgfrie og telles ikke som fullført.",
onboardingGuideProfile: "Se gjennom kontodetaljer og sikkerhet. Kontoidentiteten er atskilt fra karriereprofilen din.",
onboardingGuideCareer: "Registrer erfaring du vil gjenbruke. Import er valgfritt; se gjennom endringer før du tar dem inn i karriereprofilen.",
onboardingGuideEmail: "Koble til en postboks bare hvis du ønsker det. Innloggingsidentitet og postbokstilgang er ulike tillatelser.",
onboardingGuideJob: "Legg til en jobb manuelt eller finn en stilling. Oppsett er aldri påkrevd før du begynner å følge opp jobber.",
onboardingGuidePrevious: "Forrige steg",
onboardingGuideNext: "Neste steg",
onboardingGuideFinish: "Fullfør veiledning",
onboardingGuideClose: "Lukk veiledning",
onboardingGuideCount: "Veiledning, steg {current} av {total}",
onboardingBody: "Veiledning basert på lagrede kontoopplysninger, karriereinformasjon, tilkoblede postkasser og stillinger du følger.",
onboardingLoadFailed: "Kunne ikke laste oppsettstatus.",
onboardingJobStatusUnknown: "Status for stillinger er ikke tilgjengelig. Last oversikten på dashbordet på nytt for å sjekke stillingene dine.",
@@ -5101,6 +5147,14 @@ export const translations = {
onboardingStepEmailAction: "Koble til e-post",
dashboardResponseRate: "{rate}% svarrate",
dashboardTodayTitle: "I dag",
dashboardYourWeek: "Uken din",
dashboardGoodMorning: "God morgen, {name}",
dashboardGoodAfternoon: "God ettermiddag, {name}",
dashboardGoodEvening: "God kveld, {name}",
dashboardWeekScope: "Kommende søknadsfrister og oppfølginger i listen over det du bør følge opp. Flere datoer kan finnes utenfor listen.",
dashboardWeekDeadline: "Søknadsfrist",
dashboardWeekFollowUp: "Oppfølging",
dashboardWeekEmpty: "Ingen registrerte datoer blant disse prioriteringene de neste sju dagene.",
dashboardTodayBody: "De viktigste handlingene for søknadene dine, sortert etter hvor mye de haster.",
dashboardSummaryUnavailableTitle: "Kunne ikke laste dashboard-oppsummeringen",
dashboardSummaryUnavailableBody: "Dashboard-oppsummeringen er ikke tilgjengelig akkurat nå.",
@@ -56,6 +56,43 @@ afterEach(() => {
window.localStorage.clear();
});
test('a dismissed optional checklist resumes without losing source-backed progress', async () => {
localStorage.setItem('onboardingDismissed:owner-a', 'true');
const view = render(<Checklist />);
expect(mockedApi.get).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: 'Continue setup' }));
expect(await screen.findByText('3 / 4')).toBeInTheDocument();
expect(localStorage.getItem('onboardingDismissed:owner-a')).toBe('false');
view.unmount(); render(<Checklist />);
expect(await screen.findByText('3 / 4')).toBeInTheDocument();
expectNoWrites();
});
test('individual skips persist for their owner but do not count as completed data', async () => {
const view = render(<Checklist />); await screen.findByText('3 / 4');
fireEvent.click(screen.getByRole('button', { name: /^Skip for now:/ }));
expect(screen.getByRole('progressbar', { name: '3 of 4 done' })).toHaveAttribute('aria-valuenow', '75');
expect(localStorage.getItem('onboardingSkipped:owner-a')).toBe('["/jobs"]');
expect(screen.getByRole('button', { name: /^Resume step:/ })).toHaveFocus();
view.unmount(); render(<Checklist />); await screen.findByText('3 / 4');
fireEvent.click(screen.getByRole('button', { name: /^Resume step:/ }));
expect(screen.getByRole('button', { name: translations.en.onboardingStepJobAction })).toBeInTheDocument();
expect(screen.getByRole('button', { name: translations.en.onboardingStepJobAction })).toHaveFocus();
expectNoWrites();
});
test('on-demand walkthrough changes guidance and focus without changing completion or connecting providers', async () => {
render(<Checklist />); await screen.findByText('3 / 4');
fireEvent.click(screen.getByRole('button', { name: 'Show walkthrough' }));
const guide = screen.getByRole('region', { name: translations.en.onboardingStepProfile });
expect(within(guide).getByRole('heading')).toHaveFocus();
fireEvent.click(within(guide).getByRole('button', { name: 'Next step' }));
expect(screen.getByRole('heading', { name: translations.en.onboardingStepCv })).toHaveFocus();
expect(screen.getByText('3 / 4')).toBeInTheDocument();
expect(mockedApi.get).toHaveBeenCalledTimes(5);
expectNoWrites();
});
test("uses structured career data and connected inbox status for four source-backed onboarding steps", async () => {
render(<Checklist />);
@@ -145,7 +182,7 @@ test.each(sources)("failed %s status stays unknown and retries only the same sou
rejectFirst = false;
const retry = within(alert).getByRole("button", { name: translations.en.retry });
retry.focus();
act(() => retry.focus());
fireEvent.click(retry);
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
expect(screen.getByRole("button", { name: translations.en.onboardingDismiss })).toHaveFocus();