Files
jobtrackingapp/docs/architecture/application-workspace.md
T
cesnimda a3735299ec
CI and Deploy / test (push) Failing after 1m7s
CI and Deploy / deploy (push) Has been skipped
docs: complete application workspace phase
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

20 KiB
Raw Blame History

Application Workspace (Phase 5)

Phase 5, Milestones 12 (2026-07-19). The per-application workspace: what it is, what it deliberately is not, and how it composes existing systems. Companion to cv-builder.md, ai-career-assistant.md, career-profile-model.md.

What it is

A dedicated surface for one JobApplication at /applications/{id}, so an application is a place you work rather than a row you edit in a modal. Job tracking stays the product; the workspace is the application's home.

Core principle: the user should never ask "what do I do next?" — the overview always answers it.

What it is NOT

The workspace owns no data and duplicates none. It is an aggregate read plus a navigation shell:

Section Backed by (existing system)
Timeline JobEvent — interpreted, never replaced
Analysis / Match the advert and the master CareerProfile, read deterministically
Checklist ApplicationChecklistItem — completion state only, seeded from the readiness signals
CV Phase 4 CvVariant — a lens over the master CareerProfile; the application only points at one
Cover Letter JobApplication.CoverLetterText + CoverLetterVersions history
Analysis / Match / Interview Phase 5 AiWorkspacePanel + AiInteraction history
Documents Attachment
Communication Correspondence
Activity / Timeline JobEvent
Stage semantics JobPipeline

No career data is copied into the application. Nothing in this feature writes to the master profile, a CV variant, or a cover letter.

Backend

GET /api/jobapplications/{id}/workspaceWorkspaceOverviewDto (ApplicationWorkspaceController + ApplicationWorkspaceService).

One aggregate read instead of the page fanning out: role/company/location/salary, status + pipeline group, applied/deadline/follow-up dates, the attached CV variant (id, name, theme), cover-letter presence, document count, AI interaction count + last run, recent JobEvent activity, and the computed next recommended action.

Read-only and tenant-scoped (OwnerUserId), returning 404 for another user's application.

Milestone 2 moved this onto the checklist: the first pending checklist item, ordered by category priority (preparationsubmissionfollow-upinterviewcustom) then the user's own ordering. null means nothing is outstanding.

That means the overview cannot recommend something the user has already ticked off, a dismissed item never comes back as a recommendation, and a task the user added themselves can legitimately be the next action. There is no second ruleset to keep in sync.

The checklist

ApplicationChecklistItem — a workflow guidance layer, not a store of truth. It records only "is this step done, and does the user still want it". The CV still lives in CvVariant, documents in Attachment, history in JobEvent, follow-up in JobApplication.FollowUpAt.

System items and auto-completion

Each default item carries a stable SystemKey and usually an AutoSignal — the same signal /readiness already computed. On every read the service re-syncs:

  • signal satisfied + item pending → done, IsAutoCompleted = true
  • signal no longer satisfied + item was auto-completed → back to pending
  • a manual tick clears IsAutoCompleted and therefore sticks, even against the signal

So "readiness says the CV is missing" and "the checklist says Prepare a CV is pending" cannot drift apart — they read the same state. The user always wins over the signal.

System key Category Signal
review-job-details preparation advert text present
complete-career-profile preparation career profile with at least one experience
prepare-cv preparation CV variant attached, or tailored CV text
review-cv-match preparation manual
create-cover-letter preparation cover letter present
attach-portfolio preparation HasPortfolio
attach-supporting-documents preparation at least one Attachment
save-application-answers preparation saved answer draft in Notes
capture-recruiter-contact preparation Company.RecruiterEmail
confirm-submitted submission applied date set and out of the prospect stage
add-follow-up-reminder follow-up FollowUpAt set
set-next-action follow-up NextAction written
prepare-interview-notes interview prep notes present, or not at an interview stage
research-company interview manual

Seeding is idempotent per (JobApplicationId, SystemKey) — enforced by a unique index, so a re-read never duplicates. Custom items have a NULL SystemKey; both SQLite and MariaDB treat NULLs as distinct in a unique index, so a user can add as many as they like.

