Commit Graph

12 Commits

Author SHA1 Message Date
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 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 7f426e255c fix(infrastructure): support clean MariaDB initialization
CI and Deploy / test (push) Failing after 1m1s
CI and Deploy / deploy (push) Has been skipped
A completely empty MariaDB database could not start: the reconciler assumed
migration-owned tables already existed, and migrations assumed reconciler-owned
tables already existed. Neither could go first. Existing databases worked, so
only fresh installs were affected.

Startup is now an explicit sequence: connect, reconcile, migrate, reconcile,
start. The reconciler runs twice because neither position alone works — pass 1
repairs legacy schemas and creates the reconciler-owned tables that migrations
reference, pass 2 picks up everything that could not exist yet on a fresh
database. Every statement is existence-guarded, so the second pass is a no-op
scan on a correct database.

Untangled the overlapping ownership:

- RuleSettings is migration-owned. The reconciler also created it, which made a
  clean install fail with "Table 'RuleSettings' already exists". It now only
  seeds the default row, and only once the table exists.
- The six CareerProfile child tables are reconciler-owned. Their migration was
  scaffolded against SQLite and indexed an unbounded longtext OwnerUserId, which
  exceeds MariaDB's 3072-byte key limit; it is now a no-op and the reconciler
  carries correct per-provider DDL. OwnerUserId and ItemKey are bounded to
  varchar(255) in the model so the index fits.
- Reconciler tables that reference another table are guarded on their parent, so
  pass 1 skips them on an empty database instead of failing on the foreign key.
- All index creation goes through one EnsureMySqlIndex helper, guarded on table
  existence as well as index existence. This removes ten copies of the raw block
  that crashed on a missing table.
- The DbContext-owned connection is no longer disposed by the reconciler, and
  Open() is guarded on connection state, so the second pass can reuse it.

Verified against MariaDB 11 and SQLite: empty MariaDB (40 tables, starts),
restart on the populated database (idempotent, rows preserved), empty MariaDB
via the Docker image, fresh SQLite (42 tables), and an existing partially
migrated SQLite dev database (34 tables upgraded to 44 with all 13 applications
and 8 companies intact). 329 backend tests pass in Release.

Ownership rules, startup order, fresh install and production upgrade are
documented in docs/infrastructure/database-ownership.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:51:55 +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 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 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 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 a9a0ddecbc chore(db): resync stale EF ModelSnapshot + fix fresh-DB schema gap
CI and Deploy / test (pull_request) Successful in 2m1s
CI and Deploy / deploy (pull_request) Has been skipped
Backlog item 1. The committed ModelSnapshot was empty/stale (21 lines, no
entities) -- `dotnet ef migrations add` scaffolded the whole database from
scratch against it, including the ASP.NET Identity tables, which have never
been created by a real EF migration in this repo (always provisioned via the
raw-SQL reconciler in StartupInitializationExtensions.cs -- see
EnsureIdentityTables' own comment). Applying that diff for real would throw
"table/column already exists" on every environment.

Fix: added migration 20260711181039_SyncModelSnapshot with an intentionally
empty Up()/Down() (see its doc comment) -- it only records itself in
__EFMigrationsHistory and regenerates the snapshot to match the live model,
so `dotnet ef migrations add` produces a real diff for the next schema
change instead of the whole database again. Verified zero side effects
against a copy of the dev DB (only inserts one history row) and against a
fresh empty DB (full migration + reconciler chain runs clean).

That fresh-DB verification surfaced a real, previously-undiscovered bug:
EnsureColumn/EnsureMySqlColumn calls for JobApplications/Correspondences/
Companies/Attachments ad-hoc columns all no-op on a truly fresh database
(the tables don't exist yet -- Migrate() creates them afterward), so a
brand-new deployment's first boot would be missing dozens of columns
(LastReminderEmailSentAt, RecruiterMessageDraft, salary fields, Correspondence
Provider/Subject/Channel/etc.) until the next restart. Also caught: my own b4
change (Correspondence.Provider backfill, already merged) had the same
unguarded-on-fresh-DB bug in isolation.

Fixed by promoting the schema-reconciliation helpers (Exec/HasTable/
HasColumn/EnsureColumn and their MySQL equivalents) from local functions to
class-level statics, extracting the ad-hoc-column blocks into
ReconcileCoreAppColumns/ReconcileCoreAppColumnsMySql, and calling them a
second time right after Migrate() succeeds (reusing the connection already
opened for the CoreSchemaReady check) -- idempotent, so free on every boot
except the first one, where it's now required. No inline logic changed,
pure extraction + one additional call site.

Also added Microsoft.EntityFrameworkCore.Design to JobTrackerApi.csproj
(dotnet-ef tooling requires it on the startup project since EF Core 6+;
previously only referenced by JobTrackerBackend, where the DbContext lives).

169/169 backend tests green. Verified live: full app boot against both a
fresh empty SQLite DB and a copy of the populated dev DB, both clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:32:07 +02:00
cesnimda fcafda6f52 Polish UI, harden company creation, and add error pages 2026-03-23 19:34:29 +01:00
cesnimda 2e8a29b4d0 First Commit 2026-03-21 11:55:27 +01:00