Commit Graph

365 Commits

Author SHA1 Message Date
cesnimda f1bf92a4e0 feat(career): add career profile versioning
CI and Deploy / test (push) Failing after 1m54s
CI and Deploy / deploy (push) Has been skipped
Phase 3, version history (list + restore). CareerProfileVersions was already
populated on every save; this makes it usable.

- ICareerProfileService.ListVersionsAsync — the append-only history, newest first,
  with the current version flagged.
- RestoreVersionAsync — reapplies a past snapshot NON-DESTRUCTIVELY: it is re-saved
  as a new version, so the current state stays in history and the restore is itself
  reversible. Syncs the relational children + blob projection like any save.
- Endpoints: GET /career/profile/versions, POST /career/profile/versions/{v}/restore.

Tests (+4): versions listed newest-first with current flagged; restore reapplies
an old snapshot as a new version (history preserved, reversible); restore of a
missing version returns null.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:58:18 +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 b203120ab4 feat(career): master career profile API
CI and Deploy / test (push) Failing after 1m56s
CI and Deploy / deploy (push) Has been skipped
Phase 3, API layer. GET/PUT /api/career/profile — the endpoint the /career editor
uses to read and write the master profile.

- GET: returns the structured profile (assembled from the relational children,
  backfilled from the blob if needed) plus a completeness scorecard.
- PUT: validates limits, persists via CareerProfileService (relational children +
  append-only version), then serializes the result into
  ApplicationUser.ProfileCvStructureJson so the legacy read paths stay in sync.
  Identity fields are untouched (they belong to /profile).
- GET /completeness: just the scorecard, for the overview.
- CareerCompleteness: weighted percent + missing sections.
- CareerProfileValidator: item-count/length limits (abuse guard, NOT completeness
  — a work-in-progress profile always saves).

Tests (+4): put/get round-trip + projection sync, completeness, over-limit
rejection, empty WIP profile accepted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:43:26 +02:00
cesnimda 46ff9454a8 feat(career): relational projection and backfill for the master profile
CI and Deploy / test (push) Failing after 1m53s
CI and Deploy / deploy (push) Has been skipped
Phase 3, service layer. CareerProfileService now maintains the relational children
as the source of truth for structured career data, with the StructuredCvProfile
blob kept as a derived projection.

- SaveVersionAsync additionally syncs the relational children (replace-all,
  preserving ItemKeys from the blob item ids; SortOrder = array position) and the
  LongTailJson (contact, summary, interests, other sections, metadata).
- New LoadStructuredAsync reads the master profile from the relational children,
  lazily backfilling from the ProfileJson blob for profiles that predate Phase 3.
- CareerProfileMapper: the two-way projection between relational rows and
  StructuredCvProfile.

Tests (+5): round-trip through relational, item-key preservation, wholesale child
replacement (no orphans), backfill from a pre-Phase-3 blob, empty profile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:39:53 +02:00
cesnimda 3a4c8fbc10 feat(career): structured career profile foundation
CI and Deploy / test (push) Failing after 1m55s
CI and Deploy / deploy (push) Has been skipped
Phase 3, schema layer. Relational children of CareerProfile — the editable master
career profile. See docs/architecture/career-profile-model.md.

- New entities (Models/CareerEntities.cs): CareerExperience, CareerEducation,
  CareerSkill, CareerProject, CareerCertification, CareerLanguage. Each carries
  OwnerUserId (tenant filter), a stable ItemKey (carried from the blob so future
  CV variants can reference items), and SortOrder. List fields persist as JSON
  string columns via [NotMapped] accessors — plain TEXT, reconciler-friendly.
- CareerProfile gains typed child collections + a LongTailJson column (contact,
  summary, interests, achievements, orgs, pubs, courses, custom sections,
  metadata). ProfileJson becomes a derived projection for legacy read paths.
- DbContext: DbSets + tenant query filters + ordered indexes; FK/cascade by
  convention via the typed collections.
