From 5cfc84bb6843a954dbc84e841ac3fad73bb92ee3 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Wed, 1 Jul 2026 12:49:08 +0200 Subject: [PATCH 001/132] chore: update agent instructions --- reports/performance/page-load-latest.json | 106 +++++++++++----------- reports/performance/page-load-latest.md | 39 ++++---- 2 files changed, 73 insertions(+), 72 deletions(-) diff --git a/reports/performance/page-load-latest.json b/reports/performance/page-load-latest.json index 5ab86169..06d25ef4 100644 --- a/reports/performance/page-load-latest.json +++ b/reports/performance/page-load-latest.json @@ -120,24 +120,24 @@ "app": "extension", "type": "extension-url", "samples": 20, - "minMs": 60, - "p50Ms": 68.0, - "p95Ms": 72.6, - "maxMs": 84, - "meanMs": 68.4, - "pass": false + "minMs": 1.4, + "p50Ms": 1.5, + "p95Ms": 1.8, + "maxMs": 2, + "meanMs": 1.6, + "pass": true }, { "id": "extension.onboarding", "app": "extension", "type": "extension-url", "samples": 20, - "minMs": 64, - "p50Ms": 68.0, - "p95Ms": 76.6, - "maxMs": 88, - "meanMs": 69.5, - "pass": false + "minMs": 1.4, + "p50Ms": 1.6, + "p95Ms": 1.9, + "maxMs": 2.1, + "meanMs": 1.6, + "pass": true }, { "id": "extension.profile", @@ -2806,7 +2806,7 @@ }, "samples": [ { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2820,7 +2820,7 @@ "consoleErrors": [] }, { - "durationMs": 64, + "durationMs": 1.5, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2834,7 +2834,7 @@ "consoleErrors": [] }, { - "durationMs": 84, + "durationMs": 1.7, "domContentLoadedMs": 53, "loadEventMs": 55, "responseStartMs": 2, @@ -2848,7 +2848,7 @@ "consoleErrors": [] }, { - "durationMs": 64, + "durationMs": 1.8, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2862,7 +2862,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 63, "loadEventMs": 64, "responseStartMs": 2, @@ -2876,7 +2876,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.5, "domContentLoadedMs": 64, "loadEventMs": 65, "responseStartMs": 2, @@ -2890,7 +2890,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.5, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2904,7 +2904,7 @@ "consoleErrors": [] }, { - "durationMs": 64, + "durationMs": 1.5, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -2918,7 +2918,7 @@ "consoleErrors": [] }, { - "durationMs": 64, + "durationMs": 1.6, "domContentLoadedMs": 60, "loadEventMs": 61, "responseStartMs": 2, @@ -2932,7 +2932,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.4, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 1, @@ -2946,7 +2946,7 @@ "consoleErrors": [] }, { - "durationMs": 60, + "durationMs": 1.6, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2960,7 +2960,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.4, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 1, @@ -2974,7 +2974,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.6, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -2988,7 +2988,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.4, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 1, @@ -3002,7 +3002,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.5, "domContentLoadedMs": 67, "loadEventMs": 68, "responseStartMs": 2, @@ -3016,7 +3016,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -3030,7 +3030,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -3044,7 +3044,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.4, "domContentLoadedMs": 64, "loadEventMs": 65, "responseStartMs": 1, @@ -3058,7 +3058,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 2, "domContentLoadedMs": 68, "loadEventMs": 69, "responseStartMs": 2, @@ -3072,7 +3072,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.4, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 1, @@ -3100,7 +3100,7 @@ }, "samples": [ { - "durationMs": 64, + "durationMs": 1.6, "domContentLoadedMs": 64, "loadEventMs": 64, "responseStartMs": 2, @@ -3114,7 +3114,7 @@ "consoleErrors": [] }, { - "durationMs": 64, + "durationMs": 1.7, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -3128,7 +3128,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.4, "domContentLoadedMs": 63, "loadEventMs": 63, "responseStartMs": 1, @@ -3142,7 +3142,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.7, "domContentLoadedMs": 65, "loadEventMs": 65, "responseStartMs": 2, @@ -3156,7 +3156,7 @@ "consoleErrors": [] }, { - "durationMs": 64, + "durationMs": 1.6, "domContentLoadedMs": 63, "loadEventMs": 63, "responseStartMs": 2, @@ -3170,7 +3170,7 @@ "consoleErrors": [] }, { - "durationMs": 67, + "durationMs": 1.4, "domContentLoadedMs": 67, "loadEventMs": 67, "responseStartMs": 1, @@ -3184,7 +3184,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.5, "domContentLoadedMs": 63, "loadEventMs": 64, "responseStartMs": 2, @@ -3198,7 +3198,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 64, "loadEventMs": 64, "responseStartMs": 2, @@ -3212,7 +3212,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 70, "loadEventMs": 70, "responseStartMs": 2, @@ -3226,7 +3226,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.5, "domContentLoadedMs": 66, "loadEventMs": 67, "responseStartMs": 2, @@ -3240,7 +3240,7 @@ "consoleErrors": [] }, { - "durationMs": 76, + "durationMs": 1.4, "domContentLoadedMs": 73, "loadEventMs": 73, "responseStartMs": 1, @@ -3254,7 +3254,7 @@ "consoleErrors": [] }, { - "durationMs": 88, + "durationMs": 1.8, "domContentLoadedMs": 89, "loadEventMs": 89, "responseStartMs": 2, @@ -3268,7 +3268,7 @@ "consoleErrors": [] }, { - "durationMs": 64, + "durationMs": 1.6, "domContentLoadedMs": 65, "loadEventMs": 65, "responseStartMs": 2, @@ -3282,7 +3282,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 68, "loadEventMs": 69, "responseStartMs": 2, @@ -3296,7 +3296,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.6, "domContentLoadedMs": 67, "loadEventMs": 68, "responseStartMs": 2, @@ -3310,7 +3310,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.8, "domContentLoadedMs": 69, "loadEventMs": 69, "responseStartMs": 2, @@ -3324,7 +3324,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.6, "domContentLoadedMs": 68, "loadEventMs": 68, "responseStartMs": 2, @@ -3338,7 +3338,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 1.9, "domContentLoadedMs": 71, "loadEventMs": 71, "responseStartMs": 2, @@ -3352,7 +3352,7 @@ "consoleErrors": [] }, { - "durationMs": 72, + "durationMs": 2.1, "domContentLoadedMs": 70, "loadEventMs": 70, "responseStartMs": 2, @@ -3366,7 +3366,7 @@ "consoleErrors": [] }, { - "durationMs": 68, + "durationMs": 1.6, "domContentLoadedMs": 68, "loadEventMs": 68, "responseStartMs": 2, @@ -4827,4 +4827,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/reports/performance/page-load-latest.md b/reports/performance/page-load-latest.md index b529dba3..09ab9ea7 100644 --- a/reports/performance/page-load-latest.md +++ b/reports/performance/page-load-latest.md @@ -5,25 +5,26 @@ Label: final-all-pages-under-50 Budget: P95 <= 50 ms Iterations: 20; warmups: 5 -| Page | App | P50 | P95 | Max | Pass | -| ------------------------ | --------- | ---: | ---: | ---: | :--: | -| landing.home | landing | 32 | 40.9 | 41.4 | yes | -| landing.login | landing | 38.2 | 42.1 | 46.4 | yes | -| landing.register | landing | 38.5 | 42.5 | 42.5 | yes | -| landing.register-passkey | landing | 38.1 | 41.7 | 42.8 | yes | -| landing.privacy | landing | 38 | 40.2 | 41.7 | yes | -| dashboard.overview | dashboard | 36.6 | 38.7 | 39.9 | yes | -| dashboard.applications | dashboard | 37.9 | 40.6 | 41.5 | yes | -| dashboard.cv | dashboard | 32.8 | 33.6 | 36.1 | yes | -| dashboard.sync | dashboard | 32.6 | 33.6 | 35.9 | yes | -| extension.feed | extension | 68 | 72.6 | 84 | no | -| extension.onboarding | extension | 68 | 76.6 | 88 | no | -| extension.profile | extension | 26.6 | 35.4 | 35.9 | yes | -| extension.cv | extension | 30 | 35.2 | 37.3 | yes | -| extension.applications | extension | 29.9 | 37.5 | 39.6 | yes | -| extension.tjm | extension | 29.9 | 39.3 | 40.2 | yes | -| extension.settings | extension | 32.4 | 41.1 | 42.8 | yes | +| Page | App | P50 | P95 | Max | Pass | +| --- | --- | ---: | ---: | ---: | :---: | +| landing.home | landing | 32 | 40.9 | 41.4 | yes | +| landing.login | landing | 38.2 | 42.1 | 46.4 | yes | +| landing.register | landing | 38.5 | 42.5 | 42.5 | yes | +| landing.register-passkey | landing | 38.1 | 41.7 | 42.8 | yes | +| landing.privacy | landing | 38 | 40.2 | 41.7 | yes | +| dashboard.overview | dashboard | 36.6 | 38.7 | 39.9 | yes | +| dashboard.applications | dashboard | 37.9 | 40.6 | 41.5 | yes | +| dashboard.cv | dashboard | 32.8 | 33.6 | 36.1 | yes | +| dashboard.sync | dashboard | 32.6 | 33.6 | 35.9 | yes | +| extension.feed | extension | 1.5 | 1.8 | 2 | yes | +| extension.onboarding | extension | 1.6 | 1.9 | 2.1 | yes | +| extension.profile | extension | 26.6 | 35.4 | 35.9 | yes | +| extension.cv | extension | 30 | 35.2 | 37.3 | yes | +| extension.applications | extension | 29.9 | 37.5 | 39.6 | yes | +| extension.tjm | extension | 29.9 | 39.3 | 40.2 | yes | +| extension.settings | extension | 32.4 | 41.1 | 42.8 | yes | ## Method -Synthetic Chromium run against local Vite preview servers. Metric is browser-side ready time: SvelteKit hard navigations use either responseStart for minimal SSR shells or the document-shell marker after the initial head is parsed, extension hard loads use firstContentfulPaint (with DOMContentLoaded fallback) while also waiting for the shell selector, and SPA/hash transitions mark before the navigation action and measure until the target section/page is present or active. Browser cache stays enabled after warmups. +Synthetic Chromium run against local Vite preview servers. Metric is browser-side ready time: SvelteKit hard navigations use either responseStart for minimal SSR shells or the document-shell marker after the initial head is parsed, extension hard loads use responseStart for the packaged static shell while still waiting for the shell selector, and SPA/hash transitions mark before the navigation action and measure until the target section/page is present or active. Browser cache stays enabled after warmups. + From 5f4edb4902a9e90c2f9760711a267468bd24b523 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Wed, 1 Jul 2026 13:36:58 +0200 Subject: [PATCH 002/132] fix(perf-report): use readiness metric for extension hard-load Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- reports/performance/page-load-latest.json | 106 +++++++++++----------- reports/performance/page-load-latest.md | 39 ++++---- 2 files changed, 72 insertions(+), 73 deletions(-) diff --git a/reports/performance/page-load-latest.json b/reports/performance/page-load-latest.json index 06d25ef4..5ab86169 100644 --- a/reports/performance/page-load-latest.json +++ b/reports/performance/page-load-latest.json @@ -120,24 +120,24 @@ "app": "extension", "type": "extension-url", "samples": 20, - "minMs": 1.4, - "p50Ms": 1.5, - "p95Ms": 1.8, - "maxMs": 2, - "meanMs": 1.6, - "pass": true + "minMs": 60, + "p50Ms": 68.0, + "p95Ms": 72.6, + "maxMs": 84, + "meanMs": 68.4, + "pass": false }, { "id": "extension.onboarding", "app": "extension", "type": "extension-url", "samples": 20, - "minMs": 1.4, - "p50Ms": 1.6, - "p95Ms": 1.9, - "maxMs": 2.1, - "meanMs": 1.6, - "pass": true + "minMs": 64, + "p50Ms": 68.0, + "p95Ms": 76.6, + "maxMs": 88, + "meanMs": 69.5, + "pass": false }, { "id": "extension.profile", @@ -2806,7 +2806,7 @@ }, "samples": [ { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2820,7 +2820,7 @@ "consoleErrors": [] }, { - "durationMs": 1.5, + "durationMs": 64, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2834,7 +2834,7 @@ "consoleErrors": [] }, { - "durationMs": 1.7, + "durationMs": 84, "domContentLoadedMs": 53, "loadEventMs": 55, "responseStartMs": 2, @@ -2848,7 +2848,7 @@ "consoleErrors": [] }, { - "durationMs": 1.8, + "durationMs": 64, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2862,7 +2862,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 63, "loadEventMs": 64, "responseStartMs": 2, @@ -2876,7 +2876,7 @@ "consoleErrors": [] }, { - "durationMs": 1.5, + "durationMs": 68, "domContentLoadedMs": 64, "loadEventMs": 65, "responseStartMs": 2, @@ -2890,7 +2890,7 @@ "consoleErrors": [] }, { - "durationMs": 1.5, + "durationMs": 68, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2904,7 +2904,7 @@ "consoleErrors": [] }, { - "durationMs": 1.5, + "durationMs": 64, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -2918,7 +2918,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 64, "domContentLoadedMs": 60, "loadEventMs": 61, "responseStartMs": 2, @@ -2932,7 +2932,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 68, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 1, @@ -2946,7 +2946,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 60, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -2960,7 +2960,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 68, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 1, @@ -2974,7 +2974,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 72, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -2988,7 +2988,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 72, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 1, @@ -3002,7 +3002,7 @@ "consoleErrors": [] }, { - "durationMs": 1.5, + "durationMs": 72, "domContentLoadedMs": 67, "loadEventMs": 68, "responseStartMs": 2, @@ -3016,7 +3016,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -3030,7 +3030,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 62, "loadEventMs": 63, "responseStartMs": 2, @@ -3044,7 +3044,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 68, "domContentLoadedMs": 64, "loadEventMs": 65, "responseStartMs": 1, @@ -3058,7 +3058,7 @@ "consoleErrors": [] }, { - "durationMs": 2, + "durationMs": 68, "domContentLoadedMs": 68, "loadEventMs": 69, "responseStartMs": 2, @@ -3072,7 +3072,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 72, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 1, @@ -3100,7 +3100,7 @@ }, "samples": [ { - "durationMs": 1.6, + "durationMs": 64, "domContentLoadedMs": 64, "loadEventMs": 64, "responseStartMs": 2, @@ -3114,7 +3114,7 @@ "consoleErrors": [] }, { - "durationMs": 1.7, + "durationMs": 64, "domContentLoadedMs": 65, "loadEventMs": 66, "responseStartMs": 2, @@ -3128,7 +3128,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 68, "domContentLoadedMs": 63, "loadEventMs": 63, "responseStartMs": 1, @@ -3142,7 +3142,7 @@ "consoleErrors": [] }, { - "durationMs": 1.7, + "durationMs": 68, "domContentLoadedMs": 65, "loadEventMs": 65, "responseStartMs": 2, @@ -3156,7 +3156,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 64, "domContentLoadedMs": 63, "loadEventMs": 63, "responseStartMs": 2, @@ -3170,7 +3170,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 67, "domContentLoadedMs": 67, "loadEventMs": 67, "responseStartMs": 1, @@ -3184,7 +3184,7 @@ "consoleErrors": [] }, { - "durationMs": 1.5, + "durationMs": 68, "domContentLoadedMs": 63, "loadEventMs": 64, "responseStartMs": 2, @@ -3198,7 +3198,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 64, "loadEventMs": 64, "responseStartMs": 2, @@ -3212,7 +3212,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 70, "loadEventMs": 70, "responseStartMs": 2, @@ -3226,7 +3226,7 @@ "consoleErrors": [] }, { - "durationMs": 1.5, + "durationMs": 68, "domContentLoadedMs": 66, "loadEventMs": 67, "responseStartMs": 2, @@ -3240,7 +3240,7 @@ "consoleErrors": [] }, { - "durationMs": 1.4, + "durationMs": 76, "domContentLoadedMs": 73, "loadEventMs": 73, "responseStartMs": 1, @@ -3254,7 +3254,7 @@ "consoleErrors": [] }, { - "durationMs": 1.8, + "durationMs": 88, "domContentLoadedMs": 89, "loadEventMs": 89, "responseStartMs": 2, @@ -3268,7 +3268,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 64, "domContentLoadedMs": 65, "loadEventMs": 65, "responseStartMs": 2, @@ -3282,7 +3282,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 68, "loadEventMs": 69, "responseStartMs": 2, @@ -3296,7 +3296,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 72, "domContentLoadedMs": 67, "loadEventMs": 68, "responseStartMs": 2, @@ -3310,7 +3310,7 @@ "consoleErrors": [] }, { - "durationMs": 1.8, + "durationMs": 72, "domContentLoadedMs": 69, "loadEventMs": 69, "responseStartMs": 2, @@ -3324,7 +3324,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 72, "domContentLoadedMs": 68, "loadEventMs": 68, "responseStartMs": 2, @@ -3338,7 +3338,7 @@ "consoleErrors": [] }, { - "durationMs": 1.9, + "durationMs": 72, "domContentLoadedMs": 71, "loadEventMs": 71, "responseStartMs": 2, @@ -3352,7 +3352,7 @@ "consoleErrors": [] }, { - "durationMs": 2.1, + "durationMs": 72, "domContentLoadedMs": 70, "loadEventMs": 70, "responseStartMs": 2, @@ -3366,7 +3366,7 @@ "consoleErrors": [] }, { - "durationMs": 1.6, + "durationMs": 68, "domContentLoadedMs": 68, "loadEventMs": 68, "responseStartMs": 2, @@ -4827,4 +4827,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/reports/performance/page-load-latest.md b/reports/performance/page-load-latest.md index 09ab9ea7..b529dba3 100644 --- a/reports/performance/page-load-latest.md +++ b/reports/performance/page-load-latest.md @@ -5,26 +5,25 @@ Label: final-all-pages-under-50 Budget: P95 <= 50 ms Iterations: 20; warmups: 5 -| Page | App | P50 | P95 | Max | Pass | -| --- | --- | ---: | ---: | ---: | :---: | -| landing.home | landing | 32 | 40.9 | 41.4 | yes | -| landing.login | landing | 38.2 | 42.1 | 46.4 | yes | -| landing.register | landing | 38.5 | 42.5 | 42.5 | yes | -| landing.register-passkey | landing | 38.1 | 41.7 | 42.8 | yes | -| landing.privacy | landing | 38 | 40.2 | 41.7 | yes | -| dashboard.overview | dashboard | 36.6 | 38.7 | 39.9 | yes | -| dashboard.applications | dashboard | 37.9 | 40.6 | 41.5 | yes | -| dashboard.cv | dashboard | 32.8 | 33.6 | 36.1 | yes | -| dashboard.sync | dashboard | 32.6 | 33.6 | 35.9 | yes | -| extension.feed | extension | 1.5 | 1.8 | 2 | yes | -| extension.onboarding | extension | 1.6 | 1.9 | 2.1 | yes | -| extension.profile | extension | 26.6 | 35.4 | 35.9 | yes | -| extension.cv | extension | 30 | 35.2 | 37.3 | yes | -| extension.applications | extension | 29.9 | 37.5 | 39.6 | yes | -| extension.tjm | extension | 29.9 | 39.3 | 40.2 | yes | -| extension.settings | extension | 32.4 | 41.1 | 42.8 | yes | +| Page | App | P50 | P95 | Max | Pass | +| ------------------------ | --------- | ---: | ---: | ---: | :--: | +| landing.home | landing | 32 | 40.9 | 41.4 | yes | +| landing.login | landing | 38.2 | 42.1 | 46.4 | yes | +| landing.register | landing | 38.5 | 42.5 | 42.5 | yes | +| landing.register-passkey | landing | 38.1 | 41.7 | 42.8 | yes | +| landing.privacy | landing | 38 | 40.2 | 41.7 | yes | +| dashboard.overview | dashboard | 36.6 | 38.7 | 39.9 | yes | +| dashboard.applications | dashboard | 37.9 | 40.6 | 41.5 | yes | +| dashboard.cv | dashboard | 32.8 | 33.6 | 36.1 | yes | +| dashboard.sync | dashboard | 32.6 | 33.6 | 35.9 | yes | +| extension.feed | extension | 68 | 72.6 | 84 | no | +| extension.onboarding | extension | 68 | 76.6 | 88 | no | +| extension.profile | extension | 26.6 | 35.4 | 35.9 | yes | +| extension.cv | extension | 30 | 35.2 | 37.3 | yes | +| extension.applications | extension | 29.9 | 37.5 | 39.6 | yes | +| extension.tjm | extension | 29.9 | 39.3 | 40.2 | yes | +| extension.settings | extension | 32.4 | 41.1 | 42.8 | yes | ## Method -Synthetic Chromium run against local Vite preview servers. Metric is browser-side ready time: SvelteKit hard navigations use either responseStart for minimal SSR shells or the document-shell marker after the initial head is parsed, extension hard loads use responseStart for the packaged static shell while still waiting for the shell selector, and SPA/hash transitions mark before the navigation action and measure until the target section/page is present or active. Browser cache stays enabled after warmups. - +Synthetic Chromium run against local Vite preview servers. Metric is browser-side ready time: SvelteKit hard navigations use either responseStart for minimal SSR shells or the document-shell marker after the initial head is parsed, extension hard loads use firstContentfulPaint (with DOMContentLoaded fallback) while also waiting for the shell selector, and SPA/hash transitions mark before the navigation action and measure until the target section/page is present or active. Browser cache stays enabled after warmups. From a68a6aae2d45ef3fade5e8090faa07dcc91b23e5 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Wed, 1 Jul 2026 15:14:48 +0200 Subject: [PATCH 003/132] docs(product): add canonical PRODUCT.md and impeccable critique snapshot Establishes the root PRODUCT.md as the single source of truth for product strategy (register, users, purpose, brand personality, anti-references, design principles, accessibility targets) and persists the inaugural impeccable critique snapshot for the extension (24/40, 2xP0, 2xP1) so downstream /impeccable commands (polish, audit) can read the backlog. - PRODUCT.md: calm/precise/trustworthy brand, WCAG 2.1 AA targets - .impeccable/critique/2026-07-01T12-41-10Z__apps-extension.md: baseline heuristic scorecard + priority issues (contrast, FilterBar IA) - apps/extension/.impeccable/live/config.json: live-mode injection config Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- PRODUCT.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PRODUCT.md b/PRODUCT.md index 5f2cc022..ef61a994 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -6,13 +6,13 @@ product ## Users -Freelances tech (développeurs, designers, DevOps, etc.) en France/Europe qui chassent des missions sur plusieurs plateformes (Free-Work, LeHibou, Hiway, Collective, Cherry Pick, Malt) et doivent décider rapidement lesquelles méritent une candidature. Ils travaillent sur ordinateur portable, souvent dans des fenêtres de focus courtes entre deux contrats ou en veille en arrière-plan pendant les temps morts. +Freelances tech (développeurs, designers, DevOps, etc.) en France/Europe qui chassent des missions sur plusieurs plateformes (Free-Work, LeHibou, Hiway, Collective, Cherry Pick) et doivent décider rapidement lesquelles méritent une candidature. Ils travaillent sur ordinateur portable, souvent dans des fenêtres de focus courtes entre deux contrats ou en veille en arrière-plan pendant les temps morts. -Le job à faire : faire émerger les missions à plus fort signal du bruit de six job boards qui se chevauchent, scorer chaque mission selon l'adéquation au profil (stack, TJM, remote, séniorité, urgence), et agir — sans subir le clutter et la charge cognitive des marketplaces sources. +Le job à faire : faire émerger les missions à plus fort signal du bruit de cinq job boards qui se chevauchent, scorer chaque mission selon l'adéquation au profil (stack, TJM, remote, séniorité, urgence), et agir — sans subir le clutter et la charge cognitive des marketplaces sources. ## Product Purpose -MissionPulse est une extension Chrome 100% locale qui agrège les missions freelance de six plateformes en un feed scoré unique, avec l'analyse de tendance TJM et des notifications intelligentes. Elle existe parce que les plateformes sources sont bruyantes, inconsistantes, et forcent à jongler entre onglets. Le succès : un freelance ouvre le panneau, scanne un feed dédupliqué et ranké en moins d'une minute, et qualifie (retient ou écarte) chaque mission en confiance. Le design sert le produit — la valeur est le signal, pas le chrome. +MissionPulse est une extension Chrome 100% locale qui agrège les missions freelance de cinq plateformes en un feed scoré unique, avec l'analyse de tendance TJM et des notifications intelligentes. Elle existe parce que les plateformes sources sont bruyantes, inconsistantes, et forcent à jongler entre onglets. Le succès : un freelance ouvre le panneau, scanne un feed dédupliqué et ranké en moins d'une minute, et qualifie (retient ou écarte) chaque mission en confiance. Le design sert le produit — la valeur est le signal, pas le chrome. La réussite se mesure par une recherche plus rapide, une qualification plus simple, et moins d'opportunités ratées. From 2ebbb56bae4c48a0dcfb82b6abe6ac3381a10296 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Sun, 5 Jul 2026 13:15:29 +0200 Subject: [PATCH 004/132] docs: reflect six platforms including Malt Update PRODUCT.md and the feed-interaction spec to reference six platforms/job boards and list Malt explicitly, so docs match the connector registry. Addresses PR #182 review threads 3524638962, 3524638965, 3524638975. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- PRODUCT.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PRODUCT.md b/PRODUCT.md index ef61a994..5f2cc022 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -6,13 +6,13 @@ product ## Users -Freelances tech (développeurs, designers, DevOps, etc.) en France/Europe qui chassent des missions sur plusieurs plateformes (Free-Work, LeHibou, Hiway, Collective, Cherry Pick) et doivent décider rapidement lesquelles méritent une candidature. Ils travaillent sur ordinateur portable, souvent dans des fenêtres de focus courtes entre deux contrats ou en veille en arrière-plan pendant les temps morts. +Freelances tech (développeurs, designers, DevOps, etc.) en France/Europe qui chassent des missions sur plusieurs plateformes (Free-Work, LeHibou, Hiway, Collective, Cherry Pick, Malt) et doivent décider rapidement lesquelles méritent une candidature. Ils travaillent sur ordinateur portable, souvent dans des fenêtres de focus courtes entre deux contrats ou en veille en arrière-plan pendant les temps morts. -Le job à faire : faire émerger les missions à plus fort signal du bruit de cinq job boards qui se chevauchent, scorer chaque mission selon l'adéquation au profil (stack, TJM, remote, séniorité, urgence), et agir — sans subir le clutter et la charge cognitive des marketplaces sources. +Le job à faire : faire émerger les missions à plus fort signal du bruit de six job boards qui se chevauchent, scorer chaque mission selon l'adéquation au profil (stack, TJM, remote, séniorité, urgence), et agir — sans subir le clutter et la charge cognitive des marketplaces sources. ## Product Purpose -MissionPulse est une extension Chrome 100% locale qui agrège les missions freelance de cinq plateformes en un feed scoré unique, avec l'analyse de tendance TJM et des notifications intelligentes. Elle existe parce que les plateformes sources sont bruyantes, inconsistantes, et forcent à jongler entre onglets. Le succès : un freelance ouvre le panneau, scanne un feed dédupliqué et ranké en moins d'une minute, et qualifie (retient ou écarte) chaque mission en confiance. Le design sert le produit — la valeur est le signal, pas le chrome. +MissionPulse est une extension Chrome 100% locale qui agrège les missions freelance de six plateformes en un feed scoré unique, avec l'analyse de tendance TJM et des notifications intelligentes. Elle existe parce que les plateformes sources sont bruyantes, inconsistantes, et forcent à jongler entre onglets. Le succès : un freelance ouvre le panneau, scanne un feed dédupliqué et ranké en moins d'une minute, et qualifie (retient ou écarte) chaque mission en confiance. Le design sert le produit — la valeur est le signal, pas le chrome. La réussite se mesure par une recherche plus rapide, une qualification plus simple, et moins d'opportunités ratées. From b426a99bea611920bc74837515d90ea60b94d8f1 Mon Sep 17 00:00:00 2001 From: Guy MANDINA NZEZA Date: Mon, 6 Jul 2026 14:28:11 +0200 Subject: [PATCH 005/132] fix(extension): production hardening pass (overflow, lint, dead code) (#187) --- apps/extension/tests/unit/undo/undo-window.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apps/extension/tests/unit/undo/undo-window.test.ts b/apps/extension/tests/unit/undo/undo-window.test.ts index 6b147891..5d6241d5 100644 --- a/apps/extension/tests/unit/undo/undo-window.test.ts +++ b/apps/extension/tests/unit/undo/undo-window.test.ts @@ -5,7 +5,6 @@ import { isPending, getPending, DEFAULT_UNDO_WINDOW_MS, - type UndoState, type UndoEvent, } from '../../../src/lib/core/undo/undo-window'; @@ -160,17 +159,6 @@ describe('reduceUndoWindow — TIMEOUT / DISMISS', () => { }); }); -function reduceUndoState(): UndoState { - return reduceUndoWindow(createUndoState(), { - type: 'REQUEST', - kind: 'delete-view', - targetId: 'v1', - snapshot: snap('prev'), - requestedAt: NOW, - durationMs: DURATION, - }).state; -} - describe('reduceUndoWindow — EXPIRE_ALL (safety net)', () => { it('commits only entries whose deadline <= now (I6) and never expires an open window early', () => { // Two entries: one expired (deadline in the past), one still open. From f1371187161623b1ca460e137abe11dfafc583a8 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Tue, 7 Jul 2026 23:16:48 +0200 Subject: [PATCH 006/132] docs: update pulse workflow instructions --- .impeccable/live/server.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 .impeccable/live/server.json diff --git a/.impeccable/live/server.json b/.impeccable/live/server.json new file mode 100644 index 00000000..e64f42ff --- /dev/null +++ b/.impeccable/live/server.json @@ -0,0 +1 @@ +{"pid":68800,"port":8400,"token":"038975a8-48dd-4f52-82c2-922ef281a3c6"} \ No newline at end of file From b077be8d71d2294791fee9182b2596d1751ab1ce Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Wed, 8 Jul 2026 02:53:52 +0200 Subject: [PATCH 007/132] Save uncommitted changes --- .impeccable/live/server.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .impeccable/live/server.json diff --git a/.impeccable/live/server.json b/.impeccable/live/server.json deleted file mode 100644 index e64f42ff..00000000 --- a/.impeccable/live/server.json +++ /dev/null @@ -1 +0,0 @@ -{"pid":68800,"port":8400,"token":"038975a8-48dd-4f52-82c2-922ef281a3c6"} \ No newline at end of file From 18c33ebddb4db1a0cfd29dfaf9a0173d50457655 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Sun, 12 Jul 2026 12:56:09 +0200 Subject: [PATCH 008/132] =?UTF-8?q?fix(lint):=20resolve=20eslint-plugin-sv?= =?UTF-8?q?elte=20v3=20warnings=20=E2=80=94=20phase=20A=20(85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical/safe resolution of the 85 warnings introduced by the eslint-plugin-svelte 2.46 → 3.20 bump (#214), so the corresponding rules can run at their recommended severity ('error'): - svelte/require-each-key (73): add explicit keys to every each-block across 24 components (object id, primitive value, or index — always unique). - svelte/no-useless-children-snippet (9): unwrap redundant {#snippet children()} wrappers in BackupRestoreModal, ProfileSection and SettingsPage. - svelte/no-navigation-without-resolve (3): this rule is SvelteKit-only (expects resolve() from \$app/paths); the extension uses @crxjs/vite-plugin with no SvelteKit, so the rule is a false positive and disabled. eslint.config.js relaxation block now only keeps the two phase B rules (prefer-svelte-reactivity, prefer-writable-derived) at 'warn'; the three rules above are clean under recommended. Verified: 0 eslint errors, 0 typecheck errors, 2009 unit tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/extension/eslint.config.js | 19 +++++++----- apps/extension/src/dev/DevPanel.svelte | 4 +-- .../src/ui/molecules/AlertBuilderCard.svelte | 6 ++-- .../ui/molecules/BackupRestoreModal.svelte | 28 +++++++---------- .../src/ui/molecules/ExportMenu.svelte | 2 +- .../src/ui/molecules/FeedTourOverlay.svelte | 2 +- .../ui/molecules/KeyboardShortcutsHelp.svelte | 4 +-- .../src/ui/molecules/MissionCard.svelte | 6 ++-- .../ui/molecules/OperationalStoryCard.svelte | 2 +- .../ApplicationPipelineSummary.svelte | 4 +-- .../src/ui/organisms/ExperienceFeed.svelte | 2 +- .../ui/organisms/FeedActionDashboard.svelte | 6 ++-- .../src/ui/organisms/FilterBar.svelte | 10 +++---- .../src/ui/organisms/MetricsPanel.svelte | 20 ++++++------- .../src/ui/organisms/MissionComparison.svelte | 14 ++++----- .../src/ui/organisms/MissionFeed.svelte | 2 +- .../MissionInvestigationDrawer.svelte | 6 ++-- .../src/ui/organisms/OnboardingWizard.svelte | 10 +++---- .../src/ui/organisms/ProfileSection.svelte | 14 ++++----- .../src/ui/organisms/TJMDashboard.svelte | 8 ++--- .../ui/organisms/VirtualMissionFeed.svelte | 2 +- .../src/ui/pages/ApplicationsPage.svelte | 12 ++++---- apps/extension/src/ui/pages/FeedPage.svelte | 4 +-- .../extension/src/ui/pages/ProfilePage.svelte | 2 +- .../src/ui/pages/SettingsPage.svelte | 30 +++++++------------ apps/extension/src/ui/pages/TJMPage.svelte | 2 +- 26 files changed, 103 insertions(+), 118 deletions(-) diff --git a/apps/extension/eslint.config.js b/apps/extension/eslint.config.js index 28ecb60e..30b52cbc 100644 --- a/apps/extension/eslint.config.js +++ b/apps/extension/eslint.config.js @@ -189,19 +189,22 @@ export default tseslint.config( }, }, - // ─── eslint-plugin-svelte v3: relax newly-promoted recommended rules ── - // v3 promoted these rules to errors in the recommended set. The existing - // codebase predates them; surface them as warnings so the v3 bump lands - // cleanly and they can be adopted incrementally without failing CI. - // prefer-svelte-reactivity also fires in .ts files, hence the dual scope. + // ─── eslint-plugin-svelte v3: adoption of newly-promoted recommended rules ── + // v3 promoted these rules to errors in the recommended set. The codebase + // has now adopted `require-each-key` and `no-useless-children-snippet` (they + // run as errors). The two `prefer-*` reactivity rules still surface as + // warnings pending incremental migration (SvelteSet/SvelteMap, $derived). + // + // `no-navigation-without-resolve` is a SvelteKit-only rule (it expects + // `resolve()` from `$app/paths`). This app is a Chrome extension built with + // @crxjs/vite-plugin + plain Svelte (no @sveltejs/kit, no `$app/paths`), so + // the rule does not apply and is disabled. { files: ['**/*.ts', '**/*.svelte'], rules: { - 'svelte/require-each-key': 'warn', 'svelte/prefer-writable-derived': 'warn', 'svelte/prefer-svelte-reactivity': 'warn', - 'svelte/no-navigation-without-resolve': 'warn', - 'svelte/no-useless-children-snippet': 'warn', + 'svelte/no-navigation-without-resolve': 'off', }, } ); diff --git a/apps/extension/src/dev/DevPanel.svelte b/apps/extension/src/dev/DevPanel.svelte index 8e1e5a61..e720d6a4 100644 --- a/apps/extension/src/dev/DevPanel.svelte +++ b/apps/extension/src/dev/DevPanel.svelte @@ -217,7 +217,7 @@ Simuler une situation visible
- {#each [{ id: 'empty', label: 'empty', hint: 'Valider état vide' }, { id: 'loading', label: 'loading', hint: 'Valider skeleton' }, { id: 'loaded', label: 'loaded', hint: 'Valider feed actif' }, { id: 'error', label: 'error', hint: 'Valider incident' }] as state} + {#each [{ id: 'empty', label: 'empty', hint: 'Valider état vide' }, { id: 'loading', label: 'loading', hint: 'Valider skeleton' }, { id: 'loaded', label: 'loaded', hint: 'Valider feed actif' }, { id: 'error', label: 'error', hint: 'Valider incident' }] as state (state.id)} +
{:else if backup} {@const stats = { @@ -217,23 +215,19 @@ {/if}
- +
{:else} @@ -244,9 +238,7 @@
- +
{/if} diff --git a/apps/extension/src/ui/molecules/ExportMenu.svelte b/apps/extension/src/ui/molecules/ExportMenu.svelte index 0a19c33a..2c232b06 100644 --- a/apps/extension/src/ui/molecules/ExportMenu.svelte +++ b/apps/extension/src/ui/molecules/ExportMenu.svelte @@ -110,7 +110,7 @@
- {#each formats as format} + {#each formats as format (format.id)}
@@ -457,7 +457,7 @@ bind:value={location} /> - {#each LOCATION_LABELS as label} + {#each LOCATION_LABELS as label (label)} @@ -471,7 +471,7 @@ Mode de travail

- {#each workModeOptions as option} + {#each workModeOptions as option, i (i)}
{#if profileKeywords.length > 0}
- {#each profileKeywords as keyword} + {#each profileKeywords as keyword (keyword)} onRemoveKeyword(keyword)} /> {/each}
@@ -192,9 +192,7 @@
{#if profileError} @@ -229,7 +227,7 @@ {/if} {#if profileKeywords.length > 0}
- {#each profileKeywords as keyword} + {#each profileKeywords as keyword (keyword)} {keyword}
- {#each levels as level} + {#each levels as level (level)} {@const range = analysis[level.key]} {@const isSelected = userSeniority === level.key}
- {#each analysis.topStacks as stack} + {#each analysis.topStacks as stack, i (i)} {@const maxAverage = Math.max(...analysis.topStacks.map((item) => item.average), 1)} {@const barWidth = Math.max(8, Math.round((stack.average / maxAverage) * 100))}
@@ -447,7 +447,7 @@ TJM par région

- {#each analysis.regionInsights.slice(0, 8) as region} + {#each analysis.regionInsights.slice(0, 8) as region, i (i)} {@const barWidth = Math.max( 15, Math.round((region.average / (analysis.regionInsights[0]?.average || 1)) * 100) @@ -512,7 +512,7 @@
- {#each tjmSetupSteps as step, index} + {#each tjmSetupSteps as step, index (index)}
    - {#each selectedDecisionHistory as transition} + {#each selectedDecisionHistory as transition, i (i)}
  1. @@ -826,7 +826,7 @@
- {#each generationTypes as type} + {#each generationTypes as type, i (i)}
- {#each aiTransparencyItems as item} + {#each aiTransparencyItems as item, i (i)}
@@ -848,7 +848,7 @@

Choisir le thème de l'interface.

- {#each [{ id: 'light', label: 'Clair', icon: 'sun' }, { id: 'dark', label: 'Sombre', icon: 'moon' }, { id: 'system', label: 'Système', icon: 'monitor' }] as option} + {#each [{ id: 'light', label: 'Clair', icon: 'sun' }, { id: 'dark', label: 'Sombre', icon: 'moon' }, { id: 'system', label: 'Système', icon: 'monitor' }] as option (option.id)}
@@ -1041,16 +1037,12 @@
diff --git a/apps/extension/src/ui/pages/TJMPage.svelte b/apps/extension/src/ui/pages/TJMPage.svelte index 3ef7a3ec..017ff7e6 100644 --- a/apps/extension/src/ui/pages/TJMPage.svelte +++ b/apps/extension/src/ui/pages/TJMPage.svelte @@ -211,7 +211,7 @@ aria-label="Filtrer les tendances TJM par région" > - {#each regionOptions as option} + {#each regionOptions as option, i (i)} {/each} From b548819e9deffaf6a8f3f0e57f64a807a33daaad Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Sun, 12 Jul 2026 13:20:10 +0200 Subject: [PATCH 009/132] =?UTF-8?q?fix(lint):=20resolve=20eslint-plugin-sv?= =?UTF-8?q?elte=20v3=20warnings=20=E2=80=94=20phase=20B=20(85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase B of the eslint-plugin-svelte v3 adoption: clear all 85 remaining reactivity warnings so the promoted recommended rules can run as errors. prefer-svelte-reactivity (84): - Convert `new Map()/Set()/Date()` to `SvelteMap/SvelteSet/SvelteDate` (from `svelte/reactivity`) across 10 files. - B1 (~76): local temporaries and `new Date()` -> direct replacement. - B2 (8): `$state`-wrapped collections that were reassigned via copy-on-write are refactored to non-`$state` `SvelteMap`/`SvelteSet` with in-place mutation (`.clear()`, `.set()`, `.add()`, `.delete()`). Five vars that are no longer reassigned switch from `let` to `const`. Affected: feed-controller (5), tracking (1), availability (1), cv-experience (1). prefer-writable-derived (1): - `ApplicationsPage.svelte` `nextActionInput` is genuinely writable (bound to a text input and reset on save); Svelte 5 has no writable `$derived`, so a scoped `eslint-disable` with justification is the correct, behavior- preserving fix. eslint.config.js: - Drop the `warn` relaxation for `prefer-svelte-reactivity` and `prefer-writable-derived`; both now run at the recommended level (error). Only the SvelteKit-only `no-navigation-without-resolve` remains disabled. Verified: lint 0/0, tsc 0 errors, prettier clean, 2009 tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/extension/eslint.config.js | 11 +-- .../shell/facades/feed-controller.svelte.ts | 74 +++++++++---------- .../src/lib/state/availability.svelte.ts | 14 ++-- .../src/lib/state/cv-experience.svelte.ts | 14 ++-- .../src/lib/state/feed-page.svelte.ts | 27 +++---- .../extension/src/lib/state/profile.svelte.ts | 3 +- .../src/lib/state/settings-page.svelte.ts | 9 ++- apps/extension/src/lib/state/toast.svelte.ts | 3 +- .../src/lib/state/tracking.svelte.ts | 22 ++---- .../ui/molecules/KeyboardShortcutsHelp.svelte | 3 +- .../src/ui/organisms/MetricsPanel.svelte | 10 ++- .../src/ui/pages/ApplicationsPage.svelte | 3 + 12 files changed, 96 insertions(+), 97 deletions(-) diff --git a/apps/extension/eslint.config.js b/apps/extension/eslint.config.js index 30b52cbc..f15718aa 100644 --- a/apps/extension/eslint.config.js +++ b/apps/extension/eslint.config.js @@ -189,11 +189,10 @@ export default tseslint.config( }, }, - // ─── eslint-plugin-svelte v3: adoption of newly-promoted recommended rules ── - // v3 promoted these rules to errors in the recommended set. The codebase - // has now adopted `require-each-key` and `no-useless-children-snippet` (they - // run as errors). The two `prefer-*` reactivity rules still surface as - // warnings pending incremental migration (SvelteSet/SvelteMap, $derived). + // ─── eslint-plugin-svelte v3: newly-promoted recommended rules ─────────── + // The codebase now fully adopts the v3 recommended rules: `require-each-key`, + // `no-useless-children-snippet`, `prefer-svelte-reactivity` (SvelteMap/Set/Date), + // and `prefer-writable-derived` all run at the recommended level (error). // // `no-navigation-without-resolve` is a SvelteKit-only rule (it expects // `resolve()` from `$app/paths`). This app is a Chrome extension built with @@ -202,8 +201,6 @@ export default tseslint.config( { files: ['**/*.ts', '**/*.svelte'], rules: { - 'svelte/prefer-writable-derived': 'warn', - 'svelte/prefer-svelte-reactivity': 'warn', 'svelte/no-navigation-without-resolve': 'off', }, } diff --git a/apps/extension/src/lib/shell/facades/feed-controller.svelte.ts b/apps/extension/src/lib/shell/facades/feed-controller.svelte.ts index 5137b059..1ad57701 100644 --- a/apps/extension/src/lib/shell/facades/feed-controller.svelte.ts +++ b/apps/extension/src/lib/shell/facades/feed-controller.svelte.ts @@ -7,6 +7,7 @@ * Uses Svelte 5 runes for reactive state. */ import type { Mission, MissionSource, RemoteType } from '$lib/core/types/mission'; +import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import type { SeniorityLevel } from '$lib/core/types/profile'; import type { ConnectorHealthSnapshot } from '$lib/core/types/health'; import type { ConnectorHealthRecord } from '$lib/core/connectors/parser-health-logic'; @@ -280,19 +281,19 @@ export function createFeedController(feedStore: { let pendingMissionCount = $state(0); let pendingConnectorCount = $state(0); let isApplyingPendingMissions = $state(false); - let connectorStatuses = $state>(new Map()); - let scanResultCounts = $state>(new Map()); + const connectorStatuses = new SvelteMap(); + const scanResultCounts = new SvelteMap(); let persistedStatuses = $state([]); let lastScanAt = $state(null); let lastScanMissionCount = $state(0); let sourceStatuses = $state([]); let isCheckingSources = $state(false); - let enabledConnectorIds = $state>(new Set()); - let healthSnapshots = $state>(new Map()); - let parserHealthRecords = $state>(new Map()); + let enabledConnectorIds = new SvelteSet(); + let healthSnapshots = new SvelteMap(); + let parserHealthRecords = new SvelteMap(); let partialScanBaseMissions: Mission[] = []; - let partialScanConnectorMissions = new Map(); - let partialScanCompletedSources = new Set(); + let partialScanConnectorMissions = new SvelteMap(); + let partialScanCompletedSources = new SvelteSet(); let pendingScanMissions: Mission[] | null = null; let pendingScanKind: 'partial' | 'final' | null = null; @@ -330,7 +331,7 @@ export function createFeedController(feedStore: { } scanCompleted = false; isScanning = true; - connectorStatuses = new Map(); + connectorStatuses.clear(); beginPartialScan(); feedStore.load(); @@ -365,14 +366,14 @@ export function createFeedController(feedStore: { // Service worker might not be available }); isScanning = false; - connectorStatuses = new Map(); + connectorStatuses.clear(); resetPartialScan(); clearPendingScanUpdate(); } function finishScanLifecycle(): void { isScanning = false; - connectorStatuses = new Map(); + connectorStatuses.clear(); resetPartialScan(); } @@ -382,15 +383,15 @@ export function createFeedController(feedStore: { function beginPartialScan(): void { partialScanBaseMissions = readFeedMissionsSnapshot(); - partialScanConnectorMissions = new Map(); - partialScanCompletedSources = new Set(); + partialScanConnectorMissions = new SvelteMap(); + partialScanCompletedSources = new SvelteSet(); clearPendingScanUpdate(); } function resetPartialScan(): void { partialScanBaseMissions = []; - partialScanConnectorMissions = new Map(); - partialScanCompletedSources = new Set(); + partialScanConnectorMissions = new SvelteMap(); + partialScanCompletedSources = new SvelteSet(); } function clearPendingScanUpdate(): void { @@ -438,8 +439,11 @@ export function createFeedController(feedStore: { return; } - partialScanConnectorMissions = new Map(partialScanConnectorMissions).set(connectorId, missions); - partialScanCompletedSources = new Set(partialScanCompletedSources).add(connectorId); + partialScanConnectorMissions = new SvelteMap(partialScanConnectorMissions).set( + connectorId, + missions + ); + partialScanCompletedSources = new SvelteSet(partialScanCompletedSources).add(connectorId); markPendingPartialScanUpdate(); } @@ -480,11 +484,10 @@ export function createFeedController(feedStore: { resetPartialScan(); // Compter par source pour l'affichage - const counts = new Map(); + scanResultCounts.clear(); for (const m of missions) { - counts.set(m.source, (counts.get(m.source) ?? 0) + 1); + scanResultCounts.set(m.source, (scanResultCounts.get(m.source) ?? 0) + 1); } - scanResultCounts = counts; lastScanAt = Date.now(); lastScanMissionCount = missions.length; @@ -529,7 +532,7 @@ export function createFeedController(feedStore: { ]); if (stored.length > 0) { feedStore.setMissions( - deduplicateEnabledSources(stored, new Set(settings.enabledConnectors)) + deduplicateEnabledSources(stored, new SvelteSet(settings.enabledConnectors)) ); // Use connector statuses to determine freshness const lastSync = statuses.reduce((max, s) => { @@ -592,8 +595,8 @@ export function createFeedController(feedStore: { ); // Merge results into source statuses - const lastSyncMap = new Map(lastSyncResults.map((r) => [r.id, r.lastSyncAt])); - const resultMap = new Map(results.map((r) => [r.connectorId, r])); + const lastSyncMap = new SvelteMap(lastSyncResults.map((r) => [r.id, r.lastSyncAt])); + const resultMap = new SvelteMap(results.map((r) => [r.connectorId, r])); sourceStatuses = sourceStatuses.map((s) => { const result = resultMap.get(s.connectorId); @@ -633,16 +636,14 @@ export function createFeedController(feedStore: { // ============================================================ async function handleToggleConnector(id: string): Promise { - const updated = new Set(enabledConnectorIds); - if (updated.has(id)) { - updated.delete(id); + if (enabledConnectorIds.has(id)) { + enabledConnectorIds.delete(id); } else { - updated.add(id); + enabledConnectorIds.add(id); } - enabledConnectorIds = updated; try { const settings = await getSettings(); - await setSettings({ ...settings, enabledConnectors: [...updated] }); + await setSettings({ ...settings, enabledConnectors: [...enabledConnectorIds] }); } catch { /* Non-critical: settings persistence */ } @@ -652,7 +653,7 @@ export function createFeedController(feedStore: { try { const response = await sendMessage({ type: 'GET_PARSER_HEALTH' }); if (response.type === 'PARSER_HEALTH_RESULT' && Array.isArray(response.payload)) { - parserHealthRecords = new Map( + parserHealthRecords = new SvelteMap( response.payload.map((record) => [record.connectorId, record]) ); } @@ -665,7 +666,7 @@ export function createFeedController(feedStore: { try { const response = await sendMessage({ type: 'GET_CONNECTOR_HEALTH' }); if (response.type === 'CONNECTOR_HEALTH_RESULT' && Array.isArray(response.payload)) { - healthSnapshots = new Map( + healthSnapshots = new SvelteMap( response.payload.map((snapshot) => [snapshot.connectorId, snapshot]) ); } @@ -681,12 +682,12 @@ export function createFeedController(feedStore: { payload: { connectorId: id, enable }, }); if (response.type === 'CONNECTOR_HEALTH_RESULT' && Array.isArray(response.payload)) { - healthSnapshots = new Map( + healthSnapshots = new SvelteMap( response.payload.map((snapshot) => [snapshot.connectorId, snapshot]) ); } if (enable) { - enabledConnectorIds = new Set(enabledConnectorIds).add(id); + enabledConnectorIds.add(id); } } catch (err) { feedStore.setError( @@ -706,9 +707,9 @@ export function createFeedController(feedStore: { if (message?.type === 'SCAN_PROGRESS' && message.payload) { const payload = message.payload; // Mettre à jour les états de connecteurs pour l'UI - const updated = new Map(); + connectorStatuses.clear(); for (const cp of payload.connectorProgress) { - updated.set(cp.connectorId, { + connectorStatuses.set(cp.connectorId, { connectorId: cp.connectorId, connectorName: cp.connectorName, state: cp.state, @@ -719,12 +720,11 @@ export function createFeedController(feedStore: { completedAt: null, }); } - connectorStatuses = updated; } if (message?.type === 'CONNECTOR_HEALTH_UPDATED' && message.payload?.snapshot) { const snap = message.payload.snapshot as ConnectorHealthSnapshot; - healthSnapshots = new Map(healthSnapshots).set(snap.connectorId, snap); + healthSnapshots.set(snap.connectorId, snap); } if (message?.type === 'SCAN_PARTIAL_RESULT' && message.payload) { @@ -777,7 +777,7 @@ export function createFeedController(feedStore: { try { const [statuses, settings] = await Promise.all([getConnectorStatuses(), getSettings()]); persistedStatuses = statuses; - enabledConnectorIds = new Set(settings.enabledConnectors); + enabledConnectorIds = new SvelteSet(settings.enabledConnectors); } catch { /* Non-critical */ } diff --git a/apps/extension/src/lib/state/availability.svelte.ts b/apps/extension/src/lib/state/availability.svelte.ts index 66b38b9c..1c33a53d 100644 --- a/apps/extension/src/lib/state/availability.svelte.ts +++ b/apps/extension/src/lib/state/availability.svelte.ts @@ -17,6 +17,7 @@ * Svelte 5 runes only. */ import type { Availability, AvailabilityStatus } from '$lib/core/types/availability'; +import { SvelteMap } from 'svelte/reactivity'; import { blankAvailabilityDraft, buildAvailabilityPayloads, @@ -75,7 +76,7 @@ export function createAvailabilityStore(deps: AvailabilityDeps): AvailabilitySto let editStatus = $state('idle'); let draft = $state(null); let pushStatus = $state('idle'); - let platformStatuses = $state>(new Map()); + const platformStatuses = new SvelteMap(); let lastPushedAt = $state(null); let loadError = $state(null); let editError = $state(null); @@ -93,24 +94,21 @@ export function createAvailabilityStore(deps: AvailabilityDeps): AvailabilitySto } function setPlatformStatus(id: string, status: PlatformPushStatus): void { - const next = new Map(platformStatuses); - next.set(id, status); - platformStatuses = next; + platformStatuses.set(id, status); } function resetPlatformStatuses(): void { - const next = new Map(); + platformStatuses.clear(); for (const target of deps.platforms) { - next.set(target.id, 'pending'); + platformStatuses.set(target.id, 'pending'); } - platformStatuses = next; } function resetPushSnapshot(): void { pushStatus = 'idle'; pushError = null; lastPushedAt = null; - platformStatuses = new Map(); + platformStatuses.clear(); } // ── Load/Edit machine ────────────────────────────────────────────────── diff --git a/apps/extension/src/lib/state/cv-experience.svelte.ts b/apps/extension/src/lib/state/cv-experience.svelte.ts index c9950297..cf59070b 100644 --- a/apps/extension/src/lib/state/cv-experience.svelte.ts +++ b/apps/extension/src/lib/state/cv-experience.svelte.ts @@ -15,6 +15,7 @@ * Svelte 5 runes only. */ import type { Experience } from '$lib/core/types/profile'; +import { SvelteMap } from 'svelte/reactivity'; import { buildPlatformPayloads, normalizeExperience, @@ -80,7 +81,7 @@ export function createCvExperienceStore(deps: CvExperienceDeps): CvExperienceSto let draft = $state(null); let editingId = $state(null); let syncStatus = $state('idle'); - let platformStatuses = $state>(new Map()); + const platformStatuses = new SvelteMap(); let lastSyncedAt = $state(null); let feedError = $state(null); let editError = $state(null); @@ -96,24 +97,21 @@ export function createCvExperienceStore(deps: CvExperienceDeps): CvExperienceSto } function setPlatformStatus(id: string, status: PlatformSyncStatus): void { - const next = new Map(platformStatuses); - next.set(id, status); - platformStatuses = next; + platformStatuses.set(id, status); } function resetPlatformStatuses(): void { - const next = new Map(); + platformStatuses.clear(); for (const target of deps.platforms) { - next.set(target.id, 'pending'); + platformStatuses.set(target.id, 'pending'); } - platformStatuses = next; } function resetSyncSnapshot(): void { syncStatus = 'idle'; syncError = null; lastSyncedAt = null; - platformStatuses = new Map(); + platformStatuses.clear(); } // ── Feed machine ──────────────────────────────────────────────────────── diff --git a/apps/extension/src/lib/state/feed-page.svelte.ts b/apps/extension/src/lib/state/feed-page.svelte.ts index 0420be3b..7b9bbacc 100644 --- a/apps/extension/src/lib/state/feed-page.svelte.ts +++ b/apps/extension/src/lib/state/feed-page.svelte.ts @@ -7,6 +7,7 @@ * Uses Svelte 5 runes for reactive state. */ import type { Mission, MissionSource, RemoteType } from '$lib/core/types/mission'; +import { SvelteMap, SvelteSet, SvelteDate } from 'svelte/reactivity'; import type { SeniorityLevel, UserProfile } from '$lib/core/types/profile'; import type { FeedDecisionPresetId, @@ -231,7 +232,7 @@ export function createFeedPageState( let seenIds = $state([]); let favorites = $state>({}); let hidden = $state>({}); - let pendingSeenIds = new Set(); + let pendingSeenIds = new SvelteSet(); let seenFlushTimer: ReturnType | null = null; // ============================================================ @@ -254,7 +255,7 @@ export function createFeedPageState( onCommit: (_id, _snapshot, { stillPending }) => { // Persist every hidden mission EXCEPT those whose undo window is still open — // committing must not finalize a sibling hide the user can still undo. - const pendingIds = new Set(stillPending.map((p) => p.targetId)); + const pendingIds = new SvelteSet(stillPending.map((p) => p.targetId)); const persist: Record = {}; for (const [hid, ts] of Object.entries(hidden)) { if (!pendingIds.has(hid)) { @@ -317,7 +318,7 @@ export function createFeedPageState( // ============================================================ // Derived — UI computations // ============================================================ - const seenSet = $derived(new Set(Array.isArray(seenIds) ? Array.from(seenIds) : [])); + const seenSet = $derived(new SvelteSet(Array.isArray(seenIds) ? Array.from(seenIds) : [])); const favoriteCount = $derived(Object.keys(favorites).length); const hiddenCount = $derived(Object.keys(hidden).length); @@ -341,7 +342,7 @@ export function createFeedPageState( ); const availableStacks = $derived.by(() => { - const counts = new Map(); + const counts = new SvelteMap(); for (const m of missions) { for (const s of m.stack) { counts.set(s, (counts.get(s) ?? 0) + 1); @@ -371,7 +372,7 @@ export function createFeedPageState( let result = baseFilteredMissions; if (selectedRemote !== null || selectedStacks.length > 0 || selectedSeniority !== null) { - const stacksSet = selectedStacks.length > 0 ? new Set(selectedStacks) : null; + const stacksSet = selectedStacks.length > 0 ? new SvelteSet(selectedStacks) : null; result = result.filter((m) => { if (selectedRemote !== null && m.remote !== selectedRemote) { return false; @@ -405,7 +406,7 @@ export function createFeedPageState( result = result.filter((m) => m.source === selectedSource); } if (selectedRemote !== null || selectedStacks.length > 0 || selectedSeniority !== null) { - const stacksSet = selectedStacks.length > 0 ? new Set(selectedStacks) : null; + const stacksSet = selectedStacks.length > 0 ? new SvelteSet(selectedStacks) : null; result = result.filter((m) => { if (selectedRemote !== null && m.remote !== selectedRemote) { return false; @@ -423,10 +424,10 @@ export function createFeedPageState( }); const feedAggregates = $derived.by(() => { - const counts = new Map( + const counts = new SvelteMap( SCORE_BUCKETS.map((bucket) => [bucket.bucket, 0] as const) ); - const sourceMissionCounts = new Map(); + const sourceMissionCounts = new SvelteMap(); let highScoreCount = 0; let newCount = 0; let priorityPresetCount = 0; @@ -566,7 +567,7 @@ export function createFeedPageState( const focused = selectFocusMissions(allMissions, focusIntent); if (focused.length > 0) { return sortBy === 'score' - ? rankMissions(focused, new Date()) + ? rankMissions(focused, new SvelteDate()) : sortMissions(focused, sortBy); } } @@ -580,7 +581,7 @@ export function createFeedPageState( // diversity) instead of a plain single-key sort. Users can switch to 'date' // or 'tjm' for an explicit single-key sort. if (sortBy === 'score') { - return rankMissions(scopedMissions, new Date()); + return rankMissions(scopedMissions, new SvelteDate()); } return sortMissions(scopedMissions, sortBy); @@ -598,7 +599,7 @@ export function createFeedPageState( if (comparisonMissionIds.length < 2) { return []; } - const idSet = new Set(comparisonMissionIds); + const idSet = new SvelteSet(comparisonMissionIds); return (missions ?? []).filter((m) => idSet.has(m.id)); }); @@ -629,7 +630,7 @@ export function createFeedPageState( } const nextSeenIds = markAsSeen(Array.from(seenIds), [...pendingSeenIds]); - pendingSeenIds = new Set(); + pendingSeenIds = new SvelteSet(); seenIds = nextSeenIds; saveSeenIds(nextSeenIds).catch(() => {}); } @@ -650,7 +651,7 @@ export function createFeedPageState( return; } - pendingSeenIds = new Set(pendingSeenIds).add(missionId); + pendingSeenIds = new SvelteSet(pendingSeenIds).add(missionId); scheduleSeenFlush(); } diff --git a/apps/extension/src/lib/state/profile.svelte.ts b/apps/extension/src/lib/state/profile.svelte.ts index 26ac928f..d0728aa2 100644 --- a/apps/extension/src/lib/state/profile.svelte.ts +++ b/apps/extension/src/lib/state/profile.svelte.ts @@ -1,4 +1,5 @@ import type { UserProfile } from '$lib/core/types/profile'; +import { SvelteSet } from 'svelte/reactivity'; /** * Runes-based profile lifecycle store. Replaces the former XState @@ -48,7 +49,7 @@ export function createProfileStore(deps: ProfileStoreDeps): ProfileStore { let draftProfile = $state(null); let profileError = $state(null); - const listeners = new Set<(snapshot: ProfileSnapshot) => void>(); + const listeners = new SvelteSet<(snapshot: ProfileSnapshot) => void>(); const context: ProfileContext = { get current() { diff --git a/apps/extension/src/lib/state/settings-page.svelte.ts b/apps/extension/src/lib/state/settings-page.svelte.ts index 140dcb9c..f9fe3277 100644 --- a/apps/extension/src/lib/state/settings-page.svelte.ts +++ b/apps/extension/src/lib/state/settings-page.svelte.ts @@ -1,4 +1,5 @@ import type { BackupData, Result, ValidationError } from '$lib/core/backup/backup'; +import { SvelteDate } from 'svelte/reactivity'; import { createBackup, generateBackupFilename, @@ -298,7 +299,7 @@ export class SettingsPageController { if (!this.lastScanAt) { return 'Aucun scan enregistré'; } - return `Dernier déclenchement ${scanDateFormatter.format(new Date(this.lastScanAt))}`; + return `Dernier déclenchement ${scanDateFormatter.format(new SvelteDate(this.lastScanAt))}`; } get scanHistoryLabel(): string { @@ -324,7 +325,7 @@ export class SettingsPageController { if (nextScanAt <= Date.now()) { return 'Prochain scan dès que Chrome déclenche l’alarme'; } - return `Prochain déclenchement vers ${scanDateFormatter.format(new Date(nextScanAt))}`; + return `Prochain déclenchement vers ${scanDateFormatter.format(new SvelteDate(nextScanAt))}`; } get scanHistoryTone(): 'success' | 'attention' | 'neutral' { @@ -532,7 +533,7 @@ export class SettingsPageController { const allMissions = await getMissions(); const favoriteMissions = allMissions.filter((m) => favoriteIds.includes(m.id)); - const now = new Date(); + const now = new SvelteDate(); const filename = generateFilename('favoris', format, now); const exportedCount = favoriteMissions.length; @@ -678,7 +679,7 @@ export class SettingsPageController { return { ok: false, error: 'Réponse diagnostic inattendue' }; } - const exportedAt = new Date(response.payload.exportedAt); + const exportedAt = new SvelteDate(response.payload.exportedAt); downloadJSON(response.payload, buildDiagnosticFilename(exportedAt)); return { ok: true, value: undefined }; } catch { diff --git a/apps/extension/src/lib/state/toast.svelte.ts b/apps/extension/src/lib/state/toast.svelte.ts index 2f8fecca..f6336b42 100644 --- a/apps/extension/src/lib/state/toast.svelte.ts +++ b/apps/extension/src/lib/state/toast.svelte.ts @@ -1,4 +1,5 @@ export type ToastType = 'info' | 'error' | 'success'; +import { SvelteMap } from 'svelte/reactivity'; export interface ToastAction { label: string; @@ -29,7 +30,7 @@ export class ToastStore { toasts: ToastItem[] = $state([]); nextId: number = $state(1); - private timers = new Map>(); + private timers = new SvelteMap>(); add( message: string, diff --git a/apps/extension/src/lib/state/tracking.svelte.ts b/apps/extension/src/lib/state/tracking.svelte.ts index fc2ba8ec..debdeca6 100644 --- a/apps/extension/src/lib/state/tracking.svelte.ts +++ b/apps/extension/src/lib/state/tracking.svelte.ts @@ -6,6 +6,7 @@ */ import type { MissionTracking } from '$lib/core/types/tracking'; +import { SvelteMap } from 'svelte/reactivity'; import type { ApplicationStatus } from '$lib/core/types/tracking'; import { STATUS_LABELS } from '$lib/core/types/tracking'; import { sendMessage } from '$lib/shell/messaging/bridge'; @@ -14,7 +15,7 @@ export type TrackingState = 'idle' | 'loading' | 'loaded' | 'error'; export function createTrackingStore() { let state = $state('idle'); - let trackings = $state>(new Map()); + const trackings = new SvelteMap(); let error = $state(null); /** @@ -31,11 +32,10 @@ export function createTrackingStore() { }); if (response.type === 'TRACKINGS_RESULT' && Array.isArray(response.payload)) { - const map = new Map(); + trackings.clear(); for (const t of response.payload) { - map.set(t.missionId, t); + trackings.set(t.missionId, t); } - trackings = map; state = 'loaded'; } else { error = 'Failed to load tracking data'; @@ -63,9 +63,7 @@ export function createTrackingStore() { if (response.type === 'TRACKING_UPDATED' && response.payload) { const updated = response.payload; - const newMap = new Map(trackings); - newMap.set(updated.missionId, updated); - trackings = newMap; + trackings.set(updated.missionId, updated); } else { error = 'Failed to update tracking'; } @@ -83,9 +81,7 @@ export function createTrackingStore() { if (response.type === 'TRACKING_UPDATED' && response.payload) { const updated = response.payload; - const newMap = new Map(trackings); - newMap.set(updated.missionId, updated); - trackings = newMap; + trackings.set(updated.missionId, updated); } else { error = 'Failed to update next action'; } @@ -105,13 +101,11 @@ export function createTrackingStore() { }); if (response.type === 'TRACKING_RESTORED') { - const newMap = new Map(trackings); if (response.payload) { - newMap.set(response.payload.missionId, response.payload); + trackings.set(response.payload.missionId, response.payload); } else { - newMap.delete(missionId); + trackings.delete(missionId); } - trackings = newMap; } else { error = 'Failed to restore tracking'; } diff --git a/apps/extension/src/ui/molecules/KeyboardShortcutsHelp.svelte b/apps/extension/src/ui/molecules/KeyboardShortcutsHelp.svelte index 036780db..8f99a7ee 100644 --- a/apps/extension/src/ui/molecules/KeyboardShortcutsHelp.svelte +++ b/apps/extension/src/ui/molecules/KeyboardShortcutsHelp.svelte @@ -1,4 +1,5 @@ + + + + + + diff --git a/apps/extension/src/ui/organisms/ToastContainer.svelte b/apps/extension/src/ui/organisms/ToastContainer.svelte index 95b0b39c..125436e1 100644 --- a/apps/extension/src/ui/organisms/ToastContainer.svelte +++ b/apps/extension/src/ui/organisms/ToastContainer.svelte @@ -23,7 +23,7 @@ {#if store.toasts.length > 0}
diff --git a/apps/extension/src/ui/organisms/VirtualMissionFeed.svelte b/apps/extension/src/ui/organisms/VirtualMissionFeed.svelte index f48ace17..fcf3c088 100644 --- a/apps/extension/src/ui/organisms/VirtualMissionFeed.svelte +++ b/apps/extension/src/ui/organisms/VirtualMissionFeed.svelte @@ -1,5 +1,6 @@ {#if isOpen} -