ajoute les alertes Prometheus du self-healing et installe Larastan (dette de l'étape 8, 72 erreurs corrigées à la racine)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-12 21:32:52 +02:00
parent f2649d0f8b
commit d47e8356fc
23 changed files with 340 additions and 22 deletions

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\Eleve;
use App\Models\Seance;
use App\Models\User;
use App\Services\Visio\JetonVisio;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
@ -177,7 +178,7 @@ class VisioController extends Controller
// ---- Interne ----------------------------------------------------------
private function verifierProf(Seance $seance)
private function verifierProf(Seance $seance): User
{
$prof = Auth::guard('web')->user();
abort_if($seance->classe->user_id !== $prof->id, 403);
@ -217,6 +218,9 @@ class VisioController extends Controller
/**
* Lecture-modification-écriture sous verrou : deux élèves qui lèvent la
* main au même instant ne s'écrasent pas.
*
* @param callable(array{mains: array<int, array{nom: string, depuis: int}>, participants: array<int, string>, presences: array<int, array{nom: string, vu: int}>}): array{mains: array<int, array{nom: string, depuis: int}>, participants: array<int, string>, presences: array<int, array{nom: string, vu: int}>} $changement
* @return array{mains: array<int, array{nom: string, depuis: int}>, participants: array<int, string>, presences: array<int, array{nom: string, vu: int}>}
*/
private function modifier(Seance $seance, callable $changement): array
{

View file

@ -55,7 +55,9 @@ class CreerStackLab implements ShouldQueue
$stack = $fabrique->fabriquer(
DefinitionLab::depuis($session->lab->identifiant),
"{$base}-e{$this->attempts()}",
fn (string $nom, string $id) => $session->forceFill(['heat_stack_name' => $nom, 'heat_stack_id' => $id])->save(),
function (string $nom, string $id) use ($session): void {
$session->forceFill(['heat_stack_name' => $nom, 'heat_stack_id' => $id])->save();
},
);
if ($session->fresh()->etat !== LabSession::ADMIS) {

View file

@ -2,15 +2,17 @@
namespace App\Models;
use Carbon\Carbon;
use Database\Factories\ClasseFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
class Classe extends Model
{
/** @use HasFactory<ClasseFactory> */
use HasFactory;
protected $fillable = ['etablissement_id', 'user_id', 'nom', 'formation', 'code_classe'];
@ -35,26 +37,31 @@ class Classe extends Model
return $code;
}
/** @return BelongsTo<Etablissement, $this> */
public function etablissement(): BelongsTo
{
return $this->belongsTo(Etablissement::class);
}
/** @return BelongsTo<User, $this> */
public function prof(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/** @return HasMany<Eleve, $this> */
public function eleves(): HasMany
{
return $this->hasMany(Eleve::class);
}
/** @return HasMany<DemandeInscription, $this> */
public function demandesInscription(): HasMany
{
return $this->hasMany(DemandeInscription::class);
}
/** @return HasMany<Seance, $this> */
public function seances(): HasMany
{
return $this->hasMany(Seance::class);

View file

@ -19,6 +19,7 @@ class Correction extends Model
'details' => 'array',
];
/** @return BelongsTo<LabSession, $this> */
public function labSession(): BelongsTo
{
return $this->belongsTo(LabSession::class);

View file

@ -30,11 +30,13 @@ class DemandeInscription extends Model
'code_secret_chiffre' => 'encrypted',
];
/** @return BelongsTo<Classe, $this> */
public function classe(): BelongsTo
{
return $this->belongsTo(Classe::class);
}
/** @return BelongsTo<Eleve, $this> */
public function eleve(): BelongsTo
{
return $this->belongsTo(Eleve::class);
@ -45,6 +47,7 @@ class DemandeInscription extends Model
return hash('sha256', $jeton);
}
/** @return Builder<self> */
public function prunable(): Builder
{
return static::where('created_at', '<', now()->subHour());

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Models\Concerns\GereSessionUnique;
use Database\Factories\EleveFactory;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -12,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Eleve extends Model implements AuthenticatableContract
{
/** @use HasFactory<EleveFactory> */
use Authenticatable, GereSessionUnique, HasFactory;
protected $fillable = ['classe_id', 'prenom', 'nom', 'identifiant', 'code_secret', 'jeton_qr'];
@ -27,11 +29,13 @@ class Eleve extends Model implements AuthenticatableContract
return 'code_secret';
}
/** @return BelongsTo<Classe, $this> */
public function classe(): BelongsTo
{
return $this->belongsTo(Classe::class);
}
/** @return HasMany<LabSession, $this> */
public function labSessions(): HasMany
{
return $this->hasMany(LabSession::class);

View file

@ -2,21 +2,25 @@
namespace App\Models;
use Database\Factories\EtablissementFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Etablissement extends Model
{
/** @use HasFactory<EtablissementFactory> */
use HasFactory;
protected $fillable = ['nom', 'ville', 'uai'];
/** @return HasMany<User, $this> */
public function profs(): HasMany
{
return $this->hasMany(User::class);
}
/** @return HasMany<Classe, $this> */
public function classes(): HasMany
{
return $this->hasMany(Classe::class);

View file

@ -2,16 +2,19 @@
namespace App\Models;
use Database\Factories\LabFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Lab extends Model
{
/** @use HasFactory<LabFactory> */
use HasFactory;
protected $fillable = ['identifiant', 'titre', 'niveau', 'duree_min'];
/** @return HasMany<Seance, $this> */
public function seances(): HasMany
{
return $this->hasMany(Seance::class);

View file

@ -2,7 +2,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -11,8 +10,6 @@ use Illuminate\Support\Facades\DB;
class LabSession extends Model
{
use HasFactory;
public const EN_ATTENTE = 'en_attente';
// Admis par le prof : CreerStackLab est dispatché, la stack Heat se construit.
@ -35,21 +32,25 @@ class LabSession extends Model
'expire_a' => 'datetime',
];
/** @return BelongsTo<Seance, $this> */
public function seance(): BelongsTo
{
return $this->belongsTo(Seance::class);
}
/** @return BelongsTo<Eleve, $this> */
public function eleve(): BelongsTo
{
return $this->belongsTo(Eleve::class);
}
/** @return BelongsTo<Lab, $this> */
public function lab(): BelongsTo
{
return $this->belongsTo(Lab::class);
}
/** @return HasMany<LabSessionMachine, $this> */
public function machines(): HasMany
{
return $this->hasMany(LabSessionMachine::class);
@ -79,11 +80,13 @@ class LabSession extends Model
});
}
/** @return HasMany<Correction, $this> */
public function corrections(): HasMany
{
return $this->hasMany(Correction::class);
}
/** @return HasOne<Correction, $this> */
public function derniereCorrection(): HasOne
{
return $this->hasOne(Correction::class)->latestOfMany();

View file

@ -2,14 +2,11 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LabSessionMachine extends Model
{
use HasFactory;
// Self-healing (SurveillanceVm) : la VM a été relancée automatiquement,
// ou ne répond plus malgré les relances.
public const RELANCEE = 'relancee';
@ -22,6 +19,7 @@ class LabSessionMachine extends Model
'sante_a' => 'datetime',
];
/** @return BelongsTo<LabSession, $this> */
public function labSession(): BelongsTo
{
return $this->belongsTo(LabSession::class);

View file

@ -2,15 +2,16 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property-read int|null $servies_count Nombre de LabSession déjà admises/actives/terminées/expirées, uniquement
* quand chargé par `withCount('labSessions as servies_count', ...)` (StockChaud::cible()).
*/
class Seance extends Model
{
use HasFactory;
public const PLANIFIEE = 'planifiee';
public const SALLE_ATTENTE = 'salle_attente';
@ -34,16 +35,19 @@ class Seance extends Model
return $this->visio_ouverte_a !== null && $this->visio_salle !== null;
}
/** @return BelongsTo<Classe, $this> */
public function classe(): BelongsTo
{
return $this->belongsTo(Classe::class);
}
/** @return BelongsTo<Lab, $this> */
public function lab(): BelongsTo
{
return $this->belongsTo(Lab::class);
}
/** @return HasMany<LabSession, $this> */
public function labSessions(): HasMany
{
return $this->hasMany(LabSession::class);

View file

@ -33,11 +33,13 @@ class User extends Authenticatable
];
}
/** @return BelongsTo<Etablissement, $this> */
public function etablissement(): BelongsTo
{
return $this->belongsTo(Etablissement::class);
}
/** @return HasMany<Classe, $this> */
public function classes(): HasMany
{
return $this->hasMany(Classe::class);

View file

@ -62,6 +62,9 @@ class DefinitionLab
return count($this->machines);
}
/**
* @param array<string, mixed> $donnees
*/
private function __construct(array $donnees)
{
$this->identifiant = $donnees['id'];

View file

@ -66,7 +66,14 @@ class StockChaud
LUA;
for ($essai = 0; $essai < 3; $essai++) {
$brut = Redis::eval($script, 2, self::clePretes($lab), self::ATTRIBUEES, time());
// Redis::eval(...) (facade) : la classe réellement utilisée au
// runtime (PhpRedisConnection) redispose les arguments pour
// l'extension phpredis, mais PHPStan ne résout le mixin de la
// façade Redis que sur la classe abstraite Connection (@mixin
// \Redis, la signature native à 3 arguments) — jamais sur la
// classe effective choisie par REDIS_CLIENT. `command()` est une
// vraie méthode (pas un mixin) : même effet, correctement typé.
$brut = Redis::command('eval', [$script, [self::clePretes($lab), self::ATTRIBUEES, time()], 2]);
if (! is_string($brut)) {
return null;
@ -135,7 +142,10 @@ class StockChaud
*/
public function retirer(string $lab, string $brut): bool
{
if ((int) Redis::lrem(self::clePretes($lab), 1, $brut) === 0) {
// Même raison que dans prendre() : command() plutôt que le mixin de
// la façade, qui ne voit pas le réordonnancement des arguments fait
// par PhpRedisConnection::lrem().
if ((int) Redis::command('lrem', [self::clePretes($lab), $brut, 1]) === 0) {
return false;
}
@ -373,11 +383,18 @@ class StockChaud
}
/**
* @param array{nom: string, id: string}|null $stack
* $stack vient d'un `json_decode` (Redis) : sa forme n'est pas garantie
* par le type (contrairement à ce que laissait croire l'ancienne
* signature `array{nom: string, id: string}`, qui rendait les `isset()`
* ci-dessous inutiles aux yeux de PHPStan alors qu'ils sont le seul
* rempart contre une entrée du stock corrompue ou tronquée).
*
* @param array<string, mixed>|null $stack
*/
private function estUtilisable(?array $stack): bool
{
if ($stack === null || ! isset($stack['nom'], $stack['id'], $stack['instances'])) {
if ($stack === null || ! isset($stack['nom'], $stack['id'], $stack['instances'])
|| ! is_string($stack['nom']) || ! is_string($stack['id'])) {
return false;
}

View file

@ -17,7 +17,7 @@ use App\Services\Labs\DefinitionLab;
class GenerateurTemplateHeat
{
/**
* @return array{template: array, noms_machines: array<int, string>}
* @return array{template: array<string, mixed>, noms_machines: array<int, string>}
*/
public function genererDepuis(DefinitionLab $lab, string $prefixe): array
{

View file

@ -2,6 +2,7 @@
namespace App\Services\OpenStack;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use RuntimeException;
@ -15,6 +16,7 @@ class HeatClient
public function __construct(private readonly KeystoneAuth $auth) {}
/**
* @param array<string, mixed> $template
* @return array{id: string}
*/
public function creerStack(string $nom, array $template): array
@ -86,7 +88,7 @@ class HeatClient
}
}
private function requete()
private function requete(): PendingRequest
{
return Http::withHeaders(['X-Auth-Token' => $this->auth->jeton()]);
}