From 640bcf31f49e9d840b43f83ffb2a7fc77ceecaf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:17:33 +0000 Subject: [PATCH 1/2] feat: add manual interview count update via API and MCP Adds PATCH /api/v1/dashboard/interview-count endpoint and Update-Interview-Count MCP tool so users can correct the cumulative interview counter when the automatic detection misses transitions. https://claude.ai/code/session_015kfgrc2oHA881RT3bEYHZ6 --- .../controller/DashboardController.java | 29 ++++++++++++++++++- .../UpdateInterviewCountRequest.java | 7 +++++ .../mcp/tools/McpAnalyticsTools.java | 22 ++++++++++++++ .../service/InterviewMetricsService.java | 21 +++++++++++++- 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java diff --git a/src/main/java/com/jobtracker/controller/DashboardController.java b/src/main/java/com/jobtracker/controller/DashboardController.java index 1556d961..48edda87 100644 --- a/src/main/java/com/jobtracker/controller/DashboardController.java +++ b/src/main/java/com/jobtracker/controller/DashboardController.java @@ -1,14 +1,20 @@ package com.jobtracker.controller; import com.jobtracker.dto.dashboard.DashboardSummaryResponse; +import com.jobtracker.dto.dashboard.UpdateInterviewCountRequest; import com.jobtracker.service.DashboardService; +import com.jobtracker.service.InterviewMetricsService; +import com.jobtracker.util.SecurityUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,9 +24,15 @@ public class DashboardController { private final DashboardService dashboardService; + private final InterviewMetricsService interviewMetricsService; + private final SecurityUtils securityUtils; - public DashboardController(DashboardService dashboardService) { + public DashboardController(DashboardService dashboardService, + InterviewMetricsService interviewMetricsService, + SecurityUtils securityUtils) { this.dashboardService = dashboardService; + this.interviewMetricsService = interviewMetricsService; + this.securityUtils = securityUtils; } @Operation( @@ -36,4 +48,19 @@ public DashboardController(DashboardService dashboardService) { public ResponseEntity getSummary() { return ResponseEntity.ok(dashboardService.getSummary()); } + + @Operation( + summary = "Update interview count", + description = "Manually sets the cumulative interview count for the authenticated user", + responses = { + @ApiResponse(responseCode = "204", description = "Count updated"), + @ApiResponse(responseCode = "400", description = "Invalid count value"), + @ApiResponse(responseCode = "401", description = "Not authenticated") + } + ) + @PatchMapping("/interview-count") + public ResponseEntity updateInterviewCount(@Valid @RequestBody UpdateInterviewCountRequest request) { + interviewMetricsService.setInterviewCount(securityUtils.getCurrentUserId(), request.count()); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java b/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java new file mode 100644 index 00000000..49571bdb --- /dev/null +++ b/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java @@ -0,0 +1,7 @@ +package com.jobtracker.dto.dashboard; + +import jakarta.validation.constraints.Min; + +public record UpdateInterviewCountRequest( + @Min(0) long count +) {} diff --git a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java index 60311c6d..ab2a4d08 100644 --- a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java +++ b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java @@ -9,6 +9,7 @@ import com.jobtracker.mapper.ApplicationMapper; import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.service.ApplicationService; +import com.jobtracker.service.InterviewMetricsService; import com.jobtracker.util.SecurityUtils; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpTool.McpAnnotations; @@ -33,18 +34,39 @@ public class McpAnalyticsTools { private final ApplicationRepository applicationRepository; private final ApplicationMapper applicationMapper; private final ApplicationService applicationService; + private final InterviewMetricsService interviewMetricsService; private final SecurityUtils securityUtils; public McpAnalyticsTools(ApplicationRepository applicationRepository, ApplicationMapper applicationMapper, ApplicationService applicationService, + InterviewMetricsService interviewMetricsService, SecurityUtils securityUtils) { this.applicationRepository = applicationRepository; this.applicationMapper = applicationMapper; this.applicationService = applicationService; + this.interviewMetricsService = interviewMetricsService; this.securityUtils = securityUtils; } + @McpTool( + name = "Update-Interview-Count", + title = "Update Interview Count", + description = "Manually sets the cumulative interview count for the current user. Use this when the automatic counter missed interviews or needs correction.", + annotations = @McpAnnotations( + title = "Update Interview Count", + readOnlyHint = false, + destructiveHint = false, + idempotentHint = true, + openWorldHint = false)) + @Transactional + public String updateInterviewCount( + @McpToolParam(required = true, description = "The new total interview count (must be >= 0)") long count) { + UUID userId = securityUtils.getCurrentUserId(); + interviewMetricsService.setInterviewCount(userId, count); + return "Interview count updated to " + count; + } + @McpTool( name = "Get-Analytics", title = "Get Analytics", diff --git a/src/main/java/com/jobtracker/service/InterviewMetricsService.java b/src/main/java/com/jobtracker/service/InterviewMetricsService.java index f1f2e64e..188d8e8d 100644 --- a/src/main/java/com/jobtracker/service/InterviewMetricsService.java +++ b/src/main/java/com/jobtracker/service/InterviewMetricsService.java @@ -7,6 +7,7 @@ import com.jobtracker.entity.enums.ApplicationStatus; import com.jobtracker.repository.InterviewEventRepository; import com.jobtracker.repository.UserInterviewMetricsRepository; +import com.jobtracker.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,11 +30,14 @@ public class InterviewMetricsService { private final UserInterviewMetricsRepository metricsRepository; private final InterviewEventRepository eventRepository; + private final UserRepository userRepository; public InterviewMetricsService(UserInterviewMetricsRepository metricsRepository, - InterviewEventRepository eventRepository) { + InterviewEventRepository eventRepository, + UserRepository userRepository) { this.metricsRepository = metricsRepository; this.eventRepository = eventRepository; + this.userRepository = userRepository; } public boolean isInterviewStatus(String status) { @@ -77,6 +81,21 @@ public void recordStatusTransition(JobApplication application, eventRepository.save(event); } + @Transactional + public void setInterviewCount(UUID userId, long count) { + if (count < 0) throw new IllegalArgumentException("Interview count cannot be negative"); + UserInterviewMetrics metrics = metricsRepository.findByUser_Id(userId) + .orElseGet(() -> { + User user = userRepository.getReferenceById(userId); + UserInterviewMetrics created = new UserInterviewMetrics(); + created.setUser(user); + created.setInterviewCount(0); + return created; + }); + metrics.setInterviewCount(count); + metricsRepository.save(metrics); + } + @Transactional(readOnly = true) public long getInterviewCount(UUID userId) { return metricsRepository.findById(userId) From 1d0691bce704f7ab4da602e4db5f538ea9e805de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:34:04 +0000 Subject: [PATCH 2/2] feat: move interview count to per-application field Adds interview_count column to job_applications so each application tracks its own count. Dashboard interviewCount now sums across all applications. Removes the per-user UserInterviewMetrics path and the manual update endpoint/MCP tool (editing is now done via the application form). https://claude.ai/code/session_015kfgrc2oHA881RT3bEYHZ6 --- .../controller/DashboardController.java | 29 +---------- .../dto/application/ApplicationRequest.java | 7 ++- .../dto/application/ApplicationResponse.java | 2 + .../UpdateInterviewCountRequest.java | 7 --- .../com/jobtracker/entity/JobApplication.java | 6 +++ .../jobtracker/mapper/ApplicationMapper.java | 1 + .../mcp/tools/McpAnalyticsTools.java | 22 -------- .../repository/ApplicationRepository.java | 3 ++ .../service/ApplicationService.java | 3 ++ .../service/InterviewMetricsService.java | 52 +++---------------- ...dd_interview_count_to_job_applications.sql | 2 + 11 files changed, 31 insertions(+), 103 deletions(-) delete mode 100644 src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java create mode 100644 src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql diff --git a/src/main/java/com/jobtracker/controller/DashboardController.java b/src/main/java/com/jobtracker/controller/DashboardController.java index 48edda87..1556d961 100644 --- a/src/main/java/com/jobtracker/controller/DashboardController.java +++ b/src/main/java/com/jobtracker/controller/DashboardController.java @@ -1,20 +1,14 @@ package com.jobtracker.controller; import com.jobtracker.dto.dashboard.DashboardSummaryResponse; -import com.jobtracker.dto.dashboard.UpdateInterviewCountRequest; import com.jobtracker.service.DashboardService; -import com.jobtracker.service.InterviewMetricsService; -import com.jobtracker.util.SecurityUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -24,15 +18,9 @@ public class DashboardController { private final DashboardService dashboardService; - private final InterviewMetricsService interviewMetricsService; - private final SecurityUtils securityUtils; - public DashboardController(DashboardService dashboardService, - InterviewMetricsService interviewMetricsService, - SecurityUtils securityUtils) { + public DashboardController(DashboardService dashboardService) { this.dashboardService = dashboardService; - this.interviewMetricsService = interviewMetricsService; - this.securityUtils = securityUtils; } @Operation( @@ -48,19 +36,4 @@ public DashboardController(DashboardService dashboardService, public ResponseEntity getSummary() { return ResponseEntity.ok(dashboardService.getSummary()); } - - @Operation( - summary = "Update interview count", - description = "Manually sets the cumulative interview count for the authenticated user", - responses = { - @ApiResponse(responseCode = "204", description = "Count updated"), - @ApiResponse(responseCode = "400", description = "Invalid count value"), - @ApiResponse(responseCode = "401", description = "Not authenticated") - } - ) - @PatchMapping("/interview-count") - public ResponseEntity updateInterviewCount(@Valid @RequestBody UpdateInterviewCountRequest request) { - interviewMetricsService.setInterviewCount(securityUtils.getCurrentUserId(), request.count()); - return ResponseEntity.noContent().build(); - } } diff --git a/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java b/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java index e66283bc..27f1a5ed 100644 --- a/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java +++ b/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PastOrPresent; import jakarta.validation.constraints.Pattern; @@ -53,5 +54,9 @@ public record ApplicationRequest( String note, @Schema(description = "Platform or job board where the vacancy was found", example = "LinkedIn") - String platform + String platform, + + @Schema(description = "Number of interviews held for this application", example = "2") + @Min(0) + Integer interviewCount ) {} diff --git a/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java b/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java index 27945649..d70b340f 100644 --- a/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java +++ b/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java @@ -60,6 +60,8 @@ public record ApplicationResponse( @Schema(description = "Timestamp when the latest Google Docs resume was generated", example = "2024-06-11T14:45:00") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime driveResumeGeneratedAt, + @Schema(description = "Number of interviews held for this application", example = "2") + int interviewCount, @Schema(description = "Record creation timestamp", example = "2024-06-01T10:00:00") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime createdAt, diff --git a/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java b/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java deleted file mode 100644 index 49571bdb..00000000 --- a/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.jobtracker.dto.dashboard; - -import jakarta.validation.constraints.Min; - -public record UpdateInterviewCountRequest( - @Min(0) long count -) {} diff --git a/src/main/java/com/jobtracker/entity/JobApplication.java b/src/main/java/com/jobtracker/entity/JobApplication.java index 7613c5de..698fd905 100644 --- a/src/main/java/com/jobtracker/entity/JobApplication.java +++ b/src/main/java/com/jobtracker/entity/JobApplication.java @@ -43,6 +43,9 @@ public class JobApplication { @Column(name = "interview_scheduled", nullable = false) private boolean interviewScheduled; + @Column(name = "interview_count", nullable = false) + private int interviewCount = 0; + @Column(name = "next_step_date_time") private LocalDateTime nextStepDateTime; @@ -147,6 +150,9 @@ protected void onUpdate() { public boolean isInterviewScheduled() { return interviewScheduled; } public void setInterviewScheduled(boolean interviewScheduled) { this.interviewScheduled = interviewScheduled; } + public int getInterviewCount() { return interviewCount; } + public void setInterviewCount(int interviewCount) { this.interviewCount = interviewCount; } + public LocalDateTime getNextStepDateTime() { return nextStepDateTime; } public void setNextStepDateTime(LocalDateTime nextStepDateTime) { this.nextStepDateTime = nextStepDateTime; } diff --git a/src/main/java/com/jobtracker/mapper/ApplicationMapper.java b/src/main/java/com/jobtracker/mapper/ApplicationMapper.java index a330b0cc..48fedfb4 100644 --- a/src/main/java/com/jobtracker/mapper/ApplicationMapper.java +++ b/src/main/java/com/jobtracker/mapper/ApplicationMapper.java @@ -31,6 +31,7 @@ public ApplicationResponse toResponse(JobApplication app) { app.getDriveResumeFileName(), app.getDriveResumeDocumentUrl(), app.getDriveResumeGeneratedAt(), + app.getInterviewCount(), app.getCreatedAt(), app.getUpdatedAt() ); diff --git a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java index ab2a4d08..60311c6d 100644 --- a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java +++ b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java @@ -9,7 +9,6 @@ import com.jobtracker.mapper.ApplicationMapper; import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.service.ApplicationService; -import com.jobtracker.service.InterviewMetricsService; import com.jobtracker.util.SecurityUtils; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpTool.McpAnnotations; @@ -34,39 +33,18 @@ public class McpAnalyticsTools { private final ApplicationRepository applicationRepository; private final ApplicationMapper applicationMapper; private final ApplicationService applicationService; - private final InterviewMetricsService interviewMetricsService; private final SecurityUtils securityUtils; public McpAnalyticsTools(ApplicationRepository applicationRepository, ApplicationMapper applicationMapper, ApplicationService applicationService, - InterviewMetricsService interviewMetricsService, SecurityUtils securityUtils) { this.applicationRepository = applicationRepository; this.applicationMapper = applicationMapper; this.applicationService = applicationService; - this.interviewMetricsService = interviewMetricsService; this.securityUtils = securityUtils; } - @McpTool( - name = "Update-Interview-Count", - title = "Update Interview Count", - description = "Manually sets the cumulative interview count for the current user. Use this when the automatic counter missed interviews or needs correction.", - annotations = @McpAnnotations( - title = "Update Interview Count", - readOnlyHint = false, - destructiveHint = false, - idempotentHint = true, - openWorldHint = false)) - @Transactional - public String updateInterviewCount( - @McpToolParam(required = true, description = "The new total interview count (must be >= 0)") long count) { - UUID userId = securityUtils.getCurrentUserId(); - interviewMetricsService.setInterviewCount(userId, count); - return "Interview count updated to " + count; - } - @McpTool( name = "Get-Analytics", title = "Get Analytics", diff --git a/src/main/java/com/jobtracker/repository/ApplicationRepository.java b/src/main/java/com/jobtracker/repository/ApplicationRepository.java index 417b3df9..f2450d88 100644 --- a/src/main/java/com/jobtracker/repository/ApplicationRepository.java +++ b/src/main/java/com/jobtracker/repository/ApplicationRepository.java @@ -23,6 +23,9 @@ public interface ApplicationRepository extends JpaRepository { - User user = userRepository.getReferenceById(userId); - UserInterviewMetrics created = new UserInterviewMetrics(); - created.setUser(user); - created.setInterviewCount(0); - return created; - }); - metrics.setInterviewCount(count); - metricsRepository.save(metrics); - } - @Transactional(readOnly = true) public long getInterviewCount(UUID userId) { - return metricsRepository.findById(userId) - .map(UserInterviewMetrics::getInterviewCount) - .orElseGet(() -> eventRepository.countByUser_Id(userId)); - } - - private UserInterviewMetrics findOrCreateMetrics(User user) { - return metricsRepository.findByUser_Id(user.getId()) - .orElseGet(() -> { - UserInterviewMetrics created = new UserInterviewMetrics(); - created.setUser(user); - created.setInterviewCount(0); - return created; - }); + return applicationRepository.sumInterviewCountByUserId(userId); } } diff --git a/src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql b/src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql new file mode 100644 index 00000000..f5b5a738 --- /dev/null +++ b/src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql @@ -0,0 +1,2 @@ +ALTER TABLE job_applications + ADD COLUMN interview_count INT NOT NULL DEFAULT 0 AFTER interview_scheduled;