From 7d5a78ad5d153a2b33d3634f922ee661fc42e4e6 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 31 Aug 2026 17:20:58 +0200 Subject: [PATCH] feat(interviews): add durable debrief loop --- JobTrackerApi.Tests/InterviewPrepTests.cs | 4 +++- JobTrackerApi/Models/InterviewPrepItem.cs | 5 +++-- .../Services/InterviewPrepService.cs | 1 + docs/work-programmes/master-progress.md | 1 + job-tracker-ui/src/applicationWorkspace.ts | 1 + .../src/components/InterviewPrep.tsx | 21 +++++++++++++++++++ job-tracker-ui/src/i18n/translations.ts | 12 +++++++++++ job-tracker-ui/src/interview-prep.test.tsx | 18 ++++++++++++++++ 8 files changed, 60 insertions(+), 3 deletions(-) diff --git a/JobTrackerApi.Tests/InterviewPrepTests.cs b/JobTrackerApi.Tests/InterviewPrepTests.cs index 570c983..7e8be33 100644 --- a/JobTrackerApi.Tests/InterviewPrepTests.cs +++ b/JobTrackerApi.Tests/InterviewPrepTests.cs @@ -52,13 +52,15 @@ public sealed class InterviewPrepTests await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.Question, "What does success look like?", null, null, null), default); await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.CompanyResearch, "Funding history", "Series B in 2025.", null, null), default); await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.Technical, "System design", null, null, null), default); + await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.Debrief, "Interview debrief", "Questions asked", null, null), default); var result = await prep.GetAsync("user-1", job.Id, default); Assert.Equal( - new[] { InterviewPrepCategories.CompanyResearch, InterviewPrepCategories.Technical, InterviewPrepCategories.Question }, + new[] { InterviewPrepCategories.CompanyResearch, InterviewPrepCategories.Technical, InterviewPrepCategories.Question, InterviewPrepCategories.Debrief }, result!.Groups.Select(g => g.Category)); Assert.Equal("Company research", result.Groups[0].Label); + Assert.Equal("Interview debrief", result.Groups[^1].Label); } [Fact] diff --git a/JobTrackerApi/Models/InterviewPrepItem.cs b/JobTrackerApi/Models/InterviewPrepItem.cs index 340750b..61d3b34 100644 --- a/JobTrackerApi/Models/InterviewPrepItem.cs +++ b/JobTrackerApi/Models/InterviewPrepItem.cs @@ -16,7 +16,7 @@ public sealed class InterviewPrepItem public int JobApplicationId { get; set; } public JobApplication? JobApplication { get; set; } - // company-research | technical | behavioural | star | question | note + // company-research | technical | behavioural | star | question | note | debrief public string Category { get; set; } = InterviewPrepCategories.Note; // The prompt side: a question to answer, or the heading of a research note. @@ -45,12 +45,13 @@ public static class InterviewPrepCategories public const string Star = "star"; public const string Question = "question"; public const string Note = "note"; + public const string Debrief = "debrief"; // Display order in the workspace: understand the company, then the role, then yourself, then what // you want to ask them. public static readonly string[] Order = { - CompanyResearch, Technical, Behavioural, Star, Question, Note, + CompanyResearch, Technical, Behavioural, Star, Question, Note, Debrief, }; public static int Rank(string? category) diff --git a/JobTrackerApi/Services/InterviewPrepService.cs b/JobTrackerApi/Services/InterviewPrepService.cs index 3b545e8..ab1b518 100644 --- a/JobTrackerApi/Services/InterviewPrepService.cs +++ b/JobTrackerApi/Services/InterviewPrepService.cs @@ -59,6 +59,7 @@ public sealed class InterviewPrepService : IInterviewPrepService [InterviewPrepCategories.Star] = "STAR examples", [InterviewPrepCategories.Question] = "Questions to ask them", [InterviewPrepCategories.Note] = "Notes", + [InterviewPrepCategories.Debrief] = "Interview debrief", }; private readonly JobTrackerContext _db; diff --git a/docs/work-programmes/master-progress.md b/docs/work-programmes/master-progress.md index 744b00f..7316666 100644 --- a/docs/work-programmes/master-progress.md +++ b/docs/work-programmes/master-progress.md @@ -110,6 +110,7 @@ Updated: 2026-08-31 - Submitted-package complete regression: backend 740/740 and frontend 60 suites with 261/261 tests passed. The full run also exposed and fixed a date-sensitive reminder fixture that mixed a fixed worker clock with `DateTime.Now` seed data. - Career-evidence focused verification: backend/controller/schema gates passed 21/21, the evidence UI regression passed 1/1, ESLint passed with zero warnings, and the optimized Next build/TypeScript passed. Complete regression then passed backend 741/741 and frontend 61 suites with 262/262 tests. - Saved job-search implementation: tenant-owned NAV search definitions now retain per-vacancy first-seen, last-seen and dismissal state; repeat runs identify only genuinely new results, while one-off discovery remains unchanged. Account export/deletion and provider-safe schema ownership include the new records. Focused backend tests passed 3/3, focused frontend tests passed 6/6, schema/migration gates passed 20/20, ESLint passed with zero warnings, and the optimized Next build/TypeScript passed. +- Interview debrief loop: interview-stage workspaces now create an explicit, durable debrief in the existing user-owned preparation model, prompting for questions, strengths, improvements, feedback and next steps without generating answers. It remains editable/deletable and is localized in English and Bokmål. Focused backend tests passed 11/11, focused frontend tests passed 13/13, ESLint and the optimized Next build/TypeScript passed. - Focused frontend: 2 suites, 6 tests passed. - Full frontend: 64 suites, 272 tests passed. - Next production build and TypeScript: passed. diff --git a/job-tracker-ui/src/applicationWorkspace.ts b/job-tracker-ui/src/applicationWorkspace.ts index 019a0ba..ef3ee6b 100644 --- a/job-tracker-ui/src/applicationWorkspace.ts +++ b/job-tracker-ui/src/applicationWorkspace.ts @@ -337,6 +337,7 @@ export const INTERVIEW_PREP_CATEGORIES: { key: string; label: string }[] = [ { key: "star", label: "STAR examples" }, { key: "question", label: "Questions to ask them" }, { key: "note", label: "Notes" }, + { key: "debrief", label: "Interview debrief" }, ]; export const interviewPrepApi = { diff --git a/job-tracker-ui/src/components/InterviewPrep.tsx b/job-tracker-ui/src/components/InterviewPrep.tsx index 138d408..9547b2b 100644 --- a/job-tracker-ui/src/components/InterviewPrep.tsx +++ b/job-tracker-ui/src/components/InterviewPrep.tsx @@ -91,6 +91,14 @@ export function ApplicationInterviewPrep({ jobId }: { jobId: number }) { return mutate(() => interviewPrepApi.add(jobId, { category, title: value })); }; + const startDebrief = () => mutate(() => interviewPrepApi.add(jobId, { + category: "debrief", + title: t("interviewDebriefDefaultTitle"), + content: t("interviewDebriefTemplate"), + })); + + const hasDebrief = board?.groups.some((group) => group.category === "debrief") ?? false; + return ( + + + {t("interviewDebriefTitle")} + {t("interviewDebriefHelp")} + + + + + ) : null} + string, category: string) { star: "interviewCategoryStar", question: "interviewCategoryQuestions", note: "interviewCategoryNotes", + debrief: "interviewCategoryDebrief", }; return keys[category] ? t(keys[category]) : category; } diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index 127c707..d2d3530 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -360,6 +360,12 @@ export const translations = { interviewCategoryStar: "STAR examples", interviewCategoryQuestions: "Questions to ask them", interviewCategoryNotes: "Notes", + interviewCategoryDebrief: "Interview debrief", + interviewDebriefTitle: "Capture the interview while it is fresh", + interviewDebriefHelp: "Record the questions, what went well, what you would improve, and any feedback or next steps.", + interviewDebriefStart: "Start debrief", + interviewDebriefDefaultTitle: "Interview debrief", + interviewDebriefTemplate: "Questions asked:\n\nWhat went well:\n\nWhat I would improve:\n\nFeedback and next steps:", interviewAiTitle: "AI interview coach", interviewAiSubtitle: "Uses the linked CV, job advert and application analysis. Suggestions never overwrite your notes.", interviewAiUsing: "Using", @@ -2735,6 +2741,12 @@ export const translations = { interviewCategoryStar: "STAR-eksempler", interviewCategoryQuestions: "Spørsmål du kan stille", interviewCategoryNotes: "Notater", + interviewCategoryDebrief: "Intervjuoppsummering", + interviewDebriefTitle: "Noter intervjuet mens det er ferskt", + interviewDebriefHelp: "Skriv ned spørsmålene, hva som gikk bra, hva du ville gjort bedre, og eventuelle tilbakemeldinger eller neste steg.", + interviewDebriefStart: "Start oppsummering", + interviewDebriefDefaultTitle: "Intervjuoppsummering", + interviewDebriefTemplate: "Spørsmål som ble stilt:\n\nHva gikk bra:\n\nHva jeg ville gjort bedre:\n\nTilbakemeldinger og neste steg:", interviewAiTitle: "AI-intervjutrening", interviewAiSubtitle: "Bruker tilknyttet CV, stillingsannonse og søknadsanalyse. Forslag overskriver aldri notatene dine.", interviewAiUsing: "Bruker", diff --git a/job-tracker-ui/src/interview-prep.test.tsx b/job-tracker-ui/src/interview-prep.test.tsx index d98a066..fd5cbe9 100644 --- a/job-tracker-ui/src/interview-prep.test.tsx +++ b/job-tracker-ui/src/interview-prep.test.tsx @@ -102,6 +102,24 @@ test("adding a prep item posts the chosen category and title", async () => { )); }); +test("starts a structured interview debrief without inventing answers", async () => { + routeGet(); + mockedApi.post.mockResolvedValue({ data: board.groups[0].items[0] } as any); + render(); + + fireEvent.click(await screen.findByRole("button", { name: "Start debrief" })); + + await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith( + "/jobapplications/7/interview-prep", + { + category: "debrief", + title: "Interview debrief", + content: expect.stringContaining("Questions asked:"), + }, + )); + expect(mockedApi.post.mock.calls[0][1].content).not.toMatch(/Acme|achievement|metric/i); +}); + test("marking an item ready patches it", async () => { routeGet(); mockedApi.patch.mockResolvedValue({ data: board.groups[1].items[0] } as any);