Deleting a system item dismisses it (a hard delete would be undone by the next seed); deleting a custom item removes the row. Dismissed items leave the progress denominator entirely.

API

/api/jobapplications/{id}/checklistGET (seeds + syncs + returns items and progress), POST (custom item), PATCH /{itemId}, DELETE /{itemId}, PUT /order (array of ids). Tenant-scoped on OwnerUserId; another user's application is a 404.

Relationship to the other systems

  • Not JobEvent — the checklist is forward-looking intent; JobEvent is the append-only history.
  • Not follow-upsFollowUpAt, RulesEngine and the reminder hosted service still own scheduling. The checklist only asks whether a follow-up exists.
  • Not Attachment / CvVariant — it reads their presence as a signal and stores nothing of them.

Future AI suggestions

An AI-suggested task is just a checklist row with IsSystemGenerated = false and no AutoSignal, created after the user approves it. Nothing in the AI path may create, complete or delete an item without approval — same rule as everywhere else (ai-career-assistant.md).

Schema provisioning

ApplicationChecklistItems follows the established MariaDB-safe path: the scaffolded migration (20260719085904_AddApplicationChecklistItems) is a no-op, and the table is created by the idempotent reconciler in StartupInitializationExtensions, which has correct DDL per provider. A SQLite-scaffolded migration would emit TEXT datetimes and a PK without AUTO_INCREMENT on MariaDB — the failure that crashed prod startup for the Phase 4 tables.

Verified on MariaDB 11: int AUTO_INCREMENT PK, varchar/datetime(6)/tinyint(1) columns, both indexes inside the 3072-byte key limit, cascade delete from JobApplications, the unique index rejecting a duplicate system key, and NULL system keys not colliding.

Frontend

ApplicationWorkspacePage (/applications/:id) — a left nav plus a content pane, section selected by ?section=, so a section is linkable and survives refresh. Reached from the job dialog's "Open application workspace" button.

The dialog passes an optional onOpenWorkspace callback rather than calling useNavigate itself: JobDetailsDialog must stay renderable without a <Router> (several suites mount it standalone), so router context belongs to the caller.

The Checklist section (ApplicationChecklist) groups items by category, shows a completion bar, and supports tick/untick, add, remove and reorder. System items are labelled "Detected" when a signal completed them, custom items "Yours". Every mutation re-reads, because only the backend's sync knows the real post-mutation state.

Implemented now: Overview, Checklist, Job Details, and the sections that reuse an existing component (Analysis/Match/Interview → AiWorkspacePanel, Documents → Attachments, Communication → Correspondence). Sections owned by later milestones state their milestone instead of faking functionality.

Relationship to /readiness

GET /{id}/readiness keeps its DTO shape (score, level, completed, missing, reminders, workflowSignal) and still backs the dialog's Readiness tab — but as of Milestone 2 it no longer runs its own parallel checklist. It projects the persisted checklist:

  • completed / missing — the live (non-dismissed) items by status
  • score — the checklist completion percentage
  • level — Ready ≥ 80, Needs polish ≥ 60, otherwise Needs work
  • reminders / workflowSignal — unchanged; BuildWorkflowSignal remains the health/attention view

So the division is: the checklist is the workflow the user drives, readiness is the calculation and health indicator derived from it. One system, two projections.

