Commit Graph

61 Commits

Author SHA1 Message Date
cesnimda 56fed05d70 feat: complete phase 2 UX improvements
CI and Deploy / test (push) Successful in 2m42s
CI and Deploy / deploy (push) Failing after 9s
2026-07-30 21:35:52 +02:00
cesnimda 8f6f2ba8d6 fix(health): report configured application version
/health read the APP_VERSION environment variable directly, but
docker-compose passes App__Version, which binds to the App:Version
configuration key. The variable under that name never existed in the
container, so the endpoint always reported "unknown".

Read App:Version through IConfiguration, the approach AdminSystemController
already used for the same value. The resolution rule (configured version,
else assembly version) moves to a shared BuildMetadata helper rather than
being written twice; AdminSystemController now calls it, so the admin page
and /health cannot drift apart.

Local development is unaffected: nothing sets App:Version there, and the
assembly-version fallback still applies.

Tests pin the configuration KEY, not just the behaviour, including that an
App__Version environment variable binds to App:Version. The original bug
failed silently, so a behavioural test alone would not have caught it.

Verified against a running backend: App__Version=9.9.9-test reports
9.9.9-test; unset reports the assembly version rather than "unknown".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:59:26 +02:00
cesnimda 93462b799c chore(ops): add deployment backups restore docs and health checks
CI and Deploy / test (push) Failing after 1m8s
CI and Deploy / deploy (push) Has been skipped
Closes the three operational blockers from the production readiness review.

deploy.sh now takes a database backup before it builds, stops or replaces
anything, and aborts the deploy if the backup fails — so no deploy proceeds
without a restore point. Dumps are gzipped and timestamped into
/opt/job-tracker/backups (override with BACKUP_DIR), so one deploy never
overwrites an earlier backup. Credentials come from the existing connection
string and travel via MYSQL_PWD, never on the command line, so they cannot reach
the process list or the deploy log. A dump that is empty or missing CREATE TABLE
is rejected, because a truncated file that looks like a restore point is worse
than none. SQLite deployments get their data volume tarred instead. Nothing is
ever deleted automatically; retention is documented as manual.

deploy/README.md documents backup creation, location, retention, database
restore, application rollback, and when to use which — restore and rollback kept
distinct, because a bad deploy usually needs only the rollback and restoring
would discard everything written since the dump.

Health checks now cover backend and frontend, which previously had none. GET
/health is anonymous, cheap, and deliberately does not touch the database: a
health check that queried MariaDB would restart a healthy backend whenever the
database blipped, and would hand out an unauthenticated way to probe database
availability. The backend image gains curl on the existing chromium apt layer,
since the aspnet runtime ships neither curl nor wget. frontend now waits for
backend to be healthy rather than merely started, because nginx proxies /api and
refuses to start when the upstream cannot be resolved.

Verified against real containers, no production data: backup from a seeded
MariaDB 11; restore into a clean MariaDB 11 with rows identical; bad credentials
and a missing connection string both abort non-zero and leave no partial file;
SQLite volume backup produces a readable archive; backend and frontend both
reach healthy; and a backend pointed at an unreachable database exits and is
reported unhealthy, so a broken deploy cannot present as a running stack.

Incidentally confirmed the earlier authorization work: with Auth:Require unset,
/health returns 200 while /api/jobapplications returns 401.

393 backend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:49:31 +02:00
cesnimda 3d74baef78 feat(workspace): interview and follow-up workflow
CI and Deploy / test (push) Failing after 3m54s
CI and Deploy / deploy (push) Has been skipped
Phase 5.5. Completes the lifecycle after submission: prepare, communicate, chase.

Interview preparation gets a durable, user-owned store. There were already two
per-application AI stores, InterviewPrepNote and AiWorkspaceNote, but both are
caches that regenerate when their context signature changes — anything a user
typed into them would eventually be overwritten. InterviewPrepItem is the side
nothing regenerates, covering company research, technical notes, behavioural
answers, STAR examples and the user's own questions in one table, because those
categories differ only by label and adding one must not need a migration. Each
item records whether the user wrote it or accepted a suggestion, and an
IsPrepared flag makes the section double as the preparation checklist.

