Initial commit
This commit is contained in:
338
app/Http/Controllers/EvaluationRunController.php
Normal file
338
app/Http/Controllers/EvaluationRunController.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user