Commit Graph

203 Commits

Author SHA1 Message Date
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 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 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 de937d25dc fix(db): repair Jobs identity
CI and Deploy / test (pull_request) Successful in 4m40s
CI and Deploy / deploy (pull_request) Has been skipped
CI and Deploy / test (push) Successful in 4m11s
CI and Deploy / deploy (push) Successful in 1m36s
Production's SQLite-shaped Jobs table lacks AUTO_INCREMENT, causing the legacy opportunity backfill to abort startup. Reuse the existing idempotent MariaDB primary-key repair before backfill runs.
2026-08-02 19:22:23 +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 10d548f799 feat: show account storage usage
CI and Deploy / test (push) Successful in 2m37s
CI and Deploy / deploy (push) Successful in 58s
2026-07-31 00:10:15 +02:00
cesnimda 2126a2db5c fix: reconcile orphan migration history
CI and Deploy / test (push) Successful in 2m42s
CI and Deploy / deploy (push) Successful in 55s
2026-07-30 23:51:08 +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 efc9d83c25 fix: finish prospect workflow safeguards
CI and Deploy / test (push) Successful in 2m37s
CI and Deploy / deploy (push) Successful in 54s
2026-07-30 23:34:42 +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 e4acfbd0bf refactor: complete phase 4 builder cleanup 2026-07-30 22:23:55 +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 474654b1c1 fix(cv): add LongTailJson to reconciler CareerProfiles schema
Production GET /api/cv/outline returned 500 "Unknown column
'c.LongTailJson'". CareerProfiles is reconciler-owned, but the
reconciler's CREATE TABLE (both the SQLite and MySQL branches) only
listed Id, OwnerUserId, ProfileJson, Version, CreatedAtUtc, UpdatedAtUtc.
LongTailJson was added to the CareerProfile model in Phase 3 but neither
CREATE was updated and no column-repair existed, so:

- existing databases (prod): the MySQL CREATE is guarded on
  !HasMySqlTable, so it never runs once the table exists, and nothing
  adds the column -> LoadStructuredAsync selects a column that isn't
  there.
- fresh databases: the CREATE itself omitted the column, so even a brand
  new MariaDB/SQLite was missing it. The 420 tests never caught this
  because they build tables from the EF model, not the reconciler DDL.
  The release audit missed it because it never exercised /api/cv/outline.

Add LongTailJson to both CREATE statements and add an additive repair
(EnsureColumn / EnsureMySqlColumn) for existing tables. DEFAULT ''
backfills existing rows and matches the non-nullable model property.
This is the sanctioned reconciler repair path, not a manual ALTER, and
preserves existing data (ADD COLUMN is non-destructive).

Verified on a real MariaDB 11 container: an existing 6-column
CareerProfiles gains LongTailJson on startup (repair path), a fresh DB
gets it from the CREATE (longtext), and GET /api/cv/outline returns 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:19:52 +02:00
cesnimda 95646e1d53 fix(db): create follow-up reminder index on MariaDB
The reconciler's IX_JobApplications_OwnerUserId_FollowUpAt was declared as
(OwnerUserId(191), FollowUpAt) with no prefix length on FollowUpAt. But
FollowUpAt is `text` on MariaDB -- JobApplications is migration-owned and
the migration was scaffolded against SQLite, which stores DateTimeOffset
as TEXT. A text column cannot be indexed without a prefix length, so this
index failed the 3072-byte key check on EVERY MariaDB boot, was caught by
TryCreateIndex, and was silently skipped -- leaving the follow-up reminder
query (OwnerUserId + FollowUpAt) unindexed.

Two consequences, both real:
- the index the code intends to create never existed on MariaDB
- every healthy boot logged "Specified key was too long", which
  deploy/first-production-deployment.md lists as a STOP-AND-ROLL-BACK
  signal -- so an operator following the runbook could abort a good deploy

The author already handled the identical problem for the longtext Status
column one line below with Status(50). Apply the same fix: FollowUpAt(20).
ISO-8601 date strings sort lexicographically, so a 20-char prefix
("YYYY-MM-DD HH:MM:SS") keeps the index useful for the reminder scan.

Verified on a fresh empty MariaDB 11 container: the index is now created
(both key parts present), zero "Specified key was too long" lines, zero
skipped indexes, zero unhandled exceptions, 42 tables, app healthy. This
was the only unprefixed text column in any reconciler composite index --
the datetime columns on reconciler-owned tables are datetime(6). SQLite is
unaffected (its CREATE INDEX has no key-length limit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:52:39 +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 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
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