feat(ocr): add per-model history endpoints via path segments
Add findByPersonIdIsNullOrderByCreatedAtDesc + findByPersonIdOrderByCreatedAtDesc to
OcrTrainingRunRepository. Add dto/TrainingHistoryResponse. Expose
GET /api/ocr/training-info/global and GET /api/ocr/training-info/{personId} on
OcrController, both requiring ADMIN; getSenderTrainingHistory guards person existence
via PersonService and returns 404 for unknown personId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.dto.BatchOcrDTO;
|
||||
import org.raddatz.familienarchiv.dto.OcrStatusDTO;
|
||||
import org.raddatz.familienarchiv.dto.TrainingHistoryResponse;
|
||||
import org.raddatz.familienarchiv.dto.TrainingInfoResponse;
|
||||
import org.raddatz.familienarchiv.dto.TriggerOcrDTO;
|
||||
import org.raddatz.familienarchiv.model.AppUser;
|
||||
@@ -135,6 +136,18 @@ public class OcrController {
|
||||
return ocrTrainingService.getTrainingInfo();
|
||||
}
|
||||
|
||||
@GetMapping("/api/ocr/training-info/global")
|
||||
@RequirePermission(Permission.ADMIN)
|
||||
public TrainingHistoryResponse getGlobalTrainingHistory() {
|
||||
return ocrTrainingService.getGlobalTrainingHistory();
|
||||
}
|
||||
|
||||
@GetMapping("/api/ocr/training-info/{personId}")
|
||||
@RequirePermission(Permission.ADMIN)
|
||||
public TrainingHistoryResponse getSenderTrainingHistory(@PathVariable UUID personId) {
|
||||
return ocrTrainingService.getSenderTrainingHistory(personId);
|
||||
}
|
||||
|
||||
private UUID resolveUserId(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) return null;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.raddatz.familienarchiv.dto;
|
||||
|
||||
import org.raddatz.familienarchiv.model.OcrTrainingRun;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record TrainingHistoryResponse(
|
||||
List<OcrTrainingRun> runs,
|
||||
Map<String, String> personNames
|
||||
) {}
|
||||
@@ -19,4 +19,8 @@ public interface OcrTrainingRunRepository extends JpaRepository<OcrTrainingRun,
|
||||
boolean existsByPersonIdAndStatus(UUID personId, TrainingStatus status);
|
||||
|
||||
List<OcrTrainingRun> findTop20ByOrderByCreatedAtDesc();
|
||||
|
||||
List<OcrTrainingRun> findByPersonIdIsNullOrderByCreatedAtDesc();
|
||||
|
||||
List<OcrTrainingRun> findByPersonIdOrderByCreatedAtDesc(UUID personId);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.dto.TrainingHistoryResponse;
|
||||
import org.raddatz.familienarchiv.dto.TrainingInfoResponse;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
@@ -219,6 +220,17 @@ public class OcrTrainingService {
|
||||
);
|
||||
}
|
||||
|
||||
public TrainingHistoryResponse getGlobalTrainingHistory() {
|
||||
List<OcrTrainingRun> runs = trainingRunRepository.findByPersonIdIsNullOrderByCreatedAtDesc();
|
||||
return new TrainingHistoryResponse(runs, Map.of());
|
||||
}
|
||||
|
||||
public TrainingHistoryResponse getSenderTrainingHistory(UUID personId) {
|
||||
String personName = personService.getById(personId).getDisplayName();
|
||||
List<OcrTrainingRun> runs = trainingRunRepository.findByPersonIdOrderByCreatedAtDesc(personId);
|
||||
return new TrainingHistoryResponse(runs, Map.of(personId.toString(), personName));
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Transactional
|
||||
public void recoverOrphanedRuns() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.config.SecurityConfig;
|
||||
import org.raddatz.familienarchiv.dto.BatchOcrDTO;
|
||||
import org.raddatz.familienarchiv.dto.OcrStatusDTO;
|
||||
import org.raddatz.familienarchiv.dto.TrainingHistoryResponse;
|
||||
import org.raddatz.familienarchiv.dto.TrainingInfoResponse;
|
||||
import org.raddatz.familienarchiv.dto.TriggerOcrDTO;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
@@ -290,6 +291,71 @@ class OcrControllerTest {
|
||||
.andExpect(jsonPath("$.senderModels[0].personId").value(personId.toString()));
|
||||
}
|
||||
|
||||
// ─── GET /api/ocr/training-info/global ───────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getGlobalTrainingHistory_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/ocr/training-info/global"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "READ_ALL")
|
||||
void getGlobalTrainingHistory_returns403_whenNotAdmin() throws Exception {
|
||||
mockMvc.perform(get("/api/ocr/training-info/global"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN")
|
||||
void getGlobalTrainingHistory_returns200_withRunsList() throws Exception {
|
||||
TrainingHistoryResponse history = new TrainingHistoryResponse(List.of(), Map.of());
|
||||
when(ocrTrainingService.getGlobalTrainingHistory()).thenReturn(history);
|
||||
|
||||
mockMvc.perform(get("/api/ocr/training-info/global"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.runs").isArray());
|
||||
}
|
||||
|
||||
// ─── GET /api/ocr/training-info/{personId} ───────────────────────────────
|
||||
|
||||
@Test
|
||||
void getSenderTrainingHistory_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/ocr/training-info/{id}", UUID.randomUUID()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "READ_ALL")
|
||||
void getSenderTrainingHistory_returns403_whenNotAdmin() throws Exception {
|
||||
mockMvc.perform(get("/api/ocr/training-info/{id}", UUID.randomUUID()))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN")
|
||||
void getSenderTrainingHistory_returns404_whenPersonNotFound() throws Exception {
|
||||
UUID unknownId = UUID.randomUUID();
|
||||
when(ocrTrainingService.getSenderTrainingHistory(unknownId))
|
||||
.thenThrow(DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found"));
|
||||
|
||||
mockMvc.perform(get("/api/ocr/training-info/{id}", unknownId))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN")
|
||||
void getSenderTrainingHistory_returns200_withRunsList() throws Exception {
|
||||
UUID personId = UUID.randomUUID();
|
||||
TrainingHistoryResponse history = new TrainingHistoryResponse(List.of(), Map.of(personId.toString(), "Anna Müller"));
|
||||
when(ocrTrainingService.getSenderTrainingHistory(personId)).thenReturn(history);
|
||||
|
||||
mockMvc.perform(get("/api/ocr/training-info/{id}", personId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.runs").isArray())
|
||||
.andExpect(jsonPath("$.personNames." + personId).value("Anna Müller"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "READ_ALL")
|
||||
void getDocumentOcrStatus_returnsNone_whenNoOcrJobExists() throws Exception {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.service;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.dto.TrainingHistoryResponse;
|
||||
import org.raddatz.familienarchiv.dto.TrainingInfoResponse;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.model.OcrTrainingRun;
|
||||
@@ -312,4 +313,51 @@ class OcrTrainingServiceTest {
|
||||
|
||||
verify(runRepository, never()).save(any());
|
||||
}
|
||||
|
||||
// ─── getGlobalTrainingHistory ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getGlobalTrainingHistory_returnsOnlyGlobalRuns() {
|
||||
List<OcrTrainingRun> globalRuns = List.of(
|
||||
OcrTrainingRun.builder().id(UUID.randomUUID()).status(TrainingStatus.DONE)
|
||||
.blockCount(10).documentCount(2).modelName("german_kurrent").build()
|
||||
);
|
||||
when(runRepository.findByPersonIdIsNullOrderByCreatedAtDesc()).thenReturn(globalRuns);
|
||||
|
||||
TrainingHistoryResponse result = service.getGlobalTrainingHistory();
|
||||
|
||||
assertThat(result.runs()).hasSize(1);
|
||||
assertThat(result.personNames()).isEmpty();
|
||||
verify(runRepository).findByPersonIdIsNullOrderByCreatedAtDesc();
|
||||
}
|
||||
|
||||
// ─── getSenderTrainingHistory ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getSenderTrainingHistory_includesPersonName_andRuns() {
|
||||
UUID personId = UUID.randomUUID();
|
||||
Person person = Person.builder().id(personId).firstName("Anna").lastName("Müller").build();
|
||||
when(personService.getById(personId)).thenReturn(person);
|
||||
|
||||
List<OcrTrainingRun> senderRuns = List.of(
|
||||
OcrTrainingRun.builder().id(UUID.randomUUID()).status(TrainingStatus.DONE)
|
||||
.personId(personId).blockCount(5).documentCount(1).modelName("sender_x").build()
|
||||
);
|
||||
when(runRepository.findByPersonIdOrderByCreatedAtDesc(personId)).thenReturn(senderRuns);
|
||||
|
||||
TrainingHistoryResponse result = service.getSenderTrainingHistory(personId);
|
||||
|
||||
assertThat(result.runs()).hasSize(1);
|
||||
assertThat(result.personNames()).containsKey(personId.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSenderTrainingHistory_propagates404_whenPersonNotFound() {
|
||||
UUID unknownId = UUID.randomUUID();
|
||||
when(personService.getById(unknownId))
|
||||
.thenThrow(DomainException.notFound(org.raddatz.familienarchiv.exception.ErrorCode.PERSON_NOT_FOUND, "not found"));
|
||||
|
||||
assertThatThrownBy(() -> service.getSenderTrainingHistory(unknownId))
|
||||
.isInstanceOf(DomainException.class);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user