Implémentation locale de développement du 27 octobre 2026, base 1d741f0. Aucun déploiement. Les contrats communs ne sont pas modifiés. Voir la livraison et ses réserves (../ESPACE_ELEVE_DISTRIBUTION_LIVRAISON.md).
Décisions du propriétaire
Dans le chat D, le propriétaire a validé des libellés distincts obligatoires pour les homonymes, puis demandé 9 caractères, lettres majuscules et chiffres. Cela remplace pour D la proposition de 80 bits du dossier initial : 36^9 donne environ 46,5 bits pour un code uniforme. Un code choisi manuellement peut être bien plus prévisible. Aucun changement silencieux des codes importés ; format [A-Z0-9]{9}, doublons refusés. Le coordinateur a reçu ce changement pour revue, sans réécriture des contrats partagés.
Parcours professeur
/outils/distribution-evaluations exige le niveau Professeur (2) ou Administrateur (3). Le niveau ouvre l’outil ; il ne donne jamais accès aux publications d’un autre contexte. Les associations, capacités, documents migrés et demandes à reprendre vivent dans IndexedDB esuong-distribution-v1, table state. La transaction vérifie une version attendue pour refuser l’écrasement par un autre onglet. Une écriture ne réussit qu’à transaction.oncomplete.
- Ajouter des libellés locaux distincts et générer les codes, ou prévisualiser un CSV
nom;codeUTF-8. Le parseur réel du barème traite guillemets, séparateurs et retours ligne. L’application conserve les élèves absents de l’import et refuse un ancien CSV qui remplacerait un code. Renommer conserve learnerId. - Préparer une nouvelle évaluation, ou importer la feuille de barème. La lecture du barème local ne modifie jamais
esuong-bareme-v1.upgradeDocumentassure la migration ; le document v2 est enregistré avant de permettre publication/extraction. Pour réimporter un ancien v1, sélectionner la feuille puis choisir « Rattacher ». Un import explicitement nouveau crée de nouvelles identités ; le titre ne sert jamais de clé. - « Préparer le lien / QR » n’envoie que l’identifiant du contrôle, le contexte opaque et sa capacité. Le serveur n’en conserve qu’un vérificateur SHA-256. Le QR ne contient que l’URL et assessmentId. Exporter la feuille v2 préparée pour travailler sur cette même identité dans le barème. Un CSV v2 désignant un autre contrôle ne peut pas être rattaché silencieusement.
- Compléter les métadonnées, associer chaque ligne à un learnerId, puis enregistrer. La table locale
identitiesconserve reportId pour chaque élève de cette évaluation, même si une nouvelle feuille a d’autres identifiants de ligne. Deux lignes associées au même élève sont refusées.extractReportetvalidateReportsont réutilisés sans recopier les calculs. - Prévisualiser les bilans, choisir explicitement 1 h, 12 h, 1, 3 ou 7 jours et confirmer « Publier ». Aucune durée présélectionnée. Les noms/classes ne sont pas transmis ; un commentaire ou titre libre peut néanmoins contenir un nom. Aucune anonymisation sémantique n’est promise.
- Actualiser l’état, retirer, corriger ou republier explicitement. Les corrections actives gardent la même échéance. Un nouvel envoi après expiration exige les données locales, une durée et une place libre.
Chaque mutation conserve localement sa requête et son operationId avant l’envoi. « Reprendre » renvoie la même demande. Un conflit impose relecture ; « Relire le serveur et abandonner » ne défait pas une action déjà reçue. Le serveur n’accepte pas une révision locale identique avec un contenu différent pendant la disponibilité ; la révision serveur attendue protège aussi les deux navigateurs restaurés depuis la même sauvegarde.
L’expiration incrémente une fois la révision serveur (sans changer les révisions des bilans), invalidant les corrections/retries préparés avant l’échéance. Une correction qui traverse l’échéance pendant le calcul est refusée NOT_AVAILABLE et ses résultats restent purgés : jamais de republication implicite. Avant toute nouvelle publication, l’interface relit le statut puis affiche une confirmation distincte de republication après expiration/retrait. La nouvelle durée ne démarre qu’après cette nouvelle demande, toujours soumise au quota. Une requête déjà en attente doit d’abord être reprise ou explicitement abandonnée après relecture.
Changer un code conserve l’ancien localement, le nouveau et la liste des publications connues restant à vérifier. La propagation est explicite pour chaque publication ; un échec laisse la demande reprenable et ne prétend pas que tous les dépôts ont changé. Les anciens codes restent affichés pour reprise. Un retrait/une expiration constatés libèrent cette attente. L’export de gestion est bloqué tant que la propagation connue est inachevée. Un autre navigateur divergeant ou une publication absente de la sauvegarde locale ne peut pas être découvert automatiquement : aucun annuaire serveur n’est créé.
Réception élève et raccordement C
Route autonome /distribution/recuperer?id=<assessmentId>. Aucun téléchargement à l’ouverture ou à l’aperçu du QR. L’état public se limite à préparé/disponible/expiré/retiré/introuvable et une échéance. Saisie du code, téléchargement d’un seul paquet, validation, aperçu, puis enregistrement explicite via openStudentStore().importReport. Une erreur ne confirme pas l’écriture. Le téléchargement JSON individuel est également disponible. Aucune mémorisation de code élève n’est activée dans cette version.
La page relit également la copie locale de ce contrôle au chargement : elle demeure affichée après retrait/expiration serveur. L’affichage distingue « Absent », « Bilan sans note », « Maximum nul — note sur 20 non définie » et une note réellement égale à zéro. Aucune moyenne ajoutée.
Le lot C possède navigation, scanner et catalogue. Aucun de ses fichiers n’est modifié ici. Carte professeur à ajouter par C : /outils/distribution-evaluations, niveau 2. Le scanner peut ouvrir la route autonome ou appeler POST /api/distribution avec {op:'fetchOwnReport',id,code}, puis utiliser le socle. La réponse contient {packet,contextId,expiresAt} ; contextId est opaque et permet un futur rangement des codes par professeur. Aucun service worker/caching privé ajouté.
Serveur et limites
src/lib/distribution/server.mjs utilise node:sqlite, testé sous Node 24.18.1. Le minimum historique Node 22.12 du package ne suffit pas à garantir cette implémentation : valider le runtime cible. Aucune nouvelle dépendance npm.
L’exploitant doit fournir DISTRIBUTION_DB avec un chemin hors public, Ressources, contenus et sauvegardes ordinaires, sur disque local. Sans configuration, l’API répond STORAGE_UNAVAILABLE. Répertoire créé en 0700, fichier en 0600 sous les systèmes qui appliquent ces modes ; vérifier les ACL Windows séparément. Aucun secret du serveur n’est inclus dans les sauvegardes professeur.
SQLite BEGIN IMMEDIATE sérialise révisions, retrait, rotation et quota, y compris entre connexions concurrentes ; pas de WAL ni d’historique de résultats, journal DELETE et secure_delete ON. Deux colonnes indexées state/expiresAt évitent de relire tous les paquets pour compter les places ou sélectionner les expirations. L’ancienne base du prototype est migrée additivement sous transaction, après recontrôle du schéma dans le verrou ; le JSON et les identités restent inchangés. Migration relançable testée. Le code antérieur à cette migration n’écrit plus cette base à quatre colonnes : ne pas faire un retour arrière en remettant cette base au service de cet ancien code.
Le processus public doit rester unique, conformément au site et aux jeux. Les compteurs anti-abus sont en mémoire, donc remis à zéro au redémarrage et non partagés entre processus.
Le correctif D-05 ajoute une migration interne PRAGMA user_version=1 : les marqueurs déjà expirés de l’ancien prototype avancent une fois leur révision et oublient leur ancien reçu d’opération. Une seconde ouverture ne les incrémente plus. Identités, capacités et échéances sont préservées ; aucun résultat récupéré. Cette migration invalide les anciennes corrections/reprises mises en attente avant la mise à jour. Une simple relecture autorisée fournit la nouvelle révision pour une action explicite. Aucun contrat JSON commun n’est changé ; un retour à l’ancien code réintroduirait le défaut et n’est pas une option sûre pour ce service.
| Limite | Valeur |
|---|---|
| Publications accessibles | 50 globalement, états actifs non expirés |
| Requête JSON | 5 Mio réels, lecture bornée avant parsing |
| Requêtes en cours, corps + traitement | 4 globalement, admission avant lecture ; sinon 429 |
| Lecture du corps | 10 secondes au total ; annulation/déconnexion traitées, 8 192 fragments au plus |
| Calcul scrypt | 2 workers, 2 tâches en attente au plus, 200 codes/tâche ; 30 secondes par tâche active |
| Bilans par publication | 200 ; la limite totale de 5 Mio reste prioritaire |
| Volume de paquets actifs | Au plus environ 250 Mio, hors index/journal temporaire |
| Marqueurs préparés/expirés/retirés | 10 000 ; refus de préparation au-delà |
| Associations locales | 2 000 |
| Publications dans sauvegarde | 1 000 |
| Sauvegarde privée déchiffrée / fichier chiffré | 3 Mio / 7 Mio |
| Requêtes | 600/minute globales, 120/minute par adresse |
| Tentatives de code | 30/minute par adresse+évaluation, 240/minute par évaluation |
Les seuils sont des paramètres de prototype à mesurer derrière une adresse de classe partagée. Les vérificateurs élèves utilisent scrypt N=16384, r=8, p=1, 32 octets, sel aléatoire de 16 octets par publication. Une recherche ne dérive qu’un vérificateur et ne compare que les bilans du contrôle demandé. L’API emploie executeAsync : scrypt tourne dans un pool borné de workers, sans prendre de verrou SQLite pendant le calcul. Le worker n’exécute qu’un programme fixe avec des paramètres fixes, jamais du code reçu. Les capacités, révisions, quota, état et codes sont relus après le calcul : un retrait/une rotation concurrente ne peuvent pas être contournés par un ancien instantané. Le cœur synchrone reste réservé aux fixtures/CLI ; ne pas l’appeler depuis une route.
Admission HTTP avant lecture : 4 requêtes globales, même sans Content-Length, avec compteurs 600/minute globaux et 120/minute par adresse. Les places sont libérées en finally, même si l’annulation du flux ne termine pas. Une lecture lente est interrompue après 10 secondes ; une erreur de longueur/décodage est refusée. Le worker admet deux tâches actives et deux en attente au maximum ; 30 secondes maximum par tâche active, puis terminaison et remplacement du worker. Une tâche en attente peut attendre au plus une génération de tâches actives. Une déconnexion après réception complète n’annule pas une mutation déjà autorisée : reprendre son operationId pour connaître l’issue.
Mesures locales du 27 octobre : premier coût synchrone 1 391 ms (30 élèves), 9 340 ms (200). Benchmark reproductible node scripts/benchmark-distribution.mjs : avec workers, 2 233/8 683 ms écoulés et écart maximal du témoin de boucle principale 73/45 ms ; sans workers, 2 024/10 329 ms et aucun passage du témoin pendant le calcul. Les mesures varient avec la charge locale.
Recette HTTP sur le serveur compilé : publication de 30 en 1 334 ms, 22 GET accueil concurrents (maximum 41 ms, p95 23 ms) ; publication de 200 en 8 360 ms, 134 GET concurrents (maximum 44 ms, p95 25 ms). Ce sont des fixtures compactes et un témoin d’accueil, pas des parties jouées ni un test de charge VPS. SQLite, sérialisation et validation restent synchrones ; le pire cas de 5 Mio/publication et la purge massive restent à dimensionner. Le plafond global est testé, pas la tenue en charge maximale.
Les réponses API sont no-store ; origine de même site requise pour tous les POST. Le rôle Professeur est vérifié avant toute gestion et la capacité est vérifiée contre la publication ciblée, jamais contre un propriétaire déclaré. Aucun endpoint de liste. Corps, codes, bilans et capacités ne sont pas journalisés par ces modules. Il reste à inspecter les proxies, APM et journaux de l’installation réelle.
Sauvegarde privée et révocation
Export local uniquement : AES-256-GCM authentifié, nonce 12 octets aléatoire, PBKDF2-HMAC-SHA-256 avec 600 000 itérations et sel 16 octets aléatoire, données associées fixes versionnées. Phrase secrète distincte, au moins 12 caractères, jamais enregistrée/transmise. Le mot de passe perdu n’est pas récupérable.
L’import borne la taille, refuse version/algorithmes/itérations inconnus avant dérivation, authentifie avant parsing, valide champs exacts/références/unicité, présente un aperçu puis applique une transaction locale. Un contexte différent nécessite un profil navigateur vide. Un conflit avec les données existantes est refusé sans remplacement ; les ajouts non conflictuels sont fusionnés. Aucun droit serveur ou prolongement de durée ne résulte d’un import.
L’export contient uniquement contexte, associations et références/capacités de gestion, mapping et identités de bilans. Pas de résultats, CSV barème, cookies, connexion Professeur, configuration serveur ou TOTP. Conserver les CSV v2 séparément. L’accès Professeur ne récupère pas une capacité perdue.
La rotation de capacité remplace atomiquement son vérificateur. La nouvelle capacité est enregistrée localement avant envoi ; une réponse perdue peut être vérifiée avec la nouvelle clé. Les anciennes sauvegardes deviennent inopérantes pour ce dépôt, sans toucher aux autres publications. Une rotation en attente figure dans l’export et reste reprenable après import. Réexporter après succès. Un fichier déchiffré copié transmet ces seuls droits ; aucune promesse de risque nul.
Purge et sauvegardes d’infrastructure
Refus à l’échéance absolue indépendamment du timer, y compris après dérivation du code. Purge à l’ouverture de la base, avant accès/mutation et toutes les 60 secondes après initialisation du service. Pour une instance sans trafic, exécuter aussi node scripts/purge-distribution.mjs chaque minute avec DISTRIBUTION_DB, via l’ordonnanceur de l’exploitant. Le script échoue si la configuration/base n’est pas accessible ; surveiller son code de sortie. Aucun ordonnanceur système n’est installé par ce lot.
Les paquets, titres, structures, commentaires, vérificateurs élèves et sel sont retirés du document SQLite. Secure_delete écrase les cellules supprimées ; un test vérifie que le titre fictif n’est plus présent dans le fichier après retrait. Cela ne prouve pas l’effacement des caches disque, snapshots, SSD, copies ou sauvegardes du fournisseur. Conserver le marqueur opaque (identifiant, contexte opaque, vérificateur de gestion, révision, état et dates) sans purge temporelle automatique, plafonné à 10 000, afin de garder le QR réutilisable. La politique de retrait définitif des marqueurs doit être revue avant extension du service.
Avant une restauration serveur, maintenir le service inaccessible, restaurer sans ouvrir l’accès public puis exécuter la purge. Vérifier le chemin hors sauvegardes applicatives et le traitement des snapshots OVH. Aucune inspection des sauvegardes OVH ni garantie de suppression physique sous sept jours n’a été réalisée. Si leur exclusion est impossible, arbitrage du propriétaire avant publication ; ne pas annoncer cette garantie.
Retour arrière : arrêter uniquement le service concerné, revenir au code antérieur sans écraser les données locales récentes, garder la base privée hors service pour terminer sa purge. Ne pas republier une ancienne archive de résultats. Ce lot n’écrit ni catalogue ni ressources de production.