feat: persist candidate fit and focus plan, stop re-running on every open

Extends the interview-prep persistence pattern (previous commit) to
the other two AI-generated per-job outputs that were re-running their
full AI call chain on every tab open: candidate-fit (4 AI calls) and
focus-plan (4 AI calls). Across all three tabs that's 9 AI calls fired
every single time a user revisits a job's AI workspace tabs.

Generalized into AiWorkspaceNote (OwnerUserId, JobApplicationId,
NoteType, ResultJson) rather than duplicating InterviewPrepNote's
per-field-column shape: CandidateFitDto and FocusPlanDto are irregular
and nested (up to 13 fields including a nested guidance object),
where per-field columns would be unreasonable. One table, keyed by
note type, serving both.

Same rules as interview prep: reuse across calls, regenerate when the
attachment selection changes, regenerate on explicit refresh. Frontend
gets the same "Regenerate" button on both tabs.

3 new tests (persist+reuse for both, refresh for candidate-fit).
Verified against the real dev DB.
This commit is contained in:
cesnimda
2026-07-12 15:41:58 +02:00
parent 5916f09852
commit 00a035ea20
6 changed files with 312 additions and 6 deletions
@@ -680,12 +680,34 @@ public static class StartupInitializationExtensions
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_InterviewPrepNotes_OwnerUserId_JobApplicationId" ON "InterviewPrepNotes" ("OwnerUserId", "JobApplicationId");""");
}
// Generic AI workspace note persistence (candidate fit, focus plan) -- same
// rationale as EnsureInterviewPrepNotesTable, generalized for DTOs too irregular
// for per-field columns.
static void EnsureAiWorkspaceNotesTable(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "AiWorkspaceNotes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_AiWorkspaceNotes" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"NoteType" TEXT NOT NULL,
"AttachmentContextSignature" TEXT NOT NULL,
"ResultJson" TEXT NOT NULL,
"GeneratedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_AiWorkspaceNotes_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_AiWorkspaceNotes_OwnerUserId_JobApplicationId_NoteType" ON "AiWorkspaceNotes" ("OwnerUserId", "JobApplicationId", "NoteType");""");
}
EnsureGmailConnectionsTable(conn);
EnsureMicrosoftGraphConnectionsTable(conn);
EnsureImapConnectionsTable(conn);
EnsureCvTables(conn);
EnsureCareerProfileTables(conn);
EnsureInterviewPrepNotesTable(conn);
EnsureAiWorkspaceNotesTable(conn);
// Legacy DB signature: migration history exists (AddCorrespondence applied), but 20260310195000 not recorded,
// and at least one of the new columns already exists.
@@ -837,6 +859,15 @@ public static class StartupInitializationExtensions
cmd.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "AiWorkspaceNotes", "Id");
if (!MySqlIndexExists(conn, "AiWorkspaceNotes", "IX_AiWorkspaceNotes_OwnerUserId_JobApplicationId_NoteType"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "CREATE UNIQUE INDEX `IX_AiWorkspaceNotes_OwnerUserId_JobApplicationId_NoteType` ON `AiWorkspaceNotes` (`OwnerUserId`, `JobApplicationId`, `NoteType`);";
cmd.ExecuteNonQuery();
}
// Ad-hoc columns for the tables Migrate() creates (Companies/JobApplications/
// Correspondences/Attachments) -- re-run once more after Migrate() below via
// ReconcileCoreAppColumnsMySql, in case this is a brand-new database.
@@ -1116,6 +1147,24 @@ public static class StartupInitializationExtensions
cmd.ExecuteNonQuery();
}
// Generic AI workspace note persistence (candidate fit, focus plan).
if (!HasMySqlTable(conn, "AiWorkspaceNotes"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `AiWorkspaceNotes` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`NoteType` varchar(50) NOT NULL,
`AttachmentContextSignature` longtext NOT NULL,
`ResultJson` longtext NOT NULL,
`GeneratedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_AiWorkspaceNotes_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
if (!MySqlIndexExists(conn, "Companies", "IX_Companies_OwnerUserId"))
{
using var cmd = conn.CreateCommand();