Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@

---

## 🪧 29 septembre 2026 — La page d'accueil ne promet que ce qui existe

*Page d'accueil · feuille de route · un test empêche la dérive*

- **Des commandes qui marchent vraiment.** Sous « Choisissez votre environnement », la page listait par niveau des commandes que le terminal ne connaissait pas : `systemctl`, `launchctl`, `caffeinate`, `Get-Service`, `New-PSDrive` et dix-sept autres. Un visiteur qui en tapait une recevait « commande introuvable ». Chaque commande affichée est maintenant enseignée dans une leçon et fonctionne dans le terminal, et un test le vérifie à chaque changement.
- **Un aperçu de terminal fidèle.** L'animation de la page d'accueil montrait sous Windows des réponses de bash, et sous Linux un `ls` qui ajoutait des `/` et ne triait pas. Elle reprend maintenant ce qu'affichent un vrai bash, zsh et PowerShell 7 (le tableau `Path` de `Get-Location`, `Test-Path` qui répond `True`), et l'invite suit le dossier courant après `cd projects`.
- **Le bon résumé des modules.** « Du système de fichiers à la redirection de flux — deux niveaux » datait des débuts : le parcours va aujourd'hui jusqu'à Git et à l'IA, sur cinq niveaux.
- **Une feuille de route à jour.** Les vraies branches Git, le guide d'installation de l'app et les exemples expliqués de la Référence passent dans « Disponible ». « En cours » annonce ce qui reste vraiment à faire : les exercices en plusieurs étapes, et les commandes enseignées que le terminal ne simule pas encore (`find`, `sort`, `diff`, `tar`…).

---

## 🌳 29 septembre 2026 — Les branches Git existent pour de vrai

*Modules Git et GitHub · valeurs attendues relevées sur Git 2.56 · identifiants de commit identiques à ceux du vrai Git*
Expand Down
10 changes: 10 additions & 0 deletions STORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ Ce projet a été construit avec l'aide de Claude — l'IA d'Anthropic, des mod

---

## La page d'accueil, relue comme un visiteur (29 septembre 2026)

Thierry m'a demandé de vérifier que la page d'accueil reflète ce qui existe vraiment. Les chiffres étaient justes : un test les compare déjà au programme réel. Le problème était ailleurs. Sous chaque environnement, la page affiche des commandes par niveau, et j'ai tapé chacune d'elles dans le terminal au lieu de les relire. Vingt-deux répondaient « commande introuvable », et quatre autres n'étaient enseignées dans aucune leçon. `systemctl`, `launchctl`, `Get-Service` : des promesses écrites au début du projet, jamais tenues depuis.

La correction tient en deux gestes : ne garder que des commandes enseignées qui fonctionnent, et ajouter un test qui exécute chaque commande affichée. La prochaine promesse non tenue fera échouer la vérification avant d'arriver en ligne.

Au passage, le même test a mis au jour une dette plus profonde. Une vingtaine de commandes sont expliquées dans les leçons sans exister dans le terminal : `find`, `sort`, `diff`, `tar`, `Where-Object`… La feuille de route publique le dit maintenant, dans « En cours ».

---

## Des branches qui existent vraiment (29 septembre 2026)

Thierry avait posé la question simplement : « Pour Git, c'est très basique, je présume que c'est l'équivalent d'un premier niveau ? » C'était vrai, et pour une raison précise. On ne peut pas enseigner les branches avec un simulateur où une branche n'est qu'un nom. Il l'a dit mieux que moi : on n'apprend rien correctement sans une théorie et une pratique qui fonctionnent. Avant d'écrire la moindre leçon avancée, il fallait donc que le moteur fusionne pour de bon.
Expand Down
4 changes: 2 additions & 2 deletions docs/plan.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Terminal Learning — Plan de lancement public

> Dernière mise à jour : **26 septembre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353)** : #389 vraie couche shell (**198/198 exercices sans ligne rouge**) · #390 sorties d'un vrai shell · #391 rejeu permanent de la théorie · #392 `cp`/`mv` vers un dossier + chemins Windows natifs (écarts théorie **174 → 162**) · #393 agents d'audit remis à jour + `terminal-fidelity-auditor` · roadmap publique corrigée. Suite : messages d'erreur PowerShell, page Référence, écarts restants.
> Précédent (24 septembre 2026) : P0 livré — #383 dépendances (react-router 7.18.4, npm audit 41 → 24, 0 critique) + #384 THI-340 (preuve d'âge posée sur tous les retours OAuth). P1 « fidélité du terminal » (THI-353) en cours — #385 état de départ par leçon, #386 ligatures monospace (vérifié sur iPhone, THI-354), #387 scripts / `$PROFILE` / ExecutionPolicy / `chmod` : exercices validés malgré une erreur **44 → 3**. Suite : redirections (`2>`, `>`, `tee`), écarts théorie ↔ terminal, puis garde « pas de validation si erreur ». Précédent : 6 juin 2026 — audit complet pré-pause (voir CHANGELOG).
> Dernière mise à jour : **29 septembre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353), suite** : #396 exemples de la Référence tous exécutables · #397 tableaux, lignes vides et touches du clavier mobile · #398 haut de l'app installée dégagé du flou d'iOS 26 · #399 git lit les vrais fichiers, sorties identiques à Git 2.56 · **#400 vraies branches Git** (graphe de commits, identifiants identiques au vrai Git, fusions et vrais conflits ; deux audits de fidélité et deux revues de code) · page d'accueil : ne promet plus que des commandes simulées. Suite : exercices en plusieurs étapes (validés sur l'état du terminal), puis les ~20 commandes enseignées mais pas encore simulées (`find`, `sort`, `diff`, `tar`, `Where-Object`…).
> Précédent : **26 septembre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353)** : #389 vraie couche shell (**198/198 exercices sans ligne rouge**) · #390 sorties d'un vrai shell · #391 rejeu permanent de la théorie · #392 `cp`/`mv` vers un dossier + chemins Windows natifs (écarts théorie **174 → 162**) · #393 agents d'audit remis à jour + `terminal-fidelity-auditor` · roadmap publique corrigée. Suite : messages d'erreur PowerShell, page Référence, écarts restants.
> Statut global : **Phase 5 EN COURS** — Curriculum Expansion : 11 modules ✅, 66 leçons, **2500+ tests unitaires** + 176 E2E — **Vision consolidée** : LTI-first (ADR-001), BYOK OpenRouter 4-tiers (ADR-002), TTFR KPI central (ADR-003), Classroom Composer UI (ADR-004), AI Tutor V1 décisions gelées (ADR-005 — stockage, rate-limit, guardrails), Solo-sustainable practices (ADR-006), tuteur IA socratique dès A1, i18n FR/NL/EN — Architecture stratégique précédente (THI-35) : Terminal Sentinel (Phase 5.5) ✅, RBAC complet (Phase 7) ✅, Admin Panel (Phase 9), PWA avancée (Phase finale) — **Epic Web 2026 Compliance** (THI-96) : 6/8 sub-issues livrées (THI-97 → THI-102), reste Desktop a11y + CSS moderne 2026 — **Phase 7b (AI Tutor V1) ✅ COMPLETE + V1.5 SÉQUENCÉE** : THI-115 ✅, THI-109 ✅ (gate-zero guardrail), THI-110 ✅ (keyManager AES-GCM), THI-120 ✅ (Sentry scrubber), **THI-111 ✅ COEUR FONCTIONNEL** (PR #188 — sanitizer + 4 providers + panel + 287 AI tests, audits guardrail 9.4/10 + security 8.8/10 + ui A11y exemplary), **THI-147 ✅ FIX SAFE-AREA iPhone PWA** (PR #189), **panel actif en Production** (`VITE_AI_TUTOR_ENABLED=true` + `VITE_AI_TUTOR_OPENROUTER_MODEL=anthropic/claude-haiku-4-5` activés Production+Preview par @cowork) — **Sprint sécurité 1-2 mai 2026 ✅ CLOS** : audit security-auditor 8.1/10 → ~8.6/10 post-sprint, 11 PRs livrées (#168 à #178), 5 HIGH/MEDIUM Done (THI-133/134/135/137/140), 4 MEDIUM ciblés en backlog (THI-136/138/139/112), agent `route-attack-auditor` créé — **Session 4 mai 2026** : THI-111 livré + THI-147 livré + 5 tickets V1.5 backlog créés (THI-142 lessonContext renforcé HIGH, THI-143 frustration heuristic + détection sémantique user MEDIUM, THI-144 system prompt v1.1.0 + ADR-008 + eval suite MEDIUM, THI-145 chat assistant role-based Phase 9+ LOW, THI-146 modèle défaut Haiku HIGH déjà actif via env var, THI-148 extend tutor scope platform meta-questions V1.0.1 P1 1h30 estimé) — **Posture validée** : pas de rush deadline, qualité/scalabilité/perf non négociables, plan respecté en ordre, **mea culpa explicite** à chaque round trio @thierry / @cc-terminallearning / @cowork (estimation 30 min → 1h30, privacy `userProgress` retirée V1.0.1, hypothèse `transform` mobile réfutée par diagnostic Chrome DevTools MCP) — **Verdict empirique Haiku 4.5 (capturé 4 mai 21h par @cowork via Chrome MCP)** : 5 tests qualitatifs, score moyen **9.3/10** — Test 1 méta-plateforme 8/10, Test 2 fichiers cachés 9/10, Test 3 hallucination 9.5/10, Test 4 frustration 10/10 (Haiku bascule mode direct AUTONOMEMENT), Test 5 jailbreak 10/10. **Reprio backlog tranchée @cowork** : THI-146 ✅ SUCCÈS validé, **THI-142 → Low** (Haiku gère déjà bien le contexte leçon), **THI-143 → Low** (Haiku résout naturellement la frustration via compréhension contextuelle), **THI-148 P1 INCHANGÉ** (scope ≠ modèle prouvé Test 1, GO IMMÉDIAT), THI-144 P2 Medium (peut englober THI-148 + eval suite), THI-145 P3 Low (Phase 9+). **ROI méthode scientifique** : ~4-6h économisées (THI-142/143 reportés V2). **Next ordonné — décision @cowork 5 mai matin** : (1) **THI-150 EN COURS** (12ᵉ agent `mobile-responsive-auditor`, ex-brick 3a de THI-149 epic Done, 11 sections / ≥48 checkpoints + bonus Section 11 Desktop Preservation + checkpoints BUG-FAB-001 visibility/contrast/detachment), (2) THI-151 audit Playwright WebKit + matrice bugs (ex-brick 3b), (3) THI-152 mini-PRs fix séquentielles (ex-brick 3c, critère ABSOLU **ne pas casser desktop**), (4) THI-148 extend tutor scope méta-plateforme V1.0.1 (1h30 honnête, scope statique platformContext, bump v1.0.0→v1.0.1, audit guardrail Règle 10 obligatoire), (5) **THI-144 enrichi P1** system prompt v1.1.0 + ADR-008 + eval suite (intègre 5 micro-frictions identifiées par cross-validation ChatGPT sur session 8 tours @thierry — compound questions, sur-explication internal mechanics, indices répétés, platformContext absent confirme THI-148, conclusion ouverte), (6) THI-112 onboarding AiKeySetup + picker modèle curated, (7) THI-114 Web Worker isolation, (8) THI-145 chat role-based Phase 9+. **THI-142/143 reportés V2** (Haiku 9.3/10 résout 80% naturellement).

---
Expand Down
6 changes: 3 additions & 3 deletions src/app/components/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function Landing() {

const handleShare = async () => {
const url = 'https://terminallearning.dev';
const text = 'Apprends le terminal gratuitement — 11 modules interactifs, Linux / macOS / Windows.';
const text = `Apprends le terminal gratuitement — ${MODULE_PREVIEWS.length} modules interactifs, Linux / macOS / Windows.`;
if (navigator.share) {
try {
await navigator.share({ title: 'Terminal Learning', text, url });
Expand Down Expand Up @@ -396,7 +396,7 @@ export function Landing() {
{MODULE_PREVIEWS.length} modules progressifs
</h2>
<p className="text-[var(--github-text-secondary)] text-center mb-10">
Du système de fichiers à la redirection de flux — deux niveaux, sans prérequis pour commencer.
Du système de fichiers à Git et à l'IA — cinq niveaux, sans prérequis pour commencer.
</p>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
Expand Down Expand Up @@ -436,7 +436,7 @@ export function Landing() {
</div>

<p className="mt-8 text-center text-[var(--github-text-secondary)] text-sm">
11 modules inclus — aucun compte requis.
{MODULE_PREVIEWS.length} modules inclus — aucun compte requis.
</p>
</FadeIn>
</section>
Expand Down
87 changes: 61 additions & 26 deletions src/app/components/landing/TerminalPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,42 @@ import type { SelectedEnvironment } from '../../context/EnvironmentContext';
interface TerminalLine {
type: 'prompt' | 'output';
text: string;
/** Folder the command was typed in, below the home folder ('' = home): the prompt shows it. */
dir?: string;
}

/** A step of the demo; `into` is the folder a `cd` step enters, so the next prompts show it. */
interface Step {
command: string;
output: string[];
into?: string;
}

// ─── Per-environment sequences ────────────────────────────────────────────────
// What a real shell prints (GNU bash, macOS zsh, PowerShell 7): `ls` sorts its
// names and adds no `/` to folders; PowerShell answers Get-Location with a table.

const SEQUENCES: Record<SelectedEnvironment, Array<{ command: string; output: string[] }>> = {
const SEQUENCES: Record<SelectedEnvironment, Step[]> = {
linux: [
{ command: 'pwd', output: ['/home/user'] },
{ command: 'ls', output: ['documents/ downloads/ projects/ notes.txt'] },
{ command: 'cd projects', output: [] },
{ command: 'ls', output: ['documents downloads notes.txt projects'] },
{ command: 'cd projects', output: [], into: 'projects' },
{ command: 'mkdir my-app', output: [] },
{ command: 'ls', output: ['my-app/'] },
{ command: 'ls', output: ['my-app'] },
],
macos: [
{ command: 'pwd', output: ['/Users/user'] },
{ command: 'ls', output: ['Desktop Documents Downloads projects notes.txt'] },
{ command: 'cd projects', output: [] },
{ command: 'ls', output: ['Desktop Documents Downloads notes.txt projects'] },
{ command: 'cd projects', output: [], into: 'projects' },
{ command: 'mkdir my-app', output: [] },
{ command: 'ls', output: ['my-app/'] },
{ command: 'ls', output: ['my-app'] },
],
windows: [
{ command: 'Get-Location', output: ['Path', '----', 'C:\\Users\\user'] },
{ command: 'Get-ChildItem', output: ['Documents Downloads projects notes.txt'] },
{ command: 'Set-Location projects', output: [] },
{ command: 'New-Item -Type Directory my-app', output: [' Directory: C:\\Users\\user\\projects', '', 'my-app'] },
{ command: 'Get-ChildItem', output: ['my-app/'] },
{ command: 'Get-Location', output: ['', 'Path', '----', 'C:\\Users\\user', ''] },
{ command: 'Set-Location projects', output: [], into: 'projects' },
{ command: 'New-Item -ItemType Directory my-app | Out-Null', output: [] },
{ command: 'Test-Path my-app', output: ['True'] },
{ command: 'Get-ChildItem -Name', output: ['my-app'] },
],
};

Expand Down Expand Up @@ -62,22 +73,40 @@ function useReducedMotion(): boolean {
return reduced;
}

/** The folder each step is typed in: home, then wherever the last `cd` went. */
function stepDirs(env: SelectedEnvironment): string[] {
let dir = '';
return SEQUENCES[env].map((step) => {
const here = dir;
if (step.into !== undefined) dir = step.into;
return here;
});
}

function buildStaticLines(env: SelectedEnvironment): TerminalLine[] {
return SEQUENCES[env].flatMap((step) => [
{ type: 'prompt' as const, text: step.command },
const dirs = stepDirs(env);
return SEQUENCES[env].flatMap((step, i) => [
{ type: 'prompt' as const, text: step.command, dir: dirs[i] },
...step.output.map<TerminalLine>((text) => ({ type: 'output', text })),
]);
}

/** Where the demo ends: the folder of the prompt left waiting after the last step. */
function finalDir(env: SelectedEnvironment): string {
const last = SEQUENCES[env].filter((step) => step.into !== undefined).pop();
return last?.into ?? '';
}

// ─── Env-aware prompt renderer ────────────────────────────────────────────────

function PromptSpan({ env }: { env: SelectedEnvironment }) {
// The prompts of the practice terminal (TerminalEmulator.tsx getEnvPrompt), which show the current folder.
function PromptSpan({ env, dir = '' }: { env: SelectedEnvironment; dir?: string }) {
if (env === 'linux') {
return (
<>
<span className="text-emerald-400">user@terminal</span>
<span className="text-[var(--github-text-secondary)]">:</span>
<span className="text-blue-400">~</span>
<span className="text-blue-400">{dir ? `~/${dir}` : '~'}</span>
<span className="text-[var(--github-text-secondary)]">$ </span>
</>
);
Expand All @@ -86,16 +115,15 @@ function PromptSpan({ env }: { env: SelectedEnvironment }) {
return (
<>
<span className="text-violet-400">➜</span>
<span className="text-[var(--github-text-secondary)]"> ~ </span>
<span className="text-violet-300">% </span>
<span className="text-[var(--github-text-secondary)]">{` ${dir ? `~/${dir}` : '~'} `}</span>
</>
);
}
// windows
return (
<>
<span className="text-sky-400">PS </span>
<span className="text-[var(--github-text-primary)]">C:\Users\user</span>
<span className="text-[var(--github-text-primary)]">{dir ? `C:\\Users\\user\\${dir}` : 'C:\\Users\\user'}</span>
<span className="text-sky-400">&gt; </span>
</>
);
Expand All @@ -107,6 +135,7 @@ export function TerminalPreview() {

const [animatedLines, setAnimatedLines] = useState<TerminalLine[]>([]);
const [typingText, setTypingText] = useState('');
const [typingDir, setTypingDir] = useState('');
const [showCursor, setShowCursor] = useState(true);
const containerRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
Expand Down Expand Up @@ -145,12 +174,15 @@ export function TerminalPreview() {

async function runAnimation() {
const sequence = SEQUENCES[selectedEnv];
const dirs = stepDirs(selectedEnv);
while (!cancelledRef.current) {
setAnimatedLines([]);
setTypingText('');
setTypingDir('');

for (const step of sequence) {
for (const [index, step] of sequence.entries()) {
if (cancelledRef.current) return;
const dir = dirs[index];

for (let i = 0; i <= step.command.length; i++) {
if (cancelledRef.current) return;
Expand All @@ -160,7 +192,8 @@ export function TerminalPreview() {

if (cancelledRef.current) return;
setTypingText('');
setAnimatedLines((prev) => [...prev, { type: 'prompt', text: step.command }]);
setAnimatedLines((prev) => [...prev, { type: 'prompt', text: step.command, dir }]);
if (step.into !== undefined) setTypingDir(step.into);

await new Promise<void>((r) => { timeoutRef.current = setTimeout(r, 120); });

Expand Down Expand Up @@ -213,19 +246,21 @@ export function TerminalPreview() {
{lines.map((line, i) => (
<div key={i} className="leading-relaxed">
{line.type === 'prompt' ? (
<div>
<PromptSpan env={selectedEnv} />
// A terminal wraps a long command at the screen's edge, not after a hyphen.
<div className="break-all">
<PromptSpan env={selectedEnv} dir={line.dir} />
<span className="text-[var(--github-text-primary)]">{line.text}</span>
</div>
) : (
<div className="text-[var(--github-text-secondary)] pl-1">{line.text}</div>
// A blank line of output keeps its height (PowerShell frames its tables with them).
<div className="text-[var(--github-text-secondary)] pl-1 whitespace-pre-wrap">{line.text || '\u00a0'}</div>
)}
</div>
))}

{/* Active typing line */}
<div className="leading-relaxed">
<PromptSpan env={selectedEnv} />
<div className="leading-relaxed break-all">
<PromptSpan env={selectedEnv} dir={reducedMotion ? finalDir(selectedEnv) : typingDir} />
<span className="text-[var(--github-text-primary)]">{typingText}</span>
<span
className="inline-block w-[7px] h-[14px] bg-[#e6edf3] align-middle ml-px"
Expand Down
Loading
Loading