Commit Graph

214 Commits

Author SHA1 Message Date
cesnimda a1dd447091 feat(career): version history restore on /career
CI and Deploy / test (push) Failing after 1m54s
CI and Deploy / deploy (push) Has been skipped
Phase 3, versioning UI. The /career overview now shows a collapsible version
history with a Restore action per past version, backed by the versioning API.
Restore reapplies the chosen snapshot as a new version (non-destructive) and
refreshes the profile + completeness in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:01:54 +02:00
cesnimda 2b57d65715 feat(career): wire /career to the relational profile API + completeness overview
CI and Deploy / test (push) Failing after 1m52s
CI and Deploy / deploy (push) Has been skipped
Phase 3, frontend. /career now reads and writes the master profile through the
relational source of truth instead of the legacy blob path.

- CareerProfilePage loads GET /career/profile (structured profile from the
  relational children + cvText + completeness) and saves PUT /career/profile
  ({ profile, cvText }). This keeps the relational store authoritative — the
  previous PUT /auth/profile blob write left it stale after first load.
- Added a "Profile completeness" overview (percent bar + missing sections) at the
  top of /career, from the server scorecard.
- PUT /career/profile now accepts { profile, cvText } so the single /career save
  covers both the structured profile and the raw imported text; GET returns cvText.

Tests: career-save asserts the /career/profile payload; new completeness-overview
test; controller tests updated for the request wrapper. 75/76 frontend pass (the
1 failure is the unrelated pre-existing settings-view suite); prod build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:53:35 +02:00
cesnimda 21c9b1ea63 refactor(profile): slim ProfilePage and CareerProfilePage to their own concerns
CI and Deploy / test (push) Failing after 1m53s
CI and Deploy / deploy (push) Has been skipped
Complete the Phase 2.2 split. Each dedicated component now carries only its own
state, effects, and JSX; the shared-copy duplication from the split checkpoint
is removed.

- ProfilePage (/profile): 1372 -> ~495 lines. Dropped the 700-line master-CV
  block, all CV/rewrite/PDF state + helpers + the extraction-run polling effects.
  loadProfile now fetches only /auth/me (no runs/jobs). Saves identity only.
- CareerProfilePage (/career): dropped identity fields, password, 2FA/sessions
  and their state; loadProfile no longer sets identity fields. Saves the master
  profile only. Owns the master-CV editing surface.

Both save through the partial-update PUT /auth/profile, so neither can overwrite
the other's data. The master career profile stays the only editable source of
truth on /career.

Tests: the CV-editing tests in profile-page.test.tsx now render CareerProfilePage
(where that surface lives) — all 5 pass, fixing 4 pre-existing failures that were
caused by the display:none shared block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:11:42 +02:00
cesnimda e428274e39 refactor(profile): split account and career into dedicated components
CI and Deploy / test (push) Failing after 2m43s
CI and Deploy / deploy (push) Has been skipped
Phase 2.2 — stop backing /profile and /career from one component behind a
boolean. /career now renders a dedicated CareerProfilePage; /profile keeps
ProfilePage. Each hardcodes its mode and saves only its own concern (identity
vs master profile) via the partial-update endpoint.

This commit is the behaviour-preserving checkpoint: the two components still
share the full implementation (each carries all state, only its own JSX renders).
The per-component pruning that removes the other concern's state/JSX follows in
subsequent commits, verified by tsc at each step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:01:53 +02:00
cesnimda 66cc6a7db4 feat(phase-2): separate /profile (identity) from /career (master profile)
Phase 2 — Career/Profile separation. The master career profile is the source of
truth; identity and career data are now saved independently so neither wipes the
other. CV Builder deliberately not built yet.

Backend — PUT /auth/profile is now a partial update:
- null/omitted field -> unchanged; "" -> cleared; value -> set (trimmed).
- Email/UserName never cleared to empty (login identifiers).
This lets /profile save identity fields and /career save the master-profile
fields through the same endpoint without one nulling the other. 4 new tests
cover the data-integrity guarantees (identity save keeps the CV, career save
keeps identity, empty clears, null leaves).

Frontend:
- ProfilePage save payload is now scoped by careerOnly: /career sends only
  { profileCvText, profileCvStructureJson }, /profile sends only identity.
- CareerWorkspacePage: removed the inert "CV Builder" tab (careerView) — Phase 2
  establishes the master profile only; the builder is Phase 4.
- Dropped the dead careerView prop.
- Updated the CV-save test to render career mode and assert identity is excluded.

Source-of-truth flip (CareerProfileService authoritative) stays deferred to F5
per the branch design; CareerProfileService keeps mirroring via its dual-write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:59:38 +02:00
cesnimda 992f89e619 feat: integrate Career Workspace foundation from feature/career-workspace
Recover the F1 Career Profile foundation + AI-workspace persistence from the
unmerged feature/career-workspace branch, so Phase 2 builds on the documented,
tested target state instead of re-deriving it. Foundation only — CV Builder
commits (variants, ATS badge, rewrite diff) stay deferred per "do not build CV
Builder yet". See docs/career-workspace-branch-assessment.md.

Squashed from 3 branch commits (235e291, 5916f09, 00a035e), resolved against
main + Phase 0:

- CareerProfile + CareerProfileVersion (append-only history), dual-written from
  every profile save path via CareerProfileService. ApplicationUser.
  ProfileCvStructureJson stays authoritative; the tables mirror it. Stable item
  IDs assigned to jobs/education/certifications/projects (the prerequisite for
  future variant lineage). CvDateNormalizer for free-text -> YYYY-MM.
- InterviewPrepNote + AiWorkspaceNote: cache AI interview prep / candidate fit /
  focus plan keyed by an attachment-context signature, so they stop regenerating
  (and re-spending the provider) on every open.

Conflict resolutions (union, favouring current code + Phase 0):
- JobTrackerContext / StartupInitializationExtensions: kept Phase 0's tables and
  reconciler blocks, added the career/interview/ai-note tables (both SQLite and
  MySQL dialects).
