Commit Graph

65 Commits

Author SHA1 Message Date
cesnimda 57fabe9a97 docs(deployment): backup-restore, smoke test, runner finding C
- 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>
2026-07-19 23:14:41 +02:00
cesnimda b705cbaf60 chore: pin shell scripts to LF via .gitattributes
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>
2026-07-19 23:14:27 +02:00
cesnimda 5c5a572cfc docs(ops): record release-candidate audit findings
CI and Deploy / test (push) Failing after 1m8s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-19 22:53:43 +02:00
cesnimda 834a775c9d docs(release): final deployment checklist
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>
2026-07-19 21:36:26 +02:00
cesnimda df9322f5c0 docs(ops): verify production backup restore
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>
2026-07-19 21:33:27 +02:00
cesnimda de35947244 docs(ops): finalize release candidate status
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>
2026-07-19 19:01:49 +02:00
cesnimda 66b02bcab8 fix(deploy): load production environment before backup
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>
2026-07-19 18:44:23 +02:00
cesnimda ab53582c71 docs(ops): add release candidate review
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>
2026-07-19 18:19:37 +02:00
cesnimda 25b64bee8a docs(ops): add first production deployment runbook
CI and Deploy / test (push) Failing after 1m3s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-19 18:02:49 +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 b2b87f39a5 docs: production readiness review
CI and Deploy / test (push) Failing after 1m3s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-19 17:34:44 +02:00
cesnimda a3735299ec docs: complete application workspace phase
CI and Deploy / test (push) Failing after 1m7s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-19 17:17:56 +02:00
cesnimda 3d74baef78 feat(workspace): interview and follow-up workflow
CI and Deploy / test (push) Failing after 3m54s
CI and Deploy / deploy (push) Has been skipped
Phase 5.5. Completes the lifecycle after submission: prepare, communicate, chase.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:52:37 +02:00
cesnimda 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 1f1cbb92f3 docs(infra): docs-only commit fails CI identically — proves failure is not in the repo
CI and Deploy / test (push) Failing after 1m2s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-19 11:21:56 +02:00
cesnimda 3a906b881e feat(workspace): unified application checklist (Phase 5 milestone 2)
CI and Deploy / test (push) Failing after 1m8s
CI and Deploy / deploy (push) Has been skipped
Evolve the existing readiness workflow into one persisted, user-controlled
checklist rather than adding a second tracker.

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:57:24 +02:00
cesnimda 3b59152782 docs(infra): runner fails at different stages across runs — nondeterministic
CI and Deploy / test (push) Failing after 1m0s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 23:20:19 +02:00
cesnimda 8f73548e33 docs(infra): host telemetry rules out disk/memory/PID exhaustion and fail2ban
CI and Deploy / test (push) Failing after 1m18s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 23:14:28 +02:00
cesnimda 55962bd29b docs(infra): conclude runner + deploy investigation — both outside the repo
CI and Deploy / test (push) Failing after 1m1s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 23:00:27 +02:00
cesnimda b7dc07b045 docs(ci): report runner-only backend test failure investigation
CI and Deploy / test (push) Failing after 1m8s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 22:12:00 +02:00
cesnimda fd9bd3f47a docs(architecture): document the AI Career Assistant (Phase 5)
CI and Deploy / test (push) Failing after 1m56s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 15:26:04 +02:00
cesnimda 074c78a7ef docs(architecture): record Phase 4.5 builder polish
CI and Deploy / test (push) Failing after 2m4s
CI and Deploy / deploy (push) Has been skipped
cv-builder.md (outline-driven Content tab, DnD, rich text, preview page nav,
public-CV routing fix + deployment note), cv-theme-engine.md (rich text/ATS/
print), roadmap Phase 4.5 shipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:48:33 +02:00
cesnimda 17edf19f89 docs(architecture): document CV builder, theme engine, Phase 4 status
CI and Deploy / test (push) Failing after 1m53s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 10:07:43 +02:00
cesnimda 707d8c59d2 docs(architecture): mark Phase 3 career foundation shipped
CI and Deploy / test (push) Failing after 1m51s
CI and Deploy / deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:03:40 +02:00
cesnimda 9c8644e9f9 docs(architecture): document career profile model
CI and Deploy / test (push) Failing after 1m54s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 00:25:02 +02:00
cesnimda cf8b2fa014 docs(architecture): document profile and career ownership
CI and Deploy / test (push) Failing after 1m50s
CI and Deploy / deploy (push) Has been skipped
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>
2026-07-18 00:13:06 +02:00
cesnimda 8b5ad03808 docs: mark Phase 2 profile/career separation done (2.1)
CI and Deploy / test (push) Failing after 2m59s
CI and Deploy / deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:01:22 +02:00
cesnimda a28c47f515 docs: mark Career Workspace foundation integrated; record what remains on the branch
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:17:37 +02:00
cesnimda aedd6e32ad docs: recover Career Workspace research + strategy from feature/career-workspace
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>
2026-07-17 17:06:46 +02:00
cesnimda b176a44627 docs: reorganize tree, restore architecture + research from archive, add Phase 0 reports
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>
2026-07-17 17:04:32 +02:00
cesnimda d7d7e70d08 feat(ui): separate career and connected accounts 2026-07-15 10:37:02 +02:00
cesnimda 0ca8c95372 merge: reconcile perf/wave1-perf with main (Wave 0 features)
CI and Deploy / test (pull_request) Successful in 2m13s
CI and Deploy / deploy (pull_request) Has been skipped
Resolve conflicts from main's Wave 0 (PR #1) landing after this branch was cut:

