feat(t207): audit local procurement results
This commit is contained in:
@@ -24,12 +24,13 @@ const (
|
||||
)
|
||||
|
||||
type AdminServices struct {
|
||||
Assets *usecase.AssetService
|
||||
Tasks *usecase.TaskService
|
||||
Assets *usecase.AssetService
|
||||
Tasks *usecase.TaskService
|
||||
Results *usecase.ExecutionResultService
|
||||
}
|
||||
|
||||
func (s AdminServices) validate() error {
|
||||
if s.Assets == nil || s.Tasks == nil {
|
||||
if s.Assets == nil || s.Tasks == nil || s.Results == nil {
|
||||
return errors.New("admin services are required")
|
||||
}
|
||||
return nil
|
||||
@@ -49,10 +50,38 @@ func registerAdminAPI(routes gin.IRoutes, services AdminServices) error {
|
||||
routes.POST("/api/v1/tasks", handler.createTask)
|
||||
routes.GET("/api/v1/tasks", handler.listTasks)
|
||||
routes.GET("/api/v1/tasks/:id", handler.taskDetail)
|
||||
routes.GET(
|
||||
"/api/v1/tasks/:id/evidence/:evidence_id/content",
|
||||
handler.evidenceContent,
|
||||
)
|
||||
routes.POST("/api/v1/tasks/:id/cancel", handler.cancelTask)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *adminHandlers) evidenceContent(ctx *gin.Context) {
|
||||
result, err := h.services.Results.OpenEvidence(
|
||||
ctx.Request.Context(),
|
||||
ctx.Param("id"),
|
||||
ctx.Param("evidence_id"),
|
||||
)
|
||||
if err != nil {
|
||||
writeUsecaseError(ctx, err)
|
||||
return
|
||||
}
|
||||
defer result.Content.Close()
|
||||
ctx.Header("Cache-Control", "private, no-store")
|
||||
ctx.Header("Content-Type", result.Evidence.MediaType)
|
||||
ctx.Header("Content-Length", strconv.FormatInt(result.Evidence.SizeBytes, 10))
|
||||
ctx.Header("ETag", `"`+result.Evidence.SHA256+`"`)
|
||||
ctx.Header("X-Content-Type-Options", "nosniff")
|
||||
ctx.Header(
|
||||
"Content-Disposition",
|
||||
`inline; filename="`+result.Evidence.ID+`.jpg"`,
|
||||
)
|
||||
ctx.Status(http.StatusOK)
|
||||
_, _ = io.Copy(ctx.Writer, result.Content)
|
||||
}
|
||||
|
||||
func (h *adminHandlers) uploadAsset(ctx *gin.Context) {
|
||||
if !hasMediaType(ctx, "multipart/form-data") {
|
||||
writePublicError(
|
||||
@@ -317,6 +346,10 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
|
||||
if detail.Execution != nil {
|
||||
execution = executionResponse(*detail.Execution)
|
||||
}
|
||||
var executionReport any
|
||||
if detail.Report != nil {
|
||||
executionReport = executionReportResponse(detail.Report)
|
||||
}
|
||||
ctx.Header("Cache-Control", "no-store")
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"id": detail.Task.ID,
|
||||
@@ -337,6 +370,7 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
|
||||
"derived_requirement": nil,
|
||||
"claim": claim,
|
||||
"execution": execution,
|
||||
"execution_report": executionReport,
|
||||
"events": events,
|
||||
"assets": []gin.H{
|
||||
assetResponse(detail.Asset),
|
||||
@@ -344,6 +378,78 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func executionReportResponse(report *domain.ExecutionReport) gin.H {
|
||||
events := make([]gin.H, 0, len(report.Events))
|
||||
for _, event := range report.Events {
|
||||
events = append(events, gin.H{
|
||||
"id": event.ID,
|
||||
"step": event.Step,
|
||||
"type": event.Type,
|
||||
"message": event.Message,
|
||||
"occurred_at": formatTime(event.OccurredAt),
|
||||
"received_at": formatTime(event.ReceivedAt),
|
||||
"received_after_execution_expiry": event.ReceivedAfterExecutionExpiry,
|
||||
})
|
||||
}
|
||||
evidence := make([]gin.H, 0, len(report.EvidenceAssets))
|
||||
for _, asset := range report.EvidenceAssets {
|
||||
evidence = append(evidence, gin.H{
|
||||
"id": asset.ID,
|
||||
"media_type": asset.MediaType,
|
||||
"size_bytes": asset.SizeBytes,
|
||||
"sha256": asset.SHA256,
|
||||
"created_at": formatTime(asset.CreatedAt),
|
||||
"received_after_execution_expiry": asset.ReceivedAfterExecutionExpiry,
|
||||
})
|
||||
}
|
||||
response := gin.H{
|
||||
"events": events,
|
||||
"evidence": evidence,
|
||||
}
|
||||
if batch := report.CandidateBatch; batch != nil {
|
||||
response["candidate_batch"] = gin.H{
|
||||
"task_content_sha256": batch.TaskContentSHA256,
|
||||
"execution_mode": batch.ExecutionMode,
|
||||
"search_query": batch.SearchQuery,
|
||||
"provenance": decodedAuditJSON(batch.ProvenanceJSON),
|
||||
"candidates": decodedAuditJSON(&batch.CandidatesJSON),
|
||||
"recommendation": decodedAuditJSON(batch.RecommendationJSON),
|
||||
"received_at": formatTime(batch.ReceivedAt),
|
||||
"received_after_execution_expiry": batch.ReceivedAfterExecutionExpiry,
|
||||
}
|
||||
}
|
||||
if outcome := report.Outcome; outcome != nil {
|
||||
response["outcome"] = gin.H{
|
||||
"result_type": outcome.ResultType,
|
||||
"execution_mode": outcome.ExecutionMode,
|
||||
"task_content_sha256": outcome.TaskContentSHA256,
|
||||
"outcome": outcome.Outcome,
|
||||
"operator_reason": outcome.OperatorReason,
|
||||
"selected_candidate": decodedAuditJSON(outcome.SelectedCandidateJSON),
|
||||
"evidence_asset_ids": decodedAuditJSON(outcome.EvidenceAssetIDsJSON),
|
||||
"error_code": outcome.ErrorCode,
|
||||
"error_message": outcome.ErrorMessage,
|
||||
"error_step": outcome.ErrorStep,
|
||||
"retryable": outcome.Retryable,
|
||||
"order_submitted": outcome.OrderSubmitted,
|
||||
"received_at": formatTime(outcome.ReceivedAt),
|
||||
"received_after_execution_expiry": outcome.ReceivedAfterExecutionExpiry,
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func decodedAuditJSON(value *string) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
var decoded any
|
||||
if err := json.Unmarshal([]byte(*value), &decoded); err != nil {
|
||||
return nil
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
func (h *adminHandlers) cancelTask(ctx *gin.Context) {
|
||||
if !hasMediaType(ctx, "application/json") {
|
||||
writePublicError(
|
||||
|
||||
Reference in New Issue
Block a user