feat(workspace): application assets workflow
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>
This commit is contained in:
@@ -1044,6 +1044,25 @@ public static class StartupInitializationExtensions
|
||||
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_ApplicationChecklistItems_Owner_Job_Sort" ON "ApplicationChecklistItems" ("OwnerUserId", "JobApplicationId", "SortOrder");""");
|
||||
}
|
||||
|
||||
// Phase 5.4: append-only cover letter history.
|
||||
static void EnsureCoverLetterVersionsTable(DbConnection c)
|
||||
{
|
||||
Exec(c, """
|
||||
CREATE TABLE IF NOT EXISTS "CoverLetterVersions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_CoverLetterVersions" PRIMARY KEY AUTOINCREMENT,
|
||||
"OwnerUserId" TEXT NOT NULL,
|
||||
"JobApplicationId" INTEGER NOT NULL,
|
||||
"Version" INTEGER NOT NULL,
|
||||
"Text" TEXT NOT NULL,
|
||||
"Source" TEXT NOT NULL,
|
||||
"AiAction" TEXT NULL,
|
||||
"CreatedAtUtc" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_CoverLetterVersions_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
""");
|
||||
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CoverLetterVersions_Owner_Job_Version" ON "CoverLetterVersions" ("OwnerUserId", "JobApplicationId", "Version");""");
|
||||
}
|
||||
|
||||
EnsureGmailConnectionsTable(conn);
|
||||
EnsureMicrosoftGraphConnectionsTable(conn);
|
||||
EnsureImapConnectionsTable(conn);
|
||||
@@ -1057,6 +1076,7 @@ public static class StartupInitializationExtensions
|
||||
EnsureCvBuilderTables(conn);
|
||||
EnsureAiInteractionsTable(conn);
|
||||
EnsureApplicationChecklistTable(conn);
|
||||
EnsureCoverLetterVersionsTable(conn);
|
||||
|
||||
// Legacy DB signature: migration history exists (AddCorrespondence applied), but 20260310195000 not recorded,
|
||||
// and at least one of the new columns already exists.
|
||||
@@ -1651,6 +1671,7 @@ public static class StartupInitializationExtensions
|
||||
DropMalformedMySqlTable(conn, "CvVariants", "UpdatedAtUtc", "datetime");
|
||||
DropMalformedMySqlTable(conn, "AiInteractions", "CreatedAtUtc", "datetime");
|
||||
DropMalformedMySqlTable(conn, "ApplicationChecklistItems", "CreatedAtUtc", "datetime");
|
||||
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
|
||||
|
||||
if (!HasMySqlTable(conn, "CvVariants") && HasMySqlTable(conn, "JobApplications"))
|
||||
{
|
||||
@@ -1734,6 +1755,27 @@ public static class StartupInitializationExtensions
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (!HasMySqlTable(conn, "CoverLetterVersions") && HasMySqlTable(conn, "JobApplications"))
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CoverLetterVersions` (
|
||||
`Id` int NOT NULL AUTO_INCREMENT,
|
||||
`OwnerUserId` varchar(255) NOT NULL,
|
||||
`JobApplicationId` int NOT NULL,
|
||||
`Version` int NOT NULL,
|
||||
`Text` longtext NOT NULL,
|
||||
`Source` varchar(32) NOT NULL,
|
||||
`AiAction` varchar(32) NULL,
|
||||
`CreatedAtUtc` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
CONSTRAINT `FK_CoverLetterVersions_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
EnsureMySqlAutoIncrementPrimaryKey(conn, "CoverLetterVersions", "Id");
|
||||
EnsureMySqlIndex(conn, "CoverLetterVersions", "IX_CoverLetterVersions_Owner_Job_Version", "`OwnerUserId`, `JobApplicationId`, `Version`");
|
||||
|
||||
EnsureMySqlAutoIncrementPrimaryKey(conn, "ApplicationChecklistItems", "Id");
|
||||
EnsureMySqlAutoIncrementPrimaryKey(conn, "CvVariants", "Id");
|
||||
EnsureMySqlAutoIncrementPrimaryKey(conn, "CvVariantVersions", "Id");
|
||||
|
||||
Reference in New Issue
Block a user