Generation stays in the existing AiWorkspaceService "interview" module, appended
to AiInteraction as before. A suggestion is history until the user adds it as a
prep item; opening the section generates nothing.

Follow-up reuses what exists rather than adding a tracker. The date is
JobApplication.FollowUpAt, the same field RulesEngine and the reminder hosted
service already act on, so reminders keep working with no new wiring. The task
stays an ApplicationChecklistItem in the follow-up category — the section counts
open tasks without owning them. The record is a FollowUpSet JobEvent, the same
type the rest of the app emits.

Communication is untouched: Correspondence already owns recruiter contacts,
history and notes, and the workspace already mounted it.

The timeline interpreter learned five more types — InterviewScheduled,
InterviewCompleted and OfferReceived as milestones, FollowUpCreated and
FollowUpCompleted as routine, deliberately outside the milestone spine so it
stays a summary of what actually happened. JobEvent remains the history source.

InterviewPrepItems is reconciler-owned with a no-op migration, guarded on
JobApplications, and verified on a fresh MariaDB 11: int AUTO_INCREMENT primary
key, varchar owner and title, tinyint flag, datetime(6), composite index inside
the key limit.

371 backend tests, 128 frontend tests, Release build and the production build all
pass locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:01:33 +02:00
cesnimda 02b38f7acb feat(workspace): application assets workflow
CI and Deploy / test (push) Failing after 1m11s
CI and Deploy / deploy (push) Has been skipped
Phase 5.4. Connects the career outputs a user already has to one job
application, without building a second copy of any of them.

The flow is strictly one-directional — CareerProfile -> CvVariant ->
application output — and nothing writes back up. No code path in this phase
touches CareerProfile or its children.

CV integration re-points rather than duplicates. GET/PUT /{id}/cv attaches one
variant to an application via CvVariant.JobApplicationId; replacing detaches the
previous variant instead of deleting it. Creating, duplicating, editing, theming,
previewing, exporting PDF and version history all stay in the existing CV
builder, which the section links into. There is no second CV system.

Tailoring composes the Phase 5.3 analysis and match into skills to highlight,
experience to prioritise, projects to emphasise, keywords to include and gaps to
address. Deterministic and advisory: it says what the user could emphasise and
the user edits the variant themselves. Nothing auto-applies.

Cover letters gain the history they were missing. JobApplication.CoverLetterText
stays the current text with its API contract unchanged; CoverLetterVersions
records what it used to be, so an AI rewrite is never destructive. Restore is
additive — the old text comes back as a new version, so what you restored from
still exists. Source and AiAction record whether the user wrote a version or
approved it from a suggestion, and an AI generation only becomes a version once
the user saves it.

Documents are untouched: the existing Attachment system already covers CV, cover
letter, certificates and portfolio files with a Purpose field, so the workspace
mounts that component rather than adding a second upload path.

CoverLetterVersions is the only new table — reconciler-owned, no-op migration,
guarded on JobApplications, and verified on a fresh MariaDB 11: int
AUTO_INCREMENT primary key, varchar(255) owner, datetime(6), composite index
inside the key limit.

360 backend tests, 115 frontend tests, type check, Release build and the
production build all pass locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:36:27 +02:00
cesnimda a7cecce13d feat(workspace): add application intelligence
CI and Deploy / test (push) Failing after 1m11s
CI and Deploy / deploy (push) Has been skipped
Phase 5.3. Three read-only reads that answer "how suitable is this job", "how
does my experience match", "what am I missing", "what happened previously".

Timeline (GET /{id}/timeline) is an interpretation layer over JobEvent, which
stays the source of historical truth. Each row gains a readable summary, a
category and a milestone flag; events group by day. Milestones are returned
unfiltered, because narrowing the detail must not hide what actually happened.

Job analysis (GET /{id}/analysis) extracts role, company, location, employment
type, seniority, salary, technologies, skills, responsibilities and keywords
from the advert, reusing the existing SkillTagger so the vocabulary matches the
job importer. It also reports what the advert does NOT say, which is usually the
more useful half.

Career matching (GET /{id}/match) feeds the master CareerProfile into the same
JobCvMatchService the CV builder uses, so one application scores identically
whichever surface asks. It returns the score, matched and missing skills, and
which experience and project entries are the evidence for each match.

