diff --git a/docs/redesign/phase-2/TRACEABILITY.md b/docs/redesign/phase-2/TRACEABILITY.md index 1a790f8..7e2c26c 100644 --- a/docs/redesign/phase-2/TRACEABILITY.md +++ b/docs/redesign/phase-2/TRACEABILITY.md @@ -1,5 +1,7 @@ # Redesign traceability +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. Phase 5 planning: [57-contract change impact](../phase-5/PRESERVATION_IMPACT.md), [navigation migration](../phase-5/NAVIGATION_MIGRATION.md) and [visual evidence protocol](../phase-5/VISUAL_ACCEPTANCE.md). The successor [P5-00 readiness trace](../phase-5/p5-00/README.md#preservation-trace) records bounded shell prerequisite changes and actual evidence. P5-01 visible redesign has not begun; baseline, package/CV and production conditions remain explicit. diff --git a/docs/redesign/phase-5/OPEN_BLOCKERS.md b/docs/redesign/phase-5/OPEN_BLOCKERS.md index b5123db..aa0e521 100644 --- a/docs/redesign/phase-5/OPEN_BLOCKERS.md +++ b/docs/redesign/phase-5/OPEN_BLOCKERS.md @@ -25,7 +25,8 @@ 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 | Open source-confirmed semantic limitations | **Blocks P5-06/07 accurate presentation acceptance**; labels/values tests within those slices, backend metrics/data repair separate | +| 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 | +| 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) | | 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 14739d5..171e664 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 | 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 evidence](p5-06/README.md) supplies the Dashboard presentation proof; P5-07 must revalidate the destination it changes | 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 c298bf7..b88ae8f 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-04b Table/Board integration](p5-04b/README.md).** Jobs now has two URL-addressable views with shared filters, matching membership and a retained `/kanban` alias. The [query-readiness checkpoint](p5-04b-readiness/README.md) remains the backend contract evidence; P5-01–P5-04a records remain authoritative for their scopes. P5-02 manual screen-reader acceptance remains mandatory for P5-15. P2-D01, F04, native touch verification, Board scaling and recorded baseline timing observations remain open. P5-06 and B07 have not started. +**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. 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-06/README.md b/docs/redesign/phase-5/p5-06/README.md new file mode 100644 index 0000000..13a5474 --- /dev/null +++ b/docs/redesign/phase-5/p5-06/README.md @@ -0,0 +1,146 @@ +# P5-06 — Personal Dashboard presentation + +Source: `384dfa8a4b619fb1dec1d4e650f863f7f6fd5ebd`, `codex/ui-redesign-phase4`. Scope: [approved P5-06](../PHASE5_PLAN.md#p5-06--personal-dashboard-presentation), [metric definitions](../../phase-3/DASHBOARD_ARCHITECTURE.md), [visual protocol](../VISUAL_ACCEPTANCE.md). No P5-07, Phase 8 or B07 work. The containing commit is this slice's implementation checkpoint; validation results below determine acceptance. + +## Inspection and data contract, before implementation + +All six existing reads are owner-scoped by the API. Summary uses stats, analytics-overview and reminders(upcomingDays=14); trends uses analytics(months), tags(limit=10) and tag-trends(months,limit=5). Existing summary/trends failure boundaries are independent; preserve them without adding an endpoint or global job crawl. + +| Data / proposed presentation | Availability | Actual definition / limit | +|---|---|---| +| Tracked jobs | Direct stats.active | Non-deleted jobs, including prospects and closed statuses; never call this in-flight applications | +| Last 30 days | Direct stats.appliedLast30Days | DateApplied >= server-local now minus 30 days; existing query has no upper-date bound. Disclose recorded dates, not growth or a weekly target | +| Current interviews/offers | Direct overview.funnel | Current canonical stage counts; not meetings or historical conversion | +| Attention / preparation / package gaps | Direct reminders and workflowSignal | Existing ranked reminder feed, first five displayed, full Reminders link; interview-prep is preparation, not an appointment | +| Monthly applications and dated responses | Direct analytics | Month-aligned selected application cohort; response events only for jobs applied in that window and response months within it. Totals/ratio safely derived from those returned series; not eventual response probability | +| Pipeline and raw statuses | Direct overview.funnel + stats.byStatus | Canonical snapshot may omit custom values. Show raw breakdown separately, never subtract unlike populations to invent Other | +| Current stage age | Direct overview.timeInStage | Median current occupancy, fallback to DateApplied/SavedAt; not completed duration | +| Source / company reports | Direct overview lists | Current non-deleted jobs, including prospects; responses use flag OR response date. Sources top six, companies top eight; current Company.Source, not original capture source | +| Job tags / monthly tag trends | Direct tags / tag-trends | All-time non-deleted job tags versus selected applied cohort respectively. Tags are not verified user skills; months is ignored by tags endpoint | +| Salary | Direct overview.salaryInsights | Mean of recorded min/max midpoint; currencies/pay periods kept separate, count and range preserved | +| Other returned fields, not previously displayed | Direct stats.total/deleted/averageDaysSinceApplied; overview.totalActive/totalResponses/medianDaysToFirstResponse | total continues to drive existing hasJobs/onboarding behavior; deleted and age are not new widgets. Age averages non-deleted recorded application dates, clamping negative age to zero. Overview totals count non-deleted jobs and response flag OR date; the median uses recorded ResponseDate−DateApplied, clamped to zero, with null for no sample. None proves first-response event history or average response time. These unused fields remain deliberately unpromoted, not unavailable or removed functionality | +| Upcoming appointments, global tasks/activity, sectors, role/location aggregates, CV performance, AI recommendations, goals | Unavailable on current Dashboard reads / future contracts | No inferred schedule from status, no per-job N+1 aggregation, no generated facts, no new widgets pretending these sources exist | + +G-METRICS requires exact labels/transformations, all five owner-local flags and independent error evidence before acceptance. Inspection found a funnel flag that did not hide its panel, independently rescaled activity lines, ambiguous response/active/upcoming captions and hover-only tag values. These are directly owned presentation/preservation gates, not backend metric repair. + +## Design and preservation plan + +Use the existing MUI semantic palette and Archivo typography; no new palette/font/brand. One compact snapshot strip above a two-column working area: attention on the wider side, current pipeline alongside; trends and detailed reports below. One column on phones; no oversized KPI cards or hover-lifting noninteractive surfaces. Reuse PageSection, existing workflow action builder, resource hook and onboarding. Native details/table alternatives keep precise chart values accessible. Keep all five `dashboardPrefs:` keys and the 6/12/24 month control; no order/resize/reset or storage migration. + +| PRES contracts | Existing behavior → P5-06 change | Required evidence | +|---|---|---| +| S01 | All summary/attention/chart/detail reports → meaningful hierarchy, honest populations, exact accessible values | daily-control-loop, workflow-trust-signals, StageAnalytics/AnalyticsSalary; new Dashboard fixture and native populated/partial-error proof | +| S02 | Five owner-local visibility flags and onboarding actions/dismissal → accessible checkbox menu, explicit all-hidden guidance | Toggle/reload/A→B isolation; original onboarding tests retained | +| E07–E09 | Existing routes, themes/locales, private identity boundary → unchanged providers, localized labels and same API reads | Source link/query tests, full auth/browser regressions, light/dark EN/NB screenshots | +| W11/W12, J05/J09 | Existing reminder/job/company entry points → same workflow paths, supported Jobs filters | Existing attention tests plus native source links; no new task/calendar/AI flow | + +No unrelated component or backend owner is authorized by this plan. Phase 8 can reorganize these plain sections later; P5-06 does not introduce a widget framework. + +This is scoped evidence for the affected contracts, not exhaustive certification of all 57 groups. Other contracts retain their existing tests/route/provider owners; unchanged broad-suite passes do not replace missing provider, production, screen-reader or rendering-fidelity evidence. + +## Implementation and retained behavior + +Application changes are limited to `src/components/DashboardView.tsx` and `src/i18n/translations.ts` in `job-tracker-ui`. `DashboardView` remains the owner, with no new component layer, dependency, chart library, route, provider, API, backend, schema, CI or storage contract. Existing `PageSection`, `ViewStateNotice`, `useViewResource`, `OnboardingChecklist`, `getWorkflowAction`, `buildWorkflowPath` and MUI/Emotion are retained. Dashboard subtitle is localized through its existing key, without changing the AppShell. + +The snapshot is a compact definition list rather than four separate oversized cards. Attention is paired with the current pipeline on wider desktops; both stack at intermediate/phone widths. Existing current-stage ages, source response samples, separate salary groups, company reports, tags and monthly tag trends remain below. Source detail now exposes all six returned sources rather than only the first. Onboarding, dismissal, reminder actions and the complete Reminders destination are retained. The feed still shows its first five jobs, with total/feed category counts stated explicitly. + +Five stored visibility flags retain their existing names, defaults and owner key. Cards controls the snapshot, activity the activity report, funnel the pipeline/raw-status breakdown, companies the company report, skills the job-tag/monthly-tag reports. The previously ineffective funnel flag now hides its intended panel. Attention, stage-age, source and salary reports remain independent of these five flags, as before: hiding the five configurable groups is not a promise to hide every Dashboard section. The customization trigger always remains available, including on empty accounts and with all five groups hidden. No reset/layout/persistence engine was added. + +The menu's single interactive owner is `menuitemcheckbox` with `aria-checked`; decorative checked/unchecked icons replace nested checkbox inputs. Escape returns focus to the trigger through existing MUI behavior. Native `details/summary` controls disclose raw statuses, monthly activity values and every returned tag/month value. Explicit summary tab stops participate in the shared Phase 4 focus-visible rule. Non-interactive metrics/sections remain outside the tab order. + +Activity lines now share one zero-based scale; independent scaling previously made different magnitudes appear comparable. Applications are solid and responses dashed, with readable localized labels, a shared-scale statement and exact native table values. Chart geometry is hidden from assistive technology because equivalent textual data is present. SVG strokes use the existing CSS-variable semantic palette, with the ordinary MUI palette fallback in non-CSS-variable test contexts. Status names use the shared standard-status labels; raw custom status names remain unchanged. Bars are supplemental to names/counts, never the only status signal. + +### Routes, period, errors and empty states + +- Tracked count opens existing `/jobs`; offers opens `/offers`; full attention opens `/reminders`; each attention action uses the unchanged workflow path builder. +- Raw status links use the existing `/jobs?status=…` parser, including custom names. Company links retain `/jobs?companyId=…`. These are existing supported query contracts, proven against native Jobs results; no new preview/workflow route. +- The 6/12/24-month buttons keep local state and reload the trends group only. Captions distinguish activity/tag cohorts from all-time tags and the current summary. Removing the ignored `months` argument from the tags call changes no server population. +- Summary failure leaves successful trends usable; trends failure leaves summary usable. Both retain explicit errors/Retry and existing loading semantics. A failed read is never replaced with a successful empty/zero metric. +- A zero-job account sees existing first-job and Career Profile entry points plus its retained setup checklist. A single saved prospect shows tracked count and a truthful no-activity window; no fabricated conversion percentage or forced onboarding wizard. Zero response samples on real nonempty populations remain valid data, with denominator visible. + +## Regression protection + +`src/dashboard-presentation.test.tsx` adds eight tests: precise populations/monthly values/custom statuses/separate salary groups; all five flags and remount; independent trends failure/retry; independent summary failure; supported status/company URLs; period changes affecting trends only; zero-window behavior; and shared line scaling. Existing daily-control-loop/workflow-trust-signals tests remain unchanged (15 targeted tests across the three files). + +`e2e/dashboard-presentation.spec.ts` adds two browser cases: + +1. Native owner account, company and eight jobs across six applied/current statuses plus Saved/Preparing, a raw custom status, Norwegian names, tags and NOK salary. Response dates are persisted through the existing update DTO, not assumed to exist after create. Compare exact returned month values and sample totals to the actual UI; use keyboard disclosure/focus, native status/company links and Jobs membership; exercise all five flags/reload, UI logout/login A→B→A, a fresh account and one prospect; cover viewport/theme/language combinations and reduced motion. +2. Controlled local transport holds/503s for summary/trends, with actual application rendering. Verify loading, independent failure presentation, Retry and exact recovered values. These fixtures are deliberate error-state simulation, not proof of production metric data. + +Development checks caught and corrected invalid definition-list caption placement and nested interactive inputs in the menu. A transient post-Escape contrast finding came from scanning fading menu DOM: the test now waits for that DOM to unmount, without disabling a rule or claiming that animation frames passed contrast certification. A create-fixture response field was corrected to use the supported update operation. These checks were kept, not weakened. No existing expected failure was renamed or removed. + +The first full browser attempt also received one generic 500 from the unchanged native language-preference endpoint during concurrent Dashboard/setup reads immediately after Browser Back. Trace confirmed the response/status and overlapping reads, but supplied no underlying exception: SQLite contention is a hypothesis, **not a proven root cause**. Visual fixture preparation now configures the native preference before mounting the next Dashboard (blank document → existing preference PUT requiring 204 → Dashboard). No retry, mocked successful preference response, weaker assertion or application/backend repair was introduced. This proves native configured locale rendering, not acceptance of concurrent language writes during loading. That unchanged endpoint's intermittent 500 remains a separate baseline observation pending diagnosis; final full-suite results must be reported separately from this initial failure. + +## Accessibility and visual protocol + +Use native section headings, definition lists, links, tables with column/row headers, details disclosures and one checked menu owner. Exact values remain available without hover, color perception or SVG interpretation. Manual rendered review must inspect typography, secondary text, focus, wrapping and hierarchy; browser assertions additionally exercise keyboard disclosure, a visible 3px focus outline, menu focus entry/Escape return, no document overflow, persisted settings and account boundaries. + +Automated axe scans cover populated phone English light/dark states and the expanded customization menu, with WCAG A/AA rules retained. Violations and incomplete results are recorded separately in the evidence summary. An incomplete result is not a pass. These scans do not certify every chart/table disclosure or locale/state. The P5-02 spoken-name/live-announcement screen-reader acceptance remains **open and mandatory for P5-15**; this slice does not claim a manual screen-reader session, native touch certification, forced-colors or exhaustive zoom validation. + +The final [axe summary](evidence/dashboard-axe-summary.json) records zero detected violations in all four scans. Both expanded-menu scans have incomplete `aria-hidden-focus` (one modal-background node) and `color-contrast` (two nodes); both collapsed-page scans have incomplete `color-contrast` (two nodes). The latter concerns the invisible zero notification badge and a customization trigger whose background could not be resolved due to overlap/focus rendering. These remain manual acceptance items, not automated contrast passes. Menu entry/Escape return and visible focus have separate behavioral evidence; no background isolation or axe rule was disabled. The summary retains rule/check/target information without raw rendered HTML. + +Evidence filenames identify state, theme, language and viewport; the evidence manifest records actual capture dates/dimensions. Populated fixtures are synthetic private development accounts using the real disposable API, never production/user career data. The before image is a historical layout reference captured before this slice: its response seeding/clock differs from the corrected final fixture, so it is not a frozen-data pixel or metric comparison. No credentials, API bodies, cookie storage or raw traces are included in committed evidence. + +### Rendered review — VS-DASHBOARD + +Final static-export screenshots were inspected in all four theme/language pairs at 1440×900 and 390×844, plus dark/Bokmål at 320×740, 768×1024, 1024×768 and 1920×1080. Desktop keeps four compact snapshot values and an attention/pipeline composition; intermediate widths stack reports and use two summary columns; phones use one column, wrapped controls and readable chart captions outside the SVG. No document overflow occurred at those measured widths. Long Norwegian labels/company names remain available; no columns/actions were removed to fit. + +| State / evidence | Observed result | +|---|---| +| [Light English desktop](evidence/dashboard-desktop-light-en.png), [light Bokmål](evidence/dashboard-desktop-light-nb.png), [dark English](evidence/dashboard-desktop-dark-en.png), [dark Bokmål](evidence/dashboard-desktop-dark-nb.png) | Clear snapshot/attention hierarchy, restrained report surfaces, legible secondary text and chart/status colors; no new gradients or noninteractive hover elevation | +| [Light English phone](evidence/dashboard-phone-light-en.png), [light Bokmål phone](evidence/dashboard-phone-light-nb.png), [dark English phone](evidence/dashboard-phone-dark-en.png), [dark Bokmål phone](evidence/dashboard-phone-dark-nb.png) | Actions/period controls wrap, reports stack, names remain readable; shared surfaces/focus and state meaning retained | +| [Minimum 320px](evidence/dashboard-320-dark-nb.png), [tablet 768px](evidence/dashboard-768-dark-nb.png), [desktop threshold 1024px](evidence/dashboard-1024-dark-nb.png), [wide 1920px](evidence/dashboard-1920-dark-nb.png) | Intentional breakpoint reflow, visible customization focus, bounded content width; no missing controls or horizontal document overflow | +| [New account](evidence/dashboard-new-account.png), [one saved prospect](evidence/dashboard-one-prospect-zero-activity.png), [five groups hidden](evidence/dashboard-all-hidden.png) | Optional setup/first-job guidance, no fabricated window response percentage; customization remains available and independent report groups remain intact | +| [Loading](evidence/dashboard-loading.png), [summary error](evidence/dashboard-summary-error.png), [trends error](evidence/dashboard-trends-error.png) | Existing progress/error/Retry treatment; successful independent resource remains usable. Transport fixtures are explicitly synthetic | +| [Keyboard monthly values](evidence/dashboard-monthly-values-keyboard.png) | Native disclosure and exact twelve-month table, visible shared focus outline; keyboard assertion verifies focus remains on the disclosure | + +The keyboard full-page capture includes the fixed shell at the current scroll offset, a full-page stitching limitation. It is evidence of the focused data disclosure, not approval of that stitched header placement. Default/viewport-width captures and unchanged shell regressions separately establish the actual shell composition. The [manifest](evidence/visual-manifest.json) records capture timestamps, viewports, full-page dimensions, fixture kinds and which images were inspected. The before layout reference was captured 2026-09-30; final evidence is from 2026-10-06. + +## Deferred work and Phase 8 boundary + +- No new appointment/global-task/event history, conversion, response-time, role/location/sector aggregate, CV-performance, AI insight or weekly goal is inferred. Existing onboarding supplies CV/Profile entry points; no new first-CV wizard or invented user fact. +- Ordering, resizing, reset and cross-device layout persistence remain Phase 8. Plain sections can be hosted by that later system without inventing it now. The five existing owner-local visibility flags are preserved today. +- P5-07 owns the new destination for retained existing reports. All report families remain on Dashboard at this checkpoint; there is no `/analytics` route change here. +- Existing AppShell Dashboard breadcrumbs still use historical Analytics/Overview metadata; no AppShell/header owner was changed. Reconcile that metadata when P5-07 adds the actual destination. Backend-provided reminder explanations and fallback source labels are rendered as returned, while UI labels/dates/numbers are localized; this slice does not claim that all server-generated prose has Bokmål translations. +- F04 production ingress, P2-D01 native package failure, P5-02 manual screen-reader acceptance, incomplete accessibility checks, Board native touch/full-result scaling, CV revision/rendering gates and unrelated SQLite/toast debt remain open. No production readiness or backend metric/timezone repair is claimed. +- Current application snapshots are not historical funnel conversions. Last-30-day counting includes future recorded dates under the existing query. Current-stage fallback ages and current Company.Source remain the documented server definitions. Correcting those backend/product contracts requires separate work. + +## Validation commands and baseline + +Commands are run through the repository's RTK wrapper; frontend commands use `job-tracker-ui` as working directory. Native browser/security suites run sequentially on their disposable local API, never production. Static-export browser runs set `DOTNET_ROOT=C:/Users/Connor/.dotnet`, `DOTNET_HOST_PATH=C:/Users/Connor/.dotnet/dotnet.exe`, `PLAYWRIGHT_STATIC_EXPORT=true`, `PLAYWRIGHT_PREBUILT_EXPORT=true`. The UI export is built with `NEXT_PUBLIC_API_BASE_URL=http://localhost:5302/api`, `CI=false`. No infrastructure/test-runner/CI workaround or dependency change is needed for this slice. + +| Check | Exact 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` | 398 passed in 79 suites; +8 Dashboard cases from inherited 390; no removed tests | +| Browser | `npm.cmd run test:e2e -- --reporter=line,json` | 79 actual passes (+2), unchanged P2-D01 expected failure; 0 unexpected/skipped/retries. 16m47s. Reporter counts the expected failure in its “80 passed”; this record does not | +| Standalone TypeScript | `npm.cmd run typecheck` | Passed; historical TS2571 is not present | +| Lint | `npm.cmd run lint` | Passed, zero warnings | +| Frontend production export | `npm.cmd run build` | Passed compilation, TypeScript and static generation after final palette 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 tests do not close F04 production evidence | +| 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 | +| Secrets | `python scripts/supply-chain.py scan-secrets` | Passed against staged final candidate | +| Whitespace | `git diff --check` and `git diff --cached --check` | Passed against final candidate | +| Document/evidence checks | Python local Markdown-target and manifest file checks | 110 local document links resolve; all 20 final screenshot files present, 19 individually inspected | + +The first extension command used a nonexistent `background.test.mjs` filename; file inventory established `capture-url.test.mjs`, whose three tests passed. A direct `theme.vars` access initially failed TS18048; the standard MUI palette fallback corrected it before the final export/typecheck. These are recorded validation-development corrections, not baseline defects or suppressed failures. + +The [initial browser summary](evidence/initial-browser-summary.json) preserves the first full run's native preference failure. The [final browser summary](evidence/final-browser-summary.json) records every case's expected and actual outcome: all 77 inherited successful cases remain successful, both new Dashboard cases pass, and the native frozen-package test still fails under its unchanged P2-D01 annotation. No test was removed, skipped or retried to obtain acceptance. Screenshots and summaries were retained before the subsequent API-only suites replaced their working output directories. + +### Performance and rollback + +The Dashboard still makes six aggregate/feed requests across two resource groups, plus unchanged onboarding/shell reads. No request per job, no full Jobs crawl and no new persisted model. Top-company/source/tag lists retain existing bounded API limits; monthly series stay at 6/12/24 months. Exact-value disclosures add at most the returned months × five tag-series rows. Local populated/sparse fixtures establish presentation, not production load certification. + +Rollback is a revert of this presentation/translation/test/document checkpoint; owner preference keys and backend data remain compatible and must not be cleared. No migration, irreversible data write or production deployment is involved. + +## Slice acceptance and stopping point + +P5-06 presentation/preservation acceptance is satisfied within its approved scope: 934 backend, 398 frontend and 79 actual browser passes, plus the unchanged P2-D01 expected failure; rate-limit 9, ingress 6, PDF admission 1 and configuration 3. The increase is exactly eight frontend and two browser cases; other counts are unchanged. TypeScript, lint, both builds and final repository checks pass. Scoped automated keyboard behavior, rendered light/dark EN/Bokmål review and responsive checks pass; incomplete axe results and manual screen-reader acceptance remain explicitly open. + +P5-07 is the recommended next ticket, with G-METRICS revalidation for its destination. It has **not** begun. Native package and CV gates, production F04, Board limitations and P5-06-O01 are not closed by this checkpoint; no Phase 8 or B07 implementation occurred. diff --git a/docs/redesign/phase-5/p5-06/evidence/before-populated-light-en.png b/docs/redesign/phase-5/p5-06/evidence/before-populated-light-en.png new file mode 100644 index 0000000..c1d9464 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/before-populated-light-en.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-1024-dark-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-1024-dark-nb.png new file mode 100644 index 0000000..590341d Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-1024-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-1920-dark-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-1920-dark-nb.png new file mode 100644 index 0000000..edd5c32 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-1920-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-320-dark-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-320-dark-nb.png new file mode 100644 index 0000000..57c9663 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-320-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-768-dark-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-768-dark-nb.png new file mode 100644 index 0000000..28f88e8 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-768-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-all-hidden.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-all-hidden.png new file mode 100644 index 0000000..d1d8a89 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-all-hidden.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-axe-summary.json b/docs/redesign/phase-5/p5-06/evidence/dashboard-axe-summary.json new file mode 100644 index 0000000..68102ab --- /dev/null +++ b/docs/redesign/phase-5/p5-06/evidence/dashboard-axe-summary.json @@ -0,0 +1,196 @@ +[ + { + "name": "dashboard-light-menu-axe", + "result": { + "violations": [], + "incomplete": [ + { + "id": "aria-hidden-focus", + "impact": "serious", + "nodeCount": 1, + "nodes": [ + { + "target": [ + "#root" + ], + "checks": [ + { + "id": "focusable-modal-open", + "message": "Check that focusable elements are not tabbable in the current state", + "messageKey": null + } + ] + } + ] + }, + { + "id": "color-contrast", + "impact": "serious", + "nodeCount": 2, + "nodes": [ + { + "target": [ + ".MuiBadge-invisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element content is too short to determine if it is actual text content", + "messageKey": "shortTextContent" + } + ] + }, + { + "target": [ + ".css-17t4hgu" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element's background color could not be determined because it is overlapped by another element", + "messageKey": "bgOverlap" + } + ] + } + ] + } + ] + } + }, + { + "name": "dashboard-light-axe", + "result": { + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "nodeCount": 2, + "nodes": [ + { + "target": [ + ".MuiBadge-invisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element content is too short to determine if it is actual text content", + "messageKey": "shortTextContent" + } + ] + }, + { + "target": [ + ".Mui-focusVisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element's background color could not be determined because it is overlapped by another element", + "messageKey": "bgOverlap" + } + ] + } + ] + } + ] + } + }, + { + "name": "dashboard-dark-menu-axe", + "result": { + "violations": [], + "incomplete": [ + { + "id": "aria-hidden-focus", + "impact": "serious", + "nodeCount": 1, + "nodes": [ + { + "target": [ + "#root" + ], + "checks": [ + { + "id": "focusable-modal-open", + "message": "Check that focusable elements are not tabbable in the current state", + "messageKey": null + } + ] + } + ] + }, + { + "id": "color-contrast", + "impact": "serious", + "nodeCount": 2, + "nodes": [ + { + "target": [ + ".MuiBadge-invisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element content is too short to determine if it is actual text content", + "messageKey": "shortTextContent" + } + ] + }, + { + "target": [ + ".css-17t4hgu" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element's background color could not be determined because it is overlapped by another element", + "messageKey": "bgOverlap" + } + ] + } + ] + } + ] + } + }, + { + "name": "dashboard-dark-axe", + "result": { + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "nodeCount": 2, + "nodes": [ + { + "target": [ + ".MuiBadge-invisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element content is too short to determine if it is actual text content", + "messageKey": "shortTextContent" + } + ] + }, + { + "target": [ + ".Mui-focusVisible" + ], + "checks": [ + { + "id": "color-contrast", + "message": "Element's background color could not be determined because it is overlapped by another element", + "messageKey": "bgOverlap" + } + ] + } + ] + } + ] + } + } +] diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-dark-en.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-dark-en.png new file mode 100644 index 0000000..4b41bfa Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-dark-en.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-dark-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-dark-nb.png new file mode 100644 index 0000000..73222a5 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-light-en.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-light-en.png new file mode 100644 index 0000000..1be5713 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-light-en.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-light-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-light-nb.png new file mode 100644 index 0000000..6d2903a Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-desktop-light-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-loading.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-loading.png new file mode 100644 index 0000000..429670c Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-loading.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-monthly-values-keyboard.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-monthly-values-keyboard.png new file mode 100644 index 0000000..1bae167 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-monthly-values-keyboard.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-new-account.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-new-account.png new file mode 100644 index 0000000..4c378a7 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-new-account.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-one-prospect-zero-activity.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-one-prospect-zero-activity.png new file mode 100644 index 0000000..4838af1 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-one-prospect-zero-activity.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-dark-en.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-dark-en.png new file mode 100644 index 0000000..c4a113c Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-dark-en.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-dark-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-dark-nb.png new file mode 100644 index 0000000..c37e2bf Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-dark-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-light-en.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-light-en.png new file mode 100644 index 0000000..5e4bc50 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-light-en.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-light-nb.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-light-nb.png new file mode 100644 index 0000000..3e36be1 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-phone-light-nb.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-populated-light-en.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-populated-light-en.png new file mode 100644 index 0000000..b0fa786 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-populated-light-en.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-summary-error.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-summary-error.png new file mode 100644 index 0000000..ecbd084 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-summary-error.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/dashboard-trends-error.png b/docs/redesign/phase-5/p5-06/evidence/dashboard-trends-error.png new file mode 100644 index 0000000..dd14ee3 Binary files /dev/null and b/docs/redesign/phase-5/p5-06/evidence/dashboard-trends-error.png differ diff --git a/docs/redesign/phase-5/p5-06/evidence/final-browser-summary.json b/docs/redesign/phase-5/p5-06/evidence/final-browser-summary.json new file mode 100644 index 0000000..d340d50 --- /dev/null +++ b/docs/redesign/phase-5/p5-06/evidence/final-browser-summary.json @@ -0,0 +1,666 @@ +{ + "stats": { + "startTime": "2026-10-06T16:05:51.931Z", + "duration": 1007080.2740000001, + "expected": 80, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "actualPasses": 79, + "expectedFailures": [ + { + "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", + "actualStatus": "failed", + "outcome": "expected", + "attempts": 1 + } + ], + "unexpected": [], + "skipped": 0, + "retries": 0, + "outcomes": [ + { + "file": "accessibility.spec.ts", + "title": "public entry points meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "accessibility.spec.ts", + "title": "primary authenticated workspaces meet automated WCAG A and AA checks", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "retained profile request from A cannot write using B cookies and fresh B CSRF", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "missing and forged session contexts fail closed while same-account profile saves work", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "account-isolation.spec.ts", + "title": "crafted CV identifiers cannot cross read update delete or job-association ownership", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "board-presentation.spec.ts", + "title": "[PRES-J07 E08] Board loading, retryable error and fully empty columns", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsive presentation", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "dashboard-presentation.spec.ts", + "title": "[PRES-S01 E08] Dashboard loading and independent failed resources retain retry and exact values", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "job-discovery.spec.ts", + "title": "discovery remains usable across widths and opens the reviewed import", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "kanban-theme.spec.ts", + "title": "kanban columns use dark theme surfaces", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "failed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03 PRES-W11] responsive settings navigation keeps rule edits and persists notification preferences", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "redesign-preservation.spec.ts", + "title": "[PRES-S03] rule changes persist through the real API", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/1440 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/en/390 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/1440 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] light/nb/390 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/1440 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/en/390 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/1440 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "semantic-foundations.spec.ts", + "title": "[PRES-E08 PRES-J08] dark/nb/390 loading \u2192 error \u2192 retry \u2192 empty \u2192 populated", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-en utility search and notification states", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] light-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] light-nb utility search and notification states", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-en mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-en utility search and notification states", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-00 PRES-E07 E08 A03] dark-nb mobile labels, focus and admin reflow", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "shell-readiness.spec.ts", + "title": "[P5-02 PRES-E07 E08 E09 S02] dark-nb utility search and notification states", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "API liveness and database readiness are separate public probes", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "public plans stay honest, responsive and keyboard accessible", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "login establishes an authenticated session", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a Free account keeps manual work available while AI actions stay honestly locked", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a saved job can be created through the reviewed UI flow", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "the dedicated application workspace survives deep links, long data and unsaved navigation", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "Career Workspace loads from the authenticated application shell", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "a public CV renders anonymously and downloads as PDF", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + }, + { + "file": "smoke.spec.ts", + "title": "the Code template exports a long structured CV as searchable multi-page PDF", + "expectedStatus": "passed", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "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", + "actualStatus": "passed", + "outcome": "expected", + "attempts": 1 + } + ] +} diff --git a/docs/redesign/phase-5/p5-06/evidence/initial-browser-summary.json b/docs/redesign/phase-5/p5-06/evidence/initial-browser-summary.json new file mode 100644 index 0000000..00b41a3 --- /dev/null +++ b/docs/redesign/phase-5/p5-06/evidence/initial-browser-summary.json @@ -0,0 +1,33 @@ +{ + "sourceBase": "384dfa8a4b619fb1dec1d4e650f863f7f6fd5ebd", + "attempt": "initial full browser run, before visual setup sequencing correction", + "stats": { + "startTime": "2026-10-06T15:46:07.530Z", + "duration": 1127866.337, + "expected": 79, + "skipped": 0, + "unexpected": 1, + "flaky": 0 + }, + "actualPasses": 78, + "expectedFailures": [ + { + "title": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", + "expectedStatus": "failed", + "status": [ + "failed" + ], + "outcome": "expected" + } + ], + "unexpected": [ + { + "title": "[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsive presentation", + "expectedStatus": "passed", + "status": [ + "failed" + ], + "outcome": "unexpected" + } + ] +} diff --git a/docs/redesign/phase-5/p5-06/evidence/visual-manifest.json b/docs/redesign/phase-5/p5-06/evidence/visual-manifest.json new file mode 100644 index 0000000..9ce7102 --- /dev/null +++ b/docs/redesign/phase-5/p5-06/evidence/visual-manifest.json @@ -0,0 +1,287 @@ +{ + "sourceBase": "384dfa8a4b619fb1dec1d4e650f863f7f6fd5ebd", + "browser": "Playwright Chromium; production static export; disposable development API", + "screenReaderAcceptance": "open", + "screenshots": [ + { + "file": "dashboard-1024-dark-nb.png", + "captureTimeUtc": "2026-10-06T16:11:36.336895+00:00", + "imageWidth": 1024, + "fullPageHeight": 3179, + "fixture": "native owner data", + "viewport": { + "width": 1024, + "height": 768 + }, + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-1920-dark-nb.png", + "captureTimeUtc": "2026-10-06T16:11:36.582200+00:00", + "imageWidth": 1920, + "fullPageHeight": 2409, + "fixture": "native owner data", + "viewport": { + "width": 1920, + "height": 1080 + }, + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-320-dark-nb.png", + "captureTimeUtc": "2026-10-06T16:11:35.829015+00:00", + "imageWidth": 320, + "fullPageHeight": 3964, + "fixture": "native owner data", + "viewport": { + "width": 320, + "height": 740 + }, + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-768-dark-nb.png", + "captureTimeUtc": "2026-10-06T16:11:36.023427+00:00", + "imageWidth": 768, + "fullPageHeight": 3217, + "fixture": "native owner data", + "viewport": { + "width": 768, + "height": 1024 + }, + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-all-hidden.png", + "captureTimeUtc": "2026-10-06T16:11:40.079132+00:00", + "imageWidth": 1440, + "fullPageHeight": 1287, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-desktop-dark-en.png", + "captureTimeUtc": "2026-10-06T16:11:30.719719+00:00", + "imageWidth": 1440, + "fullPageHeight": 2409, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-desktop-dark-nb.png", + "captureTimeUtc": "2026-10-06T16:11:34.950734+00:00", + "imageWidth": 1440, + "fullPageHeight": 2429, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-desktop-light-en.png", + "captureTimeUtc": "2026-10-06T16:11:24.873112+00:00", + "imageWidth": 1440, + "fullPageHeight": 2409, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-desktop-light-nb.png", + "captureTimeUtc": "2026-10-06T16:11:28.616782+00:00", + "imageWidth": 1440, + "fullPageHeight": 2429, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "light", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-monthly-values-keyboard.png", + "captureTimeUtc": "2026-10-06T16:11:22.513429+00:00", + "imageWidth": 1440, + "fullPageHeight": 2806, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-new-account.png", + "captureTimeUtc": "2026-10-06T16:11:42.486176+00:00", + "imageWidth": 1440, + "fullPageHeight": 912, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-one-prospect-zero-activity.png", + "captureTimeUtc": "2026-10-06T16:11:44.115501+00:00", + "imageWidth": 1440, + "fullPageHeight": 1733, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-phone-dark-en.png", + "captureTimeUtc": "2026-10-06T16:11:31.003428+00:00", + "imageWidth": 390, + "fullPageHeight": 3694, + "fixture": "native owner data", + "viewport": { + "width": 390, + "height": 844 + }, + "theme": "dark", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-phone-dark-nb.png", + "captureTimeUtc": "2026-10-06T16:11:35.227596+00:00", + "imageWidth": 390, + "fullPageHeight": 3779, + "fixture": "native owner data", + "viewport": { + "width": 390, + "height": 844 + }, + "theme": "dark", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-phone-light-en.png", + "captureTimeUtc": "2026-10-06T16:11:25.067696+00:00", + "imageWidth": 390, + "fullPageHeight": 3694, + "fixture": "native owner data", + "viewport": { + "width": 390, + "height": 844 + }, + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-phone-light-nb.png", + "captureTimeUtc": "2026-10-06T16:11:28.827595+00:00", + "imageWidth": 390, + "fullPageHeight": 3779, + "fixture": "native owner data", + "viewport": { + "width": 390, + "height": 844 + }, + "theme": "light", + "language": "nb", + "renderedReview": true + }, + { + "file": "dashboard-populated-light-en.png", + "captureTimeUtc": "2026-10-06T16:11:22.162451+00:00", + "imageWidth": 1440, + "fullPageHeight": 2409, + "fixture": "native owner data", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "light", + "language": "en", + "renderedReview": false + }, + { + "file": "dashboard-loading.png", + "captureTimeUtc": "2026-10-06T16:11:47.597598+00:00", + "imageWidth": 1280, + "fullPageHeight": 1009, + "fixture": "controlled error/loading transport", + "viewport": { + "width": 1280, + "height": 720 + }, + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-summary-error.png", + "captureTimeUtc": "2026-10-06T16:11:49.074113+00:00", + "imageWidth": 1280, + "fullPageHeight": 1442, + "fixture": "controlled error/loading transport", + "viewport": { + "width": 1280, + "height": 720 + }, + "theme": "light", + "language": "en", + "renderedReview": true + }, + { + "file": "dashboard-trends-error.png", + "captureTimeUtc": "2026-10-06T16:11:47.766971+00:00", + "imageWidth": 1280, + "fullPageHeight": 1453, + "fixture": "controlled error/loading transport", + "viewport": { + "width": 1280, + "height": 720 + }, + "theme": "light", + "language": "en", + "renderedReview": true + } + ] +} diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md index 9948a45..1e06a40 100644 --- a/docs/roadmap/todo.md +++ b/docs/roadmap/todo.md @@ -1,9 +1,12 @@ # TODO -Status reconciled: 2026-09-28. +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-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-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. - **P5-04b query readiness complete — GO for integration's read prerequisite:** shared server query and URL interpretation, full Board versus paged Table ID parity, refresh/focus and failure evidence. [Record](../redesign/phase-5/p5-04b-readiness/README.md). Baseline: 934 backend, 384 frontend, 76 actual browser passes + unchanged P2-D01 expected failure; security 9/6/1, configuration 3. Earlier unchanged command-focus/CV smoke failures are recorded as R01/R02, not silently closed by passing reruns. No integration, P5-06 or B07 implemented; F04/manual screen-reader/native touch remain open. @@ -15,7 +18,7 @@ Status reconciled: 2026-09-28. - **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 — open:** address analytics population/period labels and navigation/query/error retention when those surfaces are next changed. Evidence is in the current [redesign decision records](../redesign/phase-3/OPEN_DECISIONS.md). +- **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). - **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/dashboard-presentation.spec.ts b/job-tracker-ui/e2e/dashboard-presentation.spec.ts new file mode 100644 index 0000000..a35c56d --- /dev/null +++ b/job-tracker-ui/e2e/dashboard-presentation.spec.ts @@ -0,0 +1,190 @@ +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(c => c.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] Dashboard native data, visibility and responsive presentation', async ({ page, playwright }, info) => { + test.setTimeout(240000); + const admin = await playwright.request.newContext(); + const { headers: adminHeaders } = await login(admin, 'e2e@example.test'); + const email = `dashboard-${Date.now()}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); + const { 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 companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Nordlys teknologi og fellestjenester æøå', source: 'NAV', location: 'Tromsø' } }); + expect(companyResponse.status()).toBe(201); + const companyId = (await companyResponse.json()).id; + const now = new Date(); + for (let index = 0; index < 8; index++) { + const date = new Date(now.getFullYear(), now.getMonth() - index % 3, 3).toISOString(); + const data = { companyId, jobTitle: `Plattformutvikler og rådgiver ${index}`, status: ['Applied', 'Waiting', 'Interview', 'Offer', 'Rejected', 'Faglig vurdering', 'Saved', 'Preparing'][index], dateApplied: index < 6 ? date : null, tags: 'TypeScript, Azure', salaryMin: 600000, salaryMax: 800000, salaryCurrency: 'NOK', salaryPeriod: 'year' }; + const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data }); + expect(created.status()).toBe(201); + if (index === 2 || index === 3) { + const id = (await created.json()).id; + expect((await page.request.put(`${apiUrl}/jobapplications/${id}`, { headers, data: { ...data, responseReceived: true, responseDate: date } })).status()).toBe(204); + } + } + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/dashboard'); + await expect(page.getByRole('link', { name: 'Nordlys teknologi og fellestjenester æøå', exact: true })).toBeVisible(); + const dismiss = page.getByRole('button', { name: 'Dismiss setup checklist' }); + if (await dismiss.isVisible()) await dismiss.click(); + await capture(page, info, 'dashboard-populated-light-en'); + const stats = await (await page.request.get(`${apiUrl}/jobapplications/stats`)).json(); + expect(stats.active).toBe(8); + const monthly = await (await page.request.get(`${apiUrl}/jobapplications/analytics?months=12`)).json(); + const applied = monthly.reduce((sum: number, point: { applied: number }) => sum + point.applied, 0); + const responses = monthly.reduce((sum: number, point: { responses: number }) => sum + point.responses, 0); + await expect(page.getByRole('link', { name: 'Tracked jobs: 8' })).toBeVisible(); + await expect(page.getByText(`${responses} / ${applied} recorded responses`, { exact: false })).toBeVisible(); + const activity = page.getByRole('region', { name: 'Application activity' }); + const monthlyControl = activity.getByText('View monthly values'); + await monthlyControl.focus(); await page.keyboard.press('Enter'); + await expect(activity.getByRole('table')).toBeVisible(); + await expect(monthlyControl).toBeFocused(); + const ring = await monthlyControl.evaluate(element => getComputedStyle(element).outlineWidth); + expect(parseFloat(ring)).toBeGreaterThanOrEqual(3); + await capture(page, info, 'dashboard-monthly-values-keyboard'); + 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').click(); + await page.getByText('Recorded status breakdown', { exact: true }).click(); + 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]')).toHaveCount(1); + await page.goBack(); + await page.getByRole('link', { name: 'Nordlys teknologi og fellestjenester æøå', exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/jobs\\?companyId=${companyId}`)); + await expect(page.locator('[data-job-row-id]')).toHaveCount(8); + await page.goBack(); + + 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 }); + // Configure the native preference before mounting the next visual fixture. + 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('/dashboard'); + await expect(page.getByText(t.dashboardTrackedJobs, { exact: true })).toBeVisible(); + await expect(page.locator('html')).toHaveAttribute('data-color-scheme', mode); + 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, `dashboard-${name}-${mode}-${language}`); + } + const customize = page.getByRole('button', { name: t.dashboardCustomize, exact: true }); + await customize.focus(); await page.keyboard.press('Enter'); + await expect(page.getByRole('menuitemcheckbox').first()).toBeFocused(); + if (language === 'en') { + const menuScan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); + await info.attach(`dashboard-${mode}-menu-axe`, { body: JSON.stringify({ violations: menuScan.violations, incomplete: menuScan.incomplete }), contentType: 'application/json' }); + expect(menuScan.violations).toEqual([]); + } + await page.keyboard.press('Escape'); await expect(customize).toBeFocused(); + await expect(page.locator('[role="menuitemcheckbox"]')).toHaveCount(0); + if (language === 'en') { + const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); + await info.attach(`dashboard-${mode}-axe`, { body: JSON.stringify({ violations: result.violations, incomplete: result.incomplete }), contentType: 'application/json' }); + expect(result.violations).toEqual([]); + } + } + for (const [width, height] of [[320, 740], [768, 1024], [1024, 768], [1920, 1080]]) { + await page.setViewportSize({ width, height }); + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); + await capture(page, info, `dashboard-${width}-dark-nb`); + } + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.setViewportSize({ width: 1440, height: 900 }); + expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: 'en' } })).status()).toBe(204); + await page.evaluate(() => localStorage.setItem('uiLanguage', 'en')); await page.reload(); + await page.getByRole('button', { name: 'Customize dashboard' }).click(); + for (const item of await page.getByRole('menuitemcheckbox').all()) await item.click(); + await page.keyboard.press('Escape'); + await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible(); + await page.reload(); + await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible(); + for (const name of ['Application activity', 'Current pipeline', 'Top companies by activity', 'Job tags']) await expect(page.getByRole('region', { name })).toHaveCount(0); + await capture(page, info, 'dashboard-all-hidden'); + + const emailB = `dashboard-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: 'User', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Sign out', exact: true }).click(); + await expect(page).toHaveURL(/\/login\?sessionChanged=1$/); + await page.getByLabel('Username or email', { exact: true }).fill(emailB); + await page.getByLabel('Current password', { exact: true }).fill(password); + await page.getByRole('button', { name: 'Sign in', exact: true }).click(); + await expect(page).toHaveURL(/\/dashboard$/); + await expect(page.getByText('Your search starts here')).toBeVisible(); + await expect(page.getByText('Your dashboard sections are hidden.')).toHaveCount(0); + await expect(page.getByText('Nordlys teknologi og fellestjenester æøå', { exact: true })).toHaveCount(0); + await capture(page, info, 'dashboard-new-account'); + await page.getByRole('button', { name: 'Customize dashboard' }).click(); + for (const item of await page.getByRole('menuitemcheckbox').all()) await expect(item).toHaveAttribute('aria-checked', 'true'); + await page.keyboard.press('Escape'); + const b = await login(page.request, emailB); + const bCompany = await (await page.request.post(`${apiUrl}/companies`, { headers: b.headers, data: { name: 'New search' } })).json(); + expect((await page.request.post(`${apiUrl}/jobapplications`, { headers: b.headers, data: { companyId: bCompany.id, jobTitle: 'First prospect', status: 'Saved' } })).status()).toBe(201); + await page.reload(); + await expect(page.getByRole('link', { name: 'Tracked jobs: 1' })).toBeVisible(); + await expect(page.getByText('No applications or dated responses in this period.')).toBeVisible(); + await capture(page, info, 'dashboard-one-prospect-zero-activity'); + await page.getByRole('button', { name: 'User', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Sign out', exact: true }).click(); + await expect(page).toHaveURL(/\/login\?sessionChanged=1$/); + await page.getByLabel('Username or email', { exact: true }).fill(email); + await page.getByLabel('Current password', { exact: true }).fill(password); + await page.getByRole('button', { name: 'Sign in', exact: true }).click(); + await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible(); + await expect(page.getByText('First prospect', { exact: true })).toHaveCount(0); + await admin.dispose(); +}); + +test('[PRES-S01 E08] Dashboard loading and independent failed resources retain retry and exact values', async ({ page }, info) => { + await login(page.request, 'e2e@example.test'); + let failSummary = 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 failSummary ? route.fulfill({ status: 503, json: { message: 'Summary unavailable' } }) : route.fulfill({ json: { total: 4, active: 4, deleted: 0, byStatus: { Applied: 4 }, appliedLast30Days: 4 } }); + }); + await page.route(`${apiUrl}/jobapplications/analytics-overview`, route => route.fulfill({ json: { funnel: [{ label: 'Applied', count: 4 }], topCompanies: [], responseRateBySource: [], totalActive: 4, totalResponses: 1 } })); + await page.route(`${apiUrl}/jobapplications/analytics?*`, async route => { + await hold; + return failTrends ? route.fulfill({ status: 503, json: { message: 'Trends unavailable' } }) : route.fulfill({ json: [{ month: '2026-08', applied: 4, responses: 1 }] }); + }); + await page.goto('/dashboard'); + await expect(page.getByRole('progressbar').first()).toBeVisible(); + await capture(page, info, 'dashboard-loading'); + release!(); + await expect(page.getByText('Unable to load dashboard trends')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Tracked jobs: 4' })).toBeVisible(); + await capture(page, info, 'dashboard-trends-error'); + failTrends = false; + await page.getByRole('button', { name: 'Retry', exact: true }).click(); + await expect(page.getByText('1 / 4 recorded responses', { exact: false })).toBeVisible(); + failSummary = true; await page.reload(); + await expect(page.getByText('Unable to load dashboard summary')).toBeVisible(); + await expect(page.getByRole('region', { name: 'Application activity' })).toBeVisible(); + await capture(page, info, 'dashboard-summary-error'); + failSummary = false; + await page.getByRole('button', { name: 'Retry', exact: true }).click(); + await expect(page.getByRole('link', { name: 'Tracked jobs: 4' })).toBeVisible(); +}); diff --git a/job-tracker-ui/src/components/DashboardView.tsx b/job-tracker-ui/src/components/DashboardView.tsx index e631959..7ccc59e 100644 --- a/job-tracker-ui/src/components/DashboardView.tsx +++ b/job-tracker-ui/src/components/DashboardView.tsx @@ -1,30 +1,28 @@ import React, { useCallback, useMemo, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; import { Box, Button, - Card, - Checkbox, - Chip, - LinearProgress, Menu, MenuItem, Stack, + Link, + Table, TableBody, TableCell, TableHead, TableRow, Typography, } from "@mui/material"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { alpha, useTheme } from "@mui/material/styles"; +import { useTheme } from "@mui/material/styles"; import TuneIcon from "@mui/icons-material/Tune"; -import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; -import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import CheckBoxIcon from "@mui/icons-material/CheckBox"; +import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; import { api } from "../api"; import ViewStateNotice from "./ViewStateNotice"; import OnboardingChecklist from "./OnboardingChecklist"; import { getUserKeyFromToken } from "../themePrefs"; import { useI18n } from "../i18n/I18nProvider"; -import { statusLabel } from "../pipeline"; +import { statusLabel, statusTone } from "../pipeline"; +import { PageSection } from "../layout/PageLayout"; import { buildWorkflowPath, getWorkflowAction } from "../jobWorkflowSignals"; import { JobApplication } from "../types"; import { useViewResource } from "../hooks/useViewResource"; @@ -80,36 +78,6 @@ function savePrefs(next: Prefs) { window.localStorage.setItem(prefsKey(), JSON.stringify(next)); } -function clamp(n: number, a: number, b: number) { - return Math.max(a, Math.min(b, n)); -} - -function buildLinePath(values: number[], width: number, height: number) { - if (!values.length) return ""; - const min = Math.min(...values); - const max = Math.max(...values); - const step = width / Math.max(1, values.length - 1); - const yFor = (value: number) => { - const t = max === min ? 0.5 : (value - min) / (max - min); - return height - t * height; - }; - - return values - .map((value, index) => `${index === 0 ? "M" : "L"} ${Math.round(index * step)} ${Math.round(yFor(value))}`) - .join(" "); -} - -// Mockup cards float on the grey page background purely via the theme's Card shadow/radius -- -// this wraps MuiCard instead of Paper-with-hardcoded-shadow so every section stays in sync with -// theme.ts (see 02-dashboard.png). -function SectionCard({ children, sx = {} }: { children: React.ReactNode; sx?: any }) { - return ( - - {children} - - ); -} - // "Needs your attention" row accent -- red for overdue follow-ups, amber for interview prep, // blue/info for everything else (package work, readiness review). No "positive/offer" tier // exists in the real reminder feed (see DashboardView report), so only these three appear. @@ -122,9 +90,8 @@ function reminderTone(job: ReminderJob): "error" | "warning" | "info" { export default function DashboardView() { const theme = useTheme(); - const isMobile = useMediaQuery("(max-width:767.95px)"); const navigate = useNavigate(); - const { t } = useI18n(); + const { t, language } = useI18n(); const [months, setMonths] = useState<6 | 12 | 24>(12); const [prefs, setPrefs] = useState(() => loadPrefs()); const [prefsAnchor, setPrefsAnchor] = useState(null); @@ -155,7 +122,7 @@ export default function DashboardView() { const params = { months }; const [analyticsResponse, tagsResponse, trendsResponse] = await Promise.all([ api.get("/jobapplications/analytics", { params }), - api.get("/jobapplications/tags", { params: { limit: 10, ...params } }), + api.get("/jobapplications/tags", { params: { limit: 10 } }), api.get("/jobapplications/tag-trends", { params: { months, limit: 5 } }), ]); @@ -182,499 +149,231 @@ export default function DashboardView() { const tags = trendsResource.data.tags; const tagTrends = trendsResource.data.tagTrends; - const tagColors = useMemo( - () => [theme.palette.primary.main, theme.palette.success.main, theme.palette.warning.main, theme.palette.info.main, theme.palette.error.main], - [theme.palette.error.main, theme.palette.info.main, theme.palette.primary.main, theme.palette.success.main, theme.palette.warning.main], - ); - const summaryView = useMemo(() => { - const topSource = overview?.responseRateBySource?.[0]; - const missingCvCount = reminderJobs.filter((job) => job.workflowSignal?.hasPackageGap).length; - const interviewCount = overview?.funnel?.find((item) => item.label === "Interview")?.count ?? 0; - const interviewPrepCount = reminderJobs.filter((job) => job.workflowSignal?.actionKey === "interview-prep").length; - const overdueFollowUpCount = reminderJobs.filter((job) => job.workflowSignal?.actionKey === "follow-up").length; - - return { - funnelMax: overview?.funnel?.length ? Math.max(...overview.funnel.map((item) => item.count)) : 0, - topSource, - missingCvCount, - interviewCount, - interviewPrepCount, - overdueFollowUpCount, - priorityJobs: reminderJobs.slice(0, 5), - }; - }, [overview, reminderJobs]); - - const trendsView = useMemo(() => { - const appliedValues = analytics.map((x) => x.applied); - const responseValues = analytics.map((x) => x.responses); - const chartWidth = 860; - const chartHeight = isMobile ? 210 : 250; - const totalApplied = appliedValues.reduce((sum, value) => sum + value, 0); - const totalResponses = responseValues.reduce((sum, value) => sum + value, 0); - const maxTagCount = Math.max(...tags.map((item) => item.count), 1); - - return { - appliedValues, - responseValues, - chartWidth, - chartHeight, - appliedPath: buildLinePath(appliedValues, chartWidth, chartHeight), - responsePath: buildLinePath(responseValues, chartWidth, chartHeight), - totalApplied, - totalResponses, - responseRate: totalApplied > 0 ? Math.round((totalResponses / totalApplied) * 100) : 0, - maxTagCount, - }; - }, [analytics, isMobile, tags]); - - // Mockup's 4 stat tiles (Active applications / Response rate / Interviews / Needs follow-up) - // -- every value below is derived from data this view already fetches, no new endpoints. - const metricCards = useMemo(() => { - const cards: { label: string; value: string | number; trend?: { text: string; tone: "success" | "error" }; caption?: string }[] = [ - { - label: t("dashboardActiveApplications"), - value: stats?.active ?? 0, - trend: (stats?.appliedLast30Days ?? 0) > 0 - ? { text: `+${stats?.appliedLast30Days} ${t("dashboardNewApplications").toLowerCase()}`, tone: "success" } - : undefined, - }, - { - label: t("dashboardResponseRateTile"), - value: `${trendsView.responseRate}%`, - caption: summaryView.topSource ? `${summaryView.topSource.label} · ${summaryView.topSource.rate}%` : t("dashboardResponseSources"), - }, - { - label: t("dashboardInterviews"), - value: summaryView.interviewCount, - caption: summaryView.interviewPrepCount > 0 ? t("dashboardUpcomingCount", { count: summaryView.interviewPrepCount }) : undefined, - }, - { - label: t("dashboardFollowUpsDue"), - value: reminderJobs.length, - trend: summaryView.overdueFollowUpCount > 0 - ? { text: t("dashboardOverdueCount", { count: summaryView.overdueFollowUpCount }), tone: "error" } - : undefined, - }, - ]; - return cards; - }, [reminderJobs.length, stats?.active, stats?.appliedLast30Days, summaryView.interviewCount, summaryView.interviewPrepCount, summaryView.overdueFollowUpCount, summaryView.topSource, t, trendsView.responseRate]); + const locale = language === "nb" ? "nb-NO" : "en-GB"; + const palette = theme.vars?.palette ?? theme.palette; + const number = (value: number) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value); + 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 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); + const funnel = (overview?.funnel ?? []).filter(item => item.count > 0); + const allHidden = !Object.values(prefs).some(Boolean); + const grid = { display: "grid", gridTemplateColumns: { xs: "minmax(0, 1fr)", lg: "minmax(0, 1.4fr) minmax(0, 1fr)" }, gap: 3, alignItems: "start" }; + const monthSpan = analytics.length ? `${monthLabel(analytics[0].month)} – ${monthLabel(analytics[analytics.length - 1].month)}` : t("dashboardNoWindowData"); + const stageCount = (stage: string) => overview?.funnel.find(item => item.label === stage)?.count ?? 0; const togglePref = useCallback((key: keyof Prefs) => { - setPrefs((current) => { + setPrefs(current => { const next = { ...current, [key]: !current[key] }; savePrefs(next); return next; }); }, []); - const getReminderAction = useCallback((job: ReminderJob) => getWorkflowAction(job, { - packageWork: t("jobTablePackageWork"), - followUp: t("jobTableFollowUp"), - interviewPrep: t("jobTableInterviewStage"), - readiness: t("jobTableReadiness"), - }), [t]); + // Both series share a zero baseline and maximum; independent scaling implies false equality. + const chart = useMemo(() => { + const max = Math.max(1, ...analytics.flatMap(point => [point.applied, point.responses])); + const path = (key: "applied" | "responses") => analytics.map((point, index) => + `${index ? "L" : "M"} ${20 + index * 720 / Math.max(1, analytics.length - 1)} ${180 - point[key] / max * 160}`).join(" "); + return { max, applied: path("applied"), responses: path("responses") }; + }, [analytics]); - const openReminderJob = useCallback((job: ReminderJob) => { - navigate(buildWorkflowPath(job)); - }, [navigate]); - - const timeInStageMax = overview?.timeInStage?.length ? Math.max(...overview.timeInStage.map((item) => item.medianDays), 1) : 1; - const hasJobs = (stats?.total ?? 0) > 0; - const hasActivityData = analytics.some((item) => item.applied > 0 || item.responses > 0); - const funnelItems = (overview?.funnel ?? []).filter((item) => item.count > 0); - const hasStageData = Boolean(overview?.timeInStage?.length); - const hasCompanies = Boolean(overview?.topCompanies?.length); - const hasSkills = tags.length > 0; - - const priorityPanel = !summaryResource.loading && !summaryResource.error && hasJobs ? ( - - {t("dashboardTodayTitle")} - {t("dashboardTodayBody")} - {summaryView.priorityJobs.length === 0 ? ( - {t("remindersNothing")} - ) : ( - - {summaryView.priorityJobs.map((job) => { - const action = getReminderAction(job); - const tone = reminderTone(job); - const toneColor = theme.palette[tone].main; - const urgent = tone === "error"; - return ( - - - - - {job.company?.name ?? t("jobTableCompany")} • {job.jobTitle} - {action?.detail ?? job.workflowSignal?.reason ?? job.followUpReason ?? t("remindersFollowUpLabel")} - - - - - ); - })} - - )} - - - - - ) : null; + const bar = (value: number, max: number, color: string) =>