Commit Graph

134 Commits

Author SHA1 Message Date
cesnimda 3b86ea2da0 fix(cv): enforce readable render palettes
CI and Deploy / test (pull_request) Successful in 4m37s
CI and Deploy / deploy (pull_request) Has been skipped
Choose a contrasting header foreground for custom accents and keep public colour and font overrides inside safe supported values.
2026-08-15 17:08:20 +02:00
cesnimda f0b9b222ff fix(career): preserve reviewed profile values
CI and Deploy / test (pull_request) Successful in 4m39s
CI and Deploy / deploy (pull_request) Has been skipped
Keep extraction heuristics out of manual save, version, and import paths so reviewed locations, URLs, dates, and languages round-trip unchanged.
2026-08-15 16:56:31 +02:00
cesnimda a6cffe0473 feat(admin): show deployed app version
CI and Deploy / test (pull_request) Successful in 4m48s
CI and Deploy / deploy (pull_request) Has been skipped
2026-08-15 16:47:43 +02:00
cesnimda 5203ddea72 feat(cv): harden multi-page builder
CI and Deploy / test (pull_request) Failing after 3m0s
CI and Deploy / deploy (pull_request) Has been skipped
Wrap pathological content, paginate oversized entries, measure A4 and Letter previews correctly, unify section ordering, and gate stored-output actions on saved state.
2026-08-15 14:09:51 +02:00
cesnimda 109745edb0 feat(jobs): add dedicated workspace page
CI and Deploy / test (pull_request) Failing after 2m51s
CI and Deploy / deploy (pull_request) Has been skipped
Make /jobs/:id the canonical application workspace while preserving list state and compatibility links. Replace popup and expandable-row navigation with accessible whole-row routing and richer job details.
2026-08-15 13:33:00 +02:00
cesnimda 15e5464da7 fix(admin): protect administrator access
CI and Deploy / test (pull_request) Failing after 4m31s
CI and Deploy / deploy (pull_request) Has been skipped
Reject final-admin demotion and deletion at the API boundary. Require explicit confirmation before any administrator role removal.
2026-08-15 13:11:54 +02:00
cesnimda 82f45269d0 test(jobs): cover discovery browser journey
CI and Deploy / test (pull_request) Successful in 4m53s
CI and Deploy / deploy (pull_request) Has been skipped
2026-08-10 10:51:17 +02:00
cesnimda 511a9f6795 feat(jobs): expose discovery provenance
CI and Deploy / test (pull_request) Successful in 4m15s
CI and Deploy / deploy (pull_request) Has been skipped
2026-08-10 10:38:01 +02:00
cesnimda 29de2632f6 fix(email): gate draft attempt rotation
CI and Deploy / test (pull_request) Successful in 4m23s
CI and Deploy / deploy (pull_request) Has been skipped
Rotate persisted delivery identity only after the matching owner attempt is definitively failed. Keep stale, foreign, pending, sent, and uncertain drafts non-retryable.
2026-08-10 10:21:37 +02:00
cesnimda 80b5532c2f fix(email): preserve draft send identity
CI and Deploy / test (pull_request) Successful in 4m15s
CI and Deploy / deploy (pull_request) Has been skipped
Persist and export each draft's idempotency UUID so refresh and edits cannot create a fresh delivery identity. Add reversible provider-specific migration SQL.
2026-08-10 10:11:04 +02:00
cesnimda a9bb22e530 feat(email): add tenant-safe draft API
CI and Deploy / test (pull_request) Successful in 4m27s
CI and Deploy / deploy (pull_request) Has been skipped
Expose bounded draft CRUD with owned-job validation and revision conflicts. Saving drafts never contacts providers or sends email.
2026-08-10 10:05:39 +02:00
cesnimda 2fa4e38ba9 feat(email): include drafts in user exports
CI and Deploy / test (pull_request) Successful in 4m14s
CI and Deploy / deploy (pull_request) Has been skipped
Export readable private draft content only through existing owner-filtered encrypted and daily export boundaries, with cross-tenant regression coverage.
2026-08-10 10:00:34 +02:00
cesnimda 14b396a452 feat(email): add tenant draft persistence
CI and Deploy / test (pull_request) Successful in 4m15s
CI and Deploy / deploy (pull_request) Has been skipped
Add owner-filtered, job-cascading private draft storage with reversible SQLite and MariaDB migration paths. No API or UI exposes draft content yet.
2026-08-10 09:55:16 +02:00
cesnimda 7f41cb271c test(email): protect free send access 2026-08-10 09:48:20 +02:00
cesnimda 1dabbeb233 feat(email): confirm hub thread unlink 2026-08-10 09:40:59 +02:00
cesnimda aff34cc645 feat(email): export send attempt metadata
CI and Deploy / test (pull_request) Failing after 4m32s
CI and Deploy / deploy (pull_request) Has been skipped
Add content-free provider delivery history to encrypted and daily owner exports, exclude payload hashes, and lock in tenant-safe hard-delete cascades.
2026-08-10 00:48:32 +02:00
cesnimda 8fe39031ea fix(email): retire legacy SMTP follow-up
CI and Deploy / test (pull_request) Failing after 55s
CI and Deploy / deploy (pull_request) Has been skipped
Keep follow-up draft generation but remove the direct application SMTP delivery boundary. Route users to the provider-aware Job email flow and return 410 for legacy API callers.
2026-08-10 00:43:52 +02:00
cesnimda ee5ef7e12a fix(email): reconcile abandoned sends
Age stale pre-delivery attempts to failed and in-delivery attempts to uncertain without provider I/O. Notify each owner with content-free guidance and keep recovery idempotent across replicas and restarts.
2026-08-10 00:16:56 +02:00
cesnimda 123fc5555a feat(email): add explicit send API
Require tenant-owned jobs, explicit confirmation, canonical request IDs, and rate limiting before provider delivery. Persist sent correspondence with a content-free idempotency ledger, and never retry uncertain outcomes automatically.
2026-08-09 23:57:38 +02:00
cesnimda e9937accd8 feat(email): add delivery adapters
CI and Deploy / test (pull_request) Failing after 1m37s
CI and Deploy / deploy (pull_request) Has been skipped
Gmail and Graph request explicit send consent and classify provider rejection separately from uncertain transport failure. IMAP remains read-only; no send API is exposed.
2026-08-09 23:47:21 +02:00
cesnimda 653f011be2 feat(email): add durable send ledger
CI and Deploy / test (pull_request) Failing after 1m21s
CI and Deploy / deploy (pull_request) Has been skipped
Tracks only idempotency and delivery metadata; recipient, subject, and body are excluded. No provider send path is enabled.
2026-08-09 23:32:27 +02:00
cesnimda a20775c24a feat(email): add safe message detail
CI and Deploy / test (pull_request) Failing after 1m29s
CI and Deploy / deploy (pull_request) Has been skipped
2026-08-09 21:44:24 +02:00
cesnimda 536d403b08 feat(email): expose provider-neutral reads
CI and Deploy / test (pull_request) Failing after 1m37s
CI and Deploy / deploy (pull_request) Has been skipped
2026-08-09 21:35:04 +02:00
cesnimda da1aa8bb2a fix(match): prioritize meaningful job terms 2026-08-09 18:47:21 +02:00
cesnimda c3c5af8329 feat(cv)!: queue durable processing
CI and Deploy / test (pull_request) Failing after 1m39s
CI and Deploy / deploy (pull_request) Has been skipped
CV upload now returns 202 with an owner-scoped operation instead of holding the request through parsing. Existing review approval remains required.

BREAKING CHANGE: profile-cv upload responses use the durable operation contract.
2026-08-09 15:11:03 +02:00
cesnimda a62122640c feat(ai): queue strategy snapshots 2026-08-09 12:51:46 +02:00
cesnimda 5eb9b3cb96 feat(ai): enforce local-first routing
Keep external providers behind server consent, task, and prompt-cost gates while persisting actual provider provenance.
2026-08-09 12:30:11 +02:00
cesnimda c3f4a57195 feat/Update_Controllers_to_Allow_for_Premium_Membership 2026-08-03 09:17:28 +02:00
cesnimda 955182b7c2 fix: close release preflight gaps
CI and Deploy / test (pull_request) Failing after 1m22s
CI and Deploy / deploy (pull_request) Has been skipped
Route public health checks to the API, backfill and synchronize job opportunities, stabilize SPA smoke tests, and document operator-only production steps.
2026-07-31 20:18:30 +02:00
cesnimda ce76046a29 feat: complete release readiness work
- consolidate API ownership and remove dead vendor code

- add Stripe billing, learning paths, and public CV hardening

- add migration, recovery, security, audit, and browser gates
2026-07-31 16:54:16 +02:00
cesnimda 235d22c059 feat: add salary insights
CI and Deploy / test (push) Successful in 2m41s
CI and Deploy / deploy (push) Successful in 58s
2026-07-31 00:15:38 +02:00
cesnimda af3cf1cdaa fix: keep opportunity data synchronized
CI and Deploy / test (push) Successful in 2m46s
CI and Deploy / deploy (push) Successful in 54s
2026-07-31 00:12:10 +02:00
cesnimda 988a91a151 feat: persist job source and market
CI and Deploy / test (push) Successful in 2m34s
CI and Deploy / deploy (push) Successful in 56s
2026-07-30 23:38:22 +02:00
cesnimda 792814b04e feat: cap per-user AI token spend
CI and Deploy / test (push) Successful in 2m43s
CI and Deploy / deploy (push) Successful in 57s
2026-07-30 23:32:13 +02:00
cesnimda 09fc2b03f7 feat: gate premium CV themes
CI and Deploy / test (push) Successful in 2m35s
CI and Deploy / deploy (push) Successful in 1m1s
2026-07-30 23:24:19 +02:00
cesnimda fd2c60e70f feat: complete public CV sharing
CI and Deploy / test (push) Successful in 2m34s
CI and Deploy / deploy (push) Successful in 1m0s
2026-07-30 23:19:17 +02:00
cesnimda 158970fa01 feat: enforce account usage limits
CI and Deploy / test (push) Successful in 2m39s
CI and Deploy / deploy (push) Successful in 55s
2026-07-30 23:14:42 +02:00
cesnimda 9cd2e5c2e3 feat: protect auth with Turnstile
CI and Deploy / test (push) Successful in 2m41s
CI and Deploy / deploy (push) Successful in 59s
2026-07-30 23:08:36 +02:00
cesnimda 405e6d833c feat: add NAV job discovery
CI and Deploy / test (push) Successful in 2m27s
CI and Deploy / deploy (push) Successful in 1m0s
2026-07-30 22:57:17 +02:00
cesnimda c08232b9d7 feat: meter AI usage
CI and Deploy / test (push) Failing after 58s
CI and Deploy / deploy (push) Has been skipped
2026-07-30 22:39:24 +02:00
cesnimda 4cf26405f6 feat: complete phase 3 career workspace
CI and Deploy / test (push) Failing after 1m6s
CI and Deploy / deploy (push) Has been skipped
2026-07-30 22:19:13 +02:00
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 173187dcbb feat(cv): Phase 2.1-b — extract Projects, Certifications, and languages-from-prose
The structured model and StructuredCvProfileJson.FromSections already map
Projects/Certifications/Languages headings, but the AI normalize prompt
never emitted them, so on the benchmark CV the entire Projects section and
the in-summary languages (English Native, Norwegian B1) were silently
dropped. This closes that gap upstream — no backend schema or data change.

ai-service (tools/summarizer/app.py):
- /cv/normalize: added # Projects and # Certifications headings; a
  languages-from-prose rule (pull "native English", "Norwegian at B1" out
  of the summary even with no Languages section; ignore programming
  languages); and skill-group prefix stripping ("Development:",
  "DevOps & Infrastructure:", "Practices:" dropped, only the skills kept).
- /cv/classify-block: Projects and Certifications added to the section
  enum + rules (fallback path).

Backend:
- LooksLikeNormalizedMarkdownCv now recognises # Projects / # Certifications
  so those CVs still take the markdown assembly path.

Tests:
- CvExtractionCoverageTests (4) lock the C# mapping of Projects,
  Certifications and Languages sections into the structured profile.
- ai-service test_classify_block_supports_projects_section (1).
426 backend tests, 17 ai-service tests pass; app.py compiles.

The LLM behaviour (prompt -> headings) needs Ollama to observe and was not
run here; the C# side that consumes the headings is proven and the prompt
change is additive. Merge-not-replace + the review screen are the next
increment (2.1-a, approved: always-review, conservative merge).

Deployment: these prompts live in the ai-service container, which
deploy.sh does not rebuild by default -- deploy with
DEPLOY_BUILD_AI_SERVICE=true or the change won't take effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:40:04 +02:00
cesnimda 4db8c08958 fix(security): evaluate tenant CurrentUserId live, not at construction
CI and Deploy / test (push) Successful in 2m38s
CI and Deploy / deploy (push) Failing after 7s
Production POST /api/cv/variants returned 200 but GET /api/cv/variants/{id}
returned 404, with the query logged as `... FROM CvVariants WHERE FALSE`
(no parameters). The created row had a correct OwnerUserId; the read was
excluded by the global query filter because CurrentUserId was null at
query time. Reproduced locally: it affected EVERY tenant-filtered read
(CV list returned 0 after creating 5, JobApplications returned total 0),
not just CV -- writes worked, reads came back empty.

Root cause: the "local" JwtBearer OnTokenValidated resolves the
request-scoped JobTrackerContext (to run LocalSessionValidator) BEFORE the
authentication middleware assigns HttpContext.User. JobTrackerContext
captured CurrentUserId in its constructor from ICurrentUserService.UserId,
which reads HttpContext.User -- still unauthenticated at that point -- so
CurrentUserId froze to null. That same scoped instance is reused by the
controller, so `CurrentUserId != null && OwnerUserId == CurrentUserId`
compiled to WHERE FALSE for the whole request. POST worked because
CreateAsync sets OwnerUserId from the controller-resolved user, and
inserts are not filtered.

Fix: make CurrentUserId a computed property that reads
ICurrentUserService.UserId live, so the query filters see the
authenticated user at query-execution time. Deny-on-null is preserved
(still null for an unauthenticated principal). LocalSessionValidator is
unaffected -- it already uses IgnoreQueryFilters and queries by explicit
sid.

Verified on a real MariaDB 11 container end to end: create then read a
variant returns 200, the variant list returns all rows, and
GET /api/jobapplications reads normally. Added
CurrentUserIdLiveEvaluationTests pinning the live-evaluation behaviour
(both fail against a constructor snapshot). 422 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:20:09 +02:00
cesnimda c1ff98ff4c fix(test): make timeline day-grouping test deterministic
Timeline_groups_by_day_newest_first seeded two "same day" events as
DateTime.Now.AddDays(-3) and DateTime.Now.AddDays(-3).AddHours(2). When
the wall clock was within two hours of midnight the second timestamp
crossed into the next calendar day, so the service grouped them into two
days instead of one and the test failed (expected 2 day-groups, got 3).

The service is correct -- it groups by e.At.Date, which is the intended
behaviour and what the test name asserts. The test was nondeterministic,
failing roughly two hours out of every twenty-four, including in CI
whenever CI ran late in the day.

Anchor the two older events to DateTime.Today plus fixed hours (9 and 11)
so they always land on the same calendar day regardless of wall-clock
time. The "today" event stays DateTime.Now so the "Today" label
assertion still exercises the real path.

Verified: 420 tests pass at 22:35 local (the failing window) and on Linux
with full ICU and under DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:52:24 +02:00
cesnimda 96816186cb fix(cv): preserve human languages during structured CV normalization
HumanLanguageCatalog built its lookup table solely from
CultureInfo.GetCultures, so which languages counted as human languages
depended on the host's ICU data rather than on the CV. Measured: 806
cultures on a normal Windows or Linux machine, exactly 1 under
globalization-invariant mode, and an English-only subset on a container
with trimmed ICU data.

Consequences by environment, all silent:
- full ICU: correct
- trimmed ICU: canonical names present in the reduced data survive and
  the rest are dropped, so a CV keeps English and loses Norwegian
- invariant: every language is dropped and a CV import loses its
  Languages section entirely, with no error

The tests were right and are unchanged. Seed the catalog explicitly with
the languages a CV realistically lists, before the culture enumeration,
which still runs and still adds breadth. Nothing in the seed collides
with a technical skill -- Go, Java, Swift, Rust and Basic are
deliberately absent, and Basic is also a proficiency level.

Verified 420 tests pass in four environments: Windows and Linux, each
with full ICU and with DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1. Before
this change the invariant runs failed 5 tests. No test was modified,
skipped or relaxed.

Added HumanLanguageCatalogTests to pin the seeded catalog, confirmed
non-vacuous by removing the seed and watching 15 tests fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:19:17 +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 432e1fd667 feat(timeline): emit application lifecycle events
CI and Deploy / test (push) Failing after 1m3s
CI and Deploy / deploy (push) Has been skipped
The timeline could interpret InterviewScheduled, InterviewCompleted,
OfferReceived and FollowUpCompleted, but only StatusChanged and FollowUpSet were
ever written, so those branches never rendered.

Events are now derived from the status TRANSITION in one shared emitter rather
than at each call site, so the two status-change boundaries in
JobApplicationsController cannot drift apart and a third would get the behaviour
for free. Both boundaries now call it instead of hand-writing the StatusChanged
block.

Deriving from the transition rather than the resulting state is what prevents
duplicates: one user action produces at most one lifecycle event, re-saving an
unchanged status produces none, and reaching an offer twice records it once.
Moving an application backwards is treated as a correction, not a completed
interview, so only a forward move out of an interview stage counts. An
Interview to Offer move reports the offer, which is the thing the user cares
about.

Completing a follow-up checklist item emits FollowUpCompleted, guarded on the
same transition rule so re-saving a done item stays silent. The task itself
remains a checklist item — this only records that it happened.

No new history store: every event is a JobEvent row, which stays the single
source of application history.

393 backend tests pass, including timeline rendering of the emitted events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:30:40 +02:00
cesnimda c0bf69ad56 fix(security): enforce explicit api authorization
CI and Deploy / test (push) Failing after 1m10s
CI and Deploy / deploy (push) Has been skipped
Authentication relied on a fallback policy gated on Auth:Require, which defaults
to false. Five user-owned controllers carried no [Authorize] of their own, so a
deployment that lost that flag would have served tenant data anonymously:
JobApplications, Companies, Correspondence, Rules and JobImport. All five now
declare [Authorize(AuthenticationSchemes = "local")] explicitly.

This does not affect local development, which already sets Auth:Require=true in
appsettings.Development.json — the gap was only ever in a production
configuration that omitted the flag.

Added a reflection test over every controller in the assembly so a new one
cannot ship unprotected by accident. A controller passes if the class requires
authorization, or if every action declares its own [Authorize] or
[AllowAnonymous] — the shape AuthController and TwoFactorController need, since
login and register must stay anonymous while the rest must not. Public endpoints
are an explicit allow-list, so making something anonymous is now a deliberate
edit rather than an omission.

That test found one real gap: AuthController.Logout declared neither attribute.
It is now explicitly [AllowAnonymous] — it only clears the caller's own session
cookies and leaks nothing, and requiring authentication would leave a user whose
token had already expired unable to sign out.

Also pinned: admin controllers require the Admin role rather than merely a
signed-in user, and PublicCvController stays anonymous so shared CV links keep
working.

384 backend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:27:27 +02:00
cesnimda fc132273f7 feat(ai): include application intelligence in interview preparation
CI and Deploy / test (push) Failing after 2m21s
CI and Deploy / deploy (push) Has been skipped
Interview generation saw only the profile and the advert, so it produced generic
questions. It now also receives what the workspace already computed: seniority,
employment type, key requirements and advert technologies from the job analysis,
plus the match score, the skills the candidate demonstrably has, the most
relevant experience and projects — and above all the gaps, which is exactly what
an interviewer probes.

No second pipeline. The context comes from ApplicationIntelligenceService, which
is deterministic and read-only, so this adds no AI call and cannot alter user
data. Generation still runs through AiWorkspaceService and is still appended to
AiInteraction.

The dependency is optional, so existing constructions keep working and a missing
intelligence service degrades to the previous prompt instead of failing. Only the
interview module is affected; job-analysis, career-match, cover-letter and
application-review assemble exactly as before.

Suggestion-only is unchanged and now pinned by tests: generation adds an
AiInteraction and nothing else, creates no InterviewPrepItem, leaves existing
prep items and the CareerProfile untouched, and refuses another user's
application. Context is scoped to the requesting user, so another user's profile
is never scored in.

379 backend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:07:00 +02:00