Files
jobtrackingapp/JobTrackerApi/Controllers/ApplicationIntelligenceController.cs
T
cesnimda a7cecce13d
CI and Deploy / test (push) Failing after 1m11s
CI and Deploy / deploy (push) Has been skipped
feat(workspace): add 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".

Timeline (GET /{id}/timeline) is an interpretation layer over JobEvent, which
stays the source of historical truth. Each row gains a readable summary, a
category and a milestone flag; events group by day. Milestones are returned
unfiltered, because narrowing the detail must not hide what actually happened.

Job analysis (GET /{id}/analysis) extracts role, company, location, employment
type, seniority, salary, technologies, skills, responsibilities and keywords
from the advert, reusing the existing SkillTagger so the vocabulary matches the
job importer. It also reports what the advert does NOT say, which is usually the
more useful half.

Career matching (GET /{id}/match) feeds the master CareerProfile into the same
JobCvMatchService the CV builder uses, so one application scores identically
whichever surface asks. It returns the score, matched and missing skills, and
which experience and project entries are the evidence for each match.

All three are deterministic and own no data — no new table, no new column, and
nothing writes to the CareerProfile, a CvVariant, or the JobApplication. The AI
narrative stays where it already was, in AiWorkspaceService's job-analysis and
career-match modules, generated only when the user asks and versioned by the
append-only AiInteraction history. Opening a section costs nothing and changes
nothing.

Frontend adds Timeline, Analysis and Match sections to the workspace, sharing
one loader so loading, empty and error states are consistent. The deterministic
answer renders first, with the AI panel below it.

345 backend tests, 104 frontend tests, type check, production build all pass
locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:52:37 +02:00

64 lines
2.5 KiB
C#

using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace JobTrackerApi.Controllers;
// Phase 5.3 — Application Intelligence. Three read-only endpoints on the application:
// timeline (interprets JobEvent), analysis (reads the advert), match (reads the master profile).
//
// None of them write anything. The AI narrative for analysis and match stays on the existing
// /api/jobapplications/{id}/ai routes, which are suggestion-only and versioned by AiInteraction.
// docs/architecture/application-workspace.md.
[ApiController]
[Route("api/jobapplications/{jobId:int}")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class ApplicationIntelligenceController : ControllerBase
{
private readonly UserManager<ApplicationUser> _users;
private readonly IApplicationTimelineService _timeline;
private readonly IApplicationIntelligenceService _intelligence;
public ApplicationIntelligenceController(
UserManager<ApplicationUser> users,
IApplicationTimelineService timeline,
IApplicationIntelligenceService intelligence)
{
_users = users;
_timeline = timeline;
_intelligence = intelligence;
}
[HttpGet("timeline")]
public async Task<ActionResult<TimelineDto>> GetTimeline(
int jobId, [FromQuery] string? category, [FromQuery] bool milestonesOnly, CancellationToken ct)
{
var userId = await CurrentUserIdAsync();
if (userId is null) return Unauthorized();
var result = await _timeline.GetAsync(userId, jobId, category, milestonesOnly, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("analysis")]
public async Task<ActionResult<JobAnalysisDto>> GetAnalysis(int jobId, CancellationToken ct)
{
var userId = await CurrentUserIdAsync();
if (userId is null) return Unauthorized();
var result = await _intelligence.AnalyzeAsync(userId, jobId, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("match")]
public async Task<ActionResult<CareerMatchDto>> GetMatch(int jobId, CancellationToken ct)
{
var userId = await CurrentUserIdAsync();
if (userId is null) return Unauthorized();
var result = await _intelligence.MatchAsync(userId, jobId, ct);
return result is null ? NotFound() : Ok(result);
}
private async Task<string?> CurrentUserIdAsync() => (await _users.GetUserAsync(User))?.Id;
}