- ProfileCvController: dropped the branch's in-file DTO records (main defines them
  in ProfileCvDtos.cs) and the LayoutFamily/AtsRating template fields (deferred
  ATS-badge work), keeping main's 7-arg CvTemplateDescriptor.
- JobApplicationsController: kept the branch's cache-check, restored main's
  AsNoTracking on the read-only user load.

Tables ship empty (verified dev); nothing to migrate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:11:42 +02:00
cesnimda eac34705e3 feat: Phase 0 foundation — Job entity, expanded pipeline, AI service lockdown, DateApplied history
Unblocks the documented core workflow and closes the AI-service exposure,
without changing existing behaviour.

Job/JobApplication split (additive; see ADR-002):
- New Job entity (the opportunity) with owner-scoped query filter; nullable
  JobApplication.JobId FK. Nothing reads Job yet.
- Migration AddJobEntityAndProspectStages, hand-edited to drop reconciler-owned
  tables the scaffolder re-emitted; verified against the real dev DB.

Pipeline: 10 internal stages across three concerns kept separate —
PipelineStage (workflow) / PipelineGroup (UI: NotApplied/Active/Closed) /
PipelineCategory (analytics). Adds Saved/Interested/Preparing/Withdrawn;
keeps Waiting and Ghosted. Kanban shows 3 grouped columns; cards keep a stage
chip and full transitions; drag applies only safe transitions (never infers
Ghosted/Withdrawn).

DateApplied nullable + SavedAt. Cleared when leaving Applied so analytics stay
accurate; the discarded date is preserved as an AppliedDateCleared JobEvent.

AI service lockdown: no host port; private ai_internal network (backend is the
only other member); X-Ai-Service-Token required on all non-/health endpoints;
AI_SERVICE_TOKEN mandatory via compose. Verified backend-only against the live
stack.

Also carries two pre-existing working-tree files (views/ProfilePage.tsx,
views/CareerWorkspacePage.tsx) so the tree is clean for the branch integration.

Tests: +40 backend (247 total), +5 sidecar (16), +15 frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:05:25 +02:00
cesnimda aa3567d8a8 feat(applications): guide job creation in steps 2026-07-15 10:39:13 +02:00
cesnimda d7d7e70d08 feat(ui): separate career and connected accounts 2026-07-15 10:37:02 +02:00
cesnimda dab39bd570 feat(ui): add reduced-motion floor and tasteful hover micro-interactions
CI and Deploy / test (push) Successful in 2m12s
CI and Deploy / deploy (push) Successful in 20s
Global @media (prefers-reduced-motion: reduce) rule in MuiCssBaseline
collapses every animation/transition duration to near-zero at once --
covers MUI's own Dialog/Menu/Collapse/ripple transitions and every
hover-lift added below, so accessibility doesn't need re-checking
per-component as more motion gets added later.