- Migration hand-edited to add only the 6 new tables + LongTailJson; the
  scaffolder re-emitted four reconciler-owned tables (AiWorkspaceNotes,
  CareerProfiles, InterviewPrepNotes, CareerProfileVersions) which were stripped.
  The regenerated snapshot now includes them, closing the drift. Verified against
  a copy of the real dev DB: applies cleanly, no data loss.

Long tail (achievements/orgs/pubs/courses) starts as JSON; promotable to
relational later without a source-of-truth change. Source-of-truth flip stays
deferred; the blob is kept as a derived projection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:34:33 +02:00
cesnimda 9c8644e9f9 docs(architecture): document career profile model
CI and Deploy / test (push) Failing after 1m54s
CI and Deploy / deploy (push) Has been skipped
Phase 3 foundation: entities, relationships, ownership, source-of-truth, and
snapshot rules for the structured career profile. Relational children
(Experience/Education/Skill/Project/Certification/Language) under CareerProfile;
long tail as JSON; blob (ProfileCvStructureJson) becomes a derived projection for
legacy read paths; lazy non-destructive backfill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:25:02 +02:00
cesnimda cf8b2fa014 docs(architecture): document profile and career ownership
CI and Deploy / test (push) Failing after 1m50s
CI and Deploy / deploy (push) Has been skipped
Add section 4a to docs/architecture/current.md: request flow, data ownership,
API responsibilities, and future extension points for the /profile vs /career
separation completed in Phase 2/2.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 00:13:06 +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 8b5ad03808 docs: mark Phase 2 profile/career separation done (2.1)
CI and Deploy / test (push) Failing after 2m59s
CI and Deploy / deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:01:22 +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 a28c47f515 docs: mark Career Workspace foundation integrated; record what remains on the branch
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:17:37 +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 aedd6e32ad docs: recover Career Workspace research + strategy from feature/career-workspace
Bring the four Career Workspace documents onto main as the target architecture
for Phases 2-4, and point MASTER_IMPLEMENTATION_GUIDE.md at them. Taken from the
branch tip (later commits refined them). Pure additions — none previously existed
on main.

- cv-builder-competitor-deep-research.md (Novoresume, Reactive Resume, FlowCV,
  Teal, Enhancv, Canva, Resume.io, Kickresume; matrix; pricing intelligence).
- cv-builder-product-teardown.md
- career-workspace-product-strategy.md
- career-workspace-implementation-roadmap.md (F0-F5)

MASTER_IMPLEMENTATION_GUIDE.md v1.1: adds a Source-Of-Truth Documents section and
restates the "profile is the source of truth; documents reference snapshots" rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:06:46 +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 b176a44627 docs: reorganize tree, restore architecture + research from archive, add Phase 0 reports
Active docs/ was stub scaffolding while the real docs sat in docs/_archive/.
Restore and correct them, and record the Phase 0 work.

- docs/architecture/current.md: verified system map (from archived SYSTEM_OVERVIEW,
  9 corrections against code).
- docs/research/competitors.md: sourced competitor analysis (from archived
  PRODUCT_RESEARCH, feature matrix corrected).
- docs/decisions/ADR-002-job-application-model.md: the Job/JobApplication split.
- docs/application-discovery-report.md, docs/implementation-roadmap.md,
  docs/phase-0-foundation-report.md, docs/career-workspace-branch-assessment.md.
- Remove 10 zero-byte placeholder files that advertised content that never existed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:04:32 +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 706b3ec699 Merge branch 'feature/auth-2fa-security' into main
CI and Deploy / test (push) Successful in 2m26s
CI and Deploy / deploy (push) Successful in 51s
Auth/registration/account-security overhaul: per-account lockout,
TOTP 2FA (RFC 6238) with recovery codes, trusted devices (30-day 2FA
skip), configurable email verification enforcement, and server-tracked
sessions (view/revoke/sign-out-others). Full security-settings UI and
login/OAuth 2FA challenge step.

# Conflicts:
#	JobTrackerApi/Services/StartupInitializationExtensions.cs
2026-07-13 08:10:05 +02:00
cesnimda 5e669a67e1 Merge branch 'perf/phase-7-8' into main
Phase 7 (performance) + Phase 8 (safe refactoring): MySQL startup-crash
hotfix, JobApplications/ProfileCv/Gmail DTO+helper extraction with
N+1 fixes, and remaining hot-path DB indexes.
2026-07-13 08:07:44 +02:00
cesnimda fb04088d62 fix(auth): fix SQLite DateTimeOffset comparison crash in trusted-device checks
The sessions unit's live smoke test caught the same bug it fixed in
SessionsController also present in TrustedDeviceService and
TwoFactorController's device list: SQLite/Pomelo's EF Core provider
cannot translate DateTimeOffset relational comparisons or ORDER BY to
SQL, so IsDeviceTrustedAsync (the check that skips 2FA for a trusted
browser) and ListTrustedDevices would 500 on real SQLite despite
passing on EF's InMemory test provider. Same fix: equality-only in
the DB query, expiry comparison and sort after materializing.
2026-07-13 01:49:25 +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 b914630657 feat(auth): add trusted-device 30-day 2FA skip (backend)
Adds a "trust this device" option to the 2FA challenge: on success, mints a
random token (only its SHA-256 hash is stored), sets it as a new httpOnly,
Secure, SameSite=Strict cookie, and records a TrustedDevice row. AuthController
checks that cookie for the exact signing-in user before gating on 2FA -- a
mismatched user, expired, or revoked device falls through to the normal 2FA
prompt, never errors. TwoFactorController also exposes list/revoke/revoke-all
endpoints for managing trusted devices, scoped to the owning user.

Schema added via the existing raw-SQL reconciler (SQLite + MySQL dialects),
not EF migrations, matching this repo's established pattern.
2026-07-13 01:02:35 +02:00
cesnimda b85dc1ffb7 feat(auth): add 2FA setup UI and login challenge step 2026-07-12 21:17:09 +02:00
cesnimda c68b49eda0 feat(auth): add per-account lockout and TOTP 2FA with recovery codes
Adds three layers of account-security hardening, all gated behind the
existing SignInWithAppSessionAsync-equivalent (now AppSessionIssuer) so
every sign-in path -- local, Google, Microsoft -- goes through the same
lockout/2FA checks:

- Per-account lockout: Identity's built-in lockout store (columns already
  provisioned, previously unused) is now wired up in AuthController.Login
  via IsLockedOutAsync/AccessFailedAsync/ResetAccessFailedCountAsync, 5
  failed attempts / 15 min, same generic 401 as wrong-password to avoid
  enumeration.

- RFC 6238 TOTP 2FA (Otp.NET) with QR-code setup (QRCoder, fully local/
  offline) on a new TwoFactorController: setup requires password
  re-confirmation and returns a pending (unconfirmed) secret + QR; the
  secret is only persisted as active once verify-setup checks a real
  code. Secrets are encrypted at rest via the same IDataProtector pattern
  already used for Gmail/Microsoft OAuth refresh tokens.

- Login/OAuth exchange now checks TwoFactorEnabled before issuing a real
  session. If enabled, it hands back an opaque, server-side (IMemoryCache)
  pending token via a new ITwoFactorPendingTokenService -- deliberately
  NOT a JWT, so it can never be presented as a bearer token to bypass the
  2FA check on any other endpoint. Only POST /api/auth/2fa/challenge can
  redeem it, rate-limited at 5/5min (tighter than password login, since a
  6-digit space is far more brute-forceable).

- One-time recovery codes (10 per enable/regenerate, SHA-256-hashed at
  rest, shown once in plaintext) accepted in the same challenge endpoint
  as an alternative to a TOTP code.

Schema: AspNetUsers gains TotpSecretEncrypted / TotpPendingSecretEncrypted
/ TotpEnabledAtUtc, plus a new TwoFactorRecoveryCodes table, added to both
the SQLite and MySQL dialect blocks in the startup schema reconciler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:48:09 +02:00
cesnimda 717d1b9963 perf(db): add remaining hot-path indexes (status filter, correspondence/event FKs)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:22:23 +02:00
cesnimda 3e09e74fc8 refactor(api): extract Gmail DTOs/parsers, batch N+1 loops
- Move inline DTOs to GmailDtos.cs, pure parse helpers to GmailParsing.cs
- Batch per-message existence checks in CreateSuggestedJob/RefreshLinkedThreads
- Remove redundant second pass in RelinkThread, reuse existing HashSet
- Replace ToListAsync+scan with FirstOrDefaultAsync for GmailReviewDecisions lookups

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:17:33 +02:00
cesnimda 4cfdc95b59 refactor(api): extract ProfileCv DTOs, add missing AsNoTracking on reads 2026-07-12 20:12:36 +02:00
cesnimda ea6c3650f3 refactor(api): extract JobApplications DTOs and helpers, fix N+1 aggregation
- Move inline DTOs to JobApplicationDtos.cs, pure static helpers to JobApplicationHelpers.cs
- GetStats aggregates server-side (COUNT/GROUP BY) instead of loading the full table
- Cache RuleSettings via IMemoryCache, keyed per-user (RulesEngine.GetSettings falls back
  to per-user UserRuleSettings overrides, so a single global cache key would leak settings
  across users)
- Add missing AsNoTracking() to read-only GET endpoints (GetAll, GetById, GetBoard,
  GetReminders, GetStatusSuggestion, GetMatchScore, GetCandidateFit, GetFocusPlan,
  GetInterviewPrep, GetReadiness)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:08:59 +02:00
cesnimda bd07876a41 fix(db): stop startup crash from MySQL composite-index key length
Prod was hard-down: InitializeJobTrackerAsync threw an unhandled
MySqlException ("Specified key was too long; max key length is 3072
bytes") while creating IX_JobApplications_OwnerUserId_FollowUpAt,
which crashed Program.Main before the app could start (surfaced to
users as a 500 on Google sign-in, but really affected every request).

Root cause: this reconciler assumes OwnerUserId is varchar(255), but
the live column was provisioned wider by an earlier EF migration,
close enough to the utf8mb4 3072-byte limit that pairing it with a
second column tips a composite index over.

Fix:
- Prefix-index OwnerUserId at 191 chars (safe under the legacy
  767-byte-per-column limit, still far wider than the GUID-like
  Identity ids actually stored) in every composite/unique index that
  includes it, so index creation no longer depends on the column's
  actual declared width.
- Wrap each CREATE INDEX in try/catch + LogWarning instead of letting
  it propagate: a schema reconciler is best-effort and one failed
  index must never crash startup, matching the existing non-fatal
  pattern already used a few lines below for legacy-schema ownership
  claims.

Backend build + full test suite (177 passing) verified green.
2026-07-12 19:50:58 +02:00
cesnimda 0cd1ba398e Merge pull request 'feat(ux): product/UX review implementation (onboarding, empty states, a11y, mobile kanban)' (#27) from feat/ux-review-quick-wins into main
CI and Deploy / test (push) Successful in 2m7s
CI and Deploy / deploy (push) Successful in 40s
2026-07-12 04:30:00 +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 7dadf8dde4 Merge pull request 'fix(auth): Google Sign-In audience mismatch + remove per-user accent color' (#26) from fix/google-signin-and-theming-cleanup into main
CI and Deploy / test (push) Successful in 2m5s
CI and Deploy / deploy (push) Successful in 1m10s
2026-07-12 03:06:35 +02:00