- useViewResource.ts: main's e352aae already fixes the render loop the same way
  (load in a ref, dropped from deps) — took main's canonical version. My
  independent fix is superseded (my branch predated e352aae, which is why the
  loop reproduced live).
- JobApplicationsController.cs: keep BOTH main's IJobCvMatchService and my
  AnalyticsService (ctor gets both optional params). GetAnalyticsOverview stays
  delegated to AnalyticsService.
- Fold main's H3 additions into the extracted AnalyticsService: pipeline-driven
  funnel (JobPipeline.Normalize/Stages) + time-in-stage (StageAnalytics) and add
  StageDurationDto + TimeInStage to Models/AnalyticsDtos.cs, preserving the API
  contract the frontend expects.

Build clean; backend suite 135/135 green.
2026-07-05 20:16:40 +02:00
cesnimda b8f8569e6e fix(hooks): stop infinite render loop in useViewResource
CI and Deploy / test (pull_request) Successful in 2m0s
CI and Deploy / deploy (pull_request) Has been skipped
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>
2026-07-05 15:36:55 +02:00
cesnimda 490c5b803e fix(auth): stop infinite /auth/me request loop when logged out
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>
2026-07-05 15:13:18 +02:00
cesnimda 39266c0935 docs(perf): memory-leak investigation reports (no leak found)
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>
2026-07-05 14:59:28 +02:00
cesnimda 3bd7b4b7e4 docs: add merge-request summary for review
CI and Deploy / test (pull_request) Successful in 2m10s
CI and Deploy / deploy (pull_request) Has been skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 04:29:27 +02:00
cesnimda 5a9245cf74 docs: add security review of session changes (Phase 6)
Scoped security review of Wave 0 + H1-H4: confirms tenant isolation on
new endpoints (query filters + tests), no injection/ReDoS, dev-only
OpenAPI. Flags DataProtection key rotation as the operator action item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 03:56:58 +02:00
cesnimda c38295d869 docs: add system overview, product research, and roadmap
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>
2026-07-02 21:24:03 +02:00
cesnimda 3ef3192e6c docs: record next-session skill suggestions in handoff notes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:21:45 +02:00
cesnimda 5f2f0a881a Record authorization replay findings 2026-04-11 17:07:10 +02:00
cesnimda 811963749e Fix cross-user job history leak 2026-04-11 17:05:52 +02:00
cesnimda 41595605b9 Add hostile fixture setup for authz testing 2026-04-11 16:57:15 +02:00
cesnimda ac217dab53 Record security remediation verification 2026-04-11 16:31:05 +02:00
cesnimda b4719a9916 Add adversarial security assessment findings 2026-04-11 14:30:32 +02:00
cesnimda e5bcf9d5ea feat: harden gmail sync foundation 2026-04-01 16:09:29 +02:00
cesnimda 0d65835857 feat: add cv benchmark workflow and admin visibility 2026-04-01 12:25:45 +02:00
cesnimda 44ff64896a Complete S07 daily-loop UAT closure 2026-03-27 10:03:31 +01:00