corrige le code de classe (caractères ambigus) et fige l'image Uptime Kuma par digest

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-12 21:50:53 +02:00
parent 97c04f1ad6
commit f899aac48a
11 changed files with 92 additions and 15 deletions

View file

@ -2,4 +2,8 @@
supervision_fichier_webhook: /root/.alertes-webhook
supervision_port_relais: 9099
supervision_port_uptime_kuma: 3001
# Figée par un digest, pas par le tag flottant ":1" (dette notée le
# 2026-10-09) : 1.23.17, telle que déployée le 2026-10-09. Procédure de
# mise à jour : docs/runbooks/supervision.md.
supervision_image_uptime_kuma: "louislam/uptime-kuma@sha256:70233f4acb5163fd2a59a49909cf01e44415cecff13dba69e3a86647a2919f83"
supervision_dossier_textfile: /var/lib/node_exporter/textfile

View file

@ -91,7 +91,7 @@
- name: déployer Uptime Kuma
community.docker.docker_container:
name: uptime-kuma
image: louislam/uptime-kuma:1
image: "{{ supervision_image_uptime_kuma }}"
state: started
restart_policy: unless-stopped
network_mode: host

View file

@ -5,7 +5,7 @@ retirer la ligne (et le noter dans le journal) quand c'est fait.
| Sujet | Pourquoi c'est une dette | Quand | Origine |
|---|---|---|---|
| **Image Uptime Kuma non figée** (`louislam/uptime-kuma:1`) | Une version différente peut arriver à n'importe quel redéploiement, sans test. Figer un tag précis (voire le digest) dans une variable du rôle `supervision`, et documenter la mise à jour par Ansible (changer la variable, relancer le rôle, vérifier) — jamais par le bouton de l'interface. Kuma lui-même qualifie ce tag d'« historique » (il injecte `docker-latest-warning.js` dans les images `latest`/`1`, constaté le 2026-10-09) : prévoir la migration vers un tag versionné, en testant la page de statut et les sondes. | Avant le pilote | Demande d'Adam, 2026-10-09 |
| Image Uptime Kuma encore « historique » (même contenu que `:1`, désormais figé par digest — 2026-10-12) | Le digest figé est celui publié sous le tag flottant `:1` (1.23.17), qui injecte encore `docker-latest-warning.js` (sans impact : visible seulement sur `/setup`, jamais atteint). Migrer vers le build du tag versionné `1.23.17` (digest différent, probablement une image reconstruite sans ce script) demande de re-pull et de revérifier la page de statut et les sondes — pas fait ici. | Avant le pilote | Demande d'Adam, 2026-10-09 ; réduite le 2026-10-12 |
| Projet `paillasse`, compte `paillasse-service` et son rôle créés à la main (étape 6) | Pas rejouables par Ansible (gabarit et quota le sont depuis l'étape 13, rôle `openstack-projet`). | Bloc B | Étape 6 |
| Clé age des sauvegardes : copie hors du serveur non confirmée | Sans elle, aucune sauvegarde n'est déchiffrable si le serveur est perdu (voir `docs/runbooks/sauvegardes.md`). Adam doit confirmer l'avoir mise en lieu sûr. | Dès que possible | Étape 3 |
| Vitrine : Tailwind par CDN d'exécution (`cdn.tailwindcss.com`, 19 pages) | Non destiné à la production selon Tailwind, lent, et oblige la vitrine à tolérer les scripts en ligne dans sa CSP. Compiler ces styles avec Vite. | Avant ouverture publique | Étape 12 (design) |

View file

@ -773,3 +773,24 @@ Prochaines étapes du bloc A : TURN 443 pour la visio, sauvegarde OpenStack (fai
**Comprendre**
La plupart des 72 erreurs venaient d'une seule cause répétée dix fois (des relations Eloquent sans generics), pas de dix bugs différents — las de ça, régler la cause réelle (les generics) a fait disparaître les trois quarts du travail d'un coup. Les erreurs qui restaient ensuite étaient plus intéressantes : deux ne venaient pas du code de Syllabus mais d'une imprécision des annotations de Laravel lui-même sur sa façade Redis, et une révélait un vrai défaut (une signature de type qui en disait moins que ce que le code vérifiait vraiment). La règle du prompt (« ne jamais juste faire taire l'outil ») a forcé à distinguer ces trois cas plutôt qu'à tout traiter de la même façon.
## 2026-10-12 — Deux dettes (en attendant le DNS du TURN) : code de classe et image Uptime Kuma
**Code de classe sans caractère ambigu**
- `App\Support\CodeLisible` : alphabet sans 0/O ni 1/I/L (`ABCDEFGHJKMNPQRSTUVWXYZ23456789`), repris de `GenerateurCompteEleve::genererCodeSecret` (qui l'avait en privé) pour que `Classe::genererCodeUnique` l'utilise aussi — jusqu'ici il tirait dans `Str::random(6)` (alphabet complet, ambigu). Scripts de test réels (`tests/Reels/preparer-*.php`) alignés. 1 test Pest (55 au total) : 100 codes de classe générés, tous conformes à l'alphabet.
- Dette retirée.
**Image Uptime Kuma figée par digest**
- `louislam/uptime-kuma:1` est un tag flottant : son contenu peut changer sans préavis à n'importe quel redéploiement. Vérifié sur l'API Docker Hub que le digest du tag `:1` actuellement déployé (`sha256:70233f4a...`, version 1.23.17) correspond bien à ce qui tourne sur le serveur (`docker inspect` → même `RepoDigests`) : figé tel quel dans `supervision_image_uptime_kuma` (`ansible/roles/supervision/defaults/main.yml`), au lieu du tag.
- **Nuance pour la suite** : ce digest est celui du tag `:1`, pas celui du tag versionné `1.23.17` (une image différente, reconstruite séparément — vérifié : digests différents). La vraie « migration vers un tag versionné » demandée par Adam demande donc un second pas (re-pull, revérifier la page de statut et les sondes) : dette réduite, pas fermée.
- Déployé (`supervision.yml`, `changed=1`) et vérifié : même version (1.23.17), même base de données (`kuma.db`, sondes conservées), toujours sur `127.0.0.1:3001` seulement. Procédure de mise à jour future écrite dans `docs/runbooks/supervision.md`.
**Comprendre**
Les deux corrections de ce tour-ci illustrent une même idée : une dette notée n'est réglée que quand on peut dire exactement ce qui a changé et le prouver. Pour le code de classe, prouver, c'est un test qui génère une centaine de codes et vérifie qu'aucun caractère ambigu n'y figure jamais — pas seulement relire le code une fois. Pour Uptime Kuma, prouver, c'est avoir comparé le digest réellement déployé à celui que l'API de Docker Hub donne pour le tag flottant, plutôt que de supposer qu'ils coïncident : une fois comparés, il est apparu qu'ils coïncidaient aujourd'hui, mais que « corriger » et « migrer vers une version propre » sont deux choses différentes — la première protège contre les surprises futures, la seconde reste à faire.
**Suite**
Toujours en attente du DNS `turn.syllabuslab.fr` (ADR 0006) pour le relais TURN de la visio.

View file

@ -115,3 +115,28 @@ ssh paillasse "sudo -i bash -c 'export PATH=/opt/kolla-venv/bin:\$PATH; kolla-an
`kolla.yml`/`supervision.yml` pour le copier sur le serveur, **puis**
`kolla-ansible reconfigure`. Oublier la première étape fait tourner
`reconfigure` sur l'ancienne config, sans erreur visible.
## Mettre à jour l'image Uptime Kuma
Figée par **digest**, pas par le tag flottant `louislam/uptime-kuma:1` (qui
peut changer de contenu à tout moment, sans avertissement ni changelog
attaché au tag) : `supervision_image_uptime_kuma`
(`ansible/roles/supervision/defaults/main.yml`). Version actuelle : 1.23.17.
1. Choisir la version visée sur <https://hub.docker.com/r/louislam/uptime-kuma/tags>
(préférer un tag de version exacte, ex. `1.23.18`, jamais `1` ou `latest`)
et lire les notes de version <https://github.com/louislam/uptime-kuma/releases>.
2. Trouver son digest exact (pas celui d'un tag flottant) :
```
curl -s "https://hub.docker.com/v2/namespaces/louislam/repositories/uptime-kuma/tags/1.23.18" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['digest'])"
```
3. Mettre à jour `supervision_image_uptime_kuma` dans `defaults/main.yml`
(`louislam/uptime-kuma@sha256:...`) et le commentaire de version.
4. `cd ansible && script -q /dev/null ansible-playbook supervision.yml -i inventory.ini`
(tire la nouvelle image, recrée le conteneur ; le volume `uptime-kuma-data`
est conservé, rien n'est perdu).
5. Vérifier : `ss -tlnp | grep 3001` (toujours 127.0.0.1 seulement), rouvrir
le tunnel SSH, se connecter, vérifier que les sondes existantes sont
toujours là et actives. Rejouer `visio-e2e.mjs`/`parcours-lab.py` n'est
pas nécessaire (Kuma ne fait pas partie du chemin critique d'un lab).

View file

@ -2,13 +2,13 @@
namespace App\Models;
use App\Support\CodeLisible;
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\Str;
class Classe extends Model
{
@ -31,7 +31,7 @@ class Classe extends Model
public static function genererCodeUnique(): string
{
do {
$code = Str::upper(Str::random(6));
$code = CodeLisible::genere(6);
} while (static::where('code_classe', $code)->exists());
return $code;

View file

@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Classe;
use App\Models\Eleve;
use App\Support\CodeLisible;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
@ -13,9 +14,6 @@ use Illuminate\Support\Str;
*/
class GenerateurCompteEleve
{
// Sans 0/O ni 1/I/L, pour qu'un élève ne confonde pas à la lecture.
private const ALPHABET_CODE = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
public function genererIdentifiant(string $prenom, ?string $nom): string
{
$base = Str::slug($prenom, '');
@ -35,12 +33,7 @@ class GenerateurCompteEleve
public function genererCodeSecret(): string
{
$code = '';
for ($i = 0; $i < 6; $i++) {
$code .= self::ALPHABET_CODE[random_int(0, strlen(self::ALPHABET_CODE) - 1)];
}
return $code;
return CodeLisible::genere(6);
}
/**

View file

@ -0,0 +1,26 @@
<?php
namespace App\Support;
/**
* Codes courts qu'un élève doit pouvoir lire et retaper sans erreur :
* sans 0/O ni 1/I/L, qui se confondent facilement à l'écran comme à voix
* haute. Utilisé pour le code secret de l'élève (GenerateurCompteEleve) et,
* depuis le 2026-10-12, pour le code de classe (Classe) — qui utilisait
* jusque-là un alphabet différent et ambigu (dette notée le 2026-10-09).
*/
final class CodeLisible
{
public const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
public static function genere(int $longueur): string
{
$code = '';
for ($i = 0; $i < $longueur; $i++) {
$code .= self::ALPHABET[random_int(0, strlen(self::ALPHABET) - 1)];
}
return $code;
}
}

View file

@ -65,3 +65,9 @@ test("l'inscription échoue si les mots de passe ne correspondent pas", function
expect(User::count())->toBe(0);
});
test('le code de classe ne contient aucun caractère ambigu (0/O, 1/I/L)', function () {
$codes = collect(range(1, 100))->map(fn () => Classe::genererCodeUnique());
expect($codes->every(fn ($code) => preg_match('/^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{6}$/', $code) === 1))->toBeTrue();
});

View file

@ -11,11 +11,12 @@ use App\Models\Etablissement;
use App\Models\Lab;
use App\Models\Seance;
use App\Models\User;
use App\Support\CodeLisible;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
$motDePasse = Str::password(24, symbols: false);
$code = Str::upper(Str::random(6));
$code = CodeLisible::genere(6);
$etablissement = Etablissement::create(['nom' => 'Lycée Test Étape 11', 'ville' => 'Test']);
$prof = User::create(['name' => 'Prof Test 11', 'email' => 'prof11@syllabus.test', 'password' => $motDePasse, 'etablissement_id' => $etablissement->id]);

View file

@ -10,6 +10,7 @@ use App\Models\Etablissement;
use App\Models\Lab;
use App\Models\Seance;
use App\Models\User;
use App\Support\CodeLisible;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
@ -19,7 +20,7 @@ $prof = User::create(['name' => 'Prof Visio', 'email' => 'prof-visio@syllabus.te
$classe = Classe::create(['etablissement_id' => $etablissement->id, 'user_id' => $prof->id, 'nom' => 'Classe Test Visio', 'formation' => 'Test']);
$eleves = [];
foreach ([['Lea', 'Martin', 'lea.visio'], ['Yanis', 'Bernard', 'yanis.visio']] as [$prenom, $nom, $identifiant]) {
$code = Str::upper(Str::random(6));
$code = CodeLisible::genere(6);
Eleve::create(['classe_id' => $classe->id, 'prenom' => $prenom, 'nom' => $nom, 'identifiant' => $identifiant, 'code_secret' => Hash::make($code), 'jeton_qr' => Str::random(32)]);
$eleves[] = ['identifiant' => $identifiant, 'code' => $code];
}