diff --git a/docs/redesign/phase-2/TRACEABILITY.md b/docs/redesign/phase-2/TRACEABILITY.md index 7e2c26c..6acc301 100644 --- a/docs/redesign/phase-2/TRACEABILITY.md +++ b/docs/redesign/phase-2/TRACEABILITY.md @@ -1,5 +1,7 @@ # Redesign traceability +Existing reports destination: [P5-07 inventory, scoped trace and evidence](../phase-5/p5-07/README.md#preservation-and-implementation-decision) maps PRES-S01/S02, E07–E09 and J05/J09 to the shared report owner, authenticated Analytics entry, period/query preservation and existing drill-downs. Dashboard flags/reports remain available; new routes do not imply new metric/provider/production acceptance. + Personal Dashboard: [P5-06 scoped trace, metric definitions and rendered evidence](../phase-5/p5-06/README.md#design-and-preservation-plan) maps PRES-S01/S02, E07–E09, W11/W12 and J05/J09 to retained report/preference/navigation behavior, eight unit regressions and two real-application browser cases. Exact values, independent failures and owner switching have scoped evidence; manual screen-reader acceptance and production evidence remain open. Current Jobs integration: [P5-04b scoped trace and evidence](../phase-5/p5-04b/README.md#preservation-traceability) maps PRES-J03–J07, W01/W02, E07–E09 and S03 to the shared workspace, compatibility adapter, native/unit regressions and rendered evidence. Its containing commit identifies the implementation checkpoint; earlier phase records below remain historical evidence. diff --git a/docs/redesign/phase-5/NAVIGATION_MIGRATION.md b/docs/redesign/phase-5/NAVIGATION_MIGRATION.md index 5191c24..827812e 100644 --- a/docs/redesign/phase-5/NAVIGATION_MIGRATION.md +++ b/docs/redesign/phase-5/NAVIGATION_MIGRATION.md @@ -1,5 +1,7 @@ # Navigation migration +P5-07 successor: `/analytics` is now the authenticated **Insights → Analytics** destination with the retained existing reports. The 31 original patterns below remain, plus this one addition (32 total). Dashboard stays `/dashboard`; its old Analytics breadcrumb is corrected to Home → Dashboard. Analytics uses Home → Insights → Analytics, with the supported 6/12/24-month selection in `?months=`. [Implementation/evidence](p5-07/README.md). No Documents/Admin destination or expanded command catalogue is implied; earlier planning text below remains historical. + Source: current [App](../../../job-tracker-ui/src/App.tsx), [AppShell](../../../job-tracker-ui/src/layout/AppShell.tsx), [Phase 3 route contract](../phase-3/ROUTE_PLAN.md). P5-01 changes navigation presentation; it does not replace the router, providers or page owners. ## P5-01 visible shell contract @@ -45,9 +47,9 @@ Keep existing children visible in shell secondary navigation until their workspa Proposed but absent: **Documents** waits for bounded owner-scoped cross-job metadata reads and package proof (Phase 7/NEW-11); **Analytics** becomes meaningful in P5-07; expanded **Notifications** route is deferred; aggregate **Admin dashboard** needs NEW-08/OD-09. Do not show dead/disabled placeholders or claim a complete Documents index. Existing job Documents/CV links stay reachable. Contextual AI stays in its real domain; no global AI page/panel is added by this shell plan. -## All 31 current patterns retained +## All 31 original patterns retained; one Analytics addition -No compatibility removal date is approved. Each row retains refresh, direct entry, authorization and safe error behavior. Wildcard/fallback infrastructure and inner root redirect also remain but are not counted as pages. New `/analytics` would make **32** patterns in P5-07; `/kanban` remains counted even if later an alias. +No compatibility removal date is approved. Each row retains refresh, direct entry, authorization and safe error behavior. Wildcard/fallback infrastructure and inner root redirect also remain but are not counted as pages. The P5-07 `/analytics` addition makes **32** patterns; `/kanban` remains counted even if later an alias. | Current pattern | P5 destination / compatibility decision | Owner slice | PRES | |---|---|---|---| @@ -61,6 +63,7 @@ No compatibility removal date is approved. Each row retains refresh, direct entr | `/microsoft-legacy-relink` | Existing recovery/relink | 01 regression only | E03 | | `/cv/:slug` | Published-only public page/PDF, never numeric redirect | 09–14 regression | C05, C12 | | `/dashboard` | Home | 06, 07 | S01, S02 | +| `/analytics` (P5-07 addition) | Insights → existing personal reports | 07 | S01, S02, E07–E09, J05/J09 | | `/discover` | Jobs → Discover | 01, 03 link only | J01, J02 | | `/jobs` | Jobs → Tracked; Table default | 03, 04b | J03–J07 | | `/jobs/:id` | Dedicated job workspace | 08a, 08b | W01–W12, M01, M02 | diff --git a/docs/redesign/phase-5/OPEN_BLOCKERS.md b/docs/redesign/phase-5/OPEN_BLOCKERS.md index aa0e521..fbb732f 100644 --- a/docs/redesign/phase-5/OPEN_BLOCKERS.md +++ b/docs/redesign/phase-5/OPEN_BLOCKERS.md @@ -25,8 +25,9 @@ Reconciled against [P4-06](../phase-4/p4-06/README.md#opendeferred-issue-reconci | Creation-intent idempotency | Open independent contract; not B02 identity or stale revision | **Separate backend/product work**; no Add/import/POST-on-mount expansion in Phase 5. Existing create review/no-write navigation must remain | | CV margins/templates/preview/pagination | Open, user concern reiterated during P4; current preview has extra pagination/cropping logic | **Blocks P5-09–14** via dedicated G-CV-RENDER corpus/repair checkpoint; keyboard passing does not prove geometry. No broad renderer replacement inside UI slice | | Table/Board filter/population mismatch | **Read prerequisite resolved in [P5-04b readiness](p5-04b-readiness/README.md)**: shared logical query, complete ID membership, owner/trash/derived filters and mutation refresh proven | [P5-04b integration](p5-04b/README.md) preserves that query/route contract; full-array scaling remains an explicit limitation | -| P3-O01 analytics names/periods/populations | [P5-06](p5-06/README.md) addresses Dashboard labels, cohort/snapshot/sample definitions, exact values and five flags; current server population/date limitations remain documented | Dashboard G-METRICS has scoped regression evidence. **P5-07 must preserve/revalidate these definitions** when adding its destination; backend metric/timezone repair remains separate | +| P3-O01 analytics names/periods/populations | [P5-06](p5-06/README.md) addresses Dashboard labels; [P5-07](p5-07/README.md) reuses and revalidates the same cohorts/snapshots/samples and exact values in Analytics, independent of Dashboard flags | Dashboard/Analytics G-METRICS has scoped regression evidence; current server population/date limitations remain documented and backend metric/timezone repair remains separate | | P5-06-O01 native language preference 500 during reads | One full-run fixture response was generic 500 while Dashboard/setup reads overlapped; exception/root cause not established. Visual setup now writes the native preference before mounting the next fixture, still requiring 204 | **Separate baseline auth/storage investigation**, not resolved by presentation. Concurrent language mutation during loading is not certified; [observation and test sequencing](p5-06/README.md#regression-protection) | +| P5-07-O01 attachment confirmation focus observation | Initial full run: unchanged light/English desktop Shift+Tab from Cancel did not retain Delete focus; existing transition `onEntered` also refocuses Cancel. Race explanation remains uninstrumented | Separate shared-dialog accessibility investigation; no provider/test change in Analytics, no closure from a passing rerun. [Evidence](p5-07/README.md#validation-observations-retained) | | F04/B06 production ingress | **Open**; repository/local checks are not deployed proof. Historical DNS/HTTPS observations do not prove actual peer/firewall boundary or current topology | **Production gate; does not block local P5 slices**. Operator evidence for actual Traefik peer, nginx trust, listeners/ports, direct/spoofed access, B04 client IP, firewall/NAT/IPv6, DNS/TLS/redirect and restart needed; no closure or credentials accessed here | | Protected-main CI/browser runner and deployment | Unresolved remote-runner evidence; local tests pass | **Separate release gate**; feature-branch docs/UI push is not production deployment approval | | F19 missing provider/config falls back to SQLite | Open source carryover | **Separate startup/deployment safety**, not a UI redesign dependency; no backend provider change here | diff --git a/docs/redesign/phase-5/READINESS_GATES.md b/docs/redesign/phase-5/READINESS_GATES.md index 171e664..ac13e46 100644 --- a/docs/redesign/phase-5/READINESS_GATES.md +++ b/docs/redesign/phase-5/READINESS_GATES.md @@ -71,7 +71,7 @@ This is where the reported incorrect margins/templates must be understood and co | Gate | Entry condition / scope | Does not block | |---|---|---| -| G-METRICS | Before P5-06/07: exact source/population/period/sample labels from Phase 3; browser tests for transformations, all five owner visibility flags and error vs zero. [P5-06 evidence](p5-06/README.md) supplies the Dashboard presentation proof; P5-07 must revalidate the destination it changes | Shell, Jobs | +| G-METRICS | Before P5-06/07: exact source/population/period/sample labels from Phase 3; browser tests for transformations, all five owner visibility flags and error vs zero. [P5-06](p5-06/README.md) supplies Dashboard presentation proof; [P5-07](p5-07/README.md) supplies native Analytics values, period/Back, supported query links and independent error evidence | Shell, Jobs | | G-WORKSPACE | Before P5-08a: reproduce/protect mobile progress-scroll accessibility. Before 08b: section/query/`followMode` retention, draft/AI review guards and all legacy mappings; G-PACKAGE also required | Table, independent charts | | G-CV-BEHAVIOR | Before each CV slice: source/reset/item override, browser history restore, reorder alternatives, save failure and AI accept/reject regression required by its PRES rows, in addition to G-CV-REV/RENDER | Non-editor pages; planning/fixture specification may proceed | | G-RELEASE | F04 deployed peer/listeners/ports/firewall/NAT/IPv6/DNS/TLS/restarts; production CI/browser runner, providers, backup/restore/rollback and relevant billing/configuration evidence | Local feature-branch UI planning/testing; never grants production approval | diff --git a/docs/redesign/phase-5/README.md b/docs/redesign/phase-5/README.md index b88ae8f..38cb114 100644 --- a/docs/redesign/phase-5/README.md +++ b/docs/redesign/phase-5/README.md @@ -1,6 +1,6 @@ # Phase 5 — Representative screens / major UI redesign plan -**Current successor: [P5-06 personal Dashboard presentation](p5-06/README.md).** Existing snapshot/attention and report groups now have clearer hierarchy, exact data definitions, accessible chart values and retained owner-local visibility controls. The [P5-04b integration](p5-04b/README.md) and [query-readiness checkpoint](p5-04b-readiness/README.md) remain authoritative for Jobs. P5-02 manual screen-reader acceptance remains mandatory for P5-15. P2-D01, F04, incomplete accessibility checks, native touch verification, Board scaling and recorded baseline timing observations remain open. P5-07, Phase 8 and B07 have not started. +**Current successor: [P5-07 existing-report Analytics destination](p5-07/README.md).** Insights → Analytics supplies deeper existing-report reading through the shared Dashboard owner, exact values and explicit window/current/all-time scope. [P5-06](p5-06/README.md) retains Dashboard reports, attention and all five owner-local choices. The [P5-04b integration](p5-04b/README.md) and [query-readiness checkpoint](p5-04b-readiness/README.md) remain authoritative for Jobs. P5-02 manual screen-reader acceptance remains mandatory for P5-15. P2-D01, F04, incomplete accessibility checks, native touch verification, Board scaling and recorded baseline timing/preference-save observations remain open. P5-08a, Phase 8 and B07 have not started. Historical planning checkpoint: source `2a2f9e48052c562ef599825458d0d3a6f9f9fc61`, inspected on 2026-09-13 on `codex/ui-redesign-phase4`. That checkpoint was documentation-only; the successor record above owns implementation/validation status. diff --git a/docs/redesign/phase-5/p5-07/README.md b/docs/redesign/phase-5/p5-07/README.md new file mode 100644 index 0000000..658811c --- /dev/null +++ b/docs/redesign/phase-5/p5-07/README.md @@ -0,0 +1,153 @@ +# P5-07 — Existing-report Analytics destination + +Source checkpoint: `ec6baaeddd3e950b1eb20abb992182deb0747ca9`, `codex/ui-redesign-phase4`. Scope: [approved P5-07](../PHASE5_PLAN.md#p5-07--existing-report-analytics-destination), [Dashboard source definitions](../../phase-3/DASHBOARD_ARCHITECTURE.md), [P5-06](../p5-06/README.md). No P5-08a, Phase 8 or B07. + +## Inventory before implementation + +There is no standalone Analytics route at the source checkpoint. All existing personal reports live in `DashboardView`; they are not separate backend capabilities to rebuild. Every read below uses authenticated `local` principal and the existing tenant-filtered context; non-deleted owner jobs only, except stats.total includes deleted. No admin permission or Pro entitlement is required. No new aggregate or full Jobs download is necessary. + +| Existing report | Source / actual population | Filters and dates | Representation / actions / empty behavior | +|---|---|---|---| +| Monthly applications / dated responses | `GET /jobapplications/analytics` | Existing UI 6/12/24 months, month-aligned submitted cohort; responses counted in displayed months only for that cohort | Shared-scale solid/dashed chart, exact monthly table; no date-cohort Jobs link because Jobs cannot represent the same cohort | +| Current pipeline / raw status distribution | overview.funnel + stats.byStatus | Current non-deleted snapshot; canonical normalization may omit custom stages; raw values retained separately | Bars/counts and raw status links to `/jobs?status=...`; custom names unchanged, empty canonical series distinct from no raw statuses | +| Current-stage age | overview.timeInStage | Current occupancy median, most recent matching transition or DateApplied/SavedAt fallback; not completed duration | Stage label, days and sample count; absent sample hides report as before | +| Source response samples | overview.responseRateBySource | Current Company.Source; top six, includes prospects; responses = flag OR date | All returned sources, response/total and server rate; no invented source drill-down | +| Company activity | overview.topCompanies | Current snapshot, top eight; same response rule | Counts/rate and supported `/jobs?companyId=...`; true empty state | +| Salary groups | overview.salaryInsights | Recorded non-deleted ranges; separate currency/pay period; sample required | Mean midpoint, minimum/maximum and count; missing sample hides report, no FX or inferred salary | +| Job tags | `GET /jobapplications/tags?limit=10` | All-time non-deleted tags; no months parameter consumed | Dashboard displays top eight; counts/bars, not verified user skills; no tag drill-down | +| Monthly tag trends | `GET /jobapplications/tag-trends?months=...&limit=5` | Selected applied cohort; counts per month per tag | Bars plus exact tables, no hover dependency; true no-series state | +| Snapshot / attention / onboarding | stats + reminders + existing setup reads | Tracked total, last30 dates, current interviews/offers; ranked reminder feed | Dashboard-specific immediate actions remain on Dashboard, not copied as Analytics widgets | + +All existing report UI text uses EN/Bokmål translations and locale formatting; raw user/custom labels and returned source names remain raw. Backend reminder prose is not silently translated. Summary and trends are independent resource groups, each with genuine error and Retry; a failed read must not become an empty report. Tags/date-filter API support (`from`/`to`) exists but is not exposed by the current UI and is not expanded here: tag-trends/overview cannot share that arbitrary range safely. There are no existing status/source/role/location controls on these reports to consolidate. + +## Preservation and implementation decision + +| PRES | Existing behavior → intended P5-07 change | Required evidence | +|---|---|---| +| PRES-S01 | All eight report families → separate detailed Analytics reading using one shared report owner/calculation | Dashboard regressions plus exact values, period/current scope, partial failures and report inventory tests | +| PRES-S02 | Five Dashboard owner-local flags/onboarding → unchanged Dashboard choices; Analytics details remain readable independently | Hidden Dashboard preferences do not hide Analytics; no Analytics preference writes | +| PRES-E07–E09 | Existing route/provider/auth/theme/locale owners → one authenticated `/analytics`, Insights item, honest page metadata | Native direct entry, reload/Back, mobile/rail entry, account isolation, all four theme/locale pairs | +| PRES-J05/J09 | Supported status/company report links → same query contract | Native matching IDs after drill-down, browser Back restores Analytics period | + +The approved plan explicitly permits initial reuse of the report presentation component while retaining Dashboard groups. Use a bounded reporting mode on the existing owner and a thin Analytics route. No parallel metric calculations, providers, requests within a mounted route, new charting library or backend contract. Analytics omits snapshot/attention/onboarding/customization, opens exact-value disclosures and provides more explicit period/snapshot/truncated-list context. Dashboard remains unchanged in functionality and retains access to every report and all five flags. + +Only the existing 6/12/24 month control applies to activity/tag trends; labels explicitly exclude current snapshot and all-time tags. Analytics may persist that supported selection in its URL for direct entry/refresh/Back, with unsupported values safely defaulting to 12. No new filter is implied. + +## Metric definitions and unavailable work + +- Window percentage = sum of returned dated-response monthly counts / sum of returned applied monthly counts × 100. Denominator zero means no percentage. It is a recorded application cohort/window, not eventual response probability or all employer replies in the window. +- Source/company percentages use returned response flag OR response date counts divided by current non-deleted jobs in each group, including prospects. Top lists are truncated and not population totals. Current mutable Company.Source is not original import provenance. +- Current-stage age is the server median occupancy age with fallback, not completed-stage duration. Salary average is arithmetic mean of each recorded range's midpoint (a single bound substitutes the missing bound), separated by currency/period. +- Raw status counts are not a historical conversion funnel. Never infer an Unknown count by subtracting canonical stages from another population. +- Roles, locations, sectors, historical conversions, average response time, AI/match/CV/document performance and aggregate admin statistics require separate defined reads/events. Do not activate previously unused DTO averages/medians with an unsupported interpretation. + +## Implementation and hierarchy + +`AnalyticsPage` is a thin consumer of `DashboardView`'s reporting mode. Activity comes first with exact monthly values initially open, then a two-column current pipeline/raw-status comparison, current-stage ages/source samples, separate salary groups, and company/tag reports. On phones these stack in the same DOM order; exact tables wrap within their width. Native tag-value disclosure also starts open for detailed reading. Every disclosure can still be collapsed/expanded by keyboard; no hover-only information. + +The shared owner retains all Dashboard behavior by default. Analytics does not load its reminder feed or setup checklist, read/write Dashboard flags, show its snapshot strip, or offer Dashboard customization. Its five report calls are stats, overview, analytics, tags and tag-trends, once per initial mount across two existing resource groups. Period changes reload only the trends group. Dashboard still makes its six existing calls and retains all controlled groups. There is no duplicate report tree or parallel metric calculation in either mounted route. + +Analytics displays all ten tags already returned by the existing bounded read (Dashboard remains top eight). The top-six source/top-eight company/top-five trend limits and exact denominators are visible. Tag sparklines normalize within each tag series, with totals and exact monthly values available; they are patterns within a tag, not an absolute cross-tag height comparison. No source/role/location/date-cohort drill-down is invented. Current company and raw status links retain their supported query fields; their Jobs result IDs are checked natively. + +Deliberately unchanged: report calculations, shared-scale SVG plotting, source/company response samples, occupancy-age bars, separate salary summaries and supported Jobs links. No existing report is deleted or replaced with an unsupported percentage. Analytics differs through activity-first ordering, initially expanded exact-value tables/raw-status breakdown, URL-backed period selection, explicit scope and all ten returned tags; it does not duplicate Dashboard's daily attention/onboarding/snapshot widgets. Admin aggregate reporting remains in its authorized Admin owner and is neither queried nor migrated here. + +`/analytics` uses the existing private Shell authentication gate, account-plan/provider tree and lazy route owner. It adds exactly one route and one Insights item; all original routes/aliases remain. Its page title/subtitle/breadcrumbs are localized. Dashboard's historical Home → Analytics → Overview breadcrumb is corrected to Home → Dashboard; its command still opens Dashboard and its hint now describes Dashboard instead of Analytics. All six existing commands remain: no new command catalogue or business action. + +### Related shared shell boundary + +The first rendered review showed the new Insights/current row partially clipped below the expanded sidebar's initial scroll viewport, above the fixed Account group. This is a direct consequence of the new destination, not authorization to redesign the shell. `AppShell` now uses the appropriate existing permanent/temporary Drawer ref to reveal its actual current item with native `scrollIntoView({block:'nearest',inline:'nearest'})` when route/collapse/breakpoint/open state or measured header height changes. It does not reset later manual scrolling, move focus, resize/group items or alter callbacks/permissions. Separate refs avoid targeting the hidden keep-mounted mobile copy. A native assertion checks the whole current row against every scrollable ancestor, and existing rail/drawer/focus regressions remain required. + +### Files and ownership + +- `src/components/DashboardView.tsx`: one existing report/data owner with bounded Analytics composition; same calculations/status semantics/error hook. +- `src/views/AnalyticsPage.tsx`: thin route consumer, no independent data owner. +- `src/App.tsx`: route, Insights item and page metadata only; authentication/providers/callbacks untouched. +- `src/layout/AppShell.tsx`: current-item visibility in the existing scrolling navigation only, justified above. +- `src/components/QuickCommandDialog.tsx`: Dashboard hint only; command actions/catalogue unchanged. +- `src/i18n/translations.ts`: nine localized UI labels/context/error messages per language, no raw status/source mutation. +- `src/analytics-presentation.test.tsx`, `e2e/analytics-presentation.spec.ts`: scoped regressions. No backend/schema/configuration/CI/dependency files changed. + +## Regression protection + +Five new frontend cases cover complete report/value access independent of Dashboard hidden flags, URL period/Back and no snapshot refetch, genuine snapshot failure with independent trends/Retry, empty/unsupported-period safety without an invented denominator, and exact existing status/company destinations. Existing Dashboard/daily-control-loop/workflow-trust-signals, command and shell tests remain in force. Initial regression run intentionally failed because the new route consumer did not yet exist; a report-name fixture was then aligned to the actual existing translated headings, without changing report names to satisfy tests. + +Two new browser cases use the actual production UI export and disposable local API: + +1. Native ordinary owner, six jobs including current/sparse/custom statuses, twelve raw tag categories, missing optional metadata, separate recorded salary data and an application outside the shorter window. Compare all six exact monthly rows, 1/4 dated responses versus 1/6 source response sample, and the 24-month 1/5 cohort; period Back/refresh, matching custom-status/company Jobs IDs, unchanged hidden Dashboard flags, expanded/current-row visibility, rail/drawer entry, keyboard disclosure/focus, EN/Bokmål × light/dark desktop/phone, intermediate/minimum/wide widths, and account switch to a fresh owner plus one application. Cross-owner direct job read returns 404. +2. Controlled transport holds/503s for genuine loading and independent resource failures, Retry and zero-history recovery in the actual rendered app. These are synthetic failure fixtures, not production-data/load proof; native values and ownership come from case 1. + +Locale fixture setup requires a real 204 preference save from a blank document before mounting the next Analytics view, following P5-06's documented preparation. It does not test or repair concurrent preference writes during Dashboard loading. No retry, forged identity, mocked success or permission bypass is used. + +### Validation observations retained + +The initial full browser run found one unchanged light/English desktop attachment-confirmation case failing to focus Delete after Shift+Tab from Cancel. The trace proves the initial Cancel focus assertion passed and the keyboard event occurred shortly after opening; the unchanged `ConfirmProvider` also unconditionally focuses Cancel in its transition `onEntered` callback. A transition/focus race is a plausible explanation, not an instrumented proof of callback timing. Neither this provider nor the inherited test was changed, skipped or given a retry. This observation is P5-07-O01, separate from Analytics; a later passing run does not certify the race fixed. Preserve its sanitized initial summary and compare the final full run independently. + +Rendered review also caught a reused Dashboard-specific snapshot error description on Analytics. The final localized Analytics description now identifies current job reports and independently loading activity/tag trends; both unit and browser assertions cover it. Final validation and error-state capture are repeated after that correction. + +## Accessibility and rendered evidence + +The detailed destination uses the existing shared PageSection headings, native links/disclosures, row/column-header tables, semantic theme/status colors and ViewStateNotice error/Retry owners. Noninteractive charts and metric rows are not focusable. Canonical status color is supplemented by labels and values; raw custom status names remain available separately. Monthly and tag values do not require hover, SVG interpretation or color perception. Keyboard checks exercise disclosure toggles/focus, supported report navigation and mobile route-heading focus; unchanged shared dialog/command/shell tests remain part of the full suite. + +Scoped axe scans cover the populated English phone page in light and dark mode using the existing WCAG A/AA tags, without disabling rules. The final sanitized summary records violations and incomplete checks separately. These scans do not certify all states/locales or full screen-reader operation. Spoken names, chart/table reading and live/state announcements remain an open manual P5-15 acceptance item; no manual screen-reader, native touch, forced-colors or exhaustive zoom session is claimed. + +The final [axe summary](evidence/analytics-axe-summary.json) records zero detected violations in each scan and one incomplete `color-contrast` node in each: `.MuiBadge-invisible`, whose content was too short for the checker to determine whether it is text. That is an incomplete result, not an automated contrast pass or closure of the broader manual accessibility requirement. Separate keyboard/disclosure and rendered focus evidence pass within their tested scope. + +Evidence is generated by the actual production UI export against a disposable local API. Native populated/sparse accounts contain synthetic data, not real user information. Loading/503 screenshots are explicitly controlled transport fixtures. No production request, credentials, cookies/storage snapshots, raw API bodies or traces are committed. The manifest records capture time, viewport, actual full-page dimensions, period, fixture type and individual rendered inspection. There was no pre-existing Analytics screen to manufacture a before comparison; P5-06's retained Dashboard is the prior report-layout reference. + +| State / evidence | Rendered review scope | +|---|---| +| [Desktop light EN](evidence/analytics-desktop-light-en.png), [light Bokmål](evidence/analytics-desktop-light-nb.png), [dark EN](evidence/analytics-desktop-dark-en.png), [dark Bokmål](evidence/analytics-desktop-dark-nb.png) | Activity-first hierarchy, exact six-month values, side-by-side current/canonical versus raw statuses, restrained semantic surfaces and readable labels | +| [Phone light EN](evidence/analytics-phone-light-en.png), [light Bokmål](evidence/analytics-phone-light-nb.png), [dark EN](evidence/analytics-phone-dark-en.png), [dark Bokmål](evidence/analytics-phone-dark-nb.png) | 390×844; stacked reports, wrapping labels/actions and exact values, with no document overflow in the measured browser fixture | +| [Minimum 320](evidence/analytics-320-dark-nb.png), [tablet 768](evidence/analytics-768-dark-nb.png), [wide 1920](evidence/analytics-1920-dark-nb.png) | 320×740, 768×1024, 1920×1080; twelve-month default after fresh route entry, bounded content width and responsive navigation; no 1024-specific Analytics image is claimed | +| [Keyboard exact values](evidence/analytics-exact-values-keyboard.png) | Six monthly API values match the visible rows; Enter toggles disclosure and a shared ≥3px focus outline remains visible | +| [New owner](evidence/analytics-empty-new-owner.png), [one application](evidence/analytics-one-application.png) | No invented percentage for an empty cohort; 0/1 is valid for a real application with no recorded response; missing optional metadata does not become fabricated reports | +| [Loading](evidence/analytics-loading.png), [snapshot error](evidence/analytics-snapshot-error.png), [trends error](evidence/analytics-trends-error.png), [zero history](evidence/analytics-zero-history.png) | Progress is distinct from empty data; genuine 503 messages/Retry preserve the successful independent resource; no false conversion value | + +The focused full-page screenshot includes fixed-shell stitching at its current scroll offset; it proves the data disclosure/focus, not that stitched header placement. Default captures and native current-item visibility assertions separately validate the shell. Detailed tag/month tables naturally make the page longer; disclosure controls retain the ability to condense them. The unequal company/tag column heights on desktop reflect actual detail data, not empty placeholder widgets. Rebalancing those groups is a future presentation decision, not permission to remove report values. + +## Performance, compatibility and rollback + +Analytics performs five existing bounded aggregate reads, not a request per job or a full Jobs crawl; shared shell reminder polling remains unchanged. Its 6/12/24-month series and top-six/eight/ten/five source/company/tag/trend bounds remain the server contracts. Tags are re-read with the existing trends group when the period changes, although their totals stay all-time; optimizing that owner is not needed for this slice. Local fixtures prove rendering and behavior, not production load certification. + +All 31 original route patterns are retained and `/analytics` adds the 32nd. Authentication/providers and Dashboard preference keys remain compatible. If presentation is rolled back, retain `/analytics` and its meaningful shared report consumer (or a direct compatibility entry to the retained reports); do not blindly remove the new route and produce a 404. No database migration, irreversible data write, dependency or deployment change is involved. + +## Validation commands and baseline + +Inherited: backend 934; frontend 398; browser 79 actual passes + unchanged P2-D01; rate-limit 9, ingress 6, PDF 1, configuration 3. Commands below run through `rtk proxy`; frontend commands use `job-tracker-ui`. Native browser and API security suites use separate disposable local API lifetimes sequentially, never production. Browser export configuration is `DOTNET_ROOT=C:/Users/Connor/.dotnet`, `DOTNET_HOST_PATH=C:/Users/Connor/.dotnet/dotnet.exe`, `PLAYWRIGHT_STATIC_EXPORT=true`, `PLAYWRIGHT_PREBUILT_EXPORT=true`; production export is built with `NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api`, `CI=false`. The browser run has zero configured retries. + +| Check | Command after `rtk proxy` | Final result / inherited comparison | +|---|---|---| +| Backend | `C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj -c Release --no-restore --logger 'console;verbosity=minimal'` | 934 passed, 0 failed/skipped; unchanged | +| Frontend | `npm.cmd test -- --runInBand --silent` | 403 passed in 80 suites; +5 Analytics cases, no removed tests | +| Browser | `npm.cmd run test:e2e -- --reporter=line,json` | 81 actual passes (+2 Analytics), unchanged P2-D01 expected failure; 0 unexpected/skipped/retries, 18m12s. Reporter says “82 passed” because it includes the expected failure; this record does not | +| TypeScript | `npm.cmd run typecheck` | Passed; historical TS2571 is not current | +| New E2E fixture typing | `npm.cmd exec tsc -- --noEmit --incremental false -p ../.artifacts/p5-07-e2e-tsconfig.json` | Passed; ignored local config extends UI tsconfig and includes the new fixture, no committed compiler/CI change | +| Lint | `npm.cmd run lint` | Passed, zero warnings | +| Frontend production export | `npm.cmd run build` | Passed compilation, TypeScript and static generation after final error-description change | +| API Release | `C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi/JobTrackerApi.csproj -c Release --no-restore -warnaserror` | Passed, 0 warnings/errors | +| Rate-limit | `npm.cmd run test:rate-limits -- --reporter=line` | 9 passed; unchanged | +| Ingress | `npm.cmd exec playwright test -- --config=playwright.ingress.config.ts --reporter=line` | 6 passed; unchanged; local application checks do not close production F04 | +| PDF admission | `npm.cmd run test:pdf-admission -- --reporter=line` | 1 passed; unchanged | +| Effective Compose/config | `python scripts/test-ingress.py` | 3 passed; unchanged | +| Supply-chain fixtures | `python scripts/test-supply-chain.py` | 3 passed; unchanged | +| Ollama evaluation fixtures | `python scripts/test-ollama-evaluation.py` | 5 passed; unchanged | +| Extension capture URL | `node --test browser-extension/tests/capture-url.test.mjs` | 3 passed; unchanged | +| Secret scan | `python scripts/supply-chain.py scan-secrets` | Passed against staged final candidate; no high-confidence credentials | +| Whitespace | `git diff --check` and `git diff --cached --check` | Passed against final candidate | +| Document/evidence validation | `python .artifacts/p4-04-check-doc-links.py` plus local screenshot/manifest and sanitized JSON assertions | 140 current/root Markdown documents resolve local targets; all 18 inspected PNGs/dimensions and 82 distinct outcomes validate, zero retries/skips; summaries exclude raw rendered HTML/credentials | + +The [initial full browser summary](evidence/initial-browser-summary.json) preserves 80 actual passes, unchanged P2-D01 expected failure and one unexpected attachment-focus failure, before the final error-copy correction. It is not reported as a pass. The final complete run is reported independently; passing reruns do not close P5-07-O01. No existing test was removed, skipped, renamed or given a weaker assertion. + +The [final browser summary](evidence/final-browser-summary.json) records all 79 inherited successful cases still passing, both new Analytics cases passing and the frozen-package case still failing under its original P2-D01 annotation. Final screenshots were retained before the API-only suites replaced working test output; all 18 final files match the individually inspected bytes and their manifest records actual 2026-10-06 capture dates. + +F04, P2-D01, P5-02 manual screen-reader acceptance, incomplete axe/manual checks, native Board touch/full-result scaling and P5-06-O01 preference-save 500 remain open. P5-07 does not repair the preference endpoint or certify concurrent locale writes during loading. + +No new roles/locations/sectors, historical conversions, response-time averages, AI recommendations or CV/document performance are added. Those require reliable separately approved data contracts. Aggregate Admin reporting, new charts/frameworks, arbitrary analytical date filters and Phase 8 dashboard customization remain out of scope. + +## Acceptance and stopping point + +P5-07 presentation/preservation acceptance is satisfied within its approved scope: 934 backend, 403 frontend (+5), 81 actual browser passes (+2) plus unchanged P2-D01 expected failure; rate-limit 9, ingress 6, PDF 1 and configuration 3. Existing reports, Dashboard controls, original routes and ownership boundaries remain protected. Both builds, TypeScript and lint pass after the final application change; staged repository checks are recorded above. No baseline count was reduced, unexpected failure suppressed or report silently removed. + +Scoped keyboard and rendered light/dark EN/Bokmål/responsive checks pass; the two incomplete axe checks and broader manual screen-reader acceptance remain open. P5-07-O01's initial inherited focus failure remains a separate observation despite a passing final full run. This is a local feature-branch presentation checkpoint, not production or universal accessibility approval. + +No P5-08a implementation has begun. The recommended next ticket is P5-08a only after its G-WORKSPACE mobile-progress/focus prerequisites are established; package/CV gates and production F04 remain separate. No B07, package/tailoring or CV work is authorized by this Analytics checkpoint. diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-1920-dark-nb.png b/docs/redesign/phase-5/p5-07/evidence/analytics-1920-dark-nb.png new file mode 100644 index 0000000..dc8d620 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-1920-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-320-dark-nb.png b/docs/redesign/phase-5/p5-07/evidence/analytics-320-dark-nb.png new file mode 100644 index 0000000..93e5c20 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-320-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-768-dark-nb.png b/docs/redesign/phase-5/p5-07/evidence/analytics-768-dark-nb.png new file mode 100644 index 0000000..bce139d Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-768-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-axe-summary.json b/docs/redesign/phase-5/p5-07/evidence/analytics-axe-summary.json new file mode 100644 index 0000000..c8ee57a --- /dev/null +++ b/docs/redesign/phase-5/p5-07/evidence/analytics-axe-summary.json @@ -0,0 +1,53 @@ +{ + "manualScreenReaderAcceptance": "open", + "scans": [ + { + "name": "analytics-light-axe", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "nodes": [ + { + "target": [ + ".MuiBadge-invisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element content is too short to determine if it is actual text content" + } + ] + } + ] + } + ] + }, + { + "name": "analytics-dark-axe", + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "nodes": [ + { + "target": [ + ".MuiBadge-invisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element content is too short to determine if it is actual text content" + } + ] + } + ] + } + ] + } + ] +} diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-dark-en.png b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-dark-en.png new file mode 100644 index 0000000..0d96eac Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-dark-en.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-dark-nb.png b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-dark-nb.png new file mode 100644 index 0000000..50a9de6 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-light-en.png b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-light-en.png new file mode 100644 index 0000000..37cfe1b Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-light-en.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-light-nb.png b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-light-nb.png new file mode 100644 index 0000000..a8a2a3c Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-desktop-light-nb.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-empty-new-owner.png b/docs/redesign/phase-5/p5-07/evidence/analytics-empty-new-owner.png new file mode 100644 index 0000000..b806058 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-empty-new-owner.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-exact-values-keyboard.png b/docs/redesign/phase-5/p5-07/evidence/analytics-exact-values-keyboard.png new file mode 100644 index 0000000..7379b44 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-exact-values-keyboard.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-loading.png b/docs/redesign/phase-5/p5-07/evidence/analytics-loading.png new file mode 100644 index 0000000..8edafaa Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-loading.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-one-application.png b/docs/redesign/phase-5/p5-07/evidence/analytics-one-application.png new file mode 100644 index 0000000..1f927e7 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-one-application.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-phone-dark-en.png b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-dark-en.png new file mode 100644 index 0000000..46daa02 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-dark-en.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-phone-dark-nb.png b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-dark-nb.png new file mode 100644 index 0000000..c35977f Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-phone-light-en.png b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-light-en.png new file mode 100644 index 0000000..3311fe0 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-light-en.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-phone-light-nb.png b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-light-nb.png new file mode 100644 index 0000000..c90f195 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-phone-light-nb.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-snapshot-error.png b/docs/redesign/phase-5/p5-07/evidence/analytics-snapshot-error.png new file mode 100644 index 0000000..d532ce6 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-snapshot-error.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-trends-error.png b/docs/redesign/phase-5/p5-07/evidence/analytics-trends-error.png new file mode 100644 index 0000000..0f1bda0 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-trends-error.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/analytics-zero-history.png b/docs/redesign/phase-5/p5-07/evidence/analytics-zero-history.png new file mode 100644 index 0000000..3dddc49 Binary files /dev/null and b/docs/redesign/phase-5/p5-07/evidence/analytics-zero-history.png differ diff --git a/docs/redesign/phase-5/p5-07/evidence/final-browser-summary.json b/docs/redesign/phase-5/p5-07/evidence/final-browser-summary.json new file mode 100644 index 0000000..bb797b2 --- /dev/null +++ b/docs/redesign/phase-5/p5-07/evidence/final-browser-summary.json @@ -0,0 +1,836 @@ +{ + "sourceBase": "ec6baaeddd3e950b1eb20abb992182deb0747ca9", + "statistics": { + "startTime": "2026-10-06T17:03:42.320Z", + "duration": 1091856.757, + "expected": 82, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "actualPasses": 81, + "expectedFailures": 1, + "unexpected": [], + "cases": [ + { + "file": "accessibility.spec.ts", + "title": "public entry points meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "accessibility.spec.ts", + "title": "primary authenticated workspaces meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "retained profile request from A cannot write using B cookies and fresh B CSRF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "missing and forged session contexts fail closed while same-account profile saves work", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "crafted CV identifiers cannot cross read update delete or job-association ownership", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "[P5-00 PRES-E05 E07 E08] account switch disposes open command, account and notification surfaces", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "two tabs discard A's private form before B can use it, including Back navigation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "analytics-presentation.spec.ts", + "title": "[PRES-S01 S02 E07 E08 E09 J05 J09] Analytics native reports, direct entry, period, drill-down and owner isolation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "analytics-presentation.spec.ts", + "title": "[PRES-S01 E08] Analytics independent loading and failure states never masquerade as empty reports", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J07 J05 W01 W02 E07 E08 E09] Board query membership matches Table and preserves moves, focus and route", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J05 J07 E07 W01] legacy Board replaces history and preserves query, hash and route state; default Jobs stays Table", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J07 E08] Board loading, retryable error and fully empty columns", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] light/en preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] dark/nb preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] light/nb preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] dark/en preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "csv-export.spec.ts", + "title": "CSV export has safe cells and headers, and browser download preserves its UTF-8 bytes", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsive presentation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 E08] Dashboard loading and independent failed resources retain retry and exact values", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "job-discovery.spec.ts", + "title": "discovery remains usable across widths and opens the reviewed import", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "kanban-theme.spec.ts", + "title": "kanban columns use dark theme surfaces", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] light/en/1440 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] dark/nb/1440 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] light/nb/390 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] dark/en/390 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-C01 PRES-C06 PRES-C07 PRES-C10] mobile CV override persists without changing the canonical profile or sibling CV", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", + "expectedStatus": "failed", + "outcome": "expected", + "actualStatuses": [ + "failed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03 PRES-W11] responsive settings navigation keeps rule edits and persists notification preferences", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03] rule changes persist through the real API", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-E07 PRES-W01 PRES-J04] keyboard command navigation and legacy workspace entry preserve the selected job", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-A01 PRES-S04 PRES-E07] non-admin navigation cannot grant API privileges or unlock CV AI", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J05 PRES-J07] light loaded status semantics, navigation and command focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-C06 PRES-C07 PRES-C08 PRES-C09 PRES-C11 PRES-S03] light loaded CV tabs, selects and settings", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J05 PRES-J07] dark loaded status semantics, navigation and command focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-C06 PRES-C07 PRES-C08 PRES-C09 PRES-C11 PRES-S03] dark loaded CV tabs, selects and settings", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] light/en Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] light/nb Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] dark/en Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] dark/nb Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] light-en command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-en utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] light-en real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] light-en grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E09 C06 S02 S03] light-en dirty editor survives shell utilities and cancel", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] light-nb command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-nb utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] light-nb real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] light-nb grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] dark-en command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-en utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] dark-en real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] dark-en grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] dark-nb command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-nb utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] dark-nb real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] dark-nb grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E09 C06 S02 S03] dark-nb dirty editor survives shell utilities and cancel", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "API liveness and database readiness are separate public probes", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "public plans stay honest, responsive and keyboard accessible", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "login establishes an authenticated session", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a Free account keeps manual work available while AI actions stay honestly locked", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a saved job can be created through the reviewed UI flow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "the dedicated application workspace survives deep links, long data and unsaved navigation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "Career Workspace loads from the authenticated application shell", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a public CV renders anonymously and downloads as PDF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "the Code template exports a long structured CV as searchable multi-page PDF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] light/en table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] light/nb table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] dark/en table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] dark/nb table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + } + ] +} diff --git a/docs/redesign/phase-5/p5-07/evidence/initial-browser-summary.json b/docs/redesign/phase-5/p5-07/evidence/initial-browser-summary.json new file mode 100644 index 0000000..f399e72 --- /dev/null +++ b/docs/redesign/phase-5/p5-07/evidence/initial-browser-summary.json @@ -0,0 +1,838 @@ +{ + "sourceBase": "ec6baaeddd3e950b1eb20abb992182deb0747ca9", + "statistics": { + "startTime": "2026-10-06T16:44:14.092Z", + "duration": 1109969.291, + "expected": 81, + "skipped": 0, + "unexpected": 1, + "flaky": 0 + }, + "actualPasses": 80, + "expectedFailures": 1, + "unexpected": [ + "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] light/en/1440 named controls, modal keyboard and retained draft" + ], + "cases": [ + { + "file": "accessibility.spec.ts", + "title": "public entry points meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "accessibility.spec.ts", + "title": "primary authenticated workspaces meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "retained profile request from A cannot write using B cookies and fresh B CSRF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "missing and forged session contexts fail closed while same-account profile saves work", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "crafted CV identifiers cannot cross read update delete or job-association ownership", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "[P5-00 PRES-E05 E07 E08] account switch disposes open command, account and notification surfaces", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "two tabs discard A's private form before B can use it, including Back navigation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "analytics-presentation.spec.ts", + "title": "[PRES-S01 S02 E07 E08 E09 J05 J09] Analytics native reports, direct entry, period, drill-down and owner isolation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "analytics-presentation.spec.ts", + "title": "[PRES-S01 E08] Analytics independent loading and failure states never masquerade as empty reports", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J07 J05 W01 W02 E07 E08 E09] Board query membership matches Table and preserves moves, focus and route", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J05 J07 E07 W01] legacy Board replaces history and preserves query, hash and route state; default Jobs stays Table", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J07 E08] Board loading, retryable error and fully empty columns", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] light/en preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] dark/nb preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] light/nb preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "builder-preview-keyboard.spec.ts", + "title": "[PRES-C06 PRES-C07 PRES-C10 PRES-E08] dark/en preview keyboard, pagination and responsive state", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "csv-export.spec.ts", + "title": "CSV export has safe cells and headers, and browser download preserves its UTF-8 bytes", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsive presentation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 E08] Dashboard loading and independent failed resources retain retry and exact values", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "job-discovery.spec.ts", + "title": "discovery remains usable across widths and opens the reviewed import", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "kanban-theme.spec.ts", + "title": "kanban columns use dark theme surfaces", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] light/en/1440 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "unexpected", + "actualStatuses": [ + "failed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] dark/nb/1440 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] light/nb/390 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "named-controls-modal-state.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J08 PRES-C08 PRES-C09 PRES-W06 PRES-W08] dark/en/390 named controls, modal keyboard and retained draft", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-C01 PRES-C06 PRES-C07 PRES-C10] mobile CV override persists without changing the canonical profile or sibling CV", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", + "expectedStatus": "failed", + "outcome": "expected", + "actualStatuses": [ + "failed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03 PRES-W11] responsive settings navigation keeps rule edits and persists notification preferences", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03] rule changes persist through the real API", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-E07 PRES-W01 PRES-J04] keyboard command navigation and legacy workspace entry preserve the selected job", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-A01 PRES-S04 PRES-E07] non-admin navigation cannot grant API privileges or unlock CV AI", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J05 PRES-J07] light loaded status semantics, navigation and command focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-C06 PRES-C07 PRES-C08 PRES-C09 PRES-C11 PRES-S03] light loaded CV tabs, selects and settings", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/1440 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/390 loading → error → retry → empty → populated", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E07 PRES-E08 PRES-J05 PRES-J07] dark loaded status semantics, navigation and command focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-C06 PRES-C07 PRES-C08 PRES-C09 PRES-C11 PRES-S03] dark loaded CV tabs, selects and settings", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] light/en Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] light/nb Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] dark/en Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "settings-page-layout.spec.ts", + "title": "[PRES-E07 E08 E09 S03 S04 S05 J05 J10 M06 W11] dark/nb Settings headings, reflow and retained actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] light-en command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-en utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] light-en real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] light-en grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E09 C06 S02 S03] light-en dirty editor survives shell utilities and cancel", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] light-nb command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-nb utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] light-nb real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] light-nb grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] dark-en command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-en utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] dark-en real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] dark-en grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 J03 J08] dark-nb command list, keyboard and no-write actions", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-nb utility search and notification states", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E08 C06 S05] dark-nb real Tooltip contrast and open-toast action access", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-01 PRES-E07 E08 A03] dark-nb grouped shell, rail menu and route focus", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E09 C06 S02 S03] dark-nb dirty editor survives shell utilities and cancel", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "API liveness and database readiness are separate public probes", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "public plans stay honest, responsive and keyboard accessible", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "login establishes an authenticated session", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a Free account keeps manual work available while AI actions stay honestly locked", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a saved job can be created through the reviewed UI flow", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "the dedicated application workspace survives deep links, long data and unsaved navigation", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "Career Workspace loads from the authenticated application shell", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a public CV renders anonymously and downloads as PDF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "the Code template exports a long structured CV as searchable multi-page PDF", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] light/en table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] light/nb table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] dark/en table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + }, + { + "file": "table-density.spec.ts", + "title": "[PRES-J05 J06 W01 W02 S03 E08 E09] dark/nb table and cards preserve behavior at both densities", + "expectedStatus": "passed", + "outcome": "expected", + "actualStatuses": [ + "passed" + ], + "attempts": 1 + } + ] +} diff --git a/docs/redesign/phase-5/p5-07/evidence/visual-manifest.json b/docs/redesign/phase-5/p5-07/evidence/visual-manifest.json new file mode 100644 index 0000000..1a6bbb3 --- /dev/null +++ b/docs/redesign/phase-5/p5-07/evidence/visual-manifest.json @@ -0,0 +1,277 @@ +{ + "sourceBase": "ec6baaeddd3e950b1eb20abb992182deb0747ca9", + "captureRuntime": "Chromium, production export, disposable API; actual clock 2026-10-06", + "manualScreenReaderAcceptance": "open", + "screenshots": [ + { + "file": "analytics-1920-dark-nb.png", + "capturedUtc": "2026-10-06T17:04:54.938846+00:00", + "viewport": { + "width": 1920, + "height": 1080 + }, + "fullPageWidth": 1920, + "fullPageHeight": 5277, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "nb", + "periodMonths": 12, + "renderedReview": true + }, + { + "file": "analytics-320-dark-nb.png", + "capturedUtc": "2026-10-06T17:04:53.945713+00:00", + "viewport": { + "width": 320, + "height": 740 + }, + "fullPageWidth": 320, + "fullPageHeight": 6236, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "nb", + "periodMonths": 12, + "renderedReview": true + }, + { + "file": "analytics-768-dark-nb.png", + "capturedUtc": "2026-10-06T17:04:54.301344+00:00", + "viewport": { + "width": 768, + "height": 1024 + }, + "fullPageWidth": 768, + "fullPageHeight": 5795, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "nb", + "periodMonths": 12, + "renderedReview": true + }, + { + "file": "analytics-desktop-dark-en.png", + "capturedUtc": "2026-10-06T17:04:46.955045+00:00", + "viewport": { + "width": 1440, + "height": 900 + }, + "fullPageWidth": 1440, + "fullPageHeight": 4177, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "en", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-desktop-dark-nb.png", + "capturedUtc": "2026-10-06T17:04:52.176050+00:00", + "viewport": { + "width": 1440, + "height": 900 + }, + "fullPageWidth": 1440, + "fullPageHeight": 4197, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "nb", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-desktop-light-en.png", + "capturedUtc": "2026-10-06T17:04:38.537163+00:00", + "viewport": { + "width": 1440, + "height": 900 + }, + "fullPageWidth": 1440, + "fullPageHeight": 4177, + "fixture": "native synthetic owner API", + "theme": "light", + "language": "en", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-desktop-light-nb.png", + "capturedUtc": "2026-10-06T17:04:43.657020+00:00", + "viewport": { + "width": 1440, + "height": 900 + }, + "fullPageWidth": 1440, + "fullPageHeight": 4197, + "fixture": "native synthetic owner API", + "theme": "light", + "language": "nb", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-empty-new-owner.png", + "capturedUtc": "2026-10-06T17:04:58.084377+00:00", + "viewport": { + "width": 1440, + "height": 900 + }, + "fullPageWidth": 1440, + "fullPageHeight": 1288, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "en", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-exact-values-keyboard.png", + "capturedUtc": "2026-10-06T17:04:31.773369+00:00", + "viewport": { + "width": 1440, + "height": 900 + }, + "fullPageWidth": 1440, + "fullPageHeight": 4177, + "fixture": "native synthetic owner API", + "theme": "light", + "language": "en", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-loading.png", + "capturedUtc": "2026-10-06T17:05:01.755566+00:00", + "viewport": { + "width": 1280, + "height": 720 + }, + "fullPageWidth": 1280, + "fullPageHeight": 739, + "fixture": "controlled local transport", + "theme": "light", + "language": "en", + "periodMonths": 12, + "renderedReview": true + }, + { + "file": "analytics-one-application.png", + "capturedUtc": "2026-10-06T17:04:59.675407+00:00", + "viewport": { + "width": 1440, + "height": 900 + }, + "fullPageWidth": 1440, + "fullPageHeight": 1751, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "en", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-phone-dark-en.png", + "capturedUtc": "2026-10-06T17:04:47.370530+00:00", + "viewport": { + "width": 390, + "height": 844 + }, + "fullPageWidth": 390, + "fullPageHeight": 4886, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "en", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-phone-dark-nb.png", + "capturedUtc": "2026-10-06T17:04:52.484415+00:00", + "viewport": { + "width": 390, + "height": 844 + }, + "fullPageWidth": 390, + "fullPageHeight": 4850, + "fixture": "native synthetic owner API", + "theme": "dark", + "language": "nb", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-phone-light-en.png", + "capturedUtc": "2026-10-06T17:04:38.911860+00:00", + "viewport": { + "width": 390, + "height": 844 + }, + "fullPageWidth": 390, + "fullPageHeight": 4886, + "fixture": "native synthetic owner API", + "theme": "light", + "language": "en", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-phone-light-nb.png", + "capturedUtc": "2026-10-06T17:04:44.048480+00:00", + "viewport": { + "width": 390, + "height": 844 + }, + "fullPageWidth": 390, + "fullPageHeight": 4850, + "fixture": "native synthetic owner API", + "theme": "light", + "language": "nb", + "periodMonths": 6, + "renderedReview": true + }, + { + "file": "analytics-snapshot-error.png", + "capturedUtc": "2026-10-06T17:05:03.895210+00:00", + "viewport": { + "width": 1280, + "height": 720 + }, + "fullPageWidth": 1280, + "fullPageHeight": 1168, + "fixture": "controlled local transport", + "theme": "light", + "language": "en", + "periodMonths": 12, + "renderedReview": true + }, + { + "file": "analytics-trends-error.png", + "capturedUtc": "2026-10-06T17:05:01.989691+00:00", + "viewport": { + "width": 1280, + "height": 720 + }, + "fullPageWidth": 1280, + "fullPageHeight": 936, + "fixture": "controlled local transport", + "theme": "light", + "language": "en", + "periodMonths": 12, + "renderedReview": true + }, + { + "file": "analytics-zero-history.png", + "capturedUtc": "2026-10-06T17:05:02.294443+00:00", + "viewport": { + "width": 1280, + "height": 720 + }, + "fullPageWidth": 1280, + "fullPageHeight": 1269, + "fixture": "controlled local transport", + "theme": "light", + "language": "en", + "periodMonths": 12, + "renderedReview": true + } + ] +} diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md index 1e06a40..df7b83a 100644 --- a/docs/roadmap/todo.md +++ b/docs/roadmap/todo.md @@ -4,8 +4,11 @@ Status reconciled: 2026-10-06. ## Current repository work -- **P5-06 — personal Dashboard presentation complete:** existing reports, actions and five owner-local visibility flags retained; precise population/period/sample captions, shared-scale activity and exact keyboard-accessible values, independent error/retry, and native account-switch evidence. [Record](../redesign/phase-5/p5-06/README.md). Baseline: 934 backend, 398 frontend (+8), 79 actual browser passes (+2) plus unchanged P2-D01 expected failure; security 9/6/1, configuration 3. No new metrics/backend contracts or Phase 8 customization. P5-07 and B07 are not started; F04, incomplete axe and manual screen-reader acceptance remain open. +- **P5-07 — existing-report Analytics destination complete:** meaningful authenticated `/analytics`/Insights entry, shared retained Dashboard report owner, current-versus-window definitions, exact values and existing status/company drill-downs. [Record](../redesign/phase-5/p5-07/README.md). Baseline: 934 backend, 403 frontend (+5), 81 actual browser passes (+2) plus unchanged P2-D01 expected failure. Dashboard's five owner-local flags and all original routes remain; no new metrics/backend contracts. P5-08a and B07 are not started; F04, P2-D01, incomplete axe/manual screen-reader, P5-06-O01 and the P5-07-O01 observation remain open. + +- **P5-06 — personal Dashboard presentation complete:** existing reports, actions and five owner-local visibility flags retained; precise population/period/sample captions, shared-scale activity and exact keyboard-accessible values, independent error/retry, and native account-switch evidence. [Record](../redesign/phase-5/p5-06/README.md). Baseline: 934 backend, 398 frontend (+8), 79 actual browser passes (+2) plus unchanged P2-D01 expected failure; security 9/6/1, configuration 3. No new metrics/backend contracts or Phase 8 customization. P5-07 is recorded separately above; F04, incomplete axe and manual screen-reader acceptance remain open. - **P5-06-O01 — native language-preference 500, cause unproven:** the first full browser attempt observed a generic 500 during overlapping Dashboard/setup reads after Browser Back. Visual preparation now requires the native preference save before mounting the next Dashboard; no retry or backend repair. Investigate the unchanged endpoint separately; concurrent language writes during loading remain uncertified. [Evidence](../redesign/phase-5/p5-06/README.md#regression-protection). +- **P5-07-O01 — existing confirmation focus observation:** an inherited light/English desktop attachment test failed Shift+Tab-to-Delete after initial Cancel focus. The unchanged provider refocuses Cancel on transition entry; a transition/focus race is plausible but not instrumented/proven. Keep separate from Analytics; no provider/test repair or weakened assertion. A passing rerun does not close it. [Evidence](../redesign/phase-5/p5-07/README.md#validation-observations-retained). - **P5-04b complete — Table/Board integration:** one Jobs filter owner and URL view switch, retained `/kanban` alias, Table page/selection lifetime and full Board population. [Acceptance/evidence](../redesign/phase-5/p5-04b/README.md). Baseline: 934 backend, 390 frontend, 77 actual browser passes + unchanged P2-D01 expected failure; security 9/6/1, configuration 3. No backend contract changes, P5-06 or B07. F04, P2-D01, incomplete axe/manual screen-reader/native touch and Board scaling remain open. @@ -18,7 +21,7 @@ Status reconciled: 2026-10-06. - **P5-00-D02 — closed for Jobs:** P5-03 stores the originating row focus target on the list history entry and restores it after Browser Back and data load; explicit workspace Back remains intact. Four locale/theme browser cases pass. Revalidate if P5-08 changes workspace navigation. [Evidence](../redesign/phase-5/p5-03/README.md). - **Phase 5 plan:** [bounded plan](../redesign/phase-5/README.md) maps all 57 contracts/31 routes. Board integration, native package and CV revision/rendering gates apply only to dependent slices. F04 remains open. - **P2-D01 — open:** repair the SQLite `DateTimeOffset` ordering baseline before package/tailoring restructuring; retain the expected-failure browser oracle until native capture and historical-binary preservation are proven. P2-O01's initial rules-500 behavior also remains unconfirmed. -- **P3-O01/P3-O02 — scoped follow-up:** P5-06 addresses Dashboard population/period/sample presentation and independent error retention. P5-07 must revalidate the retained reports in the Analytics destination; backend metric definitions and other navigation/query surfaces remain separately scoped. Evidence is in the current [redesign decision records](../redesign/phase-3/OPEN_DECISIONS.md). +- **P3-O01/P3-O02 — scoped follow-up:** P5-06/P5-07 address Dashboard/Analytics population/period/sample presentation and independent error retention. Backend metric definitions and other navigation/query surfaces remain separately scoped. Evidence is in the current [redesign decision records](../redesign/phase-3/OPEN_DECISIONS.md). - **P4-02-D02–D04 — open accessibility/content debt:** company save-error feedback outside the modal's accessible subtree, mobile application-progress scroll region and literal AI context `{name}`. D01 command-list semantics are addressed in P5-00; D02–D04 are unchanged. [Original evidence](../redesign/phase-4/p4-02/README.md). Do not disable modal background isolation or hide findings. - **P5-00 scoped closures:** P4-03-D01 Tooltip contrast, P4-04-O01 bottom-action toast overlap, P4-06-O01 mobile labels/focus and P4-06-O02 synthetic long admin metadata are addressed with [readiness evidence](../redesign/phase-5/p5-00/README.md). This does not certify all overlays, all accessibility targets or deployed production behavior. - **P4-04-O02 — existing SQLite notification-save contention:** overlapping native preference read/write requests reproduced HTTP 500 with SQLite error 5 in two of ten local runs. The sequential UI test now awaits and requires the save response before readback; server concurrency remains unresolved. Investigate separately from page-layout/density work. [Evidence](../redesign/phase-4/p4-04/README.md). diff --git a/job-tracker-ui/e2e/analytics-presentation.spec.ts b/job-tracker-ui/e2e/analytics-presentation.spec.ts new file mode 100644 index 0000000..b803ab5 --- /dev/null +++ b/job-tracker-ui/e2e/analytics-presentation.spec.ts @@ -0,0 +1,168 @@ +import { expect, test, type APIRequestContext, type Page, type TestInfo } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import { translations } from '../src/i18n/translations'; + +const apiUrl = 'http://localhost:5302/api'; +const password = 'E2ePassword123!'; +async function login(api: APIRequestContext, email: string) { + expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); + const me = await (await api.get(`${apiUrl}/auth/me`)).json(); + const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; + return { me, headers: { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string } }; +} +async function capture(page: Page, info: TestInfo, name: string) { + const path = info.outputPath(`${name}.png`); + await page.screenshot({ path, fullPage: true }); + await info.attach(name, { path, contentType: 'image/png' }); +} + +test('[PRES-S01 S02 E07 E08 E09 J05 J09] Analytics native reports, direct entry, period, drill-down and owner isolation', async ({ page, playwright }, info) => { + test.setTimeout(240000); + const admin = await playwright.request.newContext(); + const { headers: adminHeaders } = await login(admin, 'e2e@example.test'); + const email = `analytics-${Date.now()}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); + const { me, headers } = await login(page.request, email); + await page.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort()); + const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys langsiktige karrieremuligheter æøå', source: 'NAV', location: 'Tromsø' } }); + expect(company.status()).toBe(201); const companyId = (await company.json()).id; + const ids: number[] = []; + const now = new Date(); + for (let index = 0; index < 6; index++) { + const date = new Date(now.getFullYear(), now.getMonth() - (index === 5 ? 14 : index % 3), 3).toISOString(); + const data = { companyId, jobTitle: `Utvikler og faglig rådgiver med lang stillingstittel ${index}`, status: ['Applied', 'Interview', 'Offer', 'Faglig vurdering', 'Saved', 'Rejected'][index], dateApplied: index === 4 ? null : date, tags: index === 4 ? null : 'Azure, TypeScript, Drift, Test, Design, Data, Plattform, Sikkerhet, Analyse, Integrasjon, Norsk, Samarbeid', ...(index % 2 ? {} : { salaryMin: 600000, salaryMax: 800000, salaryCurrency: 'NOK', salaryPeriod: 'year' }) }; + const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data }); + expect(created.status()).toBe(201); ids.push((await created.json()).id); + if (index === 1) expect((await page.request.put(`${apiUrl}/jobapplications/${ids[index]}`, { headers, data: { ...data, responseReceived: true, responseDate: date } })).status()).toBe(204); + } + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/analytics?months=6'); + await expect(page.getByRole('heading', { name: 'Analytics', exact: true, level: 1 })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Analytics', exact: true })).toHaveAttribute('aria-current', 'page'); + await expect.poll(() => page.getByRole('button', { name: 'Analytics', exact: true }).evaluate(element => { + const item = element.getBoundingClientRect(); + for (let parent = element.parentElement; parent; parent = parent.parentElement) { + if (getComputedStyle(parent).overflowY === 'auto' && parent.scrollHeight > parent.clientHeight) { + const visible = parent.getBoundingClientRect(); + if (item.top < visible.top - 1 || item.bottom > visible.bottom + 1) return false; + } + } + return true; + })).toBe(true); + await expect(page.getByRole('button', { name: '6 mo' })).toHaveAttribute('aria-pressed', 'true'); + const activity = page.getByRole('region', { name: 'Application activity' }); + const monthly = await (await page.request.get(`${apiUrl}/jobapplications/analytics?months=6`)).json(); + const total = monthly.reduce((sum: number, point: { applied: number }) => sum + point.applied, 0); + const responses = monthly.reduce((sum: number, point: { responses: number }) => sum + point.responses, 0); + expect(total).toBe(4); expect(responses).toBe(1); + await expect(page.getByRole('region', { name: 'Response sources' }).getByText('1 of 6 non-deleted jobs have a recorded response.', { exact: true })).toBeVisible(); + await expect(activity.getByText(`${responses} / ${total} recorded responses`, { exact: false })).toBeVisible(); + for (const point of monthly) { + const month = new Intl.DateTimeFormat('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${point.month}-01T12:00:00Z`)); + await expect(activity.getByRole('row', { name: `${month} ${point.applied} ${point.responses}`, exact: true })).toBeVisible(); + } + await activity.getByText('View monthly values', { exact: true }).focus(); + await page.keyboard.press('Enter'); await expect(activity.getByRole('table')).toBeHidden(); + await page.keyboard.press('Enter'); await expect(activity.getByRole('table')).toBeVisible(); + expect(await activity.getByText('View monthly values', { exact: true }).evaluate(element => parseFloat(getComputedStyle(element).outlineWidth))).toBeGreaterThanOrEqual(3); + await capture(page, info, 'analytics-exact-values-keyboard'); + await page.getByRole('button', { name: '24 mo' }).click(); + await expect(page).toHaveURL(/\/analytics\?months=24$/); + await expect(activity.getByText('1 / 5 recorded responses', { exact: false })).toBeVisible(); + await page.goBack(); await expect(page.getByRole('button', { name: '6 mo' })).toHaveAttribute('aria-pressed', 'true'); + await page.reload(); await expect(activity.getByRole('table')).toBeVisible(); + await page.getByRole('link', { name: 'Faglig vurdering', exact: true }).click(); + await expect(page).toHaveURL(/\/jobs\?status=Faglig\+vurdering$/); + await expect(page.locator(`[data-job-row-id="${ids[3]}"]`)).toBeVisible(); + await expect(page.locator('[data-job-row-id]')).toHaveCount(1); + await page.goBack(); await expect(page.getByRole('button', { name: '6 mo' })).toHaveAttribute('aria-pressed', 'true'); + await page.getByRole('link', { name: 'Nordlys langsiktige karrieremuligheter æøå', exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/jobs\\?companyId=${companyId}$`)); + await expect(page.locator('[data-job-row-id]')).toHaveCount(6); await page.goBack(); + // Retained Dashboard flags must not remove the independent report destination. + await page.evaluate(owner => localStorage.setItem(`dashboardPrefs:${owner}`, JSON.stringify({ cards: false, activity: false, funnel: false, companies: false, skills: false })), me.id); + await page.goto('/dashboard'); await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible(); + await page.getByRole('button', { name: 'Analytics', exact: true }).click(); + await expect(activity.getByRole('table')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Customize dashboard' })).toHaveCount(0); + + for (const mode of ['light', 'dark'] as const) for (const language of ['en', 'nb'] as const) { + const t = translations[language]; + await page.evaluate(({ mode, language }) => { localStorage.setItem('jobtracker.themeMode', mode); localStorage.setItem('uiLanguage', language); }, { mode, language }); + await page.goto('about:blank'); + expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204); + await page.goto('/analytics?months=6'); + await expect(page.getByRole('heading', { name: t.analytics, exact: true, level: 1 })).toBeVisible(); + await expect(page.locator('html')).toHaveAttribute('data-color-scheme', mode); + await expect(page.getByRole('region', { name: t.dashboardApplicationActivity }).getByRole('table')).toBeVisible(); + for (const [width, height, name] of [[1440, 900, 'desktop'], [390, 844, 'phone']] as const) { + await page.setViewportSize({ width, height }); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); + await capture(page, info, `analytics-${name}-${mode}-${language}`); + } + await page.getByRole('link', { name: t.goToDashboard, exact: true }).click(); + await expect(page.getByText(t.dashboardAllHidden, { exact: true })).toBeVisible(); + await page.getByRole('button', { name: t.openNavigation, exact: true }).click(); + await page.getByRole('button', { name: t.analytics, exact: true }).click(); + await expect(page.getByRole('heading', { name: t.analytics, exact: true, level: 1 })).toBeFocused(); + if (language === 'en') { + const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); + await info.attach(`analytics-${mode}-axe`, { body: JSON.stringify({ violations: scan.violations, incomplete: scan.incomplete }), contentType: 'application/json' }); + expect(scan.violations).toEqual([]); + } + } + for (const [width, height] of [[320, 740], [768, 1024], [1920, 1080]]) { + await page.setViewportSize({ width, height }); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); + await capture(page, info, `analytics-${width}-dark-nb`); + } + await page.setViewportSize({ width: 1440, height: 900 }); + await page.getByRole('button', { name: translations.nb.collapseSidebar }).click(); + await expect(page.getByRole('button', { name: translations.nb.analytics, exact: true })).toHaveAttribute('aria-current', 'page'); + await page.getByRole('button', { name: translations.nb.analytics, exact: true }).click(); + await expect(page.getByRole('heading', { name: translations.nb.analytics, exact: true, level: 1 })).toBeVisible(); + // UI account switch disposes old report data; a fresh owner cannot retrieve A's report/job. + const emailB = `analytics-b-${Date.now()}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email: emailB, password, roles: [] } })).status()).toBe(200); + await page.getByRole('button', { name: translations.nb.user, exact: true }).click(); + await page.getByRole('menuitem', { name: translations.nb.signOut, exact: true }).click(); + await expect(page).toHaveURL(/\/login\?sessionChanged=1$/); + await page.goto('about:blank'); + const b = await login(page.request, emailB); + await page.goto('/analytics?months=6'); + await expect(page.getByText('No tracked jobs to report yet.', { exact: true })).toBeVisible(); + await expect(page.getByText('Nordlys langsiktige karrieremuligheter æøå', { exact: true })).toHaveCount(0); + expect((await page.request.get(`${apiUrl}/jobapplications/${ids[0]}`)).status()).toBe(404); + await capture(page, info, 'analytics-empty-new-owner'); + const bCompany = await page.request.post(`${apiUrl}/companies`, { headers: b.headers, data: { name: 'First employer' } }); + expect(bCompany.status()).toBe(201); + expect((await page.request.post(`${apiUrl}/jobapplications`, { headers: b.headers, data: { companyId: (await bCompany.json()).id, jobTitle: 'First application', status: 'Applied', dateApplied: now.toISOString() } })).status()).toBe(201); + await page.reload(); await expect(page.getByText('0 / 1 recorded responses', { exact: false })).toBeVisible(); + await capture(page, info, 'analytics-one-application'); + await admin.dispose(); +}); + +test('[PRES-S01 E08] Analytics independent loading and failure states never masquerade as empty reports', async ({ page }, info) => { + await login(page.request, 'e2e@example.test'); + let failSnapshot = false; let failTrends = true; let release: (() => void) | undefined; + const hold = new Promise(resolve => { release = resolve; }); + await page.route(`${apiUrl}/jobapplications/stats`, async route => { await hold; return failSnapshot ? route.fulfill({ status: 503, json: { message: 'Unavailable' } }) : route.fulfill({ json: { total: 4, active: 4, byStatus: { Applied: 4 } } }); }); + await page.route(`${apiUrl}/jobapplications/analytics-overview`, route => route.fulfill({ json: { funnel: [{ label: 'Applied', count: 4 }], topCompanies: [], responseRateBySource: [] } })); + await page.route(`${apiUrl}/jobapplications/analytics?*`, async route => { await hold; return failTrends ? route.fulfill({ status: 503, json: { message: 'Unavailable' } }) : route.fulfill({ json: [{ month: '2026-08', applied: 0, responses: 0 }, { month: '2026-09', applied: 0, responses: 0 }] }); }); + await page.goto('/analytics'); await expect(page.getByRole('progressbar').first()).toBeVisible(); + await capture(page, info, 'analytics-loading'); release!(); + await expect(page.getByText('Unable to load report trends')).toBeVisible(); + await expect(page.getByRole('region', { name: 'Current pipeline' })).toBeVisible(); + await capture(page, info, 'analytics-trends-error'); + failTrends = false; await page.getByRole('button', { name: 'Retry', exact: true }).click(); + await expect(page.getByText('No applications or dated responses in this period.')).toBeVisible(); + await capture(page, info, 'analytics-zero-history'); + failSnapshot = true; await page.reload(); + await expect(page.getByText('Unable to load current reports')).toBeVisible(); + await expect(page.getByText(translations.en.analyticsSnapshotUnavailableBody, { exact: true })).toBeVisible(); + await expect(page.getByRole('region', { name: 'Application activity' })).toBeVisible(); + await expect(page.getByText('No tracked jobs to report yet.')).toHaveCount(0); + await capture(page, info, 'analytics-snapshot-error'); + failSnapshot = false; await page.getByRole('button', { name: 'Retry', exact: true }).click(); + await expect(page.getByRole('region', { name: 'Current pipeline' })).toBeVisible(); +}); diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index 8f2abe2..ab9f3df 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -17,6 +17,7 @@ import MemoryIcon from "@mui/icons-material/Memory"; import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; import MailOutlineIcon from "@mui/icons-material/MailOutline"; import CompareArrowsIcon from "@mui/icons-material/CompareArrows"; +import InsightsOutlinedIcon from "@mui/icons-material/InsightsOutlined"; import { Navigate, Route, Routes, useLocation, useNavigate, useParams, createBrowserRouter, RouterProvider } from "react-router-dom"; @@ -46,6 +47,7 @@ import { isSessionInvalidated, SESSION_INVALIDATED_EVENT } from "./sessionBounda const AddJobModal = lazy(() => import("./components/AddJobModal")); const DashboardView = lazy(() => import("./components/DashboardView")); +const AnalyticsPage = lazy(() => import("./views/AnalyticsPage")); const JobDiscoveryPage = lazy(() => import("./views/JobDiscoveryPage")); const CompaniesTable = lazy(() => import("./components/CompaniesTable")); const SettingsView = lazy(() => import("./components/SettingsView")); @@ -94,7 +96,8 @@ type MeResponse = { }; function breadcrumbsFor(path: string, t: (k: any) => string): string[] { - if (path.startsWith("/dashboard")) return [t("home"), t("analytics"), t("overview")]; + if (path.startsWith("/dashboard")) return [t("home"), t("dashboard")]; + if (path.startsWith("/analytics")) return [t("home"), t("insights"), t("analytics")]; if (path.startsWith("/discover")) return [t("home"), t("discoverJobs")]; if (/^\/jobs\/\d+/.test(path)) return [t("home"), t("jobApplications"), t("jobDetails")]; if (path.startsWith("/jobs")) return [t("home"), t("jobApplications")]; @@ -119,6 +122,7 @@ function breadcrumbsFor(path: string, t: (k: any) => string): string[] { function titleFor(path: string, t: (k: any) => string): string { if (path === "/dashboard") return t("dashboard"); + if (path.startsWith("/analytics")) return t("analytics"); if (path.startsWith("/reminders")) return t("reminders"); if (path.startsWith("/operations")) return t("operations"); if (path.startsWith("/discover")) return t("discoverJobs"); @@ -144,6 +148,7 @@ function titleFor(path: string, t: (k: any) => string): string { function subtitleFor(path: string, t: (k: any) => string): string | undefined { if (/^\/settings\/?$/.test(path)) return t("settingsPageDescription"); if (path === "/dashboard") return t("dashboardPageSubtitle"); + if (path.startsWith("/analytics")) return t("analyticsPageSubtitle"); if (path.startsWith("/discover")) return t("discoverJobsSubtitle"); if (/^\/jobs\/\d+/.test(path)) return t("jobDetailsWorkspaceSubtitle"); if (path.startsWith("/jobs")) return t("jobsPageSubtitle"); @@ -306,6 +311,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo { to: "/correspondence", label: t("correspondenceInbox"), icon: , section: t("jobSearch") }, { to: "/career", label: t("careerWorkspaceProfileTab"), icon: , section: t("career") }, { to: "/career/builder", label: t("cvBuilder"), icon: , section: t("career") }, + { to: "/analytics", label: t("analytics"), icon: , section: t("insights") }, ]; const navBottom: NavItem[] = [ @@ -387,6 +393,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo } /> } /> + } /> } /> } /> } /> diff --git a/job-tracker-ui/src/analytics-presentation.test.tsx b/job-tracker-ui/src/analytics-presentation.test.tsx new file mode 100644 index 0000000..28f29c3 --- /dev/null +++ b/job-tracker-ui/src/analytics-presentation.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; +import AnalyticsPage from './views/AnalyticsPage'; +import { I18nProvider } from './i18n/I18nProvider'; +import { api } from './api'; +import { getUserKeyFromToken } from './themePrefs'; + +const mockedApi = api as jest.Mocked; +const snapshot = { total: 5, active: 4, byStatus: { Applied: 2, 'Faglig vurdering': 2 } }; +const overview = { funnel: [{ label: 'Applied', count: 2 }], responseRateBySource: [{ label: 'NAV', total: 4, responses: 1, rate: 25 }], topCompanies: [{ companyId: 7, company: 'Nordlys æøå', count: 4, responses: 1, responseRate: 25 }], timeInStage: [{ stage: 'Applied', medianDays: 3, count: 2 }], salaryInsights: [{ currency: 'NOK', period: 'year', count: 2, minimum: 600000, maximum: 800000, averageMidpoint: 700000 }] }; +function response(url: string) { + if (url.endsWith('/stats')) return snapshot; + if (url.endsWith('/analytics-overview')) return overview; + if (url.endsWith('/analytics')) return [{ month: '2026-08', applied: 2, responses: 1 }, { month: '2026-09', applied: 1, responses: 0 }]; + if (url.endsWith('/tags')) return Array.from({ length: 10 }, (_, i) => ({ tag: `Tag ${i}`, count: i + 1 })); + if (url.endsWith('/tag-trends')) return { months: ['2026-08', '2026-09'], series: [{ tag: 'Azure', counts: [2, 1] }] }; + return []; +} +function History() { const location = useLocation(); const navigate = useNavigate(); return <>{location.pathname}{location.search}; } +function open(entry = '/analytics?months=6') { return render(); } +beforeEach(() => { localStorage.clear(); mockedApi.get.mockReset(); mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: response(url) }) as any); }); + +test('[PRES-S01 S02] all existing reports have exact values independent of hidden Dashboard flags', async () => { + const key = `dashboardPrefs:${getUserKeyFromToken()}`; + localStorage.setItem(key, JSON.stringify({ cards: false, activity: false, funnel: false, companies: false, skills: false })); + const stored = localStorage.getItem(key); + open(); + const activity = await screen.findByRole('region', { name: 'Application activity' }); + expect(within(activity).getByText(/1 \/ 3 recorded responses/)).toBeVisible(); + expect(within(activity).getByRole('row', { name: 'August 2026 2 1' })).toBeVisible(); + for (const name of ['Current pipeline', 'Median current-stage age', 'Response sources', 'Salary insights', 'Top companies by activity', 'Job tags']) expect(screen.getByRole('region', { name })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Faglig vurdering' })).toBeVisible(); + expect(screen.getByText('Tag 9')).toBeVisible(); + expect(screen.getByRole('row', { name: 'September 2026 1' })).toBeVisible(); + expect(screen.queryByRole('button', { name: 'Customize dashboard' })).not.toBeInTheDocument(); + expect(mockedApi.get.mock.calls.some(([url]) => String(url).includes('reminders'))).toBe(false); + expect(localStorage.getItem(key)).toBe(stored); +}); + +test('[PRES-S01 E07] period is direct-loadable and browser Back restores the period without refetching snapshot', async () => { + open('/analytics?months=6&origin=retained'); + await screen.findByRole('region', { name: 'Application activity' }); + expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/analytics', { params: { months: 6 } }); + mockedApi.get.mockClear(); fireEvent.click(screen.getByRole('button', { name: '24 mo' })); + await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/tag-trends', { params: { months: 24, limit: 5 } })); + expect(screen.getByLabelText('Destination')).toHaveTextContent('/analytics?months=24&origin=retained'); + expect(mockedApi.get).not.toHaveBeenCalledWith('/jobapplications/stats'); + fireEvent.click(screen.getByRole('button', { name: 'Back' })); + await waitFor(() => expect(screen.getByRole('button', { name: '6 mo' })).toHaveAttribute('aria-pressed', 'true')); +}); + +test('[PRES-S01] failed snapshot remains an error while trends and retry stay available', async () => { + let fail = true; + mockedApi.get.mockImplementation((url: string) => url.endsWith('/stats') && fail ? Promise.reject(new Error('offline')) : Promise.resolve({ data: response(url) }) as any); + open(); await screen.findByText('Unable to load current reports'); + expect(screen.getByText('Current job reports are unavailable right now. Application activity and tag trends load separately.')).toBeVisible(); + expect(screen.queryByText('The dashboard summary is unavailable right now.')).not.toBeInTheDocument(); + expect(await screen.findByRole('region', { name: 'Application activity' })).toBeVisible(); + expect(screen.queryByText('No tracked jobs to report yet.')).not.toBeInTheDocument(); + fail = false; fireEvent.click(screen.getByRole('button', { name: 'Retry' })); + expect(await screen.findByRole('link', { name: 'Faglig vurdering' })).toBeVisible(); +}); + +test('[PRES-S01] empty cohorts do not invent a denominator, and unsupported URL periods default safely', async () => { + mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url.endsWith('/stats') ? { total: 0, active: 0, byStatus: {} } : url.endsWith('/analytics-overview') ? { funnel: [], responseRateBySource: [], topCompanies: [] } : url.endsWith('/analytics') ? [{ month: '2026-09', applied: 0, responses: 0 }] : url.endsWith('/tag-trends') ? { months: [], series: [] } : [] }) as any); + open('/analytics?months=999'); await screen.findByText('No tracked jobs to report yet.'); + expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/analytics', { params: { months: 12 } }); + expect(screen.queryByText(/0%/)).not.toBeInTheDocument(); + expect(screen.getByText('No applications or dated responses in this period.')).toBeVisible(); +}); + +test('[PRES-J05 J09] drill-down retains supported raw status and company identities', async () => { + open(); fireEvent.click(await screen.findByRole('link', { name: 'Faglig vurdering' })); + expect(screen.getByLabelText('Destination')).toHaveTextContent('/jobs?status=Faglig+vurdering'); + fireEvent.click(screen.getByRole('link', { name: 'Nordlys æøå' })); + expect(screen.getByLabelText('Destination')).toHaveTextContent('/jobs?companyId=7'); +}); diff --git a/job-tracker-ui/src/components/DashboardView.tsx b/job-tracker-ui/src/components/DashboardView.tsx index 7ccc59e..deb0627 100644 --- a/job-tracker-ui/src/components/DashboardView.tsx +++ b/job-tracker-ui/src/components/DashboardView.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo, useState } from "react"; -import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { Link as RouterLink, useNavigate, useSearchParams } from "react-router-dom"; import { Box, @@ -88,18 +88,29 @@ function reminderTone(job: ReminderJob): "error" | "warning" | "info" { return "info"; } -export default function DashboardView() { +export default function DashboardView({ mode = "dashboard" }: { mode?: "dashboard" | "analytics" }) { const theme = useTheme(); const navigate = useNavigate(); const { t, language } = useI18n(); - const [months, setMonths] = useState<6 | 12 | 24>(12); - const [prefs, setPrefs] = useState(() => loadPrefs()); + const reporting = mode === "analytics"; + const [query, setQuery] = useSearchParams(); + const [dashboardMonths, setDashboardMonths] = useState<6 | 12 | 24>(12); + const requestedMonths = Number(query.get("months")); + const months = reporting ? (requestedMonths === 6 || requestedMonths === 24 ? requestedMonths : 12) : dashboardMonths; + const setMonths = (value: 6 | 12 | 24) => { + if (!reporting) { setDashboardMonths(value); return; } + const next = new URLSearchParams(query); + next.set("months", String(value)); + setQuery(next); + }; + const [dashboardPrefs, setPrefs] = useState(() => reporting ? { cards: true, activity: true, funnel: true, companies: true, skills: true } : loadPrefs()); + const prefs = reporting ? { cards: true, activity: true, funnel: true, companies: true, skills: true } : dashboardPrefs; const [prefsAnchor, setPrefsAnchor] = useState(null); const loadSummary = useCallback(async () => { const [statsResponse, overviewResponse, remindersResponse] = await Promise.all([ api.get("/jobapplications/stats"), api.get("/jobapplications/analytics-overview"), - api.get("/jobapplications/reminders", { params: { upcomingDays: 14 } }), + reporting ? Promise.resolve({ data: [] as ReminderJob[] }) : api.get("/jobapplications/reminders", { params: { upcomingDays: 14 } }), ]); return { @@ -107,14 +118,14 @@ export default function DashboardView() { overview: overviewResponse.data, reminderJobs: Array.isArray(remindersResponse.data) ? remindersResponse.data : [], }; - }, []); + }, [reporting]); const summaryResource = useViewResource( loadSummary, { initialData: { stats: null as JobStats | null, overview: null as OverviewAnalytics | null, reminderJobs: [] as ReminderJob[] }, - errorMessage: t("dashboardSummaryUnavailableTitle"), - deps: [t], + errorMessage: t(reporting ? "analyticsSnapshotUnavailableTitle" : "dashboardSummaryUnavailableTitle"), + deps: [t, reporting], }, ); @@ -137,7 +148,7 @@ export default function DashboardView() { loadTrends, { initialData: { analytics: [] as AnalyticsPoint[], tags: [] as TagPoint[], tagTrends: null as TagTrendResponse | null }, - errorMessage: t("dashboardTrendsUnavailableTitle"), + errorMessage: t(reporting ? "analyticsTrendsUnavailableTitle" : "dashboardTrendsUnavailableTitle"), deps: [months, t], }, ); @@ -156,7 +167,7 @@ export default function DashboardView() { const monthLabel = (key: string) => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(new Date(key + "-01T12:00:00Z")); const summaryReady = !summaryResource.loading && !summaryResource.refreshing && !summaryResource.error; const trendsReady = !trendsResource.loading && !trendsResource.refreshing && !trendsResource.error; - const hasJobs = (stats?.total ?? 0) > 0; + const hasJobs = ((reporting ? stats?.active : stats?.total) ?? 0) > 0; const hasActivity = analytics.some(point => point.applied > 0 || point.responses > 0); const totalApplied = analytics.reduce((sum, point) => sum + point.applied, 0); const totalResponses = analytics.reduce((sum, point) => sum + point.responses, 0); @@ -187,13 +198,33 @@ export default function DashboardView() { ; 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 = ; + const pipelineReport = + + + {funnel.map(item => + {statusLabel(t, item.label)}{number(item.count)} + {bar(item.count, stats?.active ?? 0, statusTone(item.label) === "default" ? "text.secondary" : `${statusTone(item.label)}.main`)} + )} + {!funnel.length ? {t("dashboardNoPipelineData")} : null} + + + {t("dashboardRecordedStatuses")} + + {Object.entries(stats?.byStatus ?? {}).map(([status, count]) => + {statusLabel(t, status)} + {number(count)} + )} + + + + ; return ( - + {!reporting ? : null} - {t("dashboardSnapshotScope")} - {prefsButton} + {t(reporting ? "analyticsScope" : "dashboardSnapshotScope")} + {reporting ? : prefsButton} setPrefsAnchor(null)}> {([ @@ -208,14 +239,14 @@ export default function DashboardView() { - {summaryReady && !hasJobs ? + {summaryReady && !hasJobs ? {t("onboardingStepJobAction")}}> {t("careerWorkspaceProfileTab")} : null} - {allHidden ? {t("dashboardAllHidden")} : null} + {!reporting && allHidden ? {t("dashboardAllHidden")} : null} - {summaryReady && hasJobs && prefs.cards ? ( + {!reporting && summaryReady && hasJobs && prefs.cards ? ( {[ { label: t("dashboardTrackedJobs"), value: stats!.active, caption: t("dashboardTrackedBody"), to: "/jobs" }, @@ -232,8 +263,8 @@ export default function DashboardView() { ) : null} - {summaryReady && hasJobs ? - {t("reminders")}}> + {!reporting && summaryReady && hasJobs ? + {!reporting ? {t("reminders")}}> {t("dashboardAttentionSample", { count: number(reminderJobs.length), shown: number(Math.min(5, reminderJobs.length)) })} {t("dashboardAttentionBreakdown", { followUp: number(reminderJobs.filter(job => job.workflowSignal?.actionKey === "follow-up").length), @@ -254,37 +285,20 @@ export default function DashboardView() { })} {!reminderJobs.length ? {t("remindersNothing")} : null} - - {prefs.funnel ? - - {funnel.map(item => - {statusLabel(t, item.label)}{number(item.count)} - {bar(item.count, stats?.active ?? 0, statusTone(item.label) === "default" ? "text.secondary" : `${statusTone(item.label)}.main`)} - )} - {!funnel.length ? {t("dashboardNoPipelineData")} : null} - - - {t("dashboardRecordedStatuses")} - - {Object.entries(stats?.byStatus ?? {}).map(([status, count]) => - {statusLabel(t, status)} - {number(count)} - )} - - : null} + {prefs.funnel ? pipelineReport : null} : null} + title={t(reporting ? "analyticsTrendsUnavailableTitle" : "dashboardTrendsUnavailableTitle")} description={t("dashboardTrendsUnavailableBody")} onRetry={trendsResource.reload} compact /> {/* Period affects only activity and tag trends, not the current summary or all-time tags. */} - {(hasJobs || hasActivity || tags.length > 0) && (prefs.activity || prefs.skills) ? + {(reporting || hasJobs || hasActivity || tags.length > 0) && (prefs.activity || prefs.skills) ? {t("dashboardTrendPeriod")} {([6, 12, 24] as const).map(value => )} : null} - {trendsReady && (hasJobs || hasActivity) && prefs.activity ? + {trendsReady && (reporting || hasJobs || hasActivity) && prefs.activity ? {monthSpan} {totalApplied > 0 ? {t("dashboardResponseSample", { responses: number(totalResponses), total: number(totalApplied) })} ({number(totalResponses / totalApplied * 100)}%) : null} @@ -301,7 +315,7 @@ export default function DashboardView() { {t("dashboardChartLegend", { max: number(chart.max) })} } - + {t("dashboardMonthlyValues")} {(["dashboardMonth", "dashboardApplications", "dashboardResponsesLogged"] as const).map(key => {t(key)})} @@ -310,6 +324,7 @@ export default function DashboardView() { : null} + {reporting && summaryReady && hasJobs ? pipelineReport : null} {summaryReady && hasJobs ? {(overview?.timeInStage?.length ?? 0) > 0 ? {overview!.timeInStage!.map(item => @@ -346,8 +361,8 @@ export default function DashboardView() { )} {!overview?.topCompanies.length ? {t("companiesEmpty")} : null} : null} - {trendsReady && (hasJobs || tags.length > 0) && prefs.skills ? - {tags.slice(0, 8).map(tag => + {trendsReady && (reporting || hasJobs || tags.length > 0) && prefs.skills ? + {tags.slice(0, reporting ? 10 : 8).map(tag => {tag.tag}{number(tag.count)} {bar(tag.count, Math.max(1, ...tags.map(item => item.count)), "primary.main")} )} @@ -362,7 +377,7 @@ export default function DashboardView() { )} : null} - + {t("dashboardTagTrendValues")} {t("dashboardTagTrendBody")} {!tagTrends?.series.length ? {t("dashboardNoTagTrendData")} : tagTrends.series.map(series => diff --git a/job-tracker-ui/src/components/QuickCommandDialog.tsx b/job-tracker-ui/src/components/QuickCommandDialog.tsx index 3fcd6f8..f359117 100644 --- a/job-tracker-ui/src/components/QuickCommandDialog.tsx +++ b/job-tracker-ui/src/components/QuickCommandDialog.tsx @@ -97,7 +97,7 @@ export default function QuickCommandDialog({ open, onClose, onNavigate, onOpenAd const commands = useMemo(() => { const base: CommandItem[] = [ - { id: "go-dashboard", label: t("goToDashboard"), hint: t("analyticsOverview"), action: () => onNavigate("/dashboard") }, + { id: "go-dashboard", label: t("goToDashboard"), hint: t("dashboardPageSubtitle"), action: () => onNavigate("/dashboard") }, { id: "go-jobs", label: t("goToJobs"), hint: t("mainApplicationsTable"), action: () => onNavigate("/jobs") }, { id: "go-reminders", label: t("goToReminders"), hint: t("followUpQueue"), action: () => onNavigate("/reminders") }, { id: "go-companies", label: t("goToCompanies"), hint: t("crmAndSourceTracking"), action: () => onNavigate("/companies") }, diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index f42302c..c4c096b 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -30,6 +30,15 @@ export const translations = { location: "Location", home: "Home", analytics: "Analytics", + insights: "Insights", + analyticsPageSubtitle: "Explore recorded application trends and current job breakdowns.", + analyticsScope: "The period below applies only to application activity and monthly tag trends. Pipeline, stage ages, sources, companies and salary describe your current non-deleted jobs; tag totals are all-time. Reports do not measure historical conversion.", + analyticsTagsBody: "Top ten job tags across all non-deleted jobs. All time; not verified profile skills.", + analyticsEmptyTitle: "No tracked jobs to report yet.", + analyticsEmptyBody: "Add a job to start recording your search. Reports use your own saved data, with no sample statistics.", + analyticsSnapshotUnavailableTitle: "Unable to load current reports", + analyticsSnapshotUnavailableBody: "Current job reports are unavailable right now. Application activity and tag trends load separately.", + analyticsTrendsUnavailableTitle: "Unable to load report trends", overview: "Overview", dashboardPageSubtitle: "Your next actions, current pipeline and application activity.", jobsPageSubtitle: "Filter, search, and manage every application in one workspace.", @@ -2535,6 +2544,15 @@ export const translations = { location: "Sted", home: "Hjem", analytics: "Analyse", + insights: "Innsikt", + analyticsPageSubtitle: "Utforsk registrerte søknadstrender og fordelinger for nåværende jobber.", + analyticsScope: "Perioden nedenfor gjelder bare søknadsaktivitet og månedlige taggtrender. Stillingsløp, tid i fase, kilder, selskaper og lønn beskriver nåværende jobber som ikke er slettet; samlet taggantall gjelder alle perioder. Rapportene måler ikke historisk konvertering.", + analyticsTagsBody: "De ti vanligste jobbtaggene på alle jobber som ikke er slettet. Alle perioder; ikke bekreftede ferdigheter fra profilen.", + analyticsEmptyTitle: "Ingen registrerte jobber å rapportere ennå.", + analyticsEmptyBody: "Legg til en jobb for å begynne å registrere søket ditt. Rapportene bruker dine egne lagrede data, uten eksempelstatistikk.", + analyticsSnapshotUnavailableTitle: "Kunne ikke laste nåværende rapporter", + analyticsSnapshotUnavailableBody: "Rapportene for nåværende jobber er ikke tilgjengelige akkurat nå. Søknadsaktivitet og taggtrender lastes separat.", + analyticsTrendsUnavailableTitle: "Kunne ikke laste rapporttrender", overview: "Oversikt", dashboardPageSubtitle: "Neste steg, nåværende statusfordeling og søknadsaktivitet.", jobsPageSubtitle: "Filtrer, søk og administrer alle søknader på ett sted.", diff --git a/job-tracker-ui/src/layout/AppShell.tsx b/job-tracker-ui/src/layout/AppShell.tsx index bd9c680..e471325 100644 --- a/job-tracker-ui/src/layout/AppShell.tsx +++ b/job-tracker-ui/src/layout/AppShell.tsx @@ -216,6 +216,14 @@ export default function AppShell({ () => activeNavItemTo(pathname, [...nav, ...navBottom]), [pathname, nav, navBottom], ); + const desktopNavigationRef = useRef(null); + const mobileNavigationRef = useRef(null); + // New destinations can extend the existing scrollable list below its initial viewport. + // Reveal the actual current item on entry, without resetting a user's later manual scroll. + useEffect(() => { + const navigation = isMobile ? mobileNavigationRef.current : desktopNavigationRef.current; + navigation?.querySelector('[aria-current="page"]')?.scrollIntoView?.({ block: "nearest", inline: "nearest" }); + }, [activeTo, navCollapsed, isMobile, drawerOpen, headerHeight]); const renderNavList = (groups: Array<[string, NavItem[]]>) => ( @@ -577,6 +585,7 @@ export default function AppShell({ onToggleDrawer(false)} diff --git a/job-tracker-ui/src/views/AnalyticsPage.tsx b/job-tracker-ui/src/views/AnalyticsPage.tsx new file mode 100644 index 0000000..58ece9c --- /dev/null +++ b/job-tracker-ui/src/views/AnalyticsPage.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import DashboardView from "../components/DashboardView"; + +// One report/data owner; the detail destination never consumes Dashboard visibility flags. +export default function AnalyticsPage() { + return ; +}