391 lines
20 KiB
Markdown
391 lines
20 KiB
Markdown
# Application Workspace (Phase 5)
|
||
|
||
> Phase 5, Milestones 1–2 (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}/workspace` → `WorkspaceOverviewDto`
|
||
(`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.
|
||
|
||
### Next recommended action
|
||
|
||
Milestone 2 moved this onto the checklist: **the first pending checklist item**, ordered by category
|
||
priority (`preparation` → `submission` → `follow-up` → `interview` → `custom`) 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.
|
||
|
||
The deterministic match-score endpoint also synchronises `learning:{hash}` system items from its
|
||
current missing skills. They form the first job-specific learning path without another table or an
|
||
external course catalogue. Manual learned/dismissed decisions persist; only recommendations that
|
||
were auto-completed because a gap disappeared reopen when the same gap returns.
|
||
|
||
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}/checklist` — `GET` (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-ups** — `FollowUpAt`, `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}/interview-prep` | editable `InterviewPrepItem` board | user edits only |
|
||
| `GET /{id}/interview-prep/brief` | cached generated `InterviewPrepNote` | explicit refresh or attachment-context change |
|
||
| `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.** Current release and deployment dependencies are tracked in
|
||
`BLOCKERS.md`; the completion report is a historical snapshot.
|
||
7. Cover letter workflow. 8. Documents. 9. Interview preparation. 10. Dashboard improvements.
|