Files
vkv/app/Http/Controllers/EvaluationRunController.php
Zdeněk Burda 41e3ce6f25 Initial commit
2026-01-09 21:26:40 +01:00

339 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers;
use App\Models\EvaluationRun;
use App\Models\EvaluationLock;
use App\Models\EvaluationRunEvent;
use App\Services\Evaluation\EvaluationCoordinator;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Bus;
/**
* Controller: EvaluationRunController
*
* Účel:
* - HTTP API vrstva pro práci s vyhodnocovacími běhy (EvaluationRun).
* - Slouží pro:
* - monitoring průběhu
* - zobrazení detailu běhu
* - ruční řízení (resume/cancel) a CRUD nad záznamem běhu
*
* Kontext v architektuře:
* - Controller je tenká vrstva mezi frontendem a aplikační logikou.
* - Neobsahuje byznys logiku vyhodnocení.
* - Orchestrace a výpočty jsou delegovány na background joby
* (PrepareRunJob, ParseLogJob, …, FinalizeRunJob) a na RoundController,
* který spouští celý pipeline.
*
* Typické endpointy (konceptuálně):
* - POST /api/rounds/{round}/evaluation-runs/start
* → spustí vyhodnocovací pipeline (RoundController)
* - GET /api/evaluation-runs
* → vrátí seznam běžících / dokončených běhů (monitoring)
* - GET /api/evaluation-runs/{id}
* → detail konkrétního běhu (stav, progress, kroky, chyby)
* - POST /api/evaluation-runs/{id}/cancel
* → (volitelně) požádá o zrušení běhu
* - POST /api/evaluation-runs/{id}/resume
* → pokračuje v pipeline po manuální kontrole
*
* Odpovědnosti controlleru:
* - validace vstupních dat (request objekty)
* - autorizace přístupu (Policies / Gates)
* - vytvoření EvaluationRun záznamu
* - nespouští pipeline přímo (to dělá RoundController)
* - serializace odpovědí do JSON (DTO / Resource)
*
* Co controller NEDĚLÁ:
* - neparsuje EDI logy
* - neprovádí matching ani scoring
* - nečeká synchronně na dokončení vyhodnocení
*
* Zásady návrhu:
* - Všechny operace musí být rychlé (non-blocking).
* - Spuštění vyhodnocení je vždy asynchronní.
* - Stav běhu je čitelný pouze z EvaluationRun + souvisejících entit.
*/
class EvaluationRunController extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
public function __construct()
{
// zápisové operace jen pro přihlášené
$this->middleware('auth:sanctum')->only(['store', 'update', 'destroy']);
}
/**
* Seznam evaluation runů filtrování podle round_id, is_official.
*/
public function index(Request $request): JsonResponse
{
$perPage = (int) $request->get('per_page', 100);
// Seznam běhů slouží hlavně pro monitoring na RoundDetailPage.
$query = EvaluationRun::query()
->with(['round']);
if ($request->filled('round_id')) {
$query->where('round_id', (int) $request->get('round_id'));
}
if ($request->filled('is_official')) {
$query->where(
'is_official',
filter_var($request->get('is_official'), FILTER_VALIDATE_BOOL)
);
}
if ($request->filled('result_type')) {
$query->where('result_type', $request->get('result_type'));
}
$items = $query
->orderByDesc('created_at')
->paginate($perPage);
return response()->json($items);
}
/**
* Vytvoření nového evaluation runu.
* Typicky před samotným spuštěním vyhodnocovače.
*/
public function store(Request $request): JsonResponse
{
$this->authorize('create', EvaluationRun::class);
$data = $this->validateData($request);
// Samotné spuštění pipeline zajišťuje StartEvaluationRunJob.
$run = EvaluationRun::create($data);
$run->load(['round']);
return response()->json($run, 201);
}
/**
* Detail jednoho evaluation runu včetně vazeb a výsledků.
*/
public function show(EvaluationRun $evaluationRun): JsonResponse
{
// Detail běhu včetně výsledků je náročný používej s rozumem (paging/limit).
$evaluationRun->load([
'round',
'logResults',
'qsoResults',
]);
return response()->json($evaluationRun);
}
/**
* Aktualizace evaluation runu (např. změna názvu, poznámky,
* příznaku is_official).
*/
public function update(Request $request, EvaluationRun $evaluationRun): JsonResponse
{
$this->authorize('update', $evaluationRun);
$data = $this->validateData($request, partial: true);
$evaluationRun->fill($data);
$evaluationRun->save();
$evaluationRun->load(['round']);
return response()->json($evaluationRun);
}
/**
* Označí běh jako TEST/PRELIMINARY/FINAL a aktualizuje ukazatele v kole.
*/
public function setResultType(Request $request, EvaluationRun $evaluationRun): JsonResponse
{
$this->authorize('update', $evaluationRun);
$data = $request->validate([
'result_type' => ['required', 'string', 'in:TEST,PRELIMINARY,FINAL'],
]);
$resultType = $data['result_type'];
$evaluationRun->update([
'result_type' => $resultType,
'is_official' => $resultType === 'FINAL',
]);
$round = $evaluationRun->round;
if ($round) {
if ($resultType === 'FINAL') {
$round->official_evaluation_run_id = $evaluationRun->id;
$round->preliminary_evaluation_run_id = null;
$round->test_evaluation_run_id = null;
} elseif ($resultType === 'PRELIMINARY') {
$round->preliminary_evaluation_run_id = $evaluationRun->id;
$round->official_evaluation_run_id = null;
$round->test_evaluation_run_id = null;
} elseif ($resultType === 'TEST') {
$round->test_evaluation_run_id = $evaluationRun->id;
$round->official_evaluation_run_id = null;
$round->preliminary_evaluation_run_id = null;
}
$round->save();
}
$evaluationRun->load(['round']);
return response()->json($evaluationRun);
}
/**
* Smazání evaluation runu (včetně log_results / qso_results přes FK).
*/
public function destroy(EvaluationRun $evaluationRun): JsonResponse
{
$this->authorize('delete', $evaluationRun);
$evaluationRun->delete();
return response()->json(null, 204);
}
/**
* Zruší běh vyhodnocení (pokud je stále aktivní).
*/
public function cancel(EvaluationRun $evaluationRun): JsonResponse
{
$this->authorize('update', $evaluationRun);
// Cancel je povolený jen pro běhy, které ještě neskončily.
$activeStatuses = ['PENDING', 'RUNNING', 'WAITING_REVIEW_INPUT', 'WAITING_REVIEW_MATCH', 'WAITING_REVIEW_SCORE'];
if (! in_array($evaluationRun->status, $activeStatuses, true)) {
return response()->json([
'message' => 'Běh nelze zrušit v aktuálním stavu.',
], 409);
}
$evaluationRun->update([
'status' => 'CANCELED',
'finished_at' => now(),
]);
if ($evaluationRun->batch_id) {
$batch = Bus::findBatch($evaluationRun->batch_id);
if ($batch) {
$batch->cancel();
}
}
EvaluationRunEvent::create([
'evaluation_run_id' => $evaluationRun->id,
'level' => 'warning',
'message' => 'Vyhodnocení bylo zrušeno uživatelem.',
'context' => [
'step' => 'cancel',
'round_id' => $evaluationRun->round_id,
'user_id' => auth()->id(),
],
]);
// Uvolní lock, aby mohl běh navázat nebo se spustit nový.
EvaluationLock::where('evaluation_run_id', $evaluationRun->id)->delete();
return response()->json([
'status' => 'canceled',
], 200);
}
/**
* Vrátí poslední události běhu vyhodnocení.
*/
public function events(EvaluationRun $evaluationRun, Request $request): JsonResponse
{
$this->authorize('update', $evaluationRun);
$limit = (int) $request->get('limit', 10);
if ($limit < 1) {
$limit = 1;
} elseif ($limit > 100) {
$limit = 100;
}
$minLevel = $request->get('min_level');
$levels = ['debug', 'info', 'warning', 'error'];
if (! in_array($minLevel, $levels, true)) {
$minLevel = null;
}
$events = $evaluationRun->events()
->when($minLevel, function ($query) use ($minLevel, $levels) {
$query->whereIn('level', array_slice($levels, array_search($minLevel, $levels, true)));
})
->orderByDesc('id')
->limit($limit)
->get();
return response()->json($events);
}
/**
* Pokračuje v běhu vyhodnocení po manuální kontrole.
*/
public function resume(Request $request, EvaluationRun $evaluationRun): JsonResponse
{
$this->authorize('update', $evaluationRun);
if ($evaluationRun->isCanceled()) {
return response()->json([
'message' => 'Běh byl zrušen.',
], 409);
}
$ok = app(EvaluationCoordinator::class)->resume($evaluationRun, [
'rebuild_working_set' => $request->boolean('rebuild_working_set'),
]);
if ($ok) {
return response()->json([
'status' => 'queued',
], 202);
}
return response()->json([
'message' => 'Běh není ve stavu čekání na kontrolu.',
], 409);
}
/**
* Validace vstupů pro store / update.
*/
protected function validateData(Request $request, bool $partial = false): array
{
$required = $partial ? 'sometimes' : 'required';
return $request->validate([
'round_id' => [$required, 'integer', 'exists:rounds,id'],
'rule_set_id' => ['sometimes', 'nullable', 'integer', 'exists:evaluation_rule_sets,id'],
'name' => ['sometimes', 'nullable', 'string', 'max:100'],
'rules_version' => ['sometimes', 'nullable', 'string', 'in:OFFICIAL,TEST,CLAIMED'],
'result_type' => ['sometimes', 'nullable', 'string', 'in:PRELIMINARY,FINAL,TEST'],
'is_official' => ['sometimes', 'boolean'],
'notes' => ['sometimes', 'nullable', 'string'],
'scope' => ['sometimes', 'array'],
'scope.band_ids' => ['sometimes', 'array'],
'scope.band_ids.*' => ['integer', 'exists:bands,id'],
'scope.category_ids' => ['sometimes', 'array'],
'scope.category_ids.*' => ['integer', 'exists:categories,id'],
'scope.power_category_ids' => ['sometimes', 'array'],
'scope.power_category_ids.*' => ['integer', 'exists:power_categories,id'],
]);
}
}