corrige /ide, sert Mulish depuis le portail et complète les mentions légales

/ide avait une erreur de syntaxe depuis la maquette et un aperçu hors de
son bac à sable. Mulish était encore importée depuis Google par la feuille
de thème. Nouveaux tests : syntaxe des scripts de chaque page, feuilles de
style sans import externe. public/vendor exclu du rsync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-09 21:44:02 +02:00
parent e4a6aaebb1
commit 1ac8d4773a
10 changed files with 77 additions and 15 deletions

View file

@ -1,5 +1,5 @@
/* Syllabus — thème global. Surcharge les anciennes classes néo-brutalistes pour appliquer la DA "SaaS doux" de la bibliothèque v2. */
@import url('https://fonts.googleapis.com/css2?family=Mulish:wght@400;500;600;700;800&display=swap');
/* Mulish : servie par le portail (Vite::fonts(), vite.config.js), plus par Google Fonts (étape 14). */
:root { --sy-bg:#f5f6f8; --sy-surface:#fff; --sy-line:#b8c1cf; --sy-ink:#111827; --sy-muted:#6b7280; --sy-blue:#1d5cf5; --sy-blue-dark:#174bd0; --sy-navy:#1e3a8a; }
html, body { background: var(--sy-bg) !important; color: var(--sy-ink) !important; font-family: Mulish, system-ui, sans-serif !important; }
body * { font-family: inherit; }

View file

@ -31,7 +31,7 @@
<section id="responsable" aria-labelledby="t-responsable">
<h2 id="t-responsable">Qui est responsable</h2>
<div class="mt-6 grid gap-4 text-lg font-medium leading-relaxed">
<p>Syllabus est édité par Esquisse Formation (voir les <a href="{{ route('legal') }}#mentions" class="font-bold underline underline-offset-4">mentions légales</a>).</p>
<p>Syllabus est édité par Adam Belkacemi, entrepreneur individuel (EI), sous le nom commercial Esquisse Formation (voir les <a href="{{ route('legal') }}#mentions" class="font-bold underline underline-offset-4">mentions légales</a>).</p>
<p>Quand un établissement utilise Syllabus avec ses élèves, <strong>l’établissement est responsable du traitement</strong> et Esquisse Formation agit comme <strong>sous-traitant</strong>, sur ses instructions. Pour les comptes enseignants, Esquisse Formation est responsable du traitement.</p>
<p>Délégué à la protection des données : <mark class="bg-[#fef7e0] px-1 font-bold">[à compléter si désigné]</mark>. Contact : adam@belkacemi.me</p>
</div>

View file

@ -117,7 +117,7 @@
<button type="button" role="tab" data-onglet="terminal" aria-selected="false" class="border-r border-[#e3e5e8] px-4 py-2 text-white aria-selected:bg-[#fef7e0] aria-selected:text-black">Terminal</button>
</div>
<pre id="p-sortie" data-panneau="sortie" class="h-[220px] overflow-auto whitespace-pre-wrap p-4 font-mono text-sm text-[#E8EDF5]">Clique sur ▶ Exécuter pour lancer ton code.</pre>
<div data-panneau="apercu" hidden class="h-[220px] bg-white"><iframe id="p-apercu" title="Aperçu de la page" class="h-full w-full" sandbox="allow-scripts allow-same-origin"></iframe></div>
<div data-panneau="apercu" hidden class="h-[220px] bg-white"><iframe id="p-apercu" title="Aperçu de la page" class="h-full w-full" sandbox="allow-scripts"></iframe></div>
<div data-panneau="terminal" hidden class="h-[220px] p-2"><div id="p-terminal" class="h-full"></div></div>
</div>
</section>
@ -217,17 +217,23 @@
afficher([['Requête valide', true], ['Bonnes lignes, dans le bon ordre', obtenu === attendu]]);
});
if (cur.lang === 'web') {
var f = $('p-apercu'); f.srcdoc = code; onglet('apercu');
return new Promise(function (ok) { f.onload = function () {
var d = f.contentDocument, b = d && d.getElementById('bouton'), m = d && d.getElementById('message');
var r = [['Le bouton #bouton existe',.b], ['Le paragraphe #message existe',.m]];
if (b && m) { b.click(); r.push(['Le clic affiche « Bonjour ! »', /Bonjour/.test(m.textContent)]); }
afficher(r); ok();
}; });
// La vérification tourne DANS l'aperçu, isolé (sandbox sans
// allow-same-origin : le code de l'élève ne peut pas agir sur
// la page) ; elle renvoie ses résultats par postMessage.
var f = $('p-apercu'); onglet('apercu');
var verif = '<script>addEventListener("load",function(){var b=document.getElementById("bouton"),m=document.getElementById("message"),r=[["Le bouton #bouton existe",!!b],["Le paragraphe #message existe",!!m]];if(b&&m){b.click();r.push(["Le clic affiche « Bonjour ! »",/Bonjour/.test(m.textContent)]);}parent.postMessage({syllabusVerif:r},"*");});<\/script>';
return new Promise(function (ok) {
var fini = false;
function terminer(r) { if (fini) return; fini = true; removeEventListener('message', recevoir); afficher(r); ok(); }
function recevoir(e) { if (e.source === f.contentWindow && e.data && e.data.syllabusVerif) terminer(e.data.syllabusVerif); }
addEventListener('message', recevoir);
setTimeout(function () { terminer([['La page s’affiche sans erreur', false]]); }, 3000);
f.srcdoc = code + verif;
});
}
onglet('terminal');
var h = shell ? shell.home : '/home/eleve', f2 = shell && shell.files.get(h + '/tp/script.sh');
afficher([['Le dossier ~/tp existe',.(shell && shell.dirs.has(h + '/tp'))], ['Le fichier ~/tp/script.sh existe', f2 !== undefined && f2 !== null], ['Il contient echo Bonjour',.f2 && /echo\s+"?Bonjour/.test(f2)]]);
afficher([['Le dossier ~/tp existe', !!(shell && shell.dirs.has(h + '/tp'))], ['Le fichier ~/tp/script.sh existe', f2 !== undefined && f2 !== null], ['Il contient echo Bonjour', !!f2 && /echo\s+"?Bonjour/.test(f2)]]);
return Promise.resolve();
}
document.querySelectorAll('[data-ex]').forEach(function (b) { b.addEventListener('click', function () { charger(b.dataset.ex); }); });

View file

@ -25,7 +25,7 @@
<section id="mentions" class="scroll-mt-6 border border-[#e3e5e8] rounded-2xl bg-white p-7" aria-labelledby="t-mentions">
<h2 id="t-mentions" class="text-4xl font-extrabold tracking-tight">Mentions légales</h2>
<div class="mt-6 grid gap-4 text-lg font-medium leading-relaxed">
<p><strong>Éditeur :</strong> Esquisse Formation, <mark class="bg-[#fef7e0] px-1 font-bold">[forme juridique à compléter]</mark>, SIREN <mark class="bg-[#fef7e0] px-1 font-bold">[à compléter]</mark>, siège : <mark class="bg-[#fef7e0] px-1 font-bold">[adresse à compléter]</mark>.</p>
<p><strong>Éditeur :</strong> Adam Belkacemi, entrepreneur individuel (EI), sous le nom commercial Esquisse Formation. SIREN 838 093 268 (SIRET 838 093 268 00087), immatriculé au Registre national des entreprises. Adresse : 30 rue Lorenzaccio, 38100 Grenoble.</p>
<p><strong>Directeur de la publication :</strong> Adam Belkacemi.</p>
<p><strong>Contact :</strong> adam@belkacemi.me</p>
<p><strong>Hébergeur :</strong> SCALEWAY, société par actions simplifiée au capital de 142 050 €, 8 rue de la Ville-l’Évêque, 75008 Paris, RCS Paris 433 115 904. Le service et ses sauvegardes sont hébergés en France, dans des datacenters de Scaleway.</p>

View file

@ -1,7 +1,7 @@
<?php
test('les pages légales sont en ligne et la page de statut redirige vers le site de statut', function () {
$this->get(route('legal'))->assertOk()->assertSee('Esquisse Formation')->assertSee('RCS Paris 433 115 904');
$this->get(route('legal'))->assertOk()->assertSee('Esquisse Formation')->assertSee('entrepreneur individuel (EI)')->assertSee('SIREN 838 093 268')->assertSee('RCS Paris 433 115 904');
$this->get(route('confidentialite'))->assertOk()
->assertSee('90 jours après l’archivage', false)
->assertSee('aucune adresse e-mail', false)

View file

@ -1,5 +1,6 @@
<?php
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;
// Toutes les pages publiques sans paramètre doivent s'afficher : la page
@ -35,3 +36,39 @@ test('aucune page publique ne charge de police depuis un service externe', funct
expect($html)->not->toMatch('#fonts\.(googleapis|gstatic|bunny)\.(com|net)#', "page /{$route->uri()}");
}
});
test('aucune feuille de style publique n’importe de ressource externe', function () {
foreach (glob(public_path('css/*.css')) as $fichier) {
expect(file_get_contents($fichier))->not->toMatch('#@import\s+url\(\s*["\']?https?://#', basename($fichier));
}
});
// /ide a eu une erreur de syntaxe JavaScript de l'étape 8 à l'étape 14 (des
// « !! » devenus « . » dans la maquette) : chaque script en ligne de chaque
// page publique est maintenant analysé par Node.
test('les scripts en ligne des pages publiques sont du JavaScript valide', function () {
if (Process::run('node --version')->failed()) {
$this->markTestSkipped('Node absent');
}
$pages = collect(Route::getRoutes())
->filter(fn ($r) => in_array('GET', $r->methods(), true) && $r->getName() !== null && ! str_contains($r->uri(), '{'))
->reject(fn ($r) => collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')))
->reject(fn ($r) => $r->getActionName() === '\Illuminate\Routing\RedirectController');
$erreurs = [];
foreach ($pages as $route) {
preg_match_all('#<script(?![^>]*\bsrc=)[^>]*>(.*?)</script>#s', $this->get('/'.ltrim($route->uri(), '/'))->getContent(), $scripts);
foreach ($scripts[1] as $i => $code) {
$fichier = tempnam(sys_get_temp_dir(), 'js').'.js';
file_put_contents($fichier, $code);
$resultat = Process::run(['node', '--check', $fichier]);
unlink($fichier);
if ($resultat->failed()) {
$erreurs[] = "/{$route->uri()} script {$i} : ".strtok($resultat->errorOutput(), "\n");
}
}
}
expect($erreurs)->toBe([]);
});

View file

@ -21,6 +21,10 @@ export default defineConfig({
bunny('IBM Plex Mono', {
weights: [400, 600],
}),
// Police principale du thème (public/css/syllabus-theme.css).
bunny('Mulish', {
weights: [400, 500, 600, 700, 800],
}),
],
}),
tailwindcss(),