Commit Graph

25 Commits

Author SHA1 Message Date
cesnimda 582c4e07e9 feat(career): builder UX overhaul — DnD, rich text, per-item editing, richer preview
CI and Deploy / test (push) Failing after 1m56s
CI and Deploy / deploy (push) Has been skipped
Phase 4.5 (priorities 1, 2, 5, 6).
- Content tab now reads GET /api/cv/outline: each entry-based section expands
  to its entries with per-item hide, title/subtitle override, and rich-text
  bullet editing (RichTextField markdown toolbar).
- Native HTML5 drag-and-drop reorder for sections AND entries (useDragReorder,
  zero deps), with drop-line + dim animations; arrow buttons remain for keyboard.
- Preview: zoom presets (+/- , slider, Fit), measured page count with page
  navigation and dashed page-break indicators, an "updating…" chip, 300ms debounce.
- Save indicator now distinguishes Unsaved / Saving / Saved / failed (aria-live).
- Loading skeletons, better empty states, ATS-friendly theme badge, relative
  times in History; a11y: ARIA labels, keyboard-selectable theme cards, focus rings.
- Pure helpers moveItem/wrapSelection extracted + unit-tested; tsc + build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:46:37 +02:00
cesnimda 585047da9e fix(career): public CV deep links work on direct load, refresh, and shared links
CI and Deploy / test (push) Failing after 1m55s
CI and Deploy / deploy (push) Has been skipped
Phase 4.5 (priority 4). The app is a React Router SPA behind Next static
export, which only generated `/` — so a hard load of any deep path (/cv/{slug},
/login, /career/builder/…) hit Next's client not-found before React Router
could route it. Replace the single app/page.tsx with an optional catch-all
app/[[...slug]] (server page + client shell so generateStaticParams stays
server-only) that matches every path; nginx already serves index.html for
unknown paths (try_files), so React Router now owns routing on direct load.

Also: PublicCvPage shows a friendly 404 empty state and sets the document
title. Verified live — /login and /cv/{slug} both resolve on direct navigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 10:34:56 +02:00
cesnimda 158dd02b00 feat(career): CV Builder UI — 3-tab builder, live preview, variants, public CV
CI and Deploy / test (push) Failing after 1m57s
CI and Deploy / deploy (push) Has been skipped
Frontend for Phase 4. /career/builder lists CV variants; the editor has the
three spec tabs (Content / Customize / AI Tools, plus History) beside an
always-on live preview that re-renders through the server theme engine on a
debounce. Content: reorder/hide/rename sections, headline override, custom
sections. Customize: 8-theme picker, accent colour, fonts, density, page size,
photo/icons/page-number toggles. AI Tools: suggestion-only assistance (never
auto-applied). Autosave with version history + restore, public on/off with a
copyable /cv/{slug} link, PDF export. Public read-only page at /cv/:slug.

- cvBuilder.ts (types + API), CvBuilderPage, CvBuilderEditor, PublicCvPage
- routes + nav wired in App.tsx; "Open CV Builder" entry on Career Workspace
- 2 component tests; tsc + production build clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 10:05:21 +02:00
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 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 d7d7e70d08 feat(ui): separate career and connected accounts 2026-07-15 10:37:02 +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 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 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 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 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 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 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