Application d'annuaire d'épreuves du secondaire camerounais (6e →
Terminale : séquences, compositions, BEPC, Probatoire, BAC, examens
blancs...) : chaque épreuve est un fichier Markdown + images stocké
dans un stockage objet S3-compatible (Tigris Data en production, par
défaut), classé par
niveau/classe/évaluation/matière/série en base. Navigation publique
Accueil → Niveau → Classe → Catalogue, recherche globale, import massif
(CLI ou zip admin), assistant IA contextuel, abonnements payants
simulés (Orange Money / MTN MoMo), back-office de publication. Voir
CAHIER_DES_CHARGES.md pour la spécification produit et DEPLOIEMENT.md
pour les cibles de déploiement.
React/Vite ──HTTPS──► FastAPI
├── SQLAlchemy → SQLite (dev) / PostgreSQL (prod)
└── Storage Service → bucket S3-compatible (prod) / disque local (dev)
epreuves/{niveau}/{annee}/{epreuve_id}/
├── sujet.md
├── corrige.md
└── images…
- La classification métier (classe, évaluation, matière, séries) vit en
BASE — l'arborescence du stockage ne reflète que
niveau/année/épreuve; - la base ne conserve que les métadonnées +
storage_key, jamais le contenu ; - fichiers privés servis par
GET /api/files/{id}(vérification des droits, URL signées HMAC ; redirection vers URL signée du bucket en production).
Le projet a été refondu conformément à architecture technique.txt et au
prompt d'amélioration : épreuves Markdown + images dans un stockage objet
(local en dev, bucket S3-compatible en prod), classification complète
(niveau/classe/évaluation/séries multi-filières), navigation publique
Accueil → Niveau → Classe → Catalogue avec recherche globale, abonnements
scopés à la classe, import massif (CLI + upload zip admin), barre de
défilement des onglets de l'assistant révélée au survol. Le backend et le
frontend ont été testés en local (uvicorn + smoke tests API + npm run build) — re-tester le parcours de démonstration ci-dessous avant tout
déploiement. La base de développement (backend/data/bacprep.db) a été
régénérée : les tables et le modèle de données changent profondément
(epreuve_files, import_jobs, subscriptions.classe...).
Suite à la revue de code complète, les points suivants ont été corrigés ;
les nouvelles variables sont documentées dans backend/.env.example :
ENV=prod(positionné dansrender.yaml) : cookie de sessionSecure, refus du jeton admin d'exemple (admin123) — à changer de toute façon ;FILE_URL_SECRET: clé HMAC des URLs signées de fichiers — sinon dérivée d'ADMIN_TOKEN; les jetons sont valables 1 h (régénérés à chaque chargement du détail) ;AUTH_MODE=google:/api/auth/mock-loginest REFUSÉ côté serveur (fini la connexion par simple email) ;POST /api/payments/simulate-webhookexige maintenant la session de l'utilisateur propriétaire du paiement ;- rate-limit sur le login admin (5/min/IP, désactivable en test via
LOGIN_RATE_LIMIT=0) ; - catch-all SPA confiné à
frontend/dist(path traversal fermé), SVG refusé à l'upload et servi en pièce jointe.
Limitation connue : la recherche texte est insensible aux accents sous SQLite (dev) mais pas sous PostgreSQL (prod) — à traiter si nécessaire par une colonne normalisée à l'écriture.
Tests : suite pytest backend/tests/ (68 tests — cd backend && .venv/Scripts/python -m pytest tests -q), tests frontend npx vitest run. Le back-office backend est découpé en routers/admin_{misc, epreuves,import}.py ; la règle de couverture d'abonnement a une source
unique (store.covered_epreuves_condition).
Vague d'évolutions alignée sur les pratiques des produits de référence (freemium « vitrine », consentement granulaire type RGPD, back-office centré utilisateurs) — détail complet au module 16 du cahier des charges :
- Épreuves gratuites consultables SANS compte :
GET /api/epreuves/{id}accepte le visiteur pour une épreuve gratuite (aucune consultation enregistrée — l'historique reste une fonctionnalité de compte) ; une épreuve payante renvoie 401 au visiteur (carte « connexion / abonnement » dans le lecteur) et 403 à un connecté non couvert. Assistant, notes, sélection & signalements sont masqués côté lecteur et refusés côté API sans session. - Consentement à la connexion (révocable depuis le profil) :
users.consent_ia/users.consent_notes(NULL = pas encore demandé — modale affichée au premier login). Refus IA → assistant éphémère (aucune persistance, voie SSE sansconversation_id) ; refus notes → fonction masquée + garde serveur 403. Ancienconsent_given_atauto-posé à la création : abandonné. - Admin : lien et page
/adminréservés aux comptes connectés dont l'email est racine (ADMIN_ROOT) ou promu administrateur délégué depuis la table Utilisateurs —is_admincalculé serveur, la liste n'est jamais exposée ; la console reste protégée parADMIN_TOKEN+ liste blanche à chaque appel. Seul le root peut promouvoir/révoquer un admin ; le root ne peut être ni banni, ni supprimé, ni révoqué. Déconnexion automatique uniquement après avoir quitté la console : la page ouverte envoie un battement de cœur toutes les 30 s qui maintient le verrou ; le délaiADMIN_SESSION_TIMEOUT_MINUTES(défaut 3) s'applique une fois la page fermée ou quittée. - Table Utilisateurs du back-office : identité déclarée, profil, consentements, compteurs d'usage (notes, discussions IA, consultations, abonnements actifs, dépenses) — aucune donnée secrète (aucun mot de passe ni code mobile money n'est stocké dans le produit). Actions bannir (session tuée + kick-out WebSocket, login refusé 403), débannir, supprimer (effacement de toutes les données personnelles). Les comptes admin (root ∪ promus) échappent à la modération bannir/supprimer et portent un badge ; le root seul peut promouvoir / révoquer un admin délégué. La métrique « Utilisateurs » exclut les comptes admin.
- Audit & activité explicites : les évènements du journal portent le
détail de l'action (champs modifiés, fichier supprimé, métadonnées de
l'épreuve supprimée…) ; l'id d'épreuve du journal est cliquable (ouvre
l'épreuve dans la section Épreuves) ; l'activité du profil est cliquable
(épreuve → lecteur ; discussion IA → lecteur
?conv={id}qui rouvre le bon onglet). Bug corrigé : les documentssujet.md/corrige.mdn'apparaissent plus comme des images dans le détail admin (réponse séparéeassets(images) /documents). - Forfaits : la page
/abonnementest publique (tarifs consultables en visiteur, souscription réservée aux comptes avec retour automatique) ; chaque forfait est une carte (libellé, prix, description). - Démonstration :
python -m scripts_dev.seed_epreuves_payantesajoute deux épreuves PAYANTES publiées pour éprouver le paywall. Migration :python -m scripts_dev.migrate_2026_09(colonnesusers:consent_ia,consent_notes,banni,banni_motif…).
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --reload --port 8000Au premier démarrage, les 3 épreuves d'exemple sont automatiquement
importées depuis backend/data/epreuves/{sujets,corriges}/.
Vérifier : curl http://localhost:8000/api/health → {"status":"ok",...}.
cd frontend
npm install
npm run devOuvrir http://localhost:5173. Se connecter en mode simulé (n'importe
quel email/nom).
cd frontend && npm run build
cd ../backend && uvicorn app.main:app --port 8000Une fois frontend/dist présent, le backend le sert directement sur /
(voir DEPLOIEMENT.md).
🔑 Obtenir les identifiants des services externes (connexion Google, stockage objet S3-compatible, clés Gemini/Groq, génération des secrets) : guide pas-à-pas dans
DEPLOIEMENT.md.
- Ouvrir
http://localhost:5173SANS se connecter : l'accueil propose la sélection de niveaux (Secondaire cliquable, Primaire verrouillé) et la recherche globale. - Ouvrir une épreuve gratuite : lecture complète, filigrane « Consultation invitée », sans assistant ni notes (fonctions compte).
- Ouvrir une épreuve payante (seed
seed_epreuves_payantes) : carte « connexion / voir les abonnements ». /abonnement: grille des forfaits visible en visiteur ; « Se connecter pour souscrire » ramène à la page après connexion.
- Se connecter (mode simulé, n'importe quel email/nom) : la modale de consentement demande le stockage des conversations IA et des notes (accepter, refuser partiellement, ou refuser tout — modifiable au profil).
- Accueil : la rubrique Consultées récemment apparaît dès la première consultation (fonctionnalité compte).
- Ouvrir une épreuve gratuite puis sélectionner un passage : « Demander » (encre) et « Prendre une note » (vert) ; drapeau rouge pour signaler un problème. Les onglets de l'assistant se dédoublonnent (« Discussion générale 2 »…).
- Choix d'un forfait sur
/abonnement→ paiement simulé (webhook) → ouverture de l'épreuve payante. - Profil → onglet Activité : entrées enrichies (classe, séries) et cliquables (épreuve, discussion) ; rubrique Mes données pour révoquer un consentement.
- Créer au moins une épreuve publiée (étapes 2 et 3 ci-dessous) : une sauvegarde d'un catalogue vide ne démontre rien.
- Se connecter d'abord avec l'email root (
ADMIN_ROOT, ex.admin@votre-domaine.cm— le lien « Admin » n'apparaît que pour les comptes root/promus), ouvrir/adminpuis se connecter avec le jetonADMIN_TOKEN(admin123par défaut). - Créer une épreuve : selects Niveau/Classe/Évaluation, séries par puces
(multi-sélection), sujet/corrigé en Markdown, upload d'images (les
documents
sujet.md/corrige.mdapparaissent en LISTE, séparés des images). - Publier (nécessite un sujet et au moins une série).
- Onglet Import massif : uploader une archive zip d'un dossier
organisé
{annee}/{classe}/{matiere}/*.md— rapport détaillé à la fin (créées, doublons, métadonnées manquantes, erreurs). Alternative CLI :python -m app.scripts.importer --dir backend/data/imports. - Relire les épreuves importées (elles arrivent en brouillon) puis les publier.
- Onglet Utilisateurs : table par élève (consentements, compteurs,
dépenses) avec bannir/débannir/supprimer ; promouvoir/révoquer un
admin délégué (réservé au root) ; le journal d'audit trace tout
(champs modifiés, fichiers supprimés) et l'id d'épreuve y est cliquable.
La session admin se ferme automatiquement après
ADMIN_SESSION_TIMEOUT_MINUTES(défaut 3 min) d'inactivité. - Onglet Sauvegardes : « Créer une sauvegarde maintenant » (barre de progression, puis volume total occupé), puis « Lancer un essai à blanc » sur la sauvegarde créée — le rapport annonce ce qui serait écrit sans rien écrire. L'écriture réelle n'apparaît qu'après, avec sa case de confirmation. Voir « Sauvegardes » plus bas.
L'onglet Sauvegardes du back-office exporte et restaure le catalogue
entier : épreuves, sujets, corrigés, images — et les identifiants, sans quoi les
références /api/files/{id} contenues dans les sujets deviendraient fausses.
Créer une sauvegarde — un clic. Rien n'est filtré (une sauvegarde partielle
semble rassurante et protège mal : il faut alors se souvenir de ce qui n'y est
pas), et la destination est calculée par le serveur — le client ne choisit pas
la clé, ce qui empêcherait d'écraser une autre sauvegarde. Le catalogue de
quelques centaines de fichiers s'exporte en une archive unique ; au-delà de
1 Gio (SAUVEGARDE_SEUILLE_PARTIE_OCTETS), il est découpé en parties
autonomes de 256 Mio (SAUVEGARDE_TAILLE_PARTIE_OCTETS).
Une sauvegarde produite contient :
_sauvegardes/2026-10-01T211422Z-e450d4/
├── LISEZMOI.txt procédure de restauration, embarquée AVEC les données
├── manifest.json état complet et autoritaire du catalogue
├── index.json SHA-256 et taille de chaque partie
└── sauvegarde.zip (ou part-0001.zip, part-0002.zip… au-delà de 1 Gio)
Restaurer se fait toujours en deux temps. Le premier appel est un essai à
blanc : il vérifie l'empreinte de chaque partie, compare les octets lus aux
empreintes du manifeste, et dit ce qu'il écrirait — sans rien écrire. L'écriture
réelle n'est proposée qu'ensuite, et seulement si l'essai s'est terminé sans
anomalie, après confirmation explicite. C'est aussi le serveur qui l'exige :
run_restore refuse une écriture sans confirmation_recue=True.
Deux modes :
| Mode | Effet | Quand |
|---|---|---|
Recharge du stockage (bucket, par défaut) |
Réécrit seulement les fichiers manquants, à leur storage_key exacte. La base n'est pas touchée. Ré-exécutable sans dommage. |
Réparer un stockage dégradé, migrer vers un nouveau bucket |
Restauration complète (disaster) |
Recrée épreuves et fichiers en préservant les identifiants. Refusée si la base contient déjà des épreuves. | Base vide (catastrophe, nouveau serveur) |
Le refus du mode disaster sur une base peuplée n'a pas de contournement :
recréer des lignes par-dessus un catalogue existant, en conservant les
identifiants, produirait des collisions destructrices.
Ce que la restauration garantit :
- Chaque fichier est vérifié par SHA-256 avant d'être écrit. Un fichier dont l'empreinte ne correspond pas est signalé et n'est pas écrit.
- Une restauration qui laisse un fichier de côté n'est pas une restauration : les métadonnées ne sont alors pas écrites du tout, et le rapport nomme les fichiers manquants ou altérés. Le job se termine alors en échec, pas en succès partiel silencieux.
- Les identifiants sont préservés, donc les références
/api/files/{id}des sujets restent valides : aucun contenu n'est réécrit.
Ce qui n'est pas fait, volontairement : aucune sauvegarde programmée, aucune rotation, aucune purge automatique (une politique de rétention finit toujours par effacer la bonne sauvegarde au mauvais moment), et aucun envoi de sauvegarde par l'interface — pour un volume de plusieurs gigaoctets, on récupère les parties puis on pointe la restauration sur un dossier ou un préfixe de stockage.
Le détail du format et la procédure complète sont dans LISEZMOI.txt à la
racine de chaque sauvegarde. L'implémentation et ses décisions sont tracées dans
PLAN_SAUVEGARDES.md.
mon-dossier/
└── 2023/
└── Terminale/
└── Mathématiques/
├── bac-D-sujet.md
├── bac-D-corrige.md
└── figure1.png
Le moteur (backend/app/core/import_service.py) parcourt récursivement,
identifie sujet/corrigé par le nom (corrige → corrigé), déduit
année/classe/matière/série/évaluation de l'arborescence, crée les entrées
en base (brouillon), copie les fichiers vers le stockage définitif,
génère les storage_key, détecte les doublons par SHA-256 et signale les
fichiers aux métadonnées insuffisantes.
Plusieurs séries de corrections ont été apportées après la livraison initiale, au fil de retours d'usage successifs. Détail chronologique ci-dessous (les problèmes les plus significatifs, avec leur cause identifiée quand elle a pu être déterminée avec certitude).
- Contraste au survol — les boutons "Assistant", "Demander à l'assistant"
et "Connexion avec Google" pouvaient devenir illisibles au survol en mode
sombre. Corrigé en déclarant
color-schemeexplicitement (:root/.dark) et en répétant les couleurs de texte sur:hover/:focus-visiblepour chaque bouton à couleurs fixes ; le bouton Google suit désormais lui-même le thème actif (filled_blacken sombre,outlineen clair). - Panneau assistant adaptatif + disposition — le panneau suit le thème clair/sombre de la page (il ne reste plus toujours sombre). Sur bureau, il s'ouvre à côté du contenu (colonne latérale) plutôt qu'en recouvrement par-dessus ; sur mobile, feuille modale plein écran.
- Refonte des bulles de discussion — rendu Markdown/LaTeX/tableaux qui ne dépasse plus jamais de la bulle. La sélection de texte dans le lecteur envoie le Markdown brut du passage (formules, tableaux, images comprises) à l'assistant plutôt que le texte affiché nettoyé.
- Badges compacts — une épreuve multi-filières affiche un seul badge
listant toutes ses filières (ex.
A,C,E) ; le badge "corrigé disponible" devient une puce icône + texte court. - Images dans le contexte envoyé à l'assistant — si le passage sélectionné contient une image, le backend la transmet en pièce jointe à Gemini (fournisseur multimodal) ; avec Groq (texte seul), une note explicite signale qu'une image a été ignorée.
- Persistance du verrou admin — la session admin unique est stockée en
base (table
admin_lock) plutôt qu'en mémoire : un redémarrage du backend ne libère plus silencieusement l'accès administrateur. - Correctifs supplémentaires — bug d'arithmétique de dates (SQLite
perdait le fuseau horaire, corrigé par un type
UTCDateTimedédié), sélection de texte cassée par unuser-select: nonetrop large, double création de conversation à l'ouverture du panneau (garde React contre le double-effet de StrictMode), anneau de focus clavier ajouté (absent auparavant), navigation clavier complète dans les listes déroulantes, abonnement proposé à tort pour une épreuve déjà gratuite/couverte (nouveau contrôleGET /api/subscriptions/deja-couvert).
api.get()ignorait tout en-tête personnalisé — bug trouvé : le client HTTP frontend n'acceptait un en-tête (X-Admin-Session) que surpost/put/del, jamais surget. Résultat :GET /api/admin/epreuvespartait sans jeton et échouait en 401 juste après une connexion admin pourtant réussie (200). Corrigé, et l'appel.enva aussi été rendu robuste (load_dotenv()sans chemin explicite ne trouvait.envque siuvicornétait lancé depuisbackend/— désormais résolu en absolu).- Réponses en streaming, activé par défaut — l'assistant répond maintenant au fur et à mesure de sa génération. Voir section dédiée ci-dessous.
- Documentation complète (docstrings) ajoutée sur l'ensemble des fonctions backend restées sans commentaire.
- Miniatures d'images cassées en admin — bug trouvé : les vignettes
utilisaient des URL relatives (
/media/...), qui pointent vers le frontend en développement (origine différente du backend) plutôt que vers le fichier réel. Corrigé via un helperresolveMediaUrlréutilisé aussi dans le rendu Markdown du lecteur (même bug affectait les images insérées dans une épreuve). - Bascule "Rendu" de l'admin n'affichait pas de Markdown rendu — elle
injectait le texte brut tel quel dans une simple
<div>sans jamais appeler le moteur de rendu. Corrigé (utilise désormaisMarkdownContent). - Boutons × (retirer, supprime le fichier et la balise Markdown) et + (insérer/réinsérer la balise) ajoutés sur chaque vignette d'image.
- Système de notifications toast (haut-droite) remplaçant les messages de confirmation auparavant affichés en haut de formulaire (peu visibles) ; appliqué aux actions admin et aux connexions/déconnexions.
- Barre de recherche + limite de chargement (30 par défaut) dans la liste latérale du back-office ; carte sélectionnée mise en surbrillance.
- Pagination du catalogue (24 par page + "Voir plus") ; horodatage relatif ("Consulté il y a 3 j") remplaçant la mention statique "Déjà consultée".
- Ajout d'un état
:activevisible au clic et surtout d'uncursor: pointerexplicite sur tous les éléments cliquables — un<button>HTML n'a pas ce curseur par défaut dans la plupart des navigateurs (contrairement à une idée reçue), ce qui expliquait en grande partie l'impression d'interface "morte" au clic.
- Nouveau bug de contraste identifié et corrigé —
text-highlight-inkest un token fixe pensé pour du texte posé sur le fondhighlight(jaune vif, ne change pas de teinte entre les deux thèmes) ; il était utilisé par erreur surbg-highlight-soft(qui, lui, devient très sombre en mode sombre), rendant le texte quasi illisible. Touchait la bulle de message utilisateur, le bouton "Voir tout" du contexte, et la carte "Paiement en attente" de la page Abonnement. Remplacé partext-ink(adaptatif) partout où c'était le cas. - Avatars (élève / assistant) ajoutés de part et d'autre des bulles de discussion.
- "Copier dans le chat" — comportement différencié selon que le panneau assistant est ouvert ou fermé lors d'une nouvelle sélection de texte : fermé → nouvelle discussion (comme avant) ; ouvert → colle le texte dans le champ de saisie sans jamais toucher au contexte de la discussion en cours (une version intermédiaire avait, à tort, remplacé ce contexte).
- Bug vérifié et corrigé : recherche insensible aux accents dans le
back-office (
GET /api/admin/epreuves?q=...). Cause identifiée avec un test autonome : la fonctionLOWER()native de SQLite est ASCII-only (LOWER('Éducation')reste'Éducation'), ce qui cassait silencieusement toute recherche.ilike()sur du texte accentué. Une fonction Python (Unicode-correcte) est désormais enregistrée à la place au niveau de la connexion SQLite. - Nouveau statut d'accès "Ouvert" (cadenas ouvert, à côté de
"Gratuit"/"Payant") : une épreuve payante déjà couverte par un
abonnement actif de l'utilisateur l'affiche, calculé côté frontend à
partir de
GET /api/subscriptions/mine(voirlib/access.ts). - Carte récapitulative de la page Abonnement redessinée (bandeau prix + phrase de synthèse + tuiles à icônes) plutôt qu'un tableau à deux colonnes ; ajout de liens "Ouvrir l'épreuve"/"Voir le catalogue" et "Aller à mon profil" après confirmation du paiement.
- Avatar à initiales (ex. "FA" pour "Franck Albert") remplaçant l'icône générique dans l'en-tête et la page profil.
- Placeholders explicites ajoutés sur tous les champs de formulaire (admin, connexion élève et admin).
- Formulaires (connexion admin, édition d'épreuve) enveloppés dans de
vraies balises
<form>: la touche Entrée ne déclenchait aucune soumission auparavant, faute de formulaire réel.
- LaTeX toujours cassé sur certaines réponses, cause identifiée :
remark-mathtraite$$...$$comme un CONSTRUIT DE BLOC (à la manière d'un bloc de code), pas comme un délimiteur utilisable au milieu d'un paragraphe — un modèle qui écrit une formule$$...$$sans saut de ligne isolant ce bloc produit un texte que le moteur ne reconnaît pas comme formule et laisse tel quel (backslashes compris). Corrigé en forçant, côté frontend, chaque bloc$$...$$à être isolé sur ses propres lignes entourées de lignes vides, quelle que soit la manière dont le modèle l'a formaté à l'origine (voirlib/latex.ts). - Bug de défilement des onglets de discussion, cause identifiée et
corrigée : l'en-tête du panneau assistant est passé d'une disposition
flexbox (
flex-1 min-w-0, qui pouvait laisser toute la ligne déborder plutôt que confiner le défilement au bon élément selon les navigateurs) à une grille CSS (grid-cols-[minmax(0,1fr)_auto_auto_auto]), un motif plus robuste pour ce cas précis. Un nouvel onglet créé fait aussi maintenant défiler automatiquement la barre pour le rendre visible. - Bug de collage intempestif du texte sélectionné, cause identifiée et
corrigée : un panneau assistant fraîchement ouvert réappliquait à tort
la dernière sélection de texte au champ de saisie, même sans nouvelle
sélection — la référence de garde (
noncede la dernière sélection traitée) démarrait ànullsur chaque nouveau montage, alors que le signal reçu en prop pouvait déjà être non-nul (valeur laissée par une interaction précédente). Corrigée par une double protection : la référence est désormais initialisée avec le nonce déjà présent au montage (ne traite que les nonces réellement nouveaux), et le parent (ViewerPage) réinitialise aussi ce signal à chaque fermeture du panneau. - Champ de saisie du chat devenu multi-ligne (Entrée envoie, Maj+Entrée insère un saut de ligne), avec hauteur qui grandit jusqu'à un maximum.
- Recherches insensibles aux accents généralisées ("éducation" trouve
"Éducation") : le correctif SQLite de la série 4 va plus loin
(décomposition Unicode + suppression des diacritiques, pas seulement la
casse) et un équivalent JavaScript (
lib/text.ts,foldText) est utilisé côté frontend pour les recherches locales (menus déroulants, sélecteur d'épreuve de la page Abonnement). - Filtre "Accès" du catalogue enrichi d'un quatrième statut, "Ouvert" (en plus de Tous/Gratuit/Payant).
- Bugs connus du README corrigés :
- Isolation des blocs
$$...$$:lib/latex.tsréécrit en machine à états ligne par ligne (les blocs de code fencés restent intacts, plusieurs blocs par ligne gérés, ordre de lecture conservé,$$non refermé restitué tel quel) — couverte par tests vitest. - Filtre "Ouvert"/"Payant" : résolu côté SERVEUR avant pagination
(sous-requête SQL miroir de
store.has_access) ; le catalogue n'applique plus de filtrage client post-pagination — chaque page affiche désormais exactementPAGE_SIZEcartes quand il y a assez de résultats.
- Isolation des blocs
- Polish UI systématisé : règles de design documentées en tête de
index.css(arrondis, échelle z-index) ; badges unifiés viaMetaBadge(variantes tag/pill) ; couleurs hors tokens éradiquées (pastilles opérateurs, ombre du lanceur assistant →.halo-highlightsur token,text-white→text-paper) ; libellés de filtres non répétés ; tailles mono harmonisées ; verrous de contraste mutualisés (.verrou-*) ; filigrane avecuseId(). - Accueil en deck séquentiel : cartes de niveaux empilées (effet paquet
de copies) ; le choix d'un niveau révèle les cartes de classes (bouton
retour) — plus tout affiché en même temps ;
prefers-reduced-motionrespecté. - Assistant nommé "Tuteur IA Prep" : en-tête dédié, réponses signées "Assistant Pédagogique", bulles avec nom + avatar initiales (élève) / robot, onglets auto-renommés à la première question, contexte entièrement repliable pour laisser la place à la discussion.
- Notes personnelles : sélectionner un passage → « Prendre une note » ; les réponses de l'assistant peuvent être sauvegardées en note depuis le panneau ; consultation/modification/suppression dans le profil (onglet « Mes notes »). Tables et endpoints dédiés (persisté en base).
- Profil étendu + activité : niveau, classe, établissement renseignables ; onglet « Activité » (connexions, consultations, abonnements, paiements, notes, discussions IA).
- Signalements : bouton drapeau dans le lecteur (motif + message) ; traitement dans le back-office (onglet « Signalements », marquer résolu).
- Raccourcis clavier du lecteur : S = sujet, C = corrigé, N = nouvelle note (ignorés dans les champs de saisie) ; icônes sur le switch Sujet/Corrigé.
- Back-office enrichi : puces de statut avec compteurs ("Brouillon
(50)"...), vignettes images avec dimensions/poids (
object-contain), console de journal EN DIRECT pendant l'import massif, statistiques complétées (stockage objet, revenus par mois, consultations, notes, discussions IA) avec graphiques maison (barres + anneau SVG), journal d'audit complet (action, auteur, détail). - Divers : skeletons de chargement (catalogue, lecteur) ; extraits de
2 lignes + métadonnées sur les cartes du catalogue ; descriptions des
portées d'abonnement ; migration idempotente
backend/scripts_dev/migrate_2026_09.py(nouveaux champs + backfill extraits/dimensions) ;httpx→httpx2(dépréciation du TestClient).
L'assistant IA répond désormais au fur et à mesure de sa génération
plutôt qu'en un seul bloc après une attente silencieuse — perçu comme
nettement plus réactif, en particulier sur une connexion mobile lente
(persona élève camerounais, voir CAHIER_DES_CHARGES.md).
- Backend : nouvel endpoint
POST /api/assistant/ask/stream(Server-Sent Events,text/event-stream), qui consomme les endpoints de streaming natifs de Gemini (:streamGenerateContent?alt=sse) et de Groq (API compatible OpenAI,stream: true) — les deux sont supportés par l'ensemble de leur catalogue de modèles de chat, pas seulement certains modèles précis. L'endpoint historiquePOST /api/assistant/ask(réponse complète, non-streaming) reste disponible pour compatibilité. - Frontend :
AssistantPanelutilise le streaming par défaut (lib/streaming.ts), affichant le message de l'assistant qui se remplit progressivement, avec réconciliation finale sur l'état exact persisté côté serveur une fois le flux terminé. - Limite connue : la liste des modèles supportant le streaming
(
STREAMING_UNSUPPORTED_MODELSdansbackend/app/core/assistant.py) n'a pas pu être vérifiée en direct contre la documentation officielle des limites de débit de Groq au moment de la rédaction (environnement sans accès réseau sortant) — à revérifier avant mise en production si un modèle Groq non standard est configuré viaGROQ_MODELS.
| Limitation | Détail |
|---|---|
| Chiffrement au repos | Contenu stocké en clair dans le stockage objet — à traiter avant un usage au-delà de la démonstration. |
| Mode hors-ligne | Hors périmètre de ce prototype (voir CAHIER_DES_CHARGES.md §7 pour la solution envisagée en phase suivante). |
| Paiement réel | Simulé par un bouton ; voir PAIEMENT.md pour l'intégration Notch Pay/Monetbil réelle. |
| Ingestion PDF automatique | Hors périmètre : les épreuves sont exclusivement Markdown + images (voir prompt d'amélioration). |
| Application Android (Capacitor) | Hors périmètre de ce prototype ; note d'intégration dans PROMPT_DESIGN_BULLETIN_OFFICIEL.md §7. |
| Comptes admin multi-rôles | Un seul niveau "admin", restreint par liste d'emails. |
Isolation automatique des blocs $$...$$ |
lib/latex.ts normalise les délimiteurs en machine à états (tests vitest) — reste best-effort, pas un vrai parseur LaTeX : des constructions très inhabituelles peuvent encore échapper. |
Voir PROMPT_RECONSTRUCTION.md section 3 pour l'arborescence de référence
que ce projet suit.
CAHIER_DES_CHARGES.md— spécification produit complète.PROMPT_RECONSTRUCTION.md— prompt de reconstruction (architecture, backend, modèle de données, tests).PROMPT_DESIGN_BULLETIN_OFFICIEL.md— spécification visuelle faisant autorité (remplace la section 6.1 du document précédent).DEPLOIEMENT.md+render.yaml— déploiement, y compris l'obtention pas-à-pas des identifiants externes (connexion Google, stockage objet S3-compatible, clés Gemini/Groq, génération des secrets).PAIEMENT.md— intégration du paiement réel.