feat(career): wire /career to the relational profile API + completeness overview
CI and Deploy / test (push) Failing after 1m52s
CI and Deploy / deploy (push) Has been skipped

Phase 3, frontend. /career now reads and writes the master profile through the
relational source of truth instead of the legacy blob path.

- CareerProfilePage loads GET /career/profile (structured profile from the
  relational children + cvText + completeness) and saves PUT /career/profile
  ({ profile, cvText }). This keeps the relational store authoritative — the
  previous PUT /auth/profile blob write left it stale after first load.
- Added a "Profile completeness" overview (percent bar + missing sections) at the
  top of /career, from the server scorecard.
- PUT /career/profile now accepts { profile, cvText } so the single /career save
  covers both the structured profile and the raw imported text; GET returns cvText.

Tests: career-save asserts the /career/profile payload; new completeness-overview
test; controller tests updated for the request wrapper. 75/76 frontend pass (the
1 failure is the unrelated pre-existing settings-view suite); prod build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-18 00:53:35 +02:00
parent b203120ab4
commit 2b57d65715
4 changed files with 84 additions and 29 deletions
@@ -47,7 +47,7 @@ public sealed class CareerProfileControllerTests
{
var (controller, _, user) = Build();
var put = await controller.Put(Sample(), CancellationToken.None);
var put = await controller.Put(new CareerProfileSaveRequest(Sample(), null), CancellationToken.None);
Assert.IsType<OkObjectResult>(put.Result);
// The derived blob projection is kept in sync for legacy readers.
@@ -64,7 +64,7 @@ public sealed class CareerProfileControllerTests
public async Task Get_reports_completeness_with_missing_sections()
{
var (controller, _, _) = Build();
await controller.Put(Sample(), CancellationToken.None); // has personal + experience + skills
await controller.Put(new CareerProfileSaveRequest(Sample(), null), CancellationToken.None); // has personal + experience + skills
var get = await controller.Get(CancellationToken.None);
var dto = Assert.IsType<CareerProfileDto>(Assert.IsType<OkObjectResult>(get.Result).Value);
@@ -81,7 +81,7 @@ public sealed class CareerProfileControllerTests
var huge = new StructuredCvProfile();
for (var i = 0; i < 500; i++) huge.Skills.Add($"skill-{i}");
var put = await controller.Put(huge, CancellationToken.None);
var put = await controller.Put(new CareerProfileSaveRequest(huge, null), CancellationToken.None);
Assert.IsType<BadRequestObjectResult>(put.Result);
}
@@ -91,7 +91,7 @@ public sealed class CareerProfileControllerTests
{
var (controller, _, _) = Build();
var put = await controller.Put(new StructuredCvProfile(), CancellationToken.None);
var put = await controller.Put(new CareerProfileSaveRequest(new StructuredCvProfile(), null), CancellationToken.None);
// Completeness, not validation, is what flags an incomplete profile — an empty profile saves.
Assert.IsType<OkObjectResult>(put.Result);