Dashboard stat tiles and Kanban cards get a subtle hover lift (shadow
deepens, translateY(-2px)/-1px), matching the hover pattern the
landing page's feature cards already had -- these are the two places
a small lift reads as an affordance rather than noise: stat tiles are
dashboard-customization-adjacent, and kanban cards are literally
draggable, so the lift reinforces the existing grab-cursor signal
instead of competing with it. Left everything else alone -- most
cards on this app hold passive content, not something to invite a
hover response.
2026-07-13 15:07:11 +02:00
cesnimda 093f303cdd style(ui): float correspondence inbox and gmail review page wrappers
CI and Deploy / test (push) Successful in 2m48s
CI and Deploy / deploy (push) Successful in 48s
Last two views/*.tsx files not yet covered this session. Same
floating-shadow page-wrapper treatment as every other screen; the
outlined item-row Paper inside each list stays as-is, matching the
established "list rows stay flat/outlined, the page wrapper floats"
convention (same pattern already used for trusted-devices/sessions
list rows).
2026-07-13 14:36:49 +02:00
cesnimda 4d3fdc3526 fix(ui): remove kanban column border missed by earlier sweep
CI and Deploy / test (push) Successful in 2m23s
CI and Deploy / deploy (push) Successful in 40s
The mockup's kanban columns have no border, just a light grey
background -- this instance used multi-line sx formatting
(border/borderColor on separate lines) so it slipped past the earlier
single-line-substring grep sweep. Caught by a follow-up multi-line
search across the whole frontend for the same pattern; nothing else
turned up except a legitimate circular crop-tool boundary in
CropImageDialog, which correctly stays as-is.
2026-07-13 14:32:05 +02:00
cesnimda 24c7d68490 style(ui): float dialogs/menus/popovers, round chips and tooltips globally
CI and Deploy / test (push) Successful in 2m14s
CI and Deploy / deploy (push) Successful in 37s
Every Dialog/Menu/Popover in the app renders its content via MuiPaper,
which is deliberately kept flat (1px border, no shadow -- Paper is
used too broadly, e.g. as a plain content divider, to safely restyle
globally). That meant every modal (confirm/prompt, AddJobModal,
EditJobDialog, JobDetailsDialog) and every dropdown/Select menu in the
app was still rendering flat-bordered despite every other screen this
session moving to the floating-shadow mockup look.

Added targeted MuiDialog/MuiPopover/MuiMenu paper overrides -- these
win on specificity over MuiPaper's own defaults without touching
MuiPaper itself, so every dialog and dropdown in the app picks up the
rounded floating-shadow treatment from this one change instead of
patching each dialog file individually. Also added MuiChip (full pill
radius, matching every status/skill pill in the mockups) and
MuiTooltip (matching corner radius) overrides, and gave the toast
Snackbar/Alert a consistent radius + weight.
2026-07-13 14:28:22 +02:00
cesnimda 5219237613 style(ui): redesign error pages and job table empty/loading state
CI and Deploy / test (push) Successful in 2m22s
CI and Deploy / deploy (push) Successful in 44s
No mockup exists for 404/500 pages, so these follow the visual
language already established elsewhere this session: floating-shadow
card, big bold status number (matching the dashboard stat tiles'
bold-number treatment) instead of a small overline.

JobTable's first-run empty state gets an icon chip matching the
landing page's feature-card icon treatment (rounded square, tinted
primary background) instead of plain text -- the filtered "no results"
one-liner stays as-is, that's a different, correctly minimal case.
Main table Paper wrapper gets the same floating-shadow treatment as
every other card this session.

ViewStateNotice (the shared loading/error component used across the
app) reviewed and left untouched -- it's an MUI Alert used as an inline
banner, which is the correct pattern; it was never a "fake card" to
begin with.
2026-07-13 14:22:07 +02:00
cesnimda 42ba306362 style(ui): float remaining table/card containers to design system
CI and Deploy / test (push) Successful in 2m31s
CI and Deploy / deploy (push) Successful in 36s
Repo-wide sweep for the same flat 1px-border "fake card" pattern
already fixed in Dashboard/Kanban/JobDetailsDialog/auth pages this
session -- AddJobModal, Attachments, CompaniesTable, Correspondence,
EditJobDialog, and the admin audit/system/users pages all had a table
container or content box using border+divider instead of the
floating-shadow treatment used everywhere else now.

Left AppShell.tsx/App.tsx alone -- their border:1px+divider instances
are icon-button and badge outlines, not card containers; that's a
different, correct use of the pattern.
2026-07-13 09:39:46 +02:00
cesnimda 520b002026 style(ui): float Settings and Profile page cards to match design system
CI and Deploy / test (push) Successful in 2m13s
CI and Deploy / deploy (push) Successful in 39s
2026-07-13 09:38:30 +02:00
cesnimda 125235c293 style(ui): float auth/security cards to match mockup shadow language
CI and Deploy / test (push) Successful in 2m23s
CI and Deploy / deploy (push) Successful in 37s
Login/register, forgot-password, reset-password, verify-email, and the
2FA/sessions settings cards all used a bare MuiPaper (1px border, no
shadow) predating this session's theme foundation. MuiPaper itself
stays untouched (it's a lower-level primitive used too broadly across
the app -- menus, popovers -- to safely restyle globally), so these
specific card instances get the same explicit no-border/floating-shadow
treatment already applied screen-by-screen elsewhere this session.

Static shadow value again, not theme.vars.customShadows -- inline sx
callbacks execute against whatever theme is in context, and none of
this repo's tests wrap components in a ThemeProvider (see fc56f94).
2026-07-13 09:33:51 +02:00
cesnimda a82aef3dfc style(ui): use shared GradientButton on landing page
CI and Deploy / test (push) Successful in 2m18s
CI and Deploy / deploy (push) Successful in 40s
LandingPage.tsx already closely matched the mockup set (dark navy hero,
gradient CTAs, numbered step badges, feature/pricing cards) from an
earlier pass -- nothing structural needed here. Replaced the 4 places
that hand-rolled the same linear-gradient(90deg,#6366f1,#22d3ee) inline
with the shared GradientButton component introduced this session, so
the gradient can't drift out of sync between screens.
2026-07-13 09:31:20 +02:00
cesnimda fc56f94d56 style(ui): redesign job workspace dialog to match mockup
CI and Deploy / test (push) Successful in 2m35s
CI and Deploy / deploy (push) Successful in 38s
Restyle JobDetailsDialog.tsx (04-job-workspace.png mockup) within its
existing dialog/tab structure -- the real app splits Correspondence,
Attachments, and Candidate Fit into separate tabs rather than the
mockup's single-screen 2x2 card grid, so this is a visual-language
pass over the existing IA, not a restructure:

- Header: bolder title (h5/800), heavier status chip, cleaner
  no-underline tab styling.
- Every flat bordered "fake card" Box (11 instances across all tabs,
  plus the 2 in the Overview strategy-snapshot panel) becomes a
  floating shadow card with no border, matching every other screen
  redesigned this session.
- The two genuinely AI-generation actions (Generate Strategy Snapshot,
  and by extension the shared GradientButton component) get the
  mockup's signature gradient CTA treatment; the confirm-gated
  "Refresh AI summary" action stays a plain outlined button so the
  gradient doesn't get diluted by a second use on the same tab.

Also fixes a real bug surfaced by actually using GradientButton for
the first time: its sx callback read theme.vars.customShadows, which
throws when a component renders without this app's ThemeProvider --
true in production always, but true in every test in this repo (none
of them wrap with a ThemeProvider), so every test touching a
GradientButton or one of these restyled boxes crashed. Fixed by using
a static shadow value instead of a theme.vars lookup in both the
component and this file, matching the fact that inline sx callbacks
execute against whatever theme is in context (unlike theme.components
styleOverrides, which only run when this app's real theme is actually
provided).

Verified: tsc clean, full suite green (65/65, including 4 test files
that render this exact dialog). Live check: booted the backend and
loaded the dashboard through a fresh Next.js dev server + cache
(cleared .next after chasing what turned out to be a stale console-log
history in the Browser pane tooling, not a real compile error) --
confirmed real data renders with no actual runtime errors.
2026-07-13 09:27:15 +02:00
cesnimda 81512db1cb style(ui): redesign Dashboard and Kanban to match mockups
CI and Deploy / test (push) Successful in 2m16s
CI and Deploy / deploy (push) Successful in 42s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 09:05:54 +02:00
cesnimda 1ab92e5c9d feat(ui): establish premium design foundation from mockups
CI and Deploy / test (push) Successful in 2m21s
CI and Deploy / deploy (push) Successful in 39s
Extract design tokens from the mockup set (F:\Pictures\website\jobtracker\new
dashboard, pipeline, job-workspace, features, workflow screens) into the
central theme so every screen picks the change up automatically:

- Heading weight: h1-h4 go bold/black (800/700) to match the mockups' heavy
  display type; h5/h6 stay a lighter semibold so dense screens don't turn
  into a wall of black text.
- Card shadow: replace the flat 1px "section" shadow + visible border with a
  soft floating shadow and no border, matching how mockup cards sit on the
  grey page background.
- Border radius: 10/12/8px -> 14/16/10px across shape/card/button defaults,
  matching the mockups' rounder corners.
- New GradientButton component wrapping the mockup's signature indigo->cyan
  CTA gradient ("Tailor my CV for this role", "See the interface tour"),
  reserved for the single most important AI-assist/hero action per screen.

The dark navy sidebar (#0f172a) already matched the mockups from an earlier
pass -- untouched here.

Verified: tsc clean, full frontend suite green (65/65). Live visual
screenshot verification wasn't possible -- the Browser pane's screenshot
tool times out in this environment; verified structurally via read_page
and the app rendering without console errors instead.
2026-07-13 08:50:26 +02:00
cesnimda b8b7987c58 fix(auth): polish login/register/reset pages for consistency and accessibility
CI and Deploy / test (push) Successful in 2m9s
CI and Deploy / deploy (push) Successful in 39s
- LoginPage: add client-side email/password validation (inline error +
  helperText, matching the 2FA components' established pattern), and a
  proper register-mode toggle with a "Confirm password" field. The
  brief asked for confirm-password on registration but the page only
  had one shared password field; a toggle (mirroring the existing
  Tabs-for-mode pattern already used for Google/Microsoft) keeps this
  from cluttering the login form for returning users.
- Fix a real bug in ResetPasswordPage: it didn't use the app's
  getApiErrorMessage helper, so a non-string error response body would
  render as "[object Object]" in the toast. Also add a confirm-password
  field and matching client-side validation for parity with register.
- ForgotPasswordPage: add proper email format validation instead of
  only checking for non-empty.
- Add matching i18n keys (en/no) for every new validation message.

Verified live end-to-end against a running backend: register-mode
toggle, confirm-password mismatch blocking submission client-side,
and a full registration completing and landing on the dashboard.
2026-07-13 08:34:17 +02:00
cesnimda c6918cbeea feat(auth): add server-tracked sessions with view/revoke
JWTs were previously fully stateless -- the token alone was the credential
until its own expiry, with no way to list or kill a session server-side. Add
a UserSession table alongside every JWT issued (AppSessionIssuer), embed its
id as a "sid" claim, and check that claim against the DB on every "local"
scheme request (Program.cs OnTokenValidated) so a session can actually be
revoked before its JWT naturally expires. New /api/auth/sessions endpoints
(list, revoke one, revoke-others) plus a Sessions card on the profile page.

Fails closed on a missing "sid" claim: every JWT issued going forward has
one, so a token without it is either pre-deploy (forces one re-login for
already-signed-in users at deploy time, same additive-forward cost the
2FA/trusted-device work on this branch already paid) or forged.
2026-07-13 01:47:31 +02:00
cesnimda 904f3a8ec8 feat(auth): add configurable email verification enforcement
Auth:RequireEmailVerification (default off) gates whether local
register requires confirming email before login. OAuth new-user paths
are untouched -- Google/Microsoft already assert a verified email.
Adds verify-email and resend-verification-email endpoints, mirroring
the existing reset-password enumeration-avoidance and rate-limiting
patterns, plus a login-embedded resend affordance and a verify-email
landing page on the frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 01:22:26 +02:00
cesnimda 0ca2f2b261 feat(auth): add trusted-device 30-day 2FA skip (frontend)
Adds a "Trust this device for 30 days" checkbox to the 2FA challenge step,
and a "Trusted devices" section to the 2FA settings card: list devices with
a "this device" badge, per-row revoke, and a confirm-gated "sign out all
other trusted devices" action. Both flows are opt-in and additive -- default
unchecked, so nothing changes for a user who never uses them.
2026-07-13 01:02:43 +02:00
cesnimda b85dc1ffb7 feat(auth): add 2FA setup UI and login challenge step 2026-07-12 21:17:09 +02:00
cesnimda d5d82cb528 feat(ux): onboarding checklist, dashboard-first landing (fixed)
CI and Deploy / test (pull_request) Successful in 2m5s
CI and Deploy / deploy (pull_request) Has been skipped
Dashboard onboarding checklist: a dismissible card with 3 steps (add
CV, import first job, check match score), each linking straight to
where you'd do it. Auto-hides once both CV and a job exist; otherwise
persists per-user via localStorage until dismissed.

Fixes the actual authenticated-landing redirect to /dashboard: my
earlier commit changed App.tsx's inner Shell route for "/", which
turned out to be dead code -- the outer router claims "/" for
LandingPage first, so Shell's own "/" route is never reached on a
direct hit. The real redirect lives in LandingPage.tsx's post-auth-check
navigate() and LoginPage.tsx's post-login nextPath default; both now
point at /dashboard. Verified live: an authenticated visitor hitting
"/" now lands on Dashboard with the onboarding checklist visible,
confirmed via rendered page text and screenshot.
2026-07-12 04:26:17 +02:00
cesnimda 9615ee3f41 feat(ux): per-view subtitles, correspondence cross-links, mobile kanban, a11y
Continuing the product/UX review's deferred items:

- Every top-level view now gets a one-line subtitle under its title
  (Dashboard/Jobs/Kanban/Reminders/Correspondence/Gmail review) stating
  what that specific view is for, instead of navigation being the only
  signal of what each page does.
- Correspondence inbox and Gmail review queue cross-link to each other
  instead of being two unexplained flat sidebar items -- kept both nav
  entries (renaming/nesting risked breaking muscle memory) but made the
  relationship between them explicit in the UI itself.
- Kanban board switches to a horizontal scroll-snap row on phone-width
  viewports instead of stacking all 5 columns vertically, which meant
  a lot of scrolling to see anything past "Applied".
- Match-score ring gets an aria-label with the actual percentage --
  it was two nested decorative CircularProgress elements with no
  accessible text. (Keyboard-accessible status changes on kanban cards
  were already covered by the existing "..." menu -- no gap there.)
2026-07-12 04:14:37 +02:00
cesnimda 58868fc2b6 feat(ux): first-time onboarding, empty states, and copy fixes
Implements the six "propose first" items from the product/UX review:

- "/" now redirects to /dashboard instead of the empty /jobs table --
  a new user's first screen is now an overview with orientation, not
  a data table with zero rows and four filter dropdowns.
- Jobs table gets a real first-time empty state (distinct from "no
  results match your filters") pointing at Add Job and the bookmarklet,
  instead of a bare "No jobs found."
- Match Score card and Candidate Fit tab now each get a one-line
  caption explaining what they are and how they differ (deterministic
  keyword coverage vs. AI opinion) -- they previously sat side by side
  with no explanation of why there are two.
- Google sign-in hint now reflects self-serve signup when
  Auth:AllowRegistration is on, instead of always implying you need an
  existing linked account.
- Quick Search button now shows its keyboard shortcut (Ctrl+K / ⌘K)
  inline instead of being undiscoverable.
2026-07-12 04:05:32 +02:00
cesnimda 33d899c243 fix(auth): Google Sign-In audience mismatch + remove per-user accent color
CI and Deploy / test (pull_request) Successful in 2m8s
CI and Deploy / deploy (pull_request) Has been skipped
Root cause of "Google authentication failed": appsettings.Development.json
had Auth:GoogleClientId set to the literal placeholder
"CHANGE_ME_GOOGLE_CLIENT_ID" while the frontend's .env.development had a
real (already-public, already-committed) client ID -- every Google ID
token's audience check failed against the backend's placeholder. Fixed
by setting the same real client ID on both sides (a client ID is a
public identifier, not a secret, safe to commit -- unlike a client
secret). Also enabled Auth:AllowRegistration in dev so the existing
Google-first self-serve-signup path (auto-create on unmatched verified
email, auto-link on matching verified email -- built during Wave 7) is
actually exercisable locally.

Wired the previously-missing Auth__MicrosoftClientId /
NEXT_PUBLIC_MICROSOFT_CLIENT_ID into docker-compose.yml/.env.example
(distinct from the existing MICROSOFT_CLIENT_ID used for Outlook mail
linking) -- Microsoft sign-in was never deployable, a leftover gap from
when it was built. Fixed a stale env-var name in the Microsoft setup
hint copy (still said REACT_APP_*, predates the Next.js migration).

Removed the per-user accent color picker entirely: it was purely
client-side (localStorage + theme.ts), never touched the backend/DB.
theme.ts now hardcodes a single ACCENT constant; themePrefs.ts drops
get/set/clearAccentColor; App.tsx and SettingsView.tsx drop the
accentColor prop threading. Dead accent-related i18n keys removed from
both locales.

Consolidated Settings' "Account" tab (duplicated GoogleAuthCard, which
already lives on the Profile page) into Profile: moved AuthStatusCard
and EmailProviderConnections there alongside the existing Google/
Microsoft auth cards, so identity/account-linking lives in one place.
Settings drops from 5 tabs to 4 and its General tab uses a consistent
SectionCard layout instead of ad-hoc per-card styling.

Verified: dotnet build/test (177/177) and npm build/test (57/57) both
green; confirmed live against a running dev server that /auth/config
now reports googleEnabled with the corrected client ID, Settings has
no accent controls, and Profile shows the consolidated auth section.
2026-07-12 02:43:10 +02:00
cesnimda 86cdafb3ef fix(deploy): copy .npmrc before npm ci in frontend Dockerfile
CI and Deploy / test (pull_request) Successful in 2m1s
CI and Deploy / deploy (pull_request) Has been skipped
Production deploy has been broken since the Next.js migration merged:
the Dockerfile ran `npm ci` right after COPY package*.json, before the
later `COPY . .` that would bring in .npmrc -- so the legacy-peer-deps
fix for react-scripts' stale TS ^4 peer constraint (added for CI in
dbb1580) never took effect in the actual deploy image, and every
deploy since has failed with the same ERESOLVE error CI hit before
that fix. Copy .npmrc alongside package*.json so npm ci sees it.
2026-07-12 02:15:49 +02:00
cesnimda ffb9888fb4 feat(ui): circular match-score ring in job workspace
CI and Deploy / test (pull_request) Successful in 2m3s
CI and Deploy / deploy (pull_request) Has been skipped
Second UI-rework pass. The job workspace mockup's signature element is
a donut "coverage" ring for the deterministic CV match score; the app
had a linear progress bar instead. Replaced with a layered
CircularProgress ring (track + value arc, percentage centered) while
keeping every existing feature (band chip, matched/missing keyword
chips, section coverage) -- this is a pure visual upgrade to the
existing MatchScoreCard, not a feature reduction to match the mockup's
simpler single-panel layout.

Fixed match-score-panel.test.tsx's no-signal-state assertion, which
expected the removed inline "—" placeholder; restored it outside the
ring's conditional render.
2026-07-12 01:59:03 +02:00
cesnimda 7cfbdf504a feat(ui): dark navy sidebar + restrained kanban status colors
CI and Deploy / test (pull_request) Successful in 2m2s
CI and Deploy / deploy (pull_request) Has been skipped
First pass of the /frontend-design overhaul against the mockups at
F:\Pictures\website\jobtracker\new. Two highest-leverage gaps from the
backlog note ("dark sidebar, KPI cards, exact status colours"):

- AppShell: nav rail is now a fixed dark navy (#0f172a) regardless of
  the app's light/dark theme toggle, matching the mockup's signature
  look -- selected item gets an indigo-tinted pill + icon accent,
  muted slate text for the rest. Kept icon+label rows (mockup's sidebar
  is text-only) since the existing collapsed-sidebar mode depends on
  icons; that's a deliberate deviation, not an oversight.
- JobbjaktMark: replaced the briefcase glyph with the gradient
  checkmark-in-square mark used throughout the mockups (hero, dashboard,
  kanban) -- also fixed a latent SVG gradient id collision across
  multiple rendered instances via useId().
- KanbanBoard: mockup uses color sparingly (a small dot in the column
  header, a 4px accent on the card's left edge) rather than tinting the
  whole column/card background as the previous version did. Reworked
  to match; also swapped card title/subtitle order (job title bold,
  company/location as subtitle) per the mockup.

Remaining for follow-up passes: Dashboard KPI card layout and the job
workspace (candidate-fit ring, AI summary card) -- both structurally
close already but not yet pixel-matched.

Verified: `next build` clean, all 57 frontend tests green, dark
sidebar confirmed live (computed bg #0f172a) against a running dev
server with light content mode forced.
2026-07-12 01:45:20 +02:00
cesnimda 8a9e402baa Merge pull request 'build(frontend): migrate CRA to Next.js (CSR lift-and-shift)' (#21) from feature/wave6-nextjs-migration into main
CI and Deploy / test (push) Successful in 2m4s
CI and Deploy / deploy (push) Failing after 1m2s
2026-07-12 01:25:22 +02:00
cesnimda dbb15804a3 fix(frontend): relax npm peer-dep resolution for react-scripts vs TS 5.9
CI and Deploy / test (pull_request) Successful in 2m6s
CI and Deploy / deploy (pull_request) Has been skipped
CI's npm ci (strict peer resolution) rejected the TypeScript 5.9 bump
from the Next.js migration: react-scripts still declares typescript
^3.2.1||^4 as a peer. Local `npm install` didn't catch this -- it
resolves peer conflicts leniently by default; only `npm ci` enforces
them. react-scripts is kept solely as the Jest test runner now (it
doesn't type-check), so relaxing this one peer constraint is safe.
2026-07-12 01:22:18 +02:00
cesnimda acf60c2a07 build(frontend): migrate CRA to Next.js (CSR lift-and-shift)
CI and Deploy / test (pull_request) Failing after 50s
CI and Deploy / deploy (pull_request) Has been skipped
Wave 6. Swaps react-scripts' build/dev tooling for Next.js while
keeping the app's actual routing/rendering model unchanged -- the app
is almost entirely behind auth with no proven SSR/SEO need, so a real
App Router rewrite would touch ~90 files for zero user-visible benefit.

- next.config.js: output:'export' (static HTML+JS, same "single
  index.html served by nginx with try_files fallback" deploy as CRA).
- app/layout.tsx + app/page.tsx: root shell ports public/index.html's
  <head>, mounts the whole existing App tree client-only (ssr:false)
  since it reads window/localStorage during initial render and Next's
  static prerender would otherwise execute that on the server.
- Renamed src/pages/ -> src/views/ (Next's Pages Router auto-detects
  any `pages/` dir under the app root and tried to build our React
  Router page components as its own routes).
- REACT_APP_* -> NEXT_PUBLIC_* across code, .env.development,
  Dockerfile, docker-compose.yml build args.
- Replaced the CRA SVGR import (`ReactComponent` from .svg, unsupported
  under Turbopack) with a small inline JobbjaktMark component.
- TypeScript 4.9 -> 5.9 (MUI v8's type-checked build needs syntax
  4.9's parser rejects; CRA never hit this because babel doesn't
  type-check).
- Dropped CRA-only files (index.tsx, reportWebVitals, react-app-env.d.ts,
  public/index.html); kept react-scripts as the Jest test runner only
  (next/jest migration not needed -- the existing config already works).

Verified: `next build` static export succeeds, `next dev` serves the
landing page and client-side routes (login etc.) correctly, all 57
frontend tests + 172 backend tests still green.

Known caveat: deep-linking straight to a sub-route (e.g. /login) 404s
in `next dev` since there's no server route for it -- the app only
ever mounts at "/". Production is unaffected: nginx's existing
try_files fallback still serves index.html for any path.
2026-07-12 00:50:45 +02:00
cesnimda 3081d99355 feat(auth): Microsoft OAuth sign-in/link + self-serve signup via Google/Microsoft
CI and Deploy / test (pull_request) Successful in 2m9s
CI and Deploy / deploy (pull_request) Has been skipped
Wave 7. Mirrors the existing Google ID-token-exchange pattern (Program.cs
smart-scheme dispatch, JWT bearer scheme, AuthController exchange/link/
unlink endpoints, ApplicationUser fields, reconciler columns) for
Microsoft Entra ID + personal accounts via the multi-tenant "common"
endpoint.

Google/Microsoft sign-in previously only worked for accounts already
linked to an existing local user -- there was no way to actually sign
up via OAuth. Both exchange endpoints now create a new user when no
match is found and Auth:AllowRegistration is true, same gate as
email/password registration.

Frontend: new MicrosoftAuthCard (MSAL popup flow -- Microsoft has no
vanilla-JS equivalent to Google's Identity Services script) wired into
the login page's provider tabs and the profile page's account-linking
section. REACT_APP_MICROSOFT_CLIENT_ID env var, Auth:MicrosoftClientId
config gate on the backend.
2026-07-12 00:12:23 +02:00
cesnimda b4fd5e2f96 fix(jobs): derive attachment checklist flags from actual Attachments
CI and Deploy / test (pull_request) Successful in 2m4s
CI and Deploy / deploy (pull_request) Has been skipped
Backlog item 4 (Wave 3, first sub-item). HasResume/HasCoverLetter/HasPortfolio/
HasOtherAttachment were manually-editable checkboxes in EditJobDialog,
completely independent of whether a file was actually attached -- classic
drift: mark 'resume ready' by hand, later delete the resume attachment, flag
stays stuck true forever. User confirmed (asked directly, since removing the
manual-override capability is a product decision, not purely technical):
make them fully computed from Attachments, no manual override.

- AttachmentsController.RecomputeAttachmentFlagsAsync: the single place these
  four fields get written now, called after every attachment mutation
  (upload, delete, Purpose change) that could affect them. Deliberately kept
  as persisted columns (not [NotMapped] computed properties reading the
  Attachments navigation collection) -- ~15 query sites build JobApplication
  DTOs without .Include(Attachments), so a live-computed property would
  silently return false everywhere instead of throwing, the worst kind of
  bug. Recomputing at the one write funnel avoids touching any read path.
- Removed HasResume/etc from CreateJobApplicationRequest/
  UpdateJobApplicationRequest -- no longer client-settable.
- EditJobDialog: removed the manual checkboxes, kept the (now genuinely
  accurate) read-only status chips.
- AddJobModal: stopped sending has*-flags at job-creation time; the
  follow-up attachment upload call now sets them correctly via the same
  recompute path.

Caught a real bug while testing this: the Purpose-change path recomputed
before saving the Purpose change, so a fresh query missed the pending edit
and the flags never updated. Fixed by committing the mutation before
recomputing.

3 new backend tests (purpose-change sets flag, delete clears flag,
non-primary purpose counts as "other"). 172/172 backend, 25/25 frontend
suites (57 tests) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:10:39 +02:00
cesnimda 6db3bffb2f feat(email): add provider picker to Settings > Account
CI and Deploy / test (pull_request) Successful in 2m8s
CI and Deploy / deploy (pull_request) Has been skipped
b5 of the multi-provider email roadmap (frontend). Adds EmailProviderConnections
-- one card per provider (Gmail, Outlook/Microsoft 365, generic IMAP) showing
connect status and connect/disconnect actions, mounted in SettingsView's
Account tab alongside the existing app-login GoogleAuthCard (a separate
concern: that card is sign-in identity, this is mailbox linking).

Gmail and Microsoft reuse the OAuth-popup + postMessage handshake already
built server-side (mirrors Correspondence.tsx's existing Gmail-connect flow).
IMAP has no OAuth step, so it's a plain host/port/ssl/username/password form
posting to /api/imap/connect, which verifies the connection server-side
before storing it.

Deliberately NOT touched: the Gmail-specific job-candidate-matching/review UI
in Correspondence.tsx and GmailReviewPage.tsx. That backend pipeline
(ListJobCandidateMessagesAsync, GmailReviewDecisions) is still Gmail-only by
design -- generalising it now would mean building fake UI for capabilities
Microsoft/IMAP don't have yet. This is scoped to the piece that's actually
provider-neutral: connect/disconnect status.

Verified live (backend + frontend dev servers): logged in, confirmed all
three /status calls return 200, Gmail connect-url fetch succeeds, IMAP form
submit hits /api/imap/connect and surfaces the expected 400 on a bad host.

Frontend suite: 25 suites / 57 tests green (2 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:53:51 +02:00
cesnimda daa9694bc7 fix(auth): redirect unauthenticated deep links to home, not /login
CI and Deploy / test (pull_request) Successful in 2m5s
CI and Deploy / deploy (pull_request) Has been skipped
Shell (the single auth guard wrapping every protected route under /*)
redirected unauthenticated visitors straight to /login instead of the home
page, contrary to the intended behaviour. Root cause was one line in
App.tsx's Shell render gate.

Everything else in the guard was already correct: a single centralized
check (no per-page duplication), a loading gate that blocks render until
/auth/config + /auth/me resolve (no flicker-redirect), and 401-triggered
re-checks via the axios interceptor + auth-changed event for expired
sessions mid-session.

Fix:
- Shell now redirects to "/" (home) instead of "/login", still passing
  state={{ from: path }} so the originally-requested page isn't lost.
- LandingPage forwards that location.state through to /login on every
  "Sign in" CTA (6 call sites collapsed into one goToLogin() helper), so
  the home-page bounce doesn't drop the deep-link intent — sign-in still
  returns the user to the page they wanted instead of dropping them on
  the default /jobs.
- Added LandingPage.authRedirect.test.tsx covering the from-state handoff
  end to end (Landing -> click Sign in -> /login receives from). Full
  suite: 25 suites, 56 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:19:25 +02:00
cesnimda aa19edbc49 feat(ui): add product preview (mockups) to landing page
CI and Deploy / test (pull_request) Successful in 2m11s
CI and Deploy / deploy (pull_request) Has been skipped
"See it in action" section showing the dashboard, pipeline board and per-job
workspace. Uses the design-mockup SVGs (small + crisp, ~27KB total) served from
public/mockups/, honestly captioned "Interface preview". Verified live: all three
render at "/".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:23:51 +02:00
cesnimda 3d5ab8f32c feat(ui): add pricing section to landing page
CI and Deploy / test (pull_request) Successful in 2m8s
CI and Deploy / deploy (pull_request) Has been skipped
Three honest tiers (Free / Pro £9-mo / Bring-your-own-key £3-mo) billed monthly
or yearly — never by the week (the anti-Teal positioning from
docs/remaster/RESEARCH_COMPETITORS.md), with a "Most popular" highlight and the
assistive-not-autonomous trust note. Prices are indicative placeholders for the
SaaS direction.

Verified live: pricing section + all three tiers render at "/".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:45:20 +02:00
cesnimda 919f61dde6 feat(ui): public marketing landing page at "/" for logged-out visitors
CI and Deploy / test (pull_request) Successful in 2m7s
CI and Deploy / deploy (pull_request) Has been skipped
Add a LandingPage (hero + features + how-it-works + CTAs) served at "/" so
visitors learn about the product before signing in — matching the JobTrack
mockups (indigo/cyan, dark hero + light sections). If the visitor already has a
session, LandingPage redirects into the app (/jobs); otherwise it shows the
marketing page with "Sign in" CTAs. The "/" route is public (outside the
auth-gated Shell), so logged-out users no longer bounce straight to /login.

Verified live: renders at "/" with headline, feature grid, how-it-works steps
and CTAs; no console errors; type-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:25:43 +02:00
cesnimda 0ca8c95372 merge: reconcile perf/wave1-perf with main (Wave 0 features)
CI and Deploy / test (pull_request) Successful in 2m13s
CI and Deploy / deploy (pull_request) Has been skipped
Resolve conflicts from main's Wave 0 (PR #1) landing after this branch was cut:

- useViewResource.ts: main's e352aae already fixes the render loop the same way
  (load in a ref, dropped from deps) — took main's canonical version. My
  independent fix is superseded (my branch predated e352aae, which is why the
  loop reproduced live).
- JobApplicationsController.cs: keep BOTH main's IJobCvMatchService and my
  AnalyticsService (ctor gets both optional params). GetAnalyticsOverview stays
  delegated to AnalyticsService.
- Fold main's H3 additions into the extracted AnalyticsService: pipeline-driven
  funnel (JobPipeline.Normalize/Stages) + time-in-stage (StageAnalytics) and add
  StageDurationDto + TimeInStage to Models/AnalyticsDtos.cs, preserving the API
  contract the frontend expects.

Build clean; backend suite 135/135 green.
2026-07-05 20:16:40 +02:00
cesnimda b8f8569e6e fix(hooks): stop infinite render loop in useViewResource
CI and Deploy / test (pull_request) Successful in 2m0s
CI and Deploy / deploy (pull_request) Has been skipped
useViewResource built `reload` with `load` in its useCallback deps, and the
fetch effect depended on `reload`. Callers routinely pass an inline `load`
closure (e.g. JobTable), so `load` — and therefore `reload` and the effect —
changed every render, calling setState and re-rendering: an unbounded
"Maximum update depth exceeded" loop that froze the renderer on /jobs and every
other list view (DashboardView, RemindersView, CompaniesTable).

Fix: hold `load` in a ref (like the existing hasLoadedRef) and drop it from the
dependency arrays. Re-fetching is still driven by `deps`/`enabled`; the ref
always points at the latest closure. No API/behaviour change for callers.

Runtime-verified live: /jobs went from a render storm (frozen renderer, 100s of
console errors) to 0 errors in a 2s window and a clean render. Suites that drive
JobTable→useViewResource pass in isolation; the remaining full-run flakiness is
pre-existing (state-pollution/timing in the heavy RTL suites, unrelated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:36:55 +02:00
cesnimda 490c5b803e fix(auth): stop infinite /auth/me request loop when logged out
The axios 401 interceptor calls clearAuthClientState() on every 401, which
dispatched "auth-changed"; the App handler re-fetched /auth/me, which 401'd
again → interceptor → clearAuthClientState() → "auth-changed" → ... an unbounded
request storm (observed live: 100+ GET /auth/me and climbing) that ran whenever
the user was logged out (login page, expired session) — burning CPU, network and
battery and flooding the server.

Fix: make clearAuthClientState idempotent — only emit "auth-changed" when it
actually removes a stored user key (a real signed-in→out transition), so
repeated 401s can no longer re-trigger the fetch.

Runtime-verified in a live stack: /auth/me went from 100+ & growing to 0 &
stable. login-page/settings tests green. Documented in
docs/performance/PERFORMANCE_IMPROVEMENTS.md (Phase 3.5 runtime finding).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:13:18 +02:00
cesnimda eed9b1fa80 fix(profile): revoke CV-preview blob URLs on unmount, not on every change
The PDF-carousel cleanup effect had `[pdfCarousel]` deps, so its cleanup ran on
every carousel change and revoked the *previous* array's object URLs — which are
still referenced by unchanged items in the new array. Building a multi-template
deck therefore left every preview except the last with a revoked (broken) blob
URL. Drop paths are already handled explicitly in savePdfToCarousel (replace)
and resetPdfCarousel (clear), so blanket per-change revocation was both harmful
and redundant.

Fix: track the latest carousel in a ref and revoke outstanding URLs only on
unmount (empty-deps effect). No leak either way — unmount still frees them.

Found during the memory-leak/resource audit; this is a resource-release
correctness bug (over-eager revocation), not a leak. profile-page.test: 5/5
green (with an adequate timeout; the suite's 5s-timeout flakiness is pre-existing
and unrelated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:57:22 +02:00
cesnimda e5e2c65709 feat(ui): adopt JobTrack mockup design tokens (indigo brand, light dashboard)
First UI slice toward the product mockups. Shifts the shared MUI theme to match
the mockup identity without restructuring components:
- Default accent -> indigo #6366f1 (drives primary buttons, active nav, and the
  Applied/Interview status colours). Users with a custom accent keep theirs.
- Light app background -> #F4F6FB with white paper, giving the layered dashboard
  look (cards/inputs sit above a soft grey canvas).
- Global corner radius 8 -> 10 to match the mockups' rounder cards.
- Accent picker: offer indigo/cyan/violet presets first (kept #15803d).

Behaviour-only theme change. Frontend suite: 38/39 pass; the 1 failure
(end-to-end-trust-loop tailored-CV) is pre-existing on this branch (verified by
running it on the clean tree) and unrelated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:13:22 +02:00
cesnimda 30bb6a942d feat: installable PWA with mobile share-target capture
- Corrected manifest (Jobbjakt branding, matching green theme, maskable
  icons, description/categories/scope/id).
- share_target (GET) maps a shared url/link into the same /?add= capture
  flow the bookmarklet uses, so mobile 'Share -> Jobbjakt' pre-fills Add
  Job.
- resolveCaptureUrl helper (tested) extracts the link from add or from a
  link embedded in shared text; App uses it and strips the params.
- Deliberately no offline service worker: the app deploys frequently and
  an aggressive cache would risk stale builds (documented in README).
- 4 unit tests; build compiles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 04:16:44 +02:00
cesnimda fb11469a48 feat: quick-capture bookmarklet
One-click job capture from any posting, reusing the existing
jobimport/preview parser.

- AddJobModal accepts initialUrl and auto-imports once on open
- App reads a /?add=<encoded url> param, opens Add Job pre-filled, and
  strips the param from the address bar
- QuickCaptureCard in Settings offers a draggable bookmarklet (href set
  via ref since React blocks javascript: URLs) plus copyable code
- EN/NB translations; README feature note
- 2 frontend tests; full suite green (22 suites / 50 tests)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 04:12:36 +02:00