diff --git a/ansible/roles/portail/tasks/03-code.yml b/ansible/roles/portail/tasks/03-code.yml
index 1f06192..05b9e88 100644
--- a/ansible/roles/portail/tasks/03-code.yml
+++ b/ansible/roles/portail/tasks/03-code.yml
@@ -125,6 +125,10 @@
# Étape 12 : HTTPS. Cookies de session envoyés en HTTPS seulement.
- { cle: "APP_URL", valeur: "https://{{ portail_domaine }}" }
- { cle: "SESSION_SECURE_COOKIE", valeur: "true" }
+ # Étape 14 (RGPD) : journaux applicatifs par jour, gardés 14 jours
+ # (comme ceux de Nginx), au lieu d'un fichier unique jamais purgé.
+ - { cle: "LOG_STACK", valeur: "daily" }
+ - { cle: "LOG_DAILY_DAYS", valeur: "14" }
no_log: true
register: portail_env_mis_a_jour
# Horizon charge la configuration une seule fois au démarrage : sans
diff --git a/docs/dette.md b/docs/dette.md
index e5732f4..57cd40c 100644
--- a/docs/dette.md
+++ b/docs/dette.md
@@ -11,13 +11,17 @@ retirer la ligne (et le noter dans le journal) quand c'est fait.
| 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 |
| Larastan (PHPStan niveau 6) absent | Exigé par le CLAUDE.md depuis l'étape 8, jamais installé. | Bloc B | Étape 8 |
| 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) |
-| Vitrine : 52 scripts en ligne, Google Fonts, jsDelivr (démos xterm, CodeMirror) | CSP vitrine avec `'unsafe-inline'` ; CDN parfois filtrés par les réseaux scolaires. Auto-héberger polices et bibliothèques, passer les scripts en fichiers. | Avant ouverture publique | Étape 12 |
+| Vitrine : 52 scripts en ligne ; Pyodide et sql.js depuis jsDelivr (`/ide`) ; vidéo YouTube possible (`/cours/…/cm`) | CSP vitrine avec `'unsafe-inline'` ; tiers déclarés dans `/confidentialite`. Polices, xterm.js et CodeMirror sont auto-hébergés depuis l'étape 14. Héberger Pyodide/sql.js, passer les scripts en fichiers. | Avant ouverture publique | Étapes 12 et 14 |
| xterm.js 5.5.0 (6.0.0 disponible) | Version majeure à évaluer séparément. | Bloc B | Étape 12 |
| NodeSource installé par `curl … \| bash` | Script distant exécuté en root au premier déploiement. Déclarer le dépôt (deb822) avec une clé vérifiée. | Bloc B | Étape 8 |
-| Aucune vérification dans un vrai navigateur | Extension Chrome non connectée (étapes 8 et 12). La CSP n'est contrôlée que par ses rapports. | Dès que possible | Étapes 8 et 12 |
+| Aucune vérification dans un vrai navigateur | Extension Chrome non connectée (étapes 8, 12 et 14). La CSP n'est contrôlée que par ses rapports. À vérifier en priorité : `/ide` (Pyodide, WebAssembly) et les pages vitrine qui utilisent xterm.js/CodeMirror désormais servis par le portail. | Dès que possible | Étapes 8, 12, 14 |
| Pas de sonde hors du serveur | Si le serveur tombe, personne n'est prévenu. | À décider avec Adam | Étape 7 |
| Kolla installé depuis une branche, pas un tag | Reproductibilité. | Montée de version | ADR 0001 |
| Correction séquentielle (~5 s pour le lab 08) | Acceptable ; à paralléliser si la charge l'exige. | Bloc B | Étape 11 |
| Jeton de console : ≈ 1,1 s par appel Nova `remote-consoles` | Plus gros poste de l'ouverture d'un lab (2,8 s au total, critère < 5 s tenu). Préparer les jetons à l'attribution si besoin. | Bloc B | Étape 13 |
| Tâches Ansible toujours « modifiées » (composer, npm, build, cache) | Brouille la lecture de l'idempotence. | Bloc B | Étape 8 |
| `Permissions-Policy` ferme caméra et micro | À ouvrir pour la visio. | Étape 15 | Étape 12 |
+| Mentions légales à compléter | Forme juridique, SIREN et adresse du siège d'Esquisse Formation ; délégué à la protection des données s'il en est désigné ; article « Disponibilité et responsabilité » des CGU. | Avant le pilote | Étape 14 |
+| Slogans « du lycée au master » | Le catalogue va aujourd'hui du lycée au BUT. Positionnement laissé tel quel : décision éditoriale d'Adam. | À décider | Étape 14 |
+| Ancien `storage/logs/laravel.log` | Fichier unique d'avant la rotation quotidienne (étape 14) : jamais purgé, à supprimer ou archiver (accord d'Adam). | Dès que possible | Étape 14 |
+| Pages d'aperçu publiques (32) | Signalées comme aperçus avec données fictives ; à retirer ou remplacer au fur et à mesure que les fonctions réelles arrivent. | Au fil du bloc B | Étape 14 |
diff --git a/docs/journal.md b/docs/journal.md
index d24bb18..ea29e2a 100644
--- a/docs/journal.md
+++ b/docs/journal.md
@@ -566,3 +566,37 @@ Un serveur qui « marche » n'est pas un serveur qui *revient* : trois valeurs p
**Suite**
Étape 14 : pages légales et RGPD.
+
+## 2026-10-09 — Étape 14 : pages légales et RGPD
+
+**Fait**
+
+- **Purge RGPD** : bouton « Archiver la classe » côté prof (plus de séance possible, date de suppression affichée, désarchivage possible d'ici là) et commande `classes:purger-archivees`, chaque nuit : une classe archivée depuis 90 jours est supprimée avec, en cascade, ses élèves, séances, sessions, machines et corrections. Le compte du prof reste ; le journal ne garde ni nom ni prénom.
+- **Pages légales réécrites d'après le système réel** : `/legal` (mentions légales : Esquisse Formation, SIREN et adresse à compléter par Adam ; hébergeur vérifié à la source, mentions légales de Scaleway ; CGU) et `/confidentialite` (données réellement traitées, finalités et bases légales, durées vérifiées sur le serveur, hébergement et tiers, sécurité, cookies, droits, sous-traitance). `/statut` redirige vers la page de statut.
+- **Plus aucun envoi d'adresse IP chez Google** : Archivo et IBM Plex Mono sont téléchargées à la compilation et servies par le portail (`Vite::fonts()`), au lieu de Google Fonts, chargé jusque-là sur **toutes** les pages, espace élève compris. xterm.js et CodeMirror servis depuis `public/vendor/` (copiés des paquets npm à la compilation) au lieu de jsDelivr.
+- **Journaux** : Laravel passe d'un fichier unique jamais purgé à un fichier par jour gardé 14 jours (comme Nginx).
+- **Interdits du prompt** : aucun logo Marianne, ministère, drapeau de l'UE ni « logo RGPD » sur le site. Bandeau « Aperçu d'une fonction en préparation — données fictives » sur les 32 pages d'aperçu (choix d'Adam) ; vrais établissements des tableaux de bord remplacés par des noms fictifs ; « 20 cursus » corrigé en « 6 cursus » ; « 99 cours » présenté comme un programme en cours de publication ; « Conforme au RGPD » relié à `/confidentialite` ; « Hébergé en France · Paris » ramené à « Hébergé en France » (commune du datacenter non vérifiable sur le site de Scaleway).
+- 8 nouveaux tests Pest (33 au total) ; runbook `docs/runbooks/rgpd.md`.
+
+**Problèmes rencontrés et solution**
+
+1. **La page `/legal` du design décrivait un système qui n'existe pas** : « aucun traceur tiers » et « les données ne quittent pas l'UE » alors que Google Fonts était chargé partout, un « mode examen » qui enregistre les changements d'onglet (non construit), « temps passé, réponses aux QCM » (non collectés). Réécrite affirmation par affirmation.
+2. **Faux clients et chiffres inventés** dans les pages d'aperçu (« Lycée Vauban Brest », « 608 élèves »…). Mon premier test cherchait nom et ville collés et a laissé passer les tableaux où ils sont dans deux cellules (« Lycée Vauban | Brest ») : test réécrit pour chercher chaque nom seul, sur les 32 aperçus — il a ensuite trouvé `/paiement` et les dernières lignes de `/academie`.
+3. **La CSP de l'étape 12 cassait probablement l'exécution de code sur `/ide`** (Pyodide doit télécharger ses fichiers et compiler du WebAssembly) : `connect-src` et `'wasm-unsafe-eval'` ajoutés pour la vitrine. Non vérifié dans un navigateur (extension non connectée).
+4. Deux erreurs dans mes tests : `toContain($texte, $message)` de Pest traite le second argument comme un autre texte à trouver ; le test « toutes les pages en 200 » ne prévoyait pas la redirection voulue de `/statut`.
+5. Tinker lancé avec un fichier est resté bloqué en attente de saisie : remplacé par des tests Pest permanents (aucune police externe sur aucune page ; aucune ressource externe sur les pages de l'application).
+
+**Critère vérifié**
+
+- Pages en ligne : `/legal` 200, `/confidentialite` 200, `/statut` → 302 vers `https://status.syllabuslab.fr/` ; page de statut publique opérationnelle (étape 12).
+- **La purge fonctionne, prouvée par Pest** (`PurgeRgpdTest`, 5 tests) : classe archivée depuis 91 jours supprimée avec élèves, séances, sessions, machines et corrections, compte du prof conservé ; classes archivées depuis 89 jours et classes actives épargnées ; seules les classes concernées touchées ; archivage par le prof, refus pour un autre prof. En production : `classes:purger-archivees` → « 0 classe(s) purgée(s) » (aucune classe archivée), planifiée à 02:30.
+- `/connexion` ne charge plus que des ressources de `syllabuslab.fr` ; aucune mention de Google Fonts sur l'accueil ; 0 violation CSP depuis le déploiement.
+
+**Comprendre**
+
+Une politique de confidentialité n'est pas un texte juridique à recopier : c'est une description exacte de ce que fait le système, et elle engage. Celle du design promettait « aucun traceur tiers » pendant que chaque page, y compris celles des élèves mineurs, envoyait l'adresse IP du visiteur à Google pour charger une police. Le bon réflexe est de partir de la réalité (que collecte-t-on ? où ? combien de temps ? qui d'autre la voit ?), de la vérifier sur le serveur, puis de corriger le système quand la réalité n'est pas défendable — ici, servir les polices nous-mêmes — plutôt que d'adapter le texte. La purge suit la même logique : une durée de conservation écrite n'a de valeur que si un programme l'applique, et un test prouve qu'il l'applique.
+
+**Suite**
+
+- Adam : compléter forme juridique, SIREN, adresse du siège (et DPO s'il y en a un) dans `/legal` et `/confidentialite` ; vérifier `/ide` dans un navigateur.
+- Étape 15 : visio de classe — seulement après validation des étapes 1 à 11 (le prompt l'exige).
diff --git a/docs/runbooks/rgpd.md b/docs/runbooks/rgpd.md
new file mode 100644
index 0000000..dd4ff4e
--- /dev/null
+++ b/docs/runbooks/rgpd.md
@@ -0,0 +1,41 @@
+# Runbook — Données personnelles (RGPD)
+
+Politique publique : `https://syllabuslab.fr/confidentialite`
+(`portail/resources/views/confidentialite.blade.php`). **Toute nouvelle donnée
+collectée, tout nouveau service tiers ou toute nouvelle durée de conservation
+doit d'abord y être décrit.** La page affirme seulement ce qui a été vérifié.
+
+## Où sont les données
+
+| Donnée | Où | Durée |
+|---|---|---|
+| Comptes profs, classes, élèves, séances, sessions, corrections | MariaDB du portail (`portail_mariadb`) | jusqu'à 90 jours après l'archivage de la classe |
+| VM des élèves (disques) | OpenStack, une stack par session | fin de session (45 min par défaut) |
+| Journaux Nginx (IP, pages) | `/var/log/nginx/` | 14 jours (logrotate) |
+| Journaux Laravel | `storage/logs/laravel-AAAA-MM-JJ.log` | 14 jours (`LOG_STACK=daily`, `LOG_DAILY_DAYS=14`) |
+| Sauvegardes chiffrées | Dedibackup (Scaleway, France) | 7 quotidiennes + 4 hebdomadaires (≤ 5 semaines) |
+
+## Purge automatique
+
+- Le prof archive sa classe (page de la classe, « Archiver la classe ») : plus de séance possible ; la date de suppression est affichée.
+- `classes:purger-archivees` (chaque nuit à 02:30 UTC) supprime les classes archivées depuis 90 jours, et en cascade leurs élèves, séances, sessions, machines et corrections. Le compte du prof reste. Le journal ne garde que l'identifiant de la classe et le nombre d'élèves.
+- Une classe avec un lab encore en cours est reportée (anomalie : un lab expire en 45 min) et signalée dans le journal.
+- Preuve : `portail/tests/Feature/PurgeRgpdTest.php`.
+
+```
+ssh paillasse "cd /var/www/portail && sudo -u www-data HOME=/tmp php8.3 artisan classes:purger-archivees"
+```
+
+## Répondre à une demande d'exercice des droits
+
+Contact public : adam@belkacemi.me. Pour un élève, la demande passe normalement par son établissement (responsable du traitement).
+
+- **Accès / portabilité (élève)** : prénom, nom, identifiant, classe, sessions et corrections ; extraire avec `artisan tinker` (`App\Models\Eleve::with('labSessions.corrections')->where('identifiant', '…')->first()->toArray()`), sans le code secret (haché, inutile).
+- **Effacement d'un élève** : `App\Models\Eleve::where('identifiant', '…')->first()->delete()` — cascade sur ses sessions et corrections.
+- **Effacement d'un compte prof** : supprimer d'abord ses classes (ou les archiver), puis `App\Models\User::where('email', '…')->first()->delete()`.
+- Noter la demande et sa date de traitement (hors du dépôt : le registre contient des données personnelles).
+- Rappeler que les sauvegardes chiffrées contenant la donnée expirent d'elles-mêmes sous 5 semaines.
+
+## Interdits sur le site (prompt de déploiement, étape 14)
+
+Pas de logo Marianne ou ministère, de drapeau de l'UE, de faux « logo RGPD », de chiffres inventés ni de faux clients. Formulations autorisées : « Hébergé en France », « Conforme au RGPD » avec un lien vers `/confidentialite`. Les pages d'aperçu (fonctions pas encore construites) portent le bandeau `partials/apercu` et n'affichent que des noms fictifs (`PagesLegalesTest`).
diff --git a/portail/.gitignore b/portail/.gitignore
index f2e8cf6..4122d53 100644
--- a/portail/.gitignore
+++ b/portail/.gitignore
@@ -26,3 +26,4 @@ _ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db
+/public/vendor
diff --git a/portail/app/Console/Commands/PurgerClassesArchivees.php b/portail/app/Console/Commands/PurgerClassesArchivees.php
new file mode 100644
index 0000000..4ce8c1a
--- /dev/null
+++ b/portail/app/Console/Commands/PurgerClassesArchivees.php
@@ -0,0 +1,54 @@
+where('archivee_a', '<=', now()->subDays(Classe::DELAI_PURGE_JOURS))
+ ->withCount('eleves')
+ ->get();
+
+ $purgees = 0;
+
+ foreach ($classes as $classe) {
+ // Ne peut arriver qu'en cas d'anomalie (un lab expire en 45 min) :
+ // on ne supprime pas une session dont la stack Heat vivrait encore.
+ if ($classe->aDesLabsEnCours()) {
+ Log::warning('Purge RGPD reportée : lab en cours', ['classe_id' => $classe->id]);
+
+ continue;
+ }
+
+ DB::transaction(fn () => $classe->delete());
+ Log::info('Purge RGPD : classe supprimée', [
+ 'classe_id' => $classe->id,
+ 'eleves' => $classe->eleves_count,
+ 'archivee_a' => $classe->archivee_a->toDateString(),
+ ]);
+ $purgees++;
+ }
+
+ $this->info("{$purgees} classe(s) purgée(s).");
+ }
+}
diff --git a/portail/app/Http/Controllers/ClasseController.php b/portail/app/Http/Controllers/ClasseController.php
index 2b51030..58d0a71 100644
--- a/portail/app/Http/Controllers/ClasseController.php
+++ b/portail/app/Http/Controllers/ClasseController.php
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\Eleve;
+use App\Models\Seance;
use App\Services\GenerateurCompteEleve;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -124,6 +125,33 @@ class ClasseController extends Controller
];
}
+ /**
+ * Fin d'année : plus de séance possible, et suppression définitive de la
+ * classe et des données de ses élèves 90 jours plus tard.
+ */
+ public function archiver(Classe $classe): RedirectResponse
+ {
+ $this->verifierProprietaire($classe);
+
+ if ($classe->aDesLabsEnCours()) {
+ return back()->withErrors(['archivage' => 'Des élèves ont encore un lab en cours : termine la séance avant d’archiver.']);
+ }
+
+ $classe->forceFill(['archivee_a' => now()])->save();
+ $classe->seances()->whereIn('statut', [Seance::PLANIFIEE, Seance::SALLE_ATTENTE, Seance::EN_COURS])
+ ->update(['statut' => Seance::TERMINEE, 'terminee_a' => now()]);
+
+ return redirect()->route('mes-classes.show', $classe);
+ }
+
+ public function desarchiver(Classe $classe): RedirectResponse
+ {
+ $this->verifierProprietaire($classe);
+ $classe->forceFill(['archivee_a' => null])->save();
+
+ return redirect()->route('mes-classes.show', $classe);
+ }
+
private function verifierProprietaire(Classe $classe): void
{
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
diff --git a/portail/app/Http/Controllers/SeanceController.php b/portail/app/Http/Controllers/SeanceController.php
index 05c6976..a797f57 100644
--- a/portail/app/Http/Controllers/SeanceController.php
+++ b/portail/app/Http/Controllers/SeanceController.php
@@ -24,6 +24,10 @@ class SeanceController extends Controller
{
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
+ if ($classe->estArchivee()) {
+ return back()->withErrors(['prevue_a' => 'Cette classe est archivée : désarchive-la pour lancer une séance.']);
+ }
+
$donnees = $request->validate([
'lab_id' => ['required', 'exists:labs,id'],
'prevue_a' => ['nullable', 'date_format:Y-m-d\TH:i'],
@@ -130,6 +134,7 @@ class SeanceController extends Controller
{
$eleve = Auth::guard('eleve')->user();
abort_if($eleve->classe_id !== $seance->classe_id, 403);
+ abort_if($seance->classe->estArchivee(), 403);
LabSession::firstOrCreate(
['seance_id' => $seance->id, 'eleve_id' => $eleve->id],
diff --git a/portail/app/Http/Middleware/PolitiqueSecuriteContenu.php b/portail/app/Http/Middleware/PolitiqueSecuriteContenu.php
index 8dccd2d..62072af 100644
--- a/portail/app/Http/Middleware/PolitiqueSecuriteContenu.php
+++ b/portail/app/Http/Middleware/PolitiqueSecuriteContenu.php
@@ -41,7 +41,8 @@ class PolitiqueSecuriteContenu
$communes = [
'default-src' => "'self'",
'img-src' => "'self' data:",
- 'font-src' => "'self' https://fonts.gstatic.com",
+ // Polices servies par le portail (Vite), depuis l'étape 14.
+ 'font-src' => "'self'",
'object-src' => "'none'",
'base-uri' => "'self'",
'form-action' => "'self'",
@@ -54,13 +55,17 @@ class PolitiqueSecuriteContenu
'script-src' => "'self' 'nonce-".Vite::cspNonce()."'",
// Attributs style= présents dans le gabarit commun : sans
// risque d'exécution de code, tolérés.
- 'style-src' => "'self' 'unsafe-inline' https://fonts.googleapis.com",
+ 'style-src' => "'self' 'unsafe-inline'",
'connect-src' => "'self' wss://{$request->getHost()}",
]
: [
- 'script-src' => "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdn.tailwindcss.com",
- 'style-src' => "'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net",
- 'connect-src' => "'self'",
+ // jsDelivr : Pyodide et sql.js, téléchargés par /ide à la
+ // demande (WebAssembly, d'où 'wasm-unsafe-eval', qui n'autorise
+ // pas eval() en JavaScript). Tailwind : CDN d'exécution des
+ // pages du design. Voir docs/dette.md.
+ 'script-src' => "'self' 'unsafe-inline' 'wasm-unsafe-eval' https://cdn.jsdelivr.net https://cdn.tailwindcss.com",
+ 'style-src' => "'self' 'unsafe-inline'",
+ 'connect-src' => "'self' https://cdn.jsdelivr.net",
'frame-src' => 'https://www.youtube-nocookie.com',
];
diff --git a/portail/app/Models/Classe.php b/portail/app/Models/Classe.php
index 3c08788..a6254a0 100644
--- a/portail/app/Models/Classe.php
+++ b/portail/app/Models/Classe.php
@@ -6,6 +6,7 @@ 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
@@ -58,4 +59,22 @@ class Classe extends Model
{
return $this->archivee_a !== null;
}
+
+ /**
+ * RGPD (CLAUDE.md) : une classe archivée et toutes les données de ses
+ * élèves sont supprimées 90 jours après l'archivage (classes:purger-archivees).
+ */
+ public const DELAI_PURGE_JOURS = 90;
+
+ public function supprimeeLe(): ?Carbon
+ {
+ return $this->archivee_a?->copy()->addDays(self::DELAI_PURGE_JOURS);
+ }
+
+ public function aDesLabsEnCours(): bool
+ {
+ return LabSession::whereIn('seance_id', $this->seances()->select('id'))
+ ->whereIn('etat', [LabSession::ADMIS, LabSession::ACTIF])
+ ->exists();
+ }
}
diff --git a/portail/package-lock.json b/portail/package-lock.json
index 926b233..30ad09a 100644
--- a/portail/package-lock.json
+++ b/portail/package-lock.json
@@ -6,7 +6,8 @@
"": {
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
- "@xterm/xterm": "^5.5.0"
+ "@xterm/xterm": "^5.5.0",
+ "codemirror": "^5.65.16"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
@@ -806,6 +807,12 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/codemirror": {
+ "version": "5.65.16",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz",
+ "integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==",
+ "license": "MIT"
+ },
"node_modules/commander": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
diff --git a/portail/package.json b/portail/package.json
index 17f32ed..83ee511 100644
--- a/portail/package.json
+++ b/portail/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "vite build",
+ "build": "vite build && node scripts/copier-vendor.mjs",
"dev": "vite"
},
"devDependencies": {
@@ -17,6 +17,7 @@
},
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
- "@xterm/xterm": "^5.5.0"
+ "@xterm/xterm": "^5.5.0",
+ "codemirror": "^5.65.16"
}
}
diff --git a/portail/resources/views/academie.blade.php b/portail/resources/views/academie.blade.php
index ed8c857..5256741 100644
--- a/portail/resources/views/academie.blade.php
+++ b/portail/resources/views/academie.blade.php
@@ -4,8 +4,7 @@
diff --git a/portail/resources/views/confidentialite.blade.php b/portail/resources/views/confidentialite.blade.php
new file mode 100644
index 0000000..5981518
--- /dev/null
+++ b/portail/resources/views/confidentialite.blade.php
@@ -0,0 +1,144 @@
+@extends('layouts.vitrine')
+
+@section('titre', 'Confidentialité et RGPD')
+
+{{-- Étape 14 : chaque affirmation de cette page a été vérifiée sur le système
+ réel (docs/journal.md, étape 14). À mettre à jour à chaque nouveau
+ traitement de données. --}}
+@section('contenu')
+
+
Syllabus est édité par Esquisse Formation (voir les mentions légales).
+
Quand un établissement utilise Syllabus avec ses élèves, l’établissement est responsable du traitement et Esquisse Formation agit comme sous-traitant, sur ses instructions. Pour les comptes enseignants, Esquisse Formation est responsable du traitement.
+
Délégué à la protection des données : [à compléter si désigné]. Contact : adam@belkacemi.me
+
+
+
+
+
Données traitées
+
+
Élèves
+
+
prénom, nom (facultatif, visible du seul enseignant), classe ;
+
identifiant de connexion et code secret (le code est stocké sous forme hachée, illisible, y compris pour nous) ;
+
aucune adresse e-mail ;
+
sessions de lab : date, état, machines virtuelles attribuées ;
+
résultats de la vérification automatique (réussi ou non, note, message par vérification).
+
+
Ce que l’élève tape dans la console de sa machine virtuelle n’est pas enregistré par Syllabus. Son enseignant peut suivre la console en direct, en lecture seule, pendant la séance.
+
Enseignants
+
+
nom, adresse e-mail, établissement ;
+
mot de passe, stocké sous forme hachée.
+
+
Journaux techniques
+
+
journaux du serveur web : adresse IP, date, page demandée ;
+
journaux de l’application : erreurs et événements techniques, sans contenu des pages.
+
+
Finalités et bases légales
+
Fournir le service d’enseignement demandé par l’établissement (mission d’intérêt public de l’établissement, article 6.1.e du RGPD) ou par l’enseignant (exécution du contrat, article 6.1.b). Les journaux techniques servent à la sécurité et au bon fonctionnement du service (intérêt légitime, article 6.1.f). Aucune donnée n’est vendue, utilisée à des fins publicitaires, ni traitée par une intelligence artificielle.
+
+
+
+
+
Durées de conservation
+
+
+
Machines virtuelles et leur contenu : supprimées à la fin de la session de lab (45 minutes par défaut).
+
Classe et données de ses élèves : jusqu’à l’archivage de la classe par l’enseignant, puis supprimées automatiquement 90 jours après l’archivage.
+
Compte enseignant : tant qu’il est utilisé ; supprimé sur demande.
+
Journaux techniques : 14 jours.
+
Sauvegardes : chiffrées, conservées au plus 5 semaines (7 sauvegardes quotidiennes et 4 hebdomadaires). Une donnée supprimée disparaît donc des sauvegardes au plus tard 5 semaines après sa suppression.
+
+
+
+
+
+
Hébergement et services tiers
+
+
Le service, ses bases de données et ses sauvegardes chiffrées sont hébergés en France, chez Scaleway. Les données des comptes ne quittent pas l’Union européenne.
+
Les pages de l’espace connecté (élèves et enseignants) ne chargent aucune ressource depuis un service tiers : polices, scripts et styles sont servis par Syllabus.
+
Certaines pages publiques de présentation font encore appel à des services tiers, qui reçoivent alors l’adresse IP du visiteur :
+
+
des pages d’aperçu chargent la bibliothèque de styles Tailwind depuis cdn.tailwindcss.com (Tailwind Labs, États-Unis) ;
+
l’éditeur de code /ide télécharge Python (Pyodide) et SQL (sql.js) depuis cdn.jsdelivr.net, seulement quand on exécute du code ;
+
la page d’aperçu d’un cours peut afficher une vidéo depuis youtube-nocookie.com, seulement si un lien vidéo y est ajouté.
+
+
Nous travaillons à servir ces ressources nous-mêmes.
+
+
+
+
+
Sécurité
+
+
+
connexions chiffrées (HTTPS, TLS 1.2 et 1.3) ;
+
mots de passe et codes secrets hachés ;
+
une machine virtuelle par élève, isolée, sans accès à Internet ;
+
la vérification automatique n’exécute que des commandes définies à l’avance par le lab, jamais de commande libre ;
+
sauvegardes chiffrées avant leur envoi, restauration testée chaque semaine ;
+
administration du serveur par clé SSH uniquement.
+
+
+
+
+
+
Cookies
+
+
Syllabus n’utilise que deux cookies, strictement nécessaires : syllabus-session, qui vous garde connecté, et XSRF-TOKEN, qui protège les formulaires. Aucun cookie publicitaire ni de mesure d’audience. Ils ne demandent donc pas de consentement.
+
L’éditeur de code et la page de visio gardent aussi, dans le stockage local de votre navigateur, votre code et une préférence d’affichage. Ces informations ne nous sont pas transmises.
+
+
+
+
+
Vos droits
+
+
Vous disposez des droits d’accès, de rectification, d’effacement, de limitation, d’opposition et de portabilité. Élèves et familles : adressez-vous d’abord à l’enseignant ou à l’établissement, responsable du traitement. Enseignants : écrivez à adam@belkacemi.me.
+
Vous pouvez aussi adresser une réclamation à la CNIL (cnil.fr).
+
+
+
+
+
Sous-traitance (article 28 du RGPD)
+
+
Pour chaque établissement, un accord de sous-traitance peut être établi sur demande. Il précise :
+
+
l’objet, la durée et la nature des traitements ;
+
les mesures de sécurité décrites ci-dessus ;
+
la confidentialité des personnes autorisées ;
+
les sous-traitants ultérieurs : hébergement et sauvegardes chez Scaleway (France) ;
+
l’assistance pour répondre aux demandes d’exercice des droits ;
+
la notification des violations de données ;
+
la suppression ou la restitution des données en fin de contrat.
Chaque cours suit le découpage de l’Éducation nationale : un CM pour comprendre, un TD pour s’exercer sur papier, un TP sur une vraie VM, un QCM corrigé et expliqué.
Chaque cours suit le découpage de l’Éducation nationale : un CM pour comprendre, un TD pour s’exercer sur papier, un TP sur une vraie VM, un QCM corrigé et expliqué.
Programme : 99 cours · 45 TD · 59 TP sur VM · 98 QCM corrigés — en cours de publication
Abonnement, licences, documents comptables et contrats. Accès réservé à l’intendance et à la direction.
Formule Établissement
20 classes
renouvelable chaque année
Période
01/09 → 31/08
renouvellement le 01/07/2027
Licences
118 / 120
2 disponibles
Heures de VM
2 940 h
sur 9 120 h incluses
Licences
118 actives · 2 libres
Titulaire
Classe / rôle
Début
Fin
Statut
Martin Léa
BTS SIO 1 A
01/09/2026
30/06/2027
Active
Kaci Inès
BTS SIO 1 A
01/09/2026
30/06/2027
Active
Petit Hugo
BTS SIO 2 B
01/09/2025
15/10/2026
Fin programmée
Roche Anne
Enseignante
01/09/2026
—
Active
Leroy Tom
BTS SIO 1 B
01/09/2026
29/09/2026
Retirée
Import CSV : colonnes nom;prenom;email;classe;date_fin. Une licence se libère automatiquement à la date de fin ; les données de l’étudiant sont supprimées 12 mois après, sauf export demandé.