Application intelligence (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". All deterministic, all owned by nothing:

Endpoint Reads Owns
GET /{id}/timeline JobEvent nothing
GET /{id}/analysis JobApplication.Description nothing
GET /{id}/match CareerProfile + the advert nothing

No new table, no new column. ApplicationTimelineService and ApplicationIntelligenceService write nothing at all.

The AI boundary

The deterministic answer and the AI narrative are separate on purpose.

  • The three endpoints above never call the AI. Opening the Analysis or Match section costs nothing and cannot change anything — the page renders a computed answer.
  • The narrative lives where it already did: AiWorkspaceService's job-analysis and career-match modules, reached from AiWorkspacePanel, generated only when the user asks.
  • Every generation is appended as an AiInteraction — that append-only history is the versioning, and the user restores, compares or deletes from it.
  • AI output is a suggestion. Nothing in this phase writes to the CareerProfile, a CvVariant, a cover letter, or the JobApplication. Match_never_writes_to_the_career_profile pins that.

So a user gets a trustworthy number for free, and pays for prose only when they want it.

Timeline

JobEvent stays the source of historical truth; the service is an interpretation layer over it. Each row gains a readable summary (("StatusChanged", "Applied", "Interview") → "Moved from Applied to Interview"), a category (lifecycle, stage, follow-up, communication, ai) and a milestone flag. Events group by day with relative labels.

Milestones are the stages that mean something happened — applied, interview, offer, rejected, accepted, declined — plus creation and replies received. They are returned unfiltered: narrowing the detail below must not hide what actually happened.

An unrecognised future Type degrades to a humanised sentence rather than disappearing.

Job analysis

Structured extraction from the advert, reusing the existing SkillTagger so the vocabulary matches the job importer and the CV match. Facts (role, company, location, employment type, seniority, salary), lists (technologies, skills, responsibilities, requirements, keywords, interview topics), and — deliberately — what the advert does not say, which is usually the more useful half.

With no advert saved it degrades to the fields on the application and says so.

Career matching

Feeds the master CareerProfile into the same JobCvMatchService the CV builder uses, so one application scores identically whichever surface asks. Returns the score and band, matched and missing skills, and — the part that makes it actionable — which experience and project entries are the evidence for each matched keyword, ranked by hit count.

Suggestions describe what the user could change. They never change it.

With no profile it returns score 0 and asks the user to build one, rather than implying a bad match.

Application assets (Phase 5.4)

The workspace becomes the place an application is prepared. The rule is one-directional:

CareerProfile  →  CvVariant  →  application-specific output

Nothing flows back up. No code path in this phase writes to CareerProfile or its children — Tailoring_never_writes_to_the_career_profile_or_the_variant pins it.

What is owned where

Asset Owned by This phase adds
CV content CareerProfile (master) nothing
CV variant, preview, PDF, themes, version history CvVariantService / /api/cv nothing
Which variant an application uses CvVariant.JobApplicationId the attach/detach route
Documents Attachment / /api/attachments nothing
Cover letter text JobApplication.CoverLetterText version history
AI narrative AiInteraction nothing

The only new table is CoverLetterVersions — reconciler-owned, no-op migration, guarded on JobApplications (docs/infrastructure/database-ownership.md). Verified on MariaDB 11: int AUTO_INCREMENT PK, varchar(255) owner, datetime(6), composite index inside the key limit.

CV integration

GET/PUT /{id}/cv reads and re-points. Attaching sets CvVariant.JobApplicationId; one variant per application, so the workspace can answer "which CV am I sending". Replacing detaches the previous variant rather than deleting it — it is still the user's to reuse. Everything else (create, duplicate, edit, theme, preview, export PDF, versions, restore) is a link into the existing CV builder. There is deliberately no second CV system.

Tailoring

GET /{id}/tailoring composes the Phase 5.3 analysis and match into five suggestion kinds: skills to highlight, experience to prioritise, projects to emphasise, keywords to include, gaps to address.

Deterministic and advisory. It returns what the user could emphasise; the user edits the variant in the builder. Nothing auto-applies, and no suggestion mutates a variant or the profile.

Cover letter

JobApplication.CoverLetterText stays the current text and its existing API contract is unchanged. CoverLetterVersions records what it used to be, so an AI rewrite is never destructive.

  • Every save that changes the text snapshots a new version; an unchanged save is a no-op, so autosave never burns history.
  • Restore is additive — the old text returns as a new version, so what you restored from is still there.
  • Source (manual | ai | template | restore) and AiAction record how each version came about, so the history shows what the user wrote versus what they approved from a suggestion.
  • An AI generation on its own is only an AiInteraction. It becomes a version when the user saves it — that is what "requires approval" means here.

Creation methods: write it, start from the built-in template, or generate from the AI panel below the editor. The editor is always the user's; generation is never triggered by opening the page.

Documents

Unchanged. The existing Attachments component and /api/attachments already handle CV, cover letter, certificates, portfolio and other files with a Purpose field, and JobApplication.HasResume / HasCoverLetter / HasPortfolio are derived from it. The workspace mounts that component; no new storage, no duplicate upload path. Files stay private to the owning user.

Future extension points

  • Another asset type: add a section, compose the service that already owns it — do not add storage.
  • AI cover-letter actions (improve, shorten, expand, tone, tailor): already modelled by CoverLetterVersion.AiAction; wire a new mode in AiWorkspaceService and save the approved result.
  • Multiple attached variants: relax the one-per-application rule in AttachVariantAsync; the DTO already carries the full variant list.

Interview and follow-up (Phase 5.5)

Completes the lifecycle after submission: prepare, communicate, chase.

Interview preparation

There were already two per-application AI stores — InterviewPrepNote and AiWorkspaceNote — and both are caches: each regenerates when its context signature changes, so anything a user typed into them would eventually be overwritten. InterviewPrepItem is the durable, user-owned side. Nothing regenerates it.

One table covers every category (company-research, technical, behavioural, star, question, note) — they differ only by label, and adding a category must not need a migration. Each item carries the user's own Content, a Source (user | ai) recording whether they wrote it or accepted a suggestion, and IsPrepared, which makes the section double as the preparation checklist.

An accepted AI suggestion is marked ai for honesty, not to restrict editing — it is fully the user's afterwards.

The AI boundary, restated

Generation stays in AiWorkspaceService's existing interview module, reached from AiWorkspacePanel, authenticated and ownership-scoped like every other module, with each run appended to AiInteraction. A suggestion is history until the user adds it as a prep item. Opening the section generates nothing; Generating_ai_history_does_not_create_prep_items pins that.

Communication

Unchanged. Correspondence already owns recruiter contacts, message history and notes, and the workspace already mounts that component. No second messaging or history system was added.

Follow-up

Reuses what exists rather than adding a tracker:

  • The date is JobApplication.FollowUpAt — the same field RulesEngine and FollowUpReminderHostedService already act on. Writing it here means reminders keep working with no new wiring.
  • The task is an ApplicationChecklistItem in the follow-up category. The follow-up section counts open tasks; it does not own them.
  • The record is a FollowUpSet JobEvent — the same type the rest of the app emits, so the timeline reads it unchanged.

Timeline integration

JobEvent remains the source of history. The interpreter learned five more types: InterviewScheduled, InterviewCompleted, OfferReceived (milestones) and FollowUpCreated, FollowUpCompleted (routine, deliberately kept out of the milestone spine so it stays the "what actually happened" summary).

Ownership

InterviewPrepItems is reconciler-owned with a no-op migration, guarded on JobApplications (docs/infrastructure/database-ownership.md). Verified on a fresh MariaDB 11: int AUTO_INCREMENT PK, varchar(255) owner, varchar(500) title, tinyint(1) flag, datetime(6), composite index inside the key limit.

InterviewPrepBoardDto is named to avoid colliding with the pre-existing InterviewPrepDto, which belongs to the AI cache — a reminder that the two systems are genuinely different.

Extension points

  • New section: add to WORKSPACE_SECTIONS and render it; nav is data-driven.
  • New next-action rule: add one clause to ApplicationWorkspaceService.NextStep — ordered, so position is the priority.
  • More overview data: extend WorkspaceOverviewDto; the page reads one payload.

Milestones

  1. Workspace foundation — route, nav shell, aggregate overview, next recommended action.
  2. Checklist and progress tracking — persisted items, auto-completion from the readiness signals, custom items, reordering, dismissal; readiness refactored into a projection of it.
  3. Application intelligence — timeline interpretation, structured job analysis, career matching (Phase 5.3, all three deterministic and read-only).
  4. Application assets — CV variant association, tailoring suggestions, cover letter workflow with version history, documents (Phase 5.4).
  5. Interview and follow-up — user-owned interview preparation, follow-up over the existing FollowUpAt and checklist, five more timeline event types (Phase 5.5).
  6. Completion and product readiness — ownership audit, security review, full local verification (Phase 5.6). See docs/phase-5-completion-report.md.

Phase 5 is feature-complete locally. It is not deployed: CI is red for a documented environmental reason and deployment is gated on it. See the completion report's Remaining risks. 7. Cover letter workflow. 8. Documents. 9. Interview preparation. 10. Dashboard improvements.