All three are deterministic and own no data — no new table, no new column, and
nothing writes to the CareerProfile, a CvVariant, or the JobApplication. The AI
narrative stays where it already was, in AiWorkspaceService's job-analysis and
career-match modules, generated only when the user asks and versioned by the
append-only AiInteraction history. Opening a section costs nothing and changes
nothing.

Frontend adds Timeline, Analysis and Match sections to the workspace, sharing
one loader so loading, empty and error states are consistent. The deterministic
answer renders first, with the AI panel below it.

345 backend tests, 104 frontend tests, type check, production build all pass
locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:52:37 +02:00
cesnimda 3a906b881e feat(workspace): unified application checklist (Phase 5 milestone 2)
CI and Deploy / test (push) Failing after 1m8s
CI and Deploy / deploy (push) Has been skipped
Evolve the existing readiness workflow into one persisted, user-controlled
checklist rather than adding a second tracker.

ApplicationChecklistItem records only completion state and user intent. Each
default system item carries a stable SystemKey and an AutoSignal — the same
signal /readiness already computed — and re-syncs on every read: a satisfied
signal auto-completes the item, a reverted signal reopens it, and a manual tick
always wins. Users can add, reorder, dismiss and delete.

Readiness is refactored into a projection of the checklist (score = completion
percentage, completed/missing = live items by status). Its DTO shape and the
workflowSignal/reminders health view are unchanged, so no API contract breaks.

The workspace's next recommended action now comes from the first pending
checklist item in category priority order (preparation, submission, follow-up,
interview, custom), replacing the parallel ruleset — so the overview can never
recommend something already ticked off, and a user's own task can be next.

The table follows the established MariaDB-safe path: the scaffolded migration is
a no-op and the idempotent reconciler owns the DDL for both providers. Verified
on MariaDB 11 — auto_increment PK, varchar/datetime(6)/tinyint(1) columns, both
indexes inside the key limit, cascade delete, unique system key per application,
and NULL system keys not colliding for custom items.

329 backend tests, 94 frontend tests, type check, production build and both
Docker builds pass locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:15:46 +02:00
cesnimda e55a6e86b7 feat(workspace): Application Workspace foundation (Phase 5 milestone 1)
CI and Deploy / test (push) Failing after 1m13s
CI and Deploy / deploy (push) Has been skipped
Every JobApplication gets a dedicated workspace at /applications/{id} — a
surface, not a new data store. It owns no data and duplicates none: CV comes
from the Phase 4 CvVariant lens, analysis/match/interview from the existing
AiWorkspacePanel, documents from Attachments, communication from
Correspondence, activity from JobEvent, stage semantics from JobPipeline. No
career data is copied and nothing here writes.

- GET /api/jobapplications/{id}/workspace: one aggregate read (role, company,
  stage, dates, attached CV variant, cover letter, documents, AI history,
  recent activity) replacing the page fanning out across endpoints
- Next recommended action: ordered rules answering "what do I do next?", the
  core product principle for this phase
- ApplicationWorkspacePage: left nav + linkable ?section=, reusing the existing
  component for each domain; later-milestone sections say so rather than faking
- Entry point from the job dialog via an optional onOpenWorkspace callback —
  the dialog must not depend on router context (it is mounted without a
  <Router> in several suites), so the caller owns navigation
- 8 backend tests (aggregate, CV variant surfacing, counts, activity ordering,
  next-step rules, tenant scoping)

Local: 314 backend, 88/88 frontend (31 suites), tsc clean, production build ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:57:24 +02:00
cesnimda f299d7be7c feat(ai): AI Workspace per job application — modules + append-only history
CI and Deploy / test (push) Failing after 1m52s
CI and Deploy / deploy (push) Has been skipped
Phase 5 backend. A unified AI Workspace for each application, orchestrating the
five suggestion modules through the existing ISummarizerService provider
abstraction and storing every generation as append-only history (AiInteraction)
so outputs can be reused, compared, and deleted — distinct from the existing
AiWorkspaceNote cache (one row, overwritten).

Modules (all suggestion-only, "never invent facts" guardrail, never mutate the
profile/variant/application): job-analysis, career-match, cover-letter (6 modes),
interview, application-review. Each builds a prompt from the job + master profile
text and returns markdown.

- Models/AiInteraction.cs + migration AddAiInteractions (verified on container)
- Services/AiWorkspaceService.cs (prompts, history, delete)
- Controllers/AiWorkspaceController.cs (/api/jobapplications/{id}/ai:
  generate, history, delete, modules+provider)
- 7 tests (store, history filter/order, delete, mode normalization, unknown
  module, empty output, tenant scoping); 306 backend green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 15:17:13 +02:00
cesnimda a3e18e4b44 feat(career): CV builder backend — data-driven theme engine + variant model
CI and Deploy / test (push) Failing after 1m51s
CI and Deploy / deploy (push) Has been skipped
Phase 4 foundation. A CvVariant is a lens over the master CareerProfile
(section order/visibility, per-item overrides keyed by ItemKey, theme +
builder settings) — it references career data, never duplicates it. One
renderer (ThemedCvRenderer) draws every theme; a theme is pure data
(CvThemeCatalog, 8 professional themes), so adding a theme needs no renderer
change. Autosave version history + non-destructive restore, public CV via
/api/public-cv/{slug} (anonymous, noindex, filter-bypassing owner load), and
an AI-assist endpoint reusing the existing provider abstraction (suggestions
only, never auto-applied).

- Models: CvVariant/CvVariantVersion, CvVariantSettings, CvTheme + catalog
- Services: CvVariantResolver (profile+lens -> render model), ThemedCvRenderer,
  CvVariantService, CareerProfileService.LoadStructuredForOwnerAsync (public)
- API: CvVariantController (/api/cv), PublicCvController (/api/public-cv)
- Migration AddCvVariants (2 self-contained tables; verified applied on the
  running container), 16 tests (resolver/renderer/service), 296 backend green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 09:52:58 +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 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 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 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 a8e2f4dc4a feat(email): add ImapProvider (generic IMAP for unsupported providers)
CI and Deploy / test (pull_request) Successful in 2m2s
CI and Deploy / deploy (pull_request) Has been skipped
b3 of the multi-provider email roadmap. Adds ImapConnection model + table
(reconciler pattern, SQLite+MySQL), ImapService (MailKit-backed IMAP client),
ImapProvider implementing the existing IEmailProvider contract unchanged,
and ImapController for credential-based connect (no OAuth — user supplies
host/username/password directly, verified by a live connect before storage).

Scope, documented inline with ponytail: comments:
- INBOX only, no multi-folder support.
- Thread grouping approximates the References/In-Reply-To chain root rather
  than the IMAP THREAD extension, which not every server implements.
- External message ids are IMAP UIDs, scoped to the connection's current
  UIDVALIDITY.

Security: ran the security-audit skill against this diff (credential
handling + arbitrary-host connect is exactly the class of change the
standing security gate exists for). Found and fixed a real SSRF: the
connect endpoint let an authenticated user point the server at an
arbitrary host:port with no internal-range check, and connect-vs-auth
failure was distinguishable to the caller -- together a working oracle to
fingerprint internal services (loopback/RFC1918/link-local/cloud metadata)
from the server's network position. Fixed with EnsureHostIsExternalAsync
(DNS-resolve + reject internal ranges, re-checked on every reconnect to
close the DNS-rebinding gap) and a single generic failure message that no
longer distinguishes connect vs auth failure. 7 regression tests added.

Dependency: MailKit 4.17.0 (MIT license) on JobTrackerBackend.csproj --
stdlib has no IMAP client; hand-rolling IMAP4rev1 (TLS, SASL, MIME parsing)
would be a large, security-sensitive protocol implementation nobody asked
for, so this is the correct dependency, not a stdlib substitute.

168/168 green (161 existing + 7 new SSRF regression tests; the earlier
14 IMAP feature tests are included in the 161).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:40:50 +02:00
cesnimda cacad5cc94 feat(email): add MicrosoftGraphProvider (Outlook/365 via Graph OAuth)
CI and Deploy / test (pull_request) Successful in 2m1s
CI and Deploy / deploy (pull_request) Has been skipped
b2 of the multi-provider email roadmap. Mirrors the Gmail provider's shape
end-to-end so the two stay structurally interchangeable:

- MicrosoftGraphConnection model + table (reconciler pattern, SQLite+MySQL,
  same shape as GmailConnection: encrypted refresh/access token, sync state).
- MicrosoftGraphOAuthService: auth-code + offline-access flow against
  login.microsoftonline.com, encrypted token storage via IDataProtector,
  message search/thread/detail fetch against Microsoft Graph (conversationId
  stands in for Gmail's threadId), attachment listing.
- MicrosoftGraphProvider implements IEmailProvider — no contract changes;
  the existing seam was already provider-neutral.
- MicrosoftGraphController: connect-url/oauth/callback/status/disconnect,
  mirrors GmailController's OAuth surface exactly (including the popup
  postMessage handshake). Job-matching/review endpoints stay Gmail-only for
  now, per the roadmap — generalising those needs the frontend provider
  picker work, not this slice.
- Registered in DI + IEmailProviderRegistry (multi-registration of
  IEmailProvider, resolved by ProviderKey).
- Config: Microsoft:ClientId/ClientSecret/TenantId/RedirectUri, wired through
  docker-compose.yml + .env.example alongside the existing Google:Gmail* keys.
- Tests: MicrosoftGraphControllerTests (OAuth lifecycle) +
  MicrosoftGraphProviderTests (DTO mapping onto the neutral contract).
  147/147 green (135 existing + 12 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:08:11 +02:00
cesnimda c53d7978bb feat(email): introduce IEmailProvider seam + GmailProvider adapter
First slice toward multi-provider email (Gmail + Microsoft Graph + IMAP +
manual/free-text, per docs/remaster/PRODUCT_DIRECTION.md). Adds a provider-
neutral contract (search / list-thread / get-message / get-connection) with
neutral DTOs, a registry to resolve providers by key, and a GmailProvider that
adapts the existing IGmailOAuthService to it.

No behaviour change: the seam is registered in DI but not yet consumed. Follow-up
slices migrate GmailController's read paths onto IEmailProvider (folding in the
N+1 fixes) and add MicrosoftGraphProvider / ImapProvider / a manual provider.

Build clean; backend suite 135/135 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:42:46 +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 2989a6fa2c refactor(analytics): extract AnalyticsService from JobApplicationsController
First Wave 2 (safe refactor) slice. Move the read-only stats/overview aggregation
out of the 3.3k-line JobApplicationsController into a dedicated, injectable
AnalyticsService, and lift its response DTOs (JobStats, FunnelStagePoint,
ResponseRatePoint, CompanyActivityPoint, AnalyticsOverviewDto) into
Models/AnalyticsDtos.cs.

- GetStats: ~44 lines -> 3 (delegates to AnalyticsService.GetStatsAsync).
- GetAnalyticsOverview: ~82 lines -> 3 (delegates to GetAnalyticsOverviewAsync).
- Registered AddScoped<AnalyticsService>(); controller keeps an optional ctor
  param with a `?? new AnalyticsService(db)` fallback so the 6 test sites that
  construct the controller directly keep compiling.
- Logic is byte-identical (same tenant-scoped context, same projections) so
  behaviour is preserved.

Backend suite: 92/92 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 10:51:08 +02:00
cesnimda 3fad43a9e2 feat: deterministic CV-to-job match score endpoint
New JobCvMatchService: a pure, AI-free keyword-coverage scorer that
returns a stable, reproducible 0-100 match score plus matched/missing
keyword lists and per-CV-section coverage. Unlike candidate-fit (AI
narrative), it makes no model calls, so results are instant and
identical for identical inputs - the Jobscan-style differentiator.

- GET /api/jobapplications/{id}/match-score
- keywords = curated SkillTagger tags (high weight) + salient posting
  terms (title terms boosted); word-boundary matching avoids false hits
- section coverage shows where CV evidence is concentrated
- fix(SkillTagger): punctuation-tolerant C#/.NET patterns; the old \b
  boundaries silently missed 'C#,' and '.NET,' everywhere they are used
- 7 unit tests on the pure scorer; full backend suite green (104)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 03:16:32 +02:00
cesnimda 8f174cb767 feat: dev-only OpenAPI document at /openapi/v1.json
- AddOpenApi/MapOpenApi (anonymous, Development environment only).
- security: mark ProfileCvController.ProcessQueuedRunAsync [NonAction] -
  the controller-level [Route] exposed this background-service hook as a
  routable any-verb endpoint, which also broke OpenAPI generation.

96 endpoint paths documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:00:27 +02:00
cesnimda 999d6e05e7 feat: automated daily SQLite database backups with retention
New DatabaseBackupHostedService + SqliteDatabaseBackupRunner:
- daily VACUUM INTO snapshot to <Data:Root>/backups (safe with WAL)
- catch-up backup at startup when none exists from the last 24h
- retention pruning (Backups:RetainCount, default 14)
- warns and stays idle on MySQL/MariaDB where external backups apply

Production previously had no automated database backup on Linux
(the /api/backup endpoint is Windows-DPAPI-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:49:58 +02:00
cesnimda fcccecefa3 Fix startup admin seeding connection scope 2026-04-11 18:27:33 +02:00
cesnimda 48cd83b442 Clean error alerts and harden startup migration 2026-04-11 18:07:20 +02:00
cesnimda 09e96ce381 Fail closed on malformed local auth 2026-04-11 16:29:53 +02:00
cesnimda 6a223a4b70 Harden job import SSRF validation 2026-04-11 16:26:14 +02:00
cesnimda 27fd70a2d7 refactor, security updates, cv extraction upgrades 2026-04-11 01:34:32 +02:00
cesnimda 8852b501f5 Create missing MySQL rule settings tables 2026-04-09 21:35:17 +02:00
cesnimda b6a36cd860 Repair MySQL auto increment drift for core tables 2026-04-09 21:00:04 +02:00
cesnimda b87e673d38 feat: add gmail review actions 2026-04-01 21:54:05 +02:00
cesnimda 161ecb4b94 feat: add gmail review decisions 2026-04-01 21:45:01 +02:00
cesnimda 69e78d8951 refactor: extract gmail matching service 2026-04-01 16:59:29 +02:00
cesnimda f48136f04c feat: enrich gmail correspondence metadata 2026-04-01 16:27:34 +02:00
cesnimda e5bcf9d5ea feat: harden gmail sync foundation 2026-04-01 16:09:29 +02:00
cesnimda 44000f96f2 Improve CV parsing and profile editor flow 2026-03-29 14:29:18 +02:00
cesnimda 839a2ed80d Add CV template preview and PDF export pipeline 2026-03-29 00:43:54 +01:00
cesnimda 107c181506 Add canonical CV artifact pipeline 2026-03-28 23:32:54 +01:00
cesnimda 4103f84f85 Fix account and backup admin settings flows 2026-03-28 15:30:07 +01:00
cesnimda f5cede1014 Harden production schema fallback and profile/dashboard UI 2026-03-27 14:26:06 +01:00
cesnimda e52a1a1da1 Backfill missing AspNetUsers columns on MySQL startup 2026-03-27 13:39:00 +01:00
cesnimda 3e5f796326 Complete S01 Gmail matching and import workflow 2026-03-24 10:06:50 +01:00
cesnimda c33640986e Persist structured CV section data 2026-03-23 23:48:39 +01:00
cesnimda 603f5e8b74 Add attachment metadata and overview strategy snapshot 2026-03-23 22:46:44 +01:00
cesnimda 5a31f86e4d Add reminder emails and AI CV improvement tools 2026-03-23 21:46:37 +01:00
cesnimda 653f713a78 Evolve summarizer into AI service with OCR support 2026-03-23 20:12:34 +01:00
cesnimda fcafda6f52 Polish UI, harden company creation, and add error pages 2026-03-23 19:34:29 +01:00
cesnimda 8f5eab2fe4 fix: guard legacy ownership claim on mariadb startup 2026-03-23 00:25:34 +01:00
cesnimda 49b33c7b05 fix: avoid disposed mariadb connection during startup 2026-03-23 00:05:29 +01:00
cesnimda 6a722d3530 fix: bootstrap identity tables for mariadb startup 2026-03-22 23:58:15 +01:00