UI-only. No change to APIs, save payloads, extraction behaviour, or data
models. The parent CareerProfilePage still owns loading, state, saving,
and all extraction/import actions; the new sections are presentational
(value + onChange, plus a getMetadata callback for review chips).
Extracted into src/views/career/CareerProfileSections.tsx:
PersonalInformation, ProfessionalSummary, Skills, Interests, Languages,
WorkExperience, Education, OtherSections. FieldReviewNote + confidenceTone
moved there verbatim and shared with the parent. CareerProfilePage went
from 1376 to ~1200 lines.
No Projects/Certifications sections were created -- the editor never had
them (they are not editable structured fields here). Inventing them would
add functionality, which this refactor avoids; noted for a product
decision later.
Hid the duplicate CV concepts behind an "Advanced CV tools" toggle,
collapsed by default: the CV Structure Overview parse block and the
Template-driven CV builder. Both stay mounted and functional (gated with
display:none), so no tested functionality is removed -- the real CV
Builder at /career/builder is the single generation surface. Future
removal plan documented.
Tests: added "editing a field in an extracted section updates parent
state and flows into save" (render -> edit -> PUT /career/profile
{profile,cvText}); existing parse/rewrite tests reveal the advanced tools
first. The increment-1 save-invariant test still pins the payload.
Verified: tsc clean, production build clean, 137 frontend tests pass.
Backend untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI-only restructuring of the Career Profile surface. No change to
database models, CareerProfiles schema, CvVariants, extraction APIs, AI
services, CV rendering, or public CV.
Terminology -> user-facing (i18n strings):
- "Structured CV editor" -> "Career information"
- "CV structure overview" -> "Profile sections"
- "Summary bullets" -> "Professional summary"
- "Core skills" -> "Skills"
- "Analyze sections" -> "Read sections"
- "Original extraction" -> "Original import"
- hardcoded "Master career profile" -> "Career profile"
Help text de-jargoned; the Career information help now frames it as the
source the CV Builder consumes.
Component split (first step): extract ProfileCompleteness (completeness
meter + missing chips + version history) into src/views/career/. Display
only, props in, no state or API.
Save path untouched: api.put("/career/profile", { profile, cvText }). A
new test pins that exact call as the refactor invariant so the remaining
section extraction cannot silently change save behaviour. Existing
profile-page tests re-pointed to the new labels; every behavioural
assertion (save, parse, field values) kept.
Verified: tsc clean, production build clean, 136 frontend tests pass
(135 + 1 invariant). Sidebar fix from the previous task still passes.
Backend untouched.
The remaining Phase 1 work (per-section editor components, hiding the
template-driven builder and structure-overview blocks, actionable
per-section empty states) is staged in docs/career-workspace-ux-refactor.md
because it touches the live extraction test surface and is best verified
by driving the authenticated UI. This increment is a clean, non-regressing
checkpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On /career/builder/{id} both "Career Workspace" (/career) and "CV Builder"
(/career/builder) highlighted, because AppShell tested each item with
`pathname === to || pathname.startsWith(to + "/")` — so /career matched
every /career/... child. No "most specific wins" rule.
Add AppShell.activeNavTo(pathname, tos): the longest `to` that the path is
at or under wins, across both nav lists; every other item is inactive. A
child route never lights up a parent nav item. `selected` now compares
against that single computed activeTo. Exported as a pure function so the
ownership rule is unit-tested directly (sidebar-active-nav.test.ts):
exactly one active item for /career, /career/builder and
/career/builder/{id}, and no double-highlight.
Also give the breadcrumb/title in App.tsx explicit /career/builder ->
"CV Builder" ownership (it previously showed "Career Workspace"), and
reframe the Career Workspace header to the "Career Profile" product
framing: "This information powers your CVs, applications, cover letters
and AI assistance."
Frontend only — no change to CareerProfiles, CvVariants, CV generation,
extraction APIs, AI, permissions or tenant isolation. Plan for the deeper
information-architecture work is in docs/career-workspace-ux-refactor.md,
staged so the 1376-line CareerProfilePage and the live CV/extraction
pipeline are refactored incrementally with verification, not in one risky
rewrite.
Verified: tsc clean, frontend build clean, 135 frontend tests pass
(128 + 7 new nav tests). Backend untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidates release-candidate verification: backend 420 (incl. the CI
runner's exact Ubuntu 20.04/libicu66 environment), frontend 128, all
four DB scenarios, backup/restore with a byte-exact æøå round trip,
health and auth checks.
Status: READY WITH DOCUMENTED RISKS. No open code blocker. Remaining
risks separated into code (none), infrastructure (runner instability
A/B, still unconfirmed-fixed; old runner ICU), and manual owner
verification (sign-in, production backup, production scale).
Recommendation: deploy with documented risks -- re-run CI with the ICU
fix, owner runs a real backup + scratch restore, deploy, then run the
manual smoke test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docs/deployment/backup-restore.md: production backup checklist; documents
that deploy.sh loads the env, validates before backup, and validates the
dump. Adds VERIFIED UTF-8/Norwegian-character round trip (æ ø å survive a
real deploy.sh backup -> restore byte-exact; HEX compared). States plainly
that no production database was reached and the owner must run one real
backup + scratch restore.
- docs/deployment/manual-smoke-test.md: owner-run post-deploy checklist
(auth, applications, career profile, CV builder, AI, files). Each item
names what "wrong" looks like. Documents that login requires the owner.
- runner-investigation.md: Finding C -- the latest CI red was a real ICU
code bug the runner caught correctly, not instability. Amends the blanket
"outside the repository" conclusion. A and B stand as separate env issues.
- release-candidate-review.md: corrected drifted line refs after the index
fix; noted the CI ICU finding so the "purely external" verdict is honest.
All claims reflect behaviour verified this session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deploy.sh and the other scripts run on the Linux deploy host and in
Docker. A CRLF checkout breaks them with "bad interpreter: bash\r".
The committed blobs are already LF, but nothing guaranteed it against a
host with core.autocrlf=true. `*.sh text eol=lf` makes it explicit.
Verified: git ls-files --eol shows attr/text eol=lf on all .sh files;
renormalize produced no index churn (already LF).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the two issues found and fixed during the release-candidate audit to
release-candidate-review.md: the follow-up reminder index that never
created on MariaDB (fix in the preceding commit), and the
nondeterministic timeline day-grouping test.
Corrects database-ownership.md drift: the MariaDB startup scenarios now
report 42 tables (measured in every scenario this audit), not the stale
40 from before the last Phase 5 tables were added, and adds the
partially-migrated heal scenario (35 -> 42) that was verified.
All claims reflect behaviour verified in this audit: 420 tests on Windows
and Linux in both ICU modes, all three Docker images built, four database
startup scenarios against live MariaDB 11 and SQLite, and a full
backup -> restore -> app-start cycle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validation only. No application behaviour changed.
Verified against the code rather than the prose, by line number:
Before deployment -- validate_deploy_config (deploy.sh:343) and
backup_database (:347) both precede the build (:375) and the container
replacement (:382), so nothing is built, stopped or replaced without a
verified restore point.
During deployment -- startup runs ReconcileSchema (:1965),
Database.Migrate (:1973), ReconcileSchema (:1984). All seven Phase 4/5
migrations confirmed to have a literally empty Up body, which is what
makes a code rollback safe.
Health checks and rollback -- backend and frontend healthchecks present,
frontend gated on backend health, rollback documented in two places with
the code-vs-database distinction.
After deployment -- added an eight-point owner checklist covering login,
existing applications, workspace, career profile, CV builder, public CV,
AI features and attachments. Each item names what wrong looks like,
because "it loaded" is not a check. Merged the previous overlapping
"After deploying" list into it rather than leaving two competing
checklists.
Recorded the CV language ICU defect as closed, with the note that it was
invisible to a normal local test run -- the clearest evidence in this
review that passing locally and correct in the deployed container are
different claims.
Sections are now READY / BLOCKED / MANUAL VERIFICATION, with accepted
limitations kept separate. Test count updated to 420.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full backup -> verify -> restore -> start-the-app rehearsal of the
deploy.sh backup path against MariaDB 11.
Verified: the real backup_database function selected the MariaDB path
from DATABASE_PROVIDER=mariadb, produced a valid .sql.gz with 42
CREATE TABLE statements and an intact "Dump completed" trailer, restored
into a separate empty MariaDB container, and the application then
started healthy against the restored database with the reconciler
finding nothing to do. All 42 tables matched on row count, and content
survived including foreign key relationships and career profile JSON.
This is a rehearsal, NOT a verification of production data. No
production host was contacted and no production data was read. This
machine has no route to production: no /opt/job-tracker, no
DATABASE_PROVIDER or connection string in its .env, and the local stack
runs SQLite. Production host, user and key are CI secrets not available
here.
The document leads with that scope limit, records the commands to run
against production with values substituted, and ends with the checklist
that actually closes the gap -- including checking that non-ASCII CV
text survives the round trip, which the ASCII-heavy seed data did not
prove.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restructured into READY / BLOCKED / MANUAL, with accepted limitations
kept separate.
B1 (backup selected the wrong provider and reported success) and N2
(/health always reported version: unknown) are both closed and verified;
their original findings are kept because the failure modes are worth
understanding. N1 closed with the .env.example additions.
One blocker remains and it is external: the CI runner. Stated with what
it needs from the owner, and with the decision it forces -- fix the
runner, or deploy deliberately from a locally verified commit knowing CI
is red.
MANUAL now leads with backup and restore readiness: the mechanism is
verified against containers, but only the owner can prove it works on
production data. Added a note that authenticated smoke testing is not an
automation gap that more work would close -- sign-in needs a password,
and no automated step here should handle one.
Validation only. No application behaviour changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deploy.sh symlinked /opt/job-tracker/shared/.env for docker compose but
never loaded it into its own shell. Its own decisions therefore ran
against an empty environment: DATABASE_PROVIDER fell back to sqlite on a
MariaDB host, so the deploy tarred the data volume, printed "Backup
verified" and continued with no database dump. The operator saw a green
backup line and a new file in the backups directory, and had no restore
point.
Load the shared env before any decision. Parsed line by line rather than
sourced, because a compose .env is not a shell script and an unquoted
value containing spaces would execute as a command. Values already in
the environment win, so CI-provided APP_VERSION and friends still
override the file. No value is echoed.
Remove the sqlite default. DATABASE_PROVIDER must be stated; missing or
unrecognised aborts the deploy.
Validate deployment configuration before the backup, and so before
anything is built, stopped or replaced: the connection string when the
provider needs one, AI_SERVICE_TOKEN (compose declares it with :?) and
AUTH_JWT_KEY (the backend throws on a blank key). Names in the output,
never values.
Verify each backup against its own format. A dump must be valid gzip,
contain CREATE TABLE, and carry the "Dump completed" trailer, so a dump
that died partway through is rejected. An archive must contain
jobtracker.db. A tar can no longer pass the dump check.
Also resolve the SQLite volume by its project-prefixed name and fail if
absent. The bare jobtracker_data name would have silently created an
empty volume and backed that up -- the same class of bug, found while
testing this fix.
Verified against a seeded MariaDB 11 container and real Docker volumes:
provider selection, all four validation failures, both backup formats
and their failure paths, truncated and trailer-stripped dumps, and zero
secret occurrences across every test's output.
Docs updated for the drift: deploy/README.md, deploy/first-production-
deployment.md, docs/release-candidate-review.md (B1 closed) and
.env.example, which now names the two database variables.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final validation pass before the first production deployment. Findings
only; no application code changed.
Verified against the implementation rather than the other documents:
migration and reconciler ownership, startup order, authorization
coverage, tenant isolation, AI service protection, file access, the
five architecture rules, container dependency ordering and failure
behaviour.
Two blocking items, one new: deploy.sh symlinks the shared .env for
docker compose but never sources it, so DATABASE_PROVIDER is unset in
the script's own shell and backup_database takes the SQLite branch on a
MariaDB host. It tars the data volume, verify_backup only checks the
file is non-empty on that path, and the deploy reports a verified
backup that contains no database dump. Same root cause silently
disables the APP_PUBLIC_BASE_URL smoke check and the Ollama warmup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Written against the actual implementation rather than the existing docs, and
validated locally against MariaDB 11 containers. No application behaviour
changed — this commit adds two documents.
deploy/first-production-deployment.md covers pre-deployment checks, the eight
deployment steps, smoke tests for backend, database and application, and
rollback. It documents what deploy.sh really does: it backs up first and aborts
on failure, and it replaces containers with up -d --force-recreate rather than
running compose down, so the window is container start time. It also records the
startup sequence as implemented — reconcile, migrate, reconcile — and that
Database.Migrate() throws rather than limping on.
Validation surfaced things worth writing down. The connection string resolves
from inside the backend container, so Server=127.0.0.1 means the container and
not the host; this broke a validation run before it could have broken a deploy.
DATABASE_PROVIDER defaults to sqlite, and if it goes missing the backend does not
quietly serve an empty database — it exits with "no such table:
INFORMATION_SCHEMA.TABLES", which is loud but baffling if unexplained. A blank
AUTH_JWT_KEY throws at startup when auth is required, which is the right
behaviour. The runbook maps each of these log lines to its cause.
Rollback is documented with the distinction stated plainly: a code rollback
keeps all data and is almost always the whole fix, while a database restore
discards everything written since the dump. Restore only when the data itself is
wrong.
docs/release-checklist.md records the completed architecture work, local
verification results, known risks with severities, the unresolved CI runner
blocker and what would unblock it, and seven first-deployment warnings.
Validated: compose build; compose up on a fresh MariaDB (42 tables, backend
healthy); the depends_on health gate holding the frontend until the backend is
healthy; restart against the populated database with rows preserved; backup and
restore; and the failure paths. Not validated, and said so in both documents: the
authenticated end-to-end journey, because signing in needs a password.
393 backend tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Audited migrations, reconciler ownership, authentication, authorization, AI
security, file storage, public CV access, backups, logging, error handling and
configuration defaults before the first production deployment after the Phase
4/5 architecture changes.
Verdict: not ready to deploy unattended. The application verifies clean —
fresh MariaDB, populated MariaDB restart, existing SQLite upgrade, 393 backend
tests, 128 frontend tests, both Docker images — but four things stand in the way,
and the review lists them rather than declaring success.
Two are deployment blockers found by this audit. deploy.sh takes no database dump
before bringing the stack down, which is exactly backwards for a first deploy
where the reconciler will create roughly a dozen tables on a database many
commits behind; BackupController offers only an application-level encrypted
export, not an operational dump. And there is no documented restore procedure —
a backup nobody has restored is a hypothesis.
One is an operational gap: compose defines health checks for ai-service and
ollama but not for backend or frontend, so nothing detects a backend that starts
and then goes unhealthy.
One is the standing external blocker: CI is red for an environmental reason, and
deployment is gated on it.
Also recorded as accepted rather than fixed: console-only logging, no global
exception handler, the intentionally anonymous client-error endpoint, and the
fact that nobody has walked the authenticated end-to-end journey.
Includes a ten-step deployment checklist and a rollback plan. Rollback is safe
because every Phase 4/5 migration is a no-op, so reverting the code never leaves
migration state ahead of the schema — but it does not recover data users create
in the new tables during the window, which the review says plainly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5.6 — refinement and validation, no new features.
Audited the ownership rules mechanically across all six Phase 5 services rather
than asserting them in prose. None writes to CareerProfile or its children, none
writes CV variant content, and the two services that read the profile
(ApplicationChecklistService, ApplicationIntelligenceService) never save it. The
rules hold: JobEvent is the history source, the checklist is workflow guidance,
readiness is a projection of it, CareerProfile is the source of truth, CvVariant
is derived output, and AI only appends to AiInteraction.
Verified locally end to end: 379 backend tests in Release, 128 frontend tests
across 36 suites, TypeScript clean, frontend production build, both Docker
images, a fresh empty MariaDB 11 (42 tables, no exceptions), a restart against
the populated database (rows preserved), and the existing SQLite dev database.
The security review found one genuine gap, reported rather than silently
changed: authentication is enforced by a fallback policy gated on Auth:Require,
which defaults to false. docker-compose.yml hardcodes it true so every compose
deployment is protected, and every Phase 5 controller carries an explicit
Authorize attribute, but several pre-Phase-5 controllers do not — a deployment
that lost the flag would expose them. Adding explicit attributes changes local
development behaviour, so it is flagged for a deliberate decision instead of
applied unilaterally.
docs/phase-5-completion-report.md records the milestones, the architecture
decisions and their reasoning, the ownership audit, the verification matrix, and
four remaining risks: CI red for an environmental reason (a docs-only commit
fails identically), production behind and needing a backup before first deploy,
the authentication configuration gap, and prompt quality being unmeasured.
Phase 5 is feature-complete locally. It is not deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
Run 531 (8f73548) changed a single markdown file: no application code, no test,
no dependency, no workflow file. Its test job failed at 1m18s, in the same
duration band as every run before and after it, including the two Phase 5
milestone commits.
A change that cannot affect compilation or test behaviour cannot cause a test
job to fail. That settles the question the investigation was asked: the cause is
outside the repository. The specific runner mechanism is still unidentified and
still needs the access already listed (job logs via a read-scoped token,
journalctl -u act_runner, runner container config).
Also records a separately discovered, pre-existing bug: booting against a
completely empty MariaDB fails because the MySQL reconciler assumes the
migration-owned tables exist. Reproduced on clean HEAD, unrelated to CI, and
harmless to prod, whose database is populated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Two post-reboot runs: the first reproduced the failure identically (smoke 1s
pass, suite 3s fail), the second failed earlier at `dotnet restore` in 0s — a
step that succeeded in 3-4s on every previous run, same commit, same runner.
That rules out stuck state (reboot changed nothing) and rules out a
deterministic sandbox policy such as seccomp/W^X blocking runtime IL emission,
which was the leading remaining hypothesis. Combined with host telemetry
showing no disk/memory/PID pressure, confidence in any specific mechanism drops
to ~25%; confidence that application code is not the cause stays high.
Removes the pure-vs-Moq diagnostic scaffolding (it never executed). No test
skipped or weakened.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Server shows 62G free (71% used), inodes 14%, 16G /dev/shm (~32G RAM), ulimit
-u 127749, no cgroup pids.max, and no fail2ban installed. That falsifies both
resource-exhaustion hypotheses at the host level and the fail2ban explanation
for the deploy failure.
Notes the caveat that Gitea act_runner usually runs jobs inside a Docker
container, so host figures do not describe the environment the tests ran in
(separate cgroup limits, and a 64MB /dev/shm by Docker default).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves the report to docs/infrastructure/runner-investigation.md with the
requested structure (evidence, experiments, hypothesis, confidence, required
infrastructure changes, why application code is no longer suspected).
Decisive new experiment: the suite was run from a clean `git archive HEAD`
tree — byte-identical to CI's checkout, without the gitignored runtime dirs
(jobtracker.db, keys/, CvArtifacts/, backups/) that earlier local runs had
silently included. 10/10 pass in 1s. That removes the last difference between
the local tree and the runner, eliminating application code (~95% confidence).
Also establishes, by route probe, that production is healthy but stale:
/api/public-cv/{unknown} returns 404 locally (route exists, AllowAnonymous) but
401 on prod, same as a nonsense path — PublicCvController is absent, so Phase 4
and Phase 5 have never deployed. Production therefore never ran the faulty
migration: no half-built tables exist there and no data cleanup is needed.
Deploy is a second, separate infrastructure failure: the first attempt reached
deploy.sh (37s, consistent with the MariaDB crash since fixed), every attempt
since dies at 3s at SSH connection time while the host serves traffic normally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisected the failure across four CI runs (job logs are not readable via the
Gitea API) down to the AiWorkspace test classes — 10 tests that pass on
Windows, in a clean Linux container, under a 1GB memory cap, in CI's exact
step order, with a custom-dir SDK and no DOTNET_ROOT, serially, and under a
hostile locale/timezone.
Ruled out: Linux behaviour, case sensitivity, path separators, locale/culture,
time zone, environment variables, parallel execution, test ordering, shared
state, memory. Not testable remotely: host permissions/limits.
Assessment is environmental: the workflow already documents three failure
modes on this same runner with an identical signature (processes dying with no
error output — SDK cache corruption, npm ci SIGSEGV, CRA build OOM/SIGSEGV).
Report includes evidence table and recommended infrastructure fix.
Removes the temporary bisection scaffolding; keeps the restore/build/test split
and the host smoke. No test was weakened, skipped, or filtered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ai-career-assistant.md (modules, prompt flow, provider abstraction, append-only
history model, extension points, security). Master guide + roadmap Phase 5
updated with the shipped workspace and the open provider-selection extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cv-builder.md (variant model, rendering pipeline, API, builder workflow,
extension points, known deep-link limitation) + cv-theme-engine.md (how a
theme is data and how to add one). Roadmap Phase 4 marked foundation-shipped
with the remaining polish itemised.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3 foundation: entities, relationships, ownership, source-of-truth, and
snapshot rules for the structured career profile. Relational children
(Experience/Education/Skill/Project/Certification/Language) under CareerProfile;
long tail as JSON; blob (ProfileCvStructureJson) becomes a derived projection for
legacy read paths; lazy non-destructive backfill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add section 4a to docs/architecture/current.md: request flow, data ownership,
API responsibilities, and future extension points for the /profile vs /career
separation completed in Phase 2/2.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the four Career Workspace documents onto main as the target architecture
for Phases 2-4, and point MASTER_IMPLEMENTATION_GUIDE.md at them. Taken from the
branch tip (later commits refined them). Pure additions — none previously existed
on main.
- cv-builder-competitor-deep-research.md (Novoresume, Reactive Resume, FlowCV,
Teal, Enhancv, Canva, Resume.io, Kickresume; matrix; pricing intelligence).
- cv-builder-product-teardown.md
- career-workspace-product-strategy.md
- career-workspace-implementation-roadmap.md (F0-F5)
MASTER_IMPLEMENTATION_GUIDE.md v1.1: adds a Source-Of-Truth Documents section and
restates the "profile is the source of truth; documents reference snapshots" rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Active docs/ was stub scaffolding while the real docs sat in docs/_archive/.
Restore and correct them, and record the Phase 0 work.
- docs/architecture/current.md: verified system map (from archived SYSTEM_OVERVIEW,
9 corrections against code).
- docs/research/competitors.md: sourced competitor analysis (from archived
PRODUCT_RESEARCH, feature matrix corrected).
- docs/decisions/ADR-002-job-application-model.md: the Job/JobApplication split.
- docs/application-discovery-report.md, docs/implementation-roadmap.md,
docs/phase-0-foundation-report.md, docs/career-workspace-branch-assessment.md.
- Remove 10 zero-byte placeholder files that advertised content that never existed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
useViewResource built `reload` with `load` in its useCallback deps, and the
fetch effect depended on `reload`. Callers routinely pass an inline `load`
closure (e.g. JobTable), so `load` — and therefore `reload` and the effect —
changed every render, calling setState and re-rendering: an unbounded
"Maximum update depth exceeded" loop that froze the renderer on /jobs and every
other list view (DashboardView, RemindersView, CompaniesTable).
Fix: hold `load` in a ref (like the existing hasLoadedRef) and drop it from the
dependency arrays. Re-fetching is still driven by `deps`/`enabled`; the ref
always points at the latest closure. No API/behaviour change for callers.
Runtime-verified live: /jobs went from a render storm (frozen renderer, 100s of
console errors) to 0 errors in a 2s window and a clean render. Suites that drive
JobTable→useViewResource pass in isolation; the remaining full-run flakiness is
pre-existing (state-pollution/timing in the heavy RTL suites, unrelated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The axios 401 interceptor calls clearAuthClientState() on every 401, which
dispatched "auth-changed"; the App handler re-fetched /auth/me, which 401'd
again → interceptor → clearAuthClientState() → "auth-changed" → ... an unbounded
request storm (observed live: 100+ GET /auth/me and climbing) that ran whenever
the user was logged out (login page, expired session) — burning CPU, network and
battery and flooding the server.
Fix: make clearAuthClientState idempotent — only emit "auth-changed" when it
actually removes a stored user key (a real signed-in→out transition), so
repeated 401s can no longer re-trigger the fetch.
Runtime-verified in a live stack: /auth/me went from 100+ & growing to 0 &
stable. login-page/settings tests green. Documented in
docs/performance/PERFORMANCE_IMPROVEMENTS.md (Phase 3.5 runtime finding).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Evidence-based investigation across every leak vector (timers, listeners, object
URLs, observers, websockets, static server collections, IMemoryCache, Python
caches). Verdict: no confirmed memory leak — the codebase has disciplined
cleanup. One resource-release correctness bug (over-eager blob-URL revocation in
the CV carousel) was found and fixed (eed9b1f).
Adds docs/performance/: MEMORY_LEAK_REPORT.md, ROOT_CAUSE_ANALYSIS.md,
PERFORMANCE_IMPROVEMENTS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1-3 deliverables: full architecture/security/tech-debt map,
2026 market research with feature matrix, and tiered execution roadmap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>