No description
  • JavaScript 94.6%
  • CSS 4.9%
  • HTML 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-23 20:57:59 +02:00
public v0.11.1 — version unique lue depuis index.html 2026-09-23 20:57:59 +02:00
.gitignore Dépose config.local.js dans le dépôt 2026-09-17 00:49:35 +02:00
.gitlab-ci.yml Dépose config.local.js dans le dépôt 2026-09-17 00:49:35 +02:00
LICENSE Ajouter un fichier 2026-05-07 21:01:52 +00:00
README.md Update 3 files 2026-05-14 22:18:23 +00:00

📘 Livret Scolaire

PWA pour créer des livrets scolaires de la PS au CM2, basée sur le programme officiel de l'Éducation Nationale française.

Capture une photo, choisis une compétence, valide en 4 niveaux, ajoute une activité et une observation — c'est tout. L'enfant a son livret de réussites tout au long de l'année (et au-delà).

✨ Fonctionnalités

  • Programme officiel intégré : compétences PS-MS-GS, CP-CE1-CE2, CM1-CM2 (BO 2015 modifié + BO 31 octobre 2024 pour français/maths cycles 1 et 2)
  • Compétences personnelles : ajoute tes propres compétences au vol si tu en as besoin
  • Capture photo directe depuis le téléphone (compression auto pour limiter le poids)
  • 4 niveaux d'évaluation : Non acquis / En cours / Acquis / Dépassé
  • 3 champs séparés par observation : Attendu (compétence officielle), Activité, Observation
  • Photo de l'élève sur la couverture du livret PDF
  • Multi-enseignants : système de tags auteurs pour les co-interventions, choix d'un enseignant principal pour la couverture
  • Multi-années : chaque élève peut suivre l'évolution sur plusieurs années
  • Multi-classes : organisation par classe avec filtre rapide
  • Export PDF imprimable complet : couverture, introduction aux parents, observations regroupées par domaine, page de commentaire général
  • 4 styles de fond au choix pour le PDF : Minimal, Géométrique (filigrane), Couleur par domaine, Bordure décorative (arabesques)
  • Génération par période scolaire (P1 à P5) ou année complète, avec dates ajustables
  • Configuration école : nom, code postal, ville (apparaissent sur la couverture)
  • Texte d'introduction personnalisable : adresse-toi aux parents avec tes propres mots
  • Synchronisation : export/import JSON manuel + sync WebDAV avec Nextcloud
  • PWA installable : ordinateur, tablette, téléphone, fonctionnement hors-ligne
  • Aucune dépendance externe : tout est local, aucun tracker, aucune analytique
  • RGPD-friendly : les données restent sur ton appareil (sauf si tu choisis de synchroniser sur ton Nextcloud)

🚀 Installation

En ligne (GitLab Pages)

  1. Ouvre l'URL dans ton navigateur
  2. Sur mobile : "Ajouter à l'écran d'accueil"
  3. Sur ordi : icône d'installation dans la barre d'adresse

Hébergement perso

Décompresse l'archive dans n'importe quel dossier servi en HTTPS (GitLab Pages, serveur perso, Apache…). C'est une PWA statique : pas de backend, rien à installer côté serveur.

📋 Utilisation rapide

  1. Premier lancement : indique ton prénom (utilisé comme tag auteur sur tes observations)
  2. Réglages :
    • Renseigne le nom et l'adresse de l'école
    • Personnalise le texte d'introduction du livret (ou laisse celui par défaut)
    • Définis l'enseignant principal (qui apparaîtra sur la couverture)
  3. Onglet Élèves → bouton + Ajouter pour créer un élève (prénom, nom, niveau, classe, année, photo)
  4. Bouton flottant 📷 pour saisir une nouvelle observation :
    • Choisis l'élève (ou ouvre sa fiche d'abord)
    • Prends la photo de la réussite
    • Sélectionne la compétence (= "Attendu", recherche par mot-clé)
    • Précise l'activité menée (optionnel)
    • Marque le statut : Non acquis / En cours / Acquis / Dépassé
    • Ajoute une observation si besoin
  5. Fiche élève → Exporter PDF : choisis la période et le style de fond, c'est imprimé !

📄 Export PDF du livret

Le livret PDF généré contient :

  1. Couverture : niveau, "Cahier de Réussites", période, NOM Prénom de l'élève, photo, enseignant principal, école, année scolaire
  2. Introduction : texte adressé aux parents (personnalisable dans les réglages)
  3. Sections par domaine : une page par grand domaine du programme, avec sous-domaines et observations chronologiques. Chaque carte affiche : Attendu / Activité / Observation, badge coloré du statut, date, et photo si présente
  4. Commentaire général : page finale avec zone vide à remplir à la main pour un mot personnel

4 styles de fond : choix au moment de l'export, le dernier choix est mémorisé.

🔄 Synchronisation Nextcloud

Dans Réglages → Synchronisation Nextcloud :

  • URL du serveur : https://cloud.exemple.fr
  • Nom d'utilisateur : ton login Nextcloud
  • Mot de passe : utilise un mot de passe d'application (Paramètres Nextcloud → Sécurité)
  • Fichier : nom du fichier JSON sur ton drive (par défaut livret-scolaire.json)

La sync télécharge le fichier distant, fusionne avec les données locales (la version la plus récente l'emporte par lastModified), et ré-uploade le résultat.

💾 Sauvegarde JSON

Dans Réglages → Sauvegarde JSON : exporte un fichier .json complet à tout moment (élèves, observations, photos, enseignants, classes, configuration). L'import fusionne avec l'existant.

🗂️ Structure du projet

livret-scolaire/
├── index.html               Point d'entrée
├── manifest.webmanifest     Manifest PWA
├── service-worker.js        Cache hors-ligne
├── css/styles.css           Design mobile-first
├── js/
│   ├── programme.js         Compétences cycle 1 (maternelle)
│   ├── programme-c2c3.js    Compétences cycles 2 et 3
│   ├── utils.js             Utilitaires (XSS, dates, modales, compression, périodes)
│   ├── db.js                Stockage IndexedDB
│   ├── nextcloud.js         Sync WebDAV
│   ├── pdf-export.js        Export PDF imprimable (4 thèmes, périodes)
│   └── app.js               Logique principale
└── icons/                   Icônes PWA (SVG + PNG 192/512)

🔒 Sécurité et confidentialité

  • CSP strict : aucune ressource externe chargée
  • XSS protection : toute donnée utilisateur passe par esc() avant insertion DOM
  • Pas de tracker : aucune analytique, aucune télémétrie
  • Stockage local : IndexedDB, isolé par origine et navigateur
  • Sync optionnelle : tu choisis ton serveur Nextcloud, ou tu n'utilises rien

🛠️ Technique

  • HTML/CSS/JS vanilla, zéro dépendance externe, zéro framework
  • IndexedDB pour stockage (jusqu'à plusieurs Go selon le navigateur)
  • Photos compressées en base64 (canvas, JPEG 1200px max, qualité 0.8 ; 600px pour les photos d'élèves)
  • Export PDF via window.print() sur template HTML+CSS+SVG inline (pas de jsPDF)
  • WebDAV pour Nextcloud (PUT/GET d'un fichier JSON unique)
  • Service worker cache-first pour mode hors-ligne
  • Le thème "Couleur par domaine" utilise color-mix() (CSS récent — Chrome 111+, Firefox 113+, Safari 16.2+)

📝 Changelog

v1.7.4

📘 Page tutoriel autonome (pour les nouveaux utilisateurs)

Nouveau bouton ? dans le header de l'app, ouvre une page tutoriel plein écran à destination des collègues qui découvrent Livret Scolaire.

Contenu : tour complet de l'app en 10 sections avec sommaire navigable :

  1. Premiers pas (enseignant, classe, élèves)
  2. Saisir une observation (parcours type, statuts, compétences perso)
  3. Les photos (cycle de vie, cache local 200 Mo, quota)
  4. Périodes scolaires (bornes par défaut, personnalisation)
  5. Générer un livret PDF (un élève, classe entière, commentaires généraux)
  6. Synchroniser entre vos appareils (Nextcloud, mot de passe maître, multi-appareils)
  7. Mode hors-ligne (service worker, IndexedDB, retour de connexion)
  8. Effacer un élève (suppression normale vs RGPD)
  9. Astuces et bonnes pratiques (saisie au fil de l'eau, qualité vs quantité, backup)
  10. À propos (GPL v3, ICEM, Forge des Communs)

Détails techniques :

  • Nouveau fichier js/tutoriel.js avec window.Tutoriel.afficher() / fermer()
  • Overlay plein écran, fermeture par clic sur la croix, clic extérieur, ou touche Échap
  • Sommaire avec ancres pour navigation rapide
  • Encarts colorés pour les astuces (💡) et avertissements (⚠️)
  • Style responsive (mobile et desktop), CSP-compatible (pas de style inline)
  • Ton générique et pédagogique, sans jargon technique pour les non-tech
  • Mentionne explicitement le caractère libre/GPL et l'absence de tracking

v1.7.3

🚨 Effacement RGPD d'un élève + 🐛 fix critique sync commentaires

Fix critique : commentaires généraux

Bug découvert pendant l'écriture du RGPD : la clé des commentaires généraux est eleveId:periode (ex: 12:P1), mais le code de sync utilisait eleveId-periode (avec un tiret) pour le filtrage. Conséquence : les commentaires généraux n'étaient probablement jamais inclus dans le fichier de sync de l'élève depuis la refonte v1.6 (sync multi-fichiers). Bug présent dans :

  • db.js lastModifiedEleve() → ne prenait pas en compte les commentaires pour calculer la fraîcheur
  • db.js exportPourSyncMultiFichiers() → les commentaires n'étaient jamais ajoutés au fichier élève

Si tu as des commentaires généraux qui ne se retrouvent pas sur tes autres appareils, c'est ce bug. Il est corrigé en v1.7.3, et au prochain push tes commentaires seront synchronisés.

Nouvelle fonctionnalité : effacement RGPD

Bouton ajouté dans Réglages → Zone dangereuse → Effacement RGPD d'un élève.

Pour répondre à une demande de droit à l'effacement (RGPD article 17) :

  1. Sélectionne l'élève dans le déroulant (vivants ET déjà supprimés)
  2. Confirmation 1 : alerte explicative
  3. Confirmation 2 : tape exactement le prénom de l'élève (anti-clic-accidentel)
  4. Action automatique :
    • Téléchargement de chaque archive quotidienne sur Nextcloud
    • Retrait de l'élève + ses obs + ses commentaires de chaque archive
    • Ré-upload de l'archive nettoyée
    • Suppression immédiate de eleves/eleve-X.json et photos/eleve-X/
    • Soft-delete local + sync forcée pour propager le tombstone
    • Nettoyage du cache photos local

Différence avec une suppression normale :

Action Suppression normale Effacement RGPD
Photos binaires .jpg Effacées immédiatement Effacées immédiatement
Fichier eleves/eleve-X.json Tombstone 30 jours puis purge Effacé immédiatement
Dossier photos/eleve-X/ Tombstone 30 jours puis purge Effacé immédiatement
Archives quotidiennes Rotation auto (10 max) → naturelle Nettoyées une par une
Cache photos local Auto-évincé par LRU Effacé immédiatement
Tombstone propagé aux autres appareils Oui, 30 jours Oui, 30 jours

Pour le backup PRE-MIGRATION

Le fichier livret-scolaire-PRE-MIGRATION-*.json est un événement unique (créé une seule fois lors du passage v1.5 → v1.6). Il n'est pas géré par le bouton RGPD. Si tu en as un et qu'il contient l'élève, tu dois le supprimer manuellement dans l'interface Nextcloud.

v1.7.2

🧹 Purge automatique des fichiers d'élèves supprimés (option 2)

Le contexte

Avant cette version, quand on supprimait un élève :

  • ✅ Les photos binaires (.jpg) étaient supprimées immédiatement de Nextcloud
  • ✅ Le tombstone était propagé aux autres appareils
  • 🟡 MAIS le fichier eleves/eleve-X.json continuait à exister (avec deleted=true)
  • 🟡 Et le dossier vide photos/eleve-X/ restait aussi
  • 🟡 Au cleanup tombstones (>30j local), les fichiers Nextcloud n'étaient pas supprimés → fuite à long terme

Sur 30 ans de carrière avec 25 élèves/an, ça représente ~6 Mo de fichiers fantômes. Pas grave en pratique, mais pas propre.

Le fix : option 2 (suppression différée à 30 jours)

Le cleanup local des tombstones (cleanupTombstones dans db.js) renvoie maintenant la liste des IDs d'élèves dont le tombstone vient d'être effacé. Au démarrage de l'app, après ce cleanup, on appelle la nouvelle fonction purgerFichiersEleves(ids) côté Nextcloud qui :

  1. DELETE de eleves/eleve-X.json
  2. DELETE du dossier photos/eleve-X/ (vide à ce stade)

Au bout de 30 jours, plus aucune trace dédiée à un élève supprimé sur Nextcloud :

  • ✅ Fichier élève : effacé
  • ✅ Dossier photos : effacé
  • ✅ Index global : tombstone retiré (déjà géré)

Pourquoi 30 jours ? C'est le délai du cleanup tombstones existant. Il laisse le temps à tous tes appareils de voir la suppression avant qu'on fasse le ménage côté serveur.

Ce qui reste à 30 jours (RGPD)

  • Archives quotidiennes : rotation à 10 archives → si tu utilises l'app tous les jours, après 10 jours, plus aucune archive ne contient l'élève vivant. Si tu n'utilises pas l'app pendant un mois (vacances), une archive ancienne peut survivre.
  • Backup PRE-MIGRATION (créé une seule fois lors du passage v1.5 → v1.6) : reste à vie. Pour purger : suppression manuelle dans Nextcloud.

Pour un nettoyage RGPD strict (incluant les archives), une fonctionnalité dédiée pourrait être ajoutée plus tard.

v1.7.1

🐛 Fix cache photos vide après sync + 🔧 quota 200 Mo + 📊 affichage diagnostic

  • Fix bug : dans extraireEtUploaderPhotos, la data-url était supprimée AVANT d'être mise en cache → photosCache restait vide après chaque sync. Conséquence : impossible de voir une photo récemment synchronisée hors-ligne. Fix : sauvegarde de la data-url dans une variable temporaire avant suppression.
  • Quota cache photos : éviction LRU automatique à 200 Mo, appelée après chaque sync push réussie. Constante exposée en window.QUOTA_PHOTOS_CACHE_BYTES (modifiable en un seul endroit).
  • Diagnostic : affichage de la taille actuelle du cache + nombre de photos + pourcentage du quota dans Réglages → Diagnostic.
  • Bouton "Vider le cache photos" : action manuelle sans toucher aux données ni à Nextcloud (les photos restent disponibles, juste re-téléchargées à la consultation suivante).
  • Logs : entrée cache-evict dans le journal de sync quand des photos sont évincées (avec nombre et quota).

v1.7.0

📷 Photos en fichiers binaires séparés (refonte stockage)

Le problème

v1.6 séparait les fiches élèves en fichiers JSON individuels, mais les photos restaient encodées en base64 à l'intérieur de chaque fichier élève. Avec 50 photos × 1 Mo par élève, modifier une seule observation entraînait le ré-upload des 50 Mo. Sur 4G c'était 30 secondes par sauvegarde.

Le fix

Les photos deviennent des fichiers binaires individuels sur Nextcloud :

livretscolaire/
├── livret-scolaire.json         ← config globale (~50 Ko)
├── eleves/
│   └── eleve-12.json            ← Léa : identité + obs sans photo (~50 Ko)
├── photos/
│   └── eleve-12/
│       ├── m9k3p4ax2q.jpg       ← une photo = un fichier binaire (~700 Ko en JPEG)
│       ├── m9k3p4cy7z.jpg
│       └── ...
└── archives/...

L'observation contient juste une référence :

obs.photoRef = "photos/eleve-12/m9k3p4ax2q.jpg"

Bénéfices concrets :

  • Ajouter 1 obs avec photo → upload 1 fichier élève (~50 Ko) + 1 photo binaire (~700 Ko). Total : 750 Ko au lieu de 50 Mo. 66× plus rapide.
  • Modifier une obs sans toucher la photo → upload 50 Ko (fichier élève seul). Avant : 50 Mo.
  • Stockage binaire : pas d'overhead base64 (~33% d'économie de bande passante)

Comment ça marche

Saisie : tu prends une photo, elle est stockée temporairement en data-url dans IndexedDB local (obs.photo). Visible immédiatement, même hors-ligne.

À la sync :

  1. Pour chaque photo obs.photo sans photoRef → upload binaire vers Nextcloud, génération de la photoRef, vidage de la data-url locale
  2. Le fichier élève contient désormais juste les références (~50 Ko au lieu de 50 Mo)
  3. Suppression cascade : si tu supprimes une obs, sa photo est aussi supprimée du serveur

À l'affichage : un MutationObserver détecte les <img data-photo-obs-id="...">, résout en cascade :

  1. Si data-url locale → affichage immédiat
  2. Sinon, si déjà en cache local IndexedDB → affichage immédiat
  3. Sinon, fetch depuis Nextcloud + mise en cache

Mode hors-ligne :

  • L'app continue à fonctionner normalement (saisie, navigation, observations sans photo)
  • Photos prises hors-ligne : visibles immédiatement (data-url temporaire)
  • Photos déjà synchronisées (sur Nextcloud) : visibles uniquement si déjà en cache local OU avec connexion (acceptable car wifi en classe)

Détails techniques

  • Nouveau store IndexedDB photosCache (clé = photoRef, valeur = {dataUrl, taille, ajouteeLe, derniereLecture}) avec éviction LRU
  • DB version 3 → 4
  • Nouveau module js/photo-manager.js :
    • genererPhotoId() / genererPhotoRef(eleveId, photoId) : identifiants uniques
    • dataUrlVersBlob() / blobVersDataUrl() : conversions
    • obtenirUrlPhoto(obs) : résolution en cascade
  • Nouvelles fonctions nextcloud.js :
    • uploadPhoto() : PUT binaire (avec création auto du sous-dossier élève)
    • downloadPhoto() : GET binaire → data-url
    • extraireEtUploaderPhotos() : appelée avant chaque pushMulti
    • nettoyerPhotosObsoletes() : DELETE des photos remplacées ou orphelines
    • supprimerPhoto() : DELETE simple (best-effort)
  • Adaptation des vues d'affichage (cards, formulaire d'obs) : pattern data-attribute + résolution post-render
  • Adaptation pdf-export.js : pré-chargement des photos via PhotoManager.obtenirUrlPhoto avant la capture html2canvas

Migration et rétrocompatibilité

Les obs existantes avec obs.photo en data-url restent fonctionnelles tant qu'elles ne sont pas re-syncées. À la première sync depuis v1.7, chaque photo est automatiquement extraite + uploadée comme fichier binaire individuel + référencée par photoRef. Pas d'action utilisateur requise, pas de perte de données.

v1.6.0

🏗️ Refonte architecturale : sync multi-fichiers (un fichier par élève)

Le problème

Avant v1.6, toutes les données (élèves, observations, photos en base64) étaient sérialisées dans un seul JSON livret-scolaire.json sur Nextcloud. Avec une classe bien remplie, ça commençait à coincer :

Photos par élève    Total fichier   Verdict
──────────────────────────────────────────
~5 photos            ~15 Mo         ✅ ok
~10 photos           ~90 Mo         ⚠️  limite Nextcloud
~20 photos           ~180 Mo        ❌ casse (timeout, RAM)
~50 photos           ~450 Mo        ❌ inutilisable

3 problèmes en cascade : limite d'upload Nextcloud (typiquement 100-512 Mo), saturation IndexedDB (iOS ~1 Go max), et chaque modification re-uploadait tout le pack.

Le fix

Découpage du stockage en plusieurs fichiers sur Nextcloud :

livretscolaire/
├── livret-scolaire.json       ← config globale (~50 Ko)
│   ├── enseignants
│   ├── classes
│   ├── paramètres
│   ├── compétences perso
│   └── elevesIndex[]          ← {id, prénom, nom, niveau, lastModified}
├── eleves/
│   ├── eleve-1.json           ← Léa : identité + observations + photos (~15 Mo max)
│   ├── eleve-2.json           ← Manu
│   └── ...
└── archives/                  ← inchangé : un snapshot quotidien complet
    └── livret-scolaire-2026-05-10.json

Avantages :

  • 🚀 Sync incrémentale : ajouter une obs sur Léa → seul son fichier (~15 Mo max) est ré-uploadé. Avant : tout le JSON (450 Mo).
  • 📈 Plus de mur : 30 élèves × 50 photos = 1500 photos réparties sur 30 fichiers de 15 Mo. Plus aucun fichier individuel ne dépasse les limites.
  • 🔄 Conflits multi-appareils mieux isolés : modifier Léa sur le tel et Manu sur l'ordi en parallèle = 0 conflit (fichiers différents).
  • 🛡️ Robustesse : une corruption sur Léa n'affecte pas les autres.
  • 💾 Économie de bande passante : sur 4G, les sauvegardes deviennent quasi-instantanées.

Migration automatique au premier lancement

Quand v1.6 détecte un fichier global au format ancien, elle :

  1. Sauvegarde l'ancien fichier sous archives/livret-scolaire-PRE-MIGRATION-{date}.json (rollback possible)
  2. Importe les données dans IndexedDB (fusion)
  3. Réexporte au nouveau format multi-fichiers
  4. Supprime plus rien : tout est tracé dans le journal de sync

Aucune action utilisateur. Aucune perte de données.

Détails techniques

  • Nouvelle fonction DB.exportPourSyncMultiFichiers() : produit {global, eleves} au lieu d'un objet plat
  • Nouvelle fonction DB.importDepuisSyncMultiFichiers() : reconstitue depuis le format multi
  • Nouvelle fonction DB.lastModifiedEleve(id) : calcule le lastModified effectif (max de l'élève + ses obs + ses commentaires)
  • Nouvelles fonctions sync pullMulti() et pushMulti() dans nextcloud.js
  • Le pull est incrémental : compare les lastModified de l'index global, ne télécharge que les fichiers élèves dont la version distante est plus récente
  • Le push est incrémental : n'upload que les fichiers élèves modifiés localement + le fichier global (toujours)
  • Tous les writes utilisent l'écriture atomique putJsonAtomique (PUT + MOVE) introduite en v1.5

Format des archives quotidiennes : inchangé

Les archives restent un snapshot global au format ancien (un seul JSON). C'est le choix le plus simple : restauration triviale, récupération auto identique. Au moment d'une restauration depuis archive, l'app détecte que c'est un format legacy et déclenche automatiquement la re-migration vers multi-fichiers.

v1.5.1

Fix : conflits ETag à répétition après v1.5.0

Symptôme : après le passage en sync atomique (PUT + MOVE), des séries de « Conflit ETag tentative N/5 » apparaissaient à chaque sync, alors qu'aucun autre appareil ne poussait en parallèle.

Cause : sur un MOVE WebDAV, le header If-Match s'applique à la ressource source (le .tmp qu'on vient juste de créer), pas à la destination — c'est ce que dit le RFC 7232 §3.1, et c'est ce que fait Nextcloud (sabre.io). Comme l'ETag fourni venait de la destination originale, il ne matchait jamais avec celui du .tmp fraîchement créé → 412 systématique.

Fix : utilisation du header WebDAV If avec syntaxe taguée (RFC 4918 §10.4) qui permet de cibler une URL précise pour la condition :

AVANT  If-Match: "<etag>"                              ← sur le source (.tmp)
APRÈS  If: <destination-url> (["<etag>"])              ← sur la destination ✓

C'est la syntaxe standard et c'est ce que sabre.io / Nextcloud attend.

Note : Last-Modified n'est pas supporté par le header If (RFC 4918 ne permet que ETag ou lock-token comme condition). Quand seul Last-Modified est exposé par le serveur (ETag bloqué par CORS), on tombe en mode "force" pour le MOVE — la fenêtre de course entre PUT et MOVE est si courte (quelques ms) que le risque de collision est minime.

v1.5.0

🛡️ Sync robuste — écriture atomique + récupération automatique

Le problème

Quand l'app se ferme au milieu d'un upload Nextcloud (téléphone qui sort de l'app, perte de réseau, etc.), le PUT WebDAV peut être interrompu et le fichier livret-scolaire.json se retrouve tronqué sur le serveur. Symptôme : icône triangle rouge persistant, erreur "JSON.parse: unterminated string" à chaque sync suivante. Solution manuelle pénible : "Réinitialiser le fichier serveur".

Fix : écriture atomique (PUT + MOVE)

À la place d'un PUT direct sur livret-scolaire.json, la sync utilise maintenant un pattern en 2 étapes :

  1. PUT vers livret-scolaire.json.tmp-deviceXX (suffixe unique par appareil — pas de collision en multi-appareil)
  2. MOVE WebDAV atomique du .tmp vers le chemin final, AVEC If-Match (ETag) ou If-Unmodified-Since (Last-Modified) sur la destination pour préserver l'anti-écrasement multi-appareils.

Le MOVE WebDAV est atomique côté serveur (équivalent rename POSIX). Le fichier final n'est ainsi jamais dans un état tronqué — il a soit l'ancienne version complète, soit la nouvelle complète.

Si l'upload du .tmp est interrompu (réseau qui coupe), le .tmp peut être tronqué sur le serveur, mais le fichier livret-scolaire.json reste intact. Au prochain démarrage de la sync, le .tmp orphelin de cet appareil est automatiquement supprimé (nettoyerTmpOrphelin).

Fix : récupération automatique transparente

Si malgré tout un livret-scolaire.json corrompu est rencontré (par exemple installé avant v1.5 ou poussé par un autre client buggé), la nouvelle fonction getJsonAvecRecuperation :

  1. Détecte l'erreur CORRUPTED_JSON
  2. Liste les archives quotidiennes (livretscolaire/archives/)
  3. Tente la plus récente, puis la suivante, etc., jusqu'à en trouver une qui parse correctement
  4. Écrit cette archive saine comme nouveau fichier live (en atomique, force) — auto-réparation
  5. Continue la sync normalement

Tout cela sans interaction utilisateur. Dans le pire cas (toutes les archives également corrompues), on retombe sur le message d'erreur explicite avec le bouton "Réinitialiser le fichier serveur" comme avant.

Une entrée détaillée est ajoutée au journal de sync à chaque récupération automatique (visible dans Réglages → Diagnostic).

Architecture résultante

livretscolaire/
├── livret-scolaire.json              ← toujours intègre (atomique)
├── livret-scolaire.json.tmp-aaaa1234 ← apparait/disparait pendant les push
└── archives/
    ├── livret-scolaire-2026-05-10.json ← restauration de secours auto
    ├── livret-scolaire-2026-05-09.json
    └── ...

v1.4.0

🔒 Durcissement sécurité — 4 axes

1. Refus du HTTP non chiffré pour Nextcloud

La fonction tester(cfg) rejette maintenant explicitement les URLs en http:// (sauf localhost / 127.0.0.1 pour le développement local). Empêche un mauvais paramétrage qui exposerait les identifiants Basic Auth en clair sur le réseau.

2. Avertissement RGPD photos

Une alerte visible (cadre orangé, ⚠) s'affiche désormais en haut de la section Réglages → ☁️ Synchronisation Nextcloud pour rappeler que les données sensibles d'élèves mineurs (observations + photos) seront stockées sur le serveur Nextcloud, et que c'est à l'utilisateur de vérifier le niveau de protection de ce serveur.

3. Mot de passe maître pour la config Nextcloud

Nouvelle option dans Réglages → ☁️ Synchronisation Nextcloud : 🔒 Activer la protection. Quand activée, l'URL + identifiants + mot de passe d'application Nextcloud sont chiffrés dans IndexedDB par un mot de passe maître que l'utilisateur définit. Implémentation :

  • Module js/crypto.js qui utilise la Web Crypto API native (pas de dépendance externe)
  • Dérivation de clé : PBKDF2-HMAC-SHA256, 250 000 itérations (≈ 250 ms par essai sur CPU moderne, anti brute-force)
  • Chiffrement : AES-GCM 256 bits (chiffrement authentifié — toute altération du ciphertext est détectée)
  • Format payload : {v:1, salt: 16 bytes, iv: 12 bytes, ct: ciphertext+tag} en base64 dans IndexedDB
  • Le mot de passe maître ne quitte jamais la mémoire de session — il n'est pas stocké
  • À chaque ouverture de l'app, si la config est chiffrée, une modale demande le mot de passe maître. Si l'utilisateur clique « Plus tard », l'app continue à tourner en local (les données métier — élèves, observations — restent accessibles, seule la sync Nextcloud est désactivée).
  • Si le mot de passe est perdu : la config Nextcloud est irrécupérable (volontaire — sinon le chiffrement n'aurait pas de sens). L'utilisateur doit la ressaisir.

4. CSP durcie : retrait de 'unsafe-inline' sur style-src-attr

Avant v1.4.0, la CSP autorisait style="..." inline dans le HTML, ce qui ouvrait théoriquement une porte à l'injection CSS via XSS hypothétique. Maintenant la CSP refuse les attributs style="..." inline.

Refonte effectuée :

  • Styles statiques (margin, padding, color fixe) → 11 classes utilitaires .u-mt-1, .u-text-error, .u-section-divider, .u-hidden, .loading-spinner-icon, etc.
  • Couleurs dynamiques (couleurs des domaines, statuts, sections) → passées en data-couleur= / data-border-couleur= puis appliquées via setProperty() en JS (autorisé par la CSP).
  • MutationObserver global dans app.js : détecte automatiquement tout nouveau noeud avec data-couleur ou data-border-couleur et applique la variable CSS correspondante. Plus besoin de penser à appeler un helper après chaque rendu.
  • Couleurs PDF (5 variantes par domaine pour le thème "Couleur par domaine") : passées en data-pdf-couleur, data-pdf-couleur-bg, etc. et appliquées via setProperty() dans injecterHtmlDansContainer() AVANT la capture html2canvas.

style-src-elem garde 'unsafe-inline' car nécessaire pour les <style> injectés dynamiquement par le rendu PDF — c'est moins risqué qu'unsafe-inline sur les attributs car la portée est limitée et les styles sont générés intégralement par notre code.

Fix collatéral

Bug class="..." id="..." class="..." (deux attributs class sur un même élément, le second écrasait le premier) sur la barre de progression d'export en lot, corrigé en fusionnant en class="progress-fill u-progress-fill-empty".

v1.3.1

Fix : tableau de bord plantait sur PROGRAMME.toutesCompetences is not a function

J'avais inventé une méthode qui n'existe pas dans l'API de PROGRAMME. À la place, on utilise maintenant PROGRAMME.competenceParId(id) (qui existe et est utilisée ailleurs dans le code) pour résoudre le libellé d'une compétence à partir de son id, à la volée.

v1.3.0

🆕 Tableau de bord d'accueil + heatmap calendaire par élève

🏠 Nouvel onglet « Accueil » (devient l'onglet par défaut)

La barre de navigation passe à 4 onglets : 🏠 Accueil, 👥 Élèves, 📚 Livrets, ⚙️ Réglages. À l'ouverture de l'app, tu arrives maintenant sur un tableau de bord plutôt que sur la liste des élèves.

Le tableau de bord affiche :

  1. Salutation contextuelle : « Bonjour M. Herold » avec adaptation matin/après-midi/soir, et la date longue en français.

  2. 3 stats de la semaine en cours (lundi 0h → maintenant) :

    • Nombre d'observations
    • Nombre d'élèves différents concernés
    • Nombre de compétences travaillées
  3. 📍 À observer prochainement : grille des élèves qui n'ont pas eu d'observation depuis ≥ 14 jours, triés du plus ancien au plus récent. Affichage compact avec photo + prénom + « il y a X jours ». Clic → ouvre la fiche élève. Vide ? Message « Tous tes élèves ont eu au moins une observation… 👏 ».

  4. 🕒 Dernières observations : les 5 plus récentes avec prénom de l'élève, libellé de compétence, début du texte, date. Clic → ouvre la fiche élève.

L'onboarding (page « Bienvenue » avec les 2 étapes) reste affiché tant que l'app est complètement vide. Dès qu'au moins un enseignant, une classe ou un élève existe, on bascule sur le vrai tableau de bord.

📅 Heatmap calendaire dans la fiche élève

Sur la fiche de chaque élève, juste sous les infos identité, un mini-calendrier façon "GitHub contributions" montre l'activité d'observation sur l'année scolaire en cours (1er sept → 31 août).

  • Une case = un jour
  • Plus la case est foncée, plus il y a eu d'observations ce jour-là (5 niveaux : aucune, 1, 2, 3-4, 5+)
  • Étiquettes des mois en haut, légende « Moins → Plus » en bas
  • La case du jour est entourée d'une bordure pour la repérer
  • Tooltip au survol avec date + nombre d'observations
  • Total annuel affiché à droite du titre
  • Scrollable horizontalement sur mobile si nécessaire

Permet de voir d'un coup d'œil les périodes d'activité et les creux pour chaque élève.

v1.2.5

3 fixes : color-mix résiduel, fonds économiques, page blanche en début

🐛 Erreur "color function color" qui revenait

Cause : il restait un color-mix(in srgb, var(--accent) 10%, transparent) dans css/styles.css (sur .radio-option:has(input:checked), ajouté pour la modale enseignant). Quand html2canvas inspecte les feuilles CSS du DOM, il les parse toutes, y compris celles qui ne s'appliquent pas au container — d'où l'erreur.

Fix : remplacement par une simple couleur rgba(20, 116, 111, 0.10). Plus aucune fonction CSS récente dans le projet.

🖨️ Fonds Minimal + Bordure décorative en blanc (économie d'encre)

  • Minimal : repassé à fond blanc pur. Cards avec bordure grise plus marquée (1.5px solid #b8b8b8) pour structurer sans remplir.
  • Bordure décorative : repassé à fond blanc + arabesques aux coins (qui suffisent à l'identité visuelle). Header de domaine sans fond ambré (avant : aplat coloré qui consommait du toner).
  • Géométrique : pattern de points en SVG inline (au lieu de radial-gradient que html2canvas capture mal). Points teal bien visibles maintenant.
  • Couleur par domaine : inchangé (c'est le seul thème "coloré" assumé, et c'est volontaire pour ce style).

🐛 Page blanche au début du PDF

La logique de découpage partait de cuts = [0] quoi qu'il arrive, même si le contenu réel commençait après (à cause de browser quirks ou whitespace résiduel). Maintenant elle démarre au top du premier bloc et s'arrête au bottom du dernier + petit padding. Plus de page blanche au début ni à la fin.

v1.2.4

Fix : génération PDF qui plantait + warning CSP Firefox

🐛 Bug critique : unsupported color function "color"

La génération PDF plantait avec cette erreur. Cause : le thème "Couleur par domaine" utilisait la fonction CSS color-mix(in srgb, ...) qui est trop récente pour la version de html2canvas embarquée. La lib levait l'erreur et abandonnait la capture.

Fix : remplacement par une fonction JS melangerAvecBlanc(hex, ratio) qui calcule directement les teintes claires. 4 variables CSS pré-calculées (--couleur-bg, --couleur-bord, --couleur-sd-bg, --couleur-sd-bord) sont injectées en inline-style sur chaque section de domaine. Plus de fonction CSS récente, html2canvas digère sans problème.

🐛 Warning CSP Firefox

Firefox bloquait l'application de css/styles.css avec l'erreur :

Content-Security-Policy : Les paramètres de la page ont empêché l'application d'un style (style-src-elem) car il enfreint la directive « style-src 'self' 'unsafe-inline' »

Cause : Firefox CSP3 distingue style-src (tout style) en style-src-elem (pour les <link> et <style>) et style-src-attr (pour les attributs style="..."). Sans directive explicite, Firefox refusait de fallback sur style-src lors de l'analyse par html2canvas.

Fix : ajout explicite de style-src-elem 'self' 'unsafe-inline' et style-src-attr 'self' 'unsafe-inline' dans la meta CSP de index.html.

v1.2.3

Fix : page blanche en début de PDF + fonds des thèmes vraiment visibles

Diagnostic en 2 bugs liés :

Bug 1 : page blanche au tout début

Cause : un bloc @media screen dans le CSS commun mettait padding: 2rem 0 sur le body (32px en haut + 32px en bas) ET margin-bottom: 2rem entre chaque .page. html2canvas considère qu'on est en contexte "screen" lors de la capture → ces espaces blancs étaient capturés et donnaient une page blanche en début de PDF + des bandes blanches entre les pages.

Fix : ce bloc est devenu body.preview-mode { ... } — la classe preview-mode est appliquée UNIQUEMENT dans l'iframe du mode aperçu fallback, pas dans la capture html2canvas. Le PDF généré n'a plus de padding parasite.

Bug 2 : fonds des thèmes invisibles malgré v1.2.1

Cause : html2canvas(container, { backgroundColor: '#fff', ... }) forçait le fond du canvas en blanc, écrasant tous les fonds CSS définis sur les .page. Peu importe le thème choisi, html2canvas peignait du blanc partout.

Fix : backgroundColor: null → html2canvas respecte les fonds CSS de chaque page. Les 4 thèmes apparaissent maintenant tels qu'ils ont été conçus en v1.2.1 (crème papier pour Minimal, points teal pour Géométrique, onglets colorés pour Couleur par domaine, ivoire + arabesques pour Bordure décorative).

v1.2.2

Vignettes des thèmes en grille 2×2 (au lieu d'empilées)

La modale de sélection du thème PDF affichait les 4 vignettes l'une sous l'autre, ce qui obligeait à scroller pour les voir toutes. Maintenant elles sont en grille 2×2 (toutes visibles d'un coup) avec :

  • 2 colonnes par défaut
  • 1 colonne sur très petit écran (<380px) pour rester lisibles
  • Vignette plus large (100% de la carte au lieu de 60px fixes), ratio A4 préservé via aspect-ratio: 3 / 4
  • Carte en colonne (vignette en haut, nom + description en bas) pour bien tenir en largeur étroite

v1.2.1

Refonte des 4 thèmes de fond du PDF — bien visibles et bien distincts

Avant, les 4 thèmes (Minimal, Géométrique, Couleur par domaine, Bordure décorative) étaient tous quasi-blancs avec des nuances trop subtiles pour être perceptibles à l'impression. Maintenant chacun a une identité visuelle bien marquée :

  • Minimal : fond crème papier #faf5e9 (au lieu de blanc pur), cards blanches qui ressortent, bordures warm gray. Sobre mais chaleureux.
  • Géométrique : grille de points en teal accent à 18% d'opacité (avant : noir 5%, invisible), espacement 18px. Identifiable d'un coup d'œil. Cards blanches bien tranchantes sur le pointillé.
  • Couleur par domaine : effet "classeur à onglets colorés". Chaque card a une bande gauche épaisse de 8px de la couleur du domaine, fond légèrement teinté de la même couleur. Header de domaine plein avec ombre. Très distinctif.
  • Bordure décorative : papier ivoire chaud #f6ead0 (au lieu de #fefdf8 à peine visible), arabesques aux 4 coins en couleur ambrée à 85% d'opacité (au lieu de 50% trop fades), agrandies à 95×95px. Cards crème pour cohérence palette chaude.

Les vignettes d'aperçu dans la modale de configuration ont aussi été mises à jour pour refléter le nouveau rendu réel — tu choisis ton thème en sachant ce que tu vas obtenir.

v1.2.0

Améliorations esthétiques du PDF + civilité enseignant

👤 Civilité de l'enseignant configurable

La création / modification d'un enseignant ouvre maintenant une vraie modale (au lieu d'un simple prompt) avec choix de civilité :

  • Mme → "Mme Marie Dupont" sur le PDF
  • M. → "M. Jean Martin"
  • Sans préfixe → juste "Camille Lefèvre" (forme non genrée)

Stocké comme civilite sur l'objet enseignant. Les enseignants créés avant cette version n'ont pas de civilité par défaut → comportement "sans préfixe" (à éditer pour mettre à jour si besoin).

📘 Couverture du livret PDF plus marquée

  • Titre « Cahier de Réussites » agrandi (36pt → 48pt), couleur plus contrastée, letter-spacing ajusté
  • Ajout d'un petit ornement décoratif (trait teal de 80px) sous le titre pour aérer et donner du caractère
  • Sous-titre période en italique pour renforcer la hiérarchie
  • Padding du cadre cover augmenté pour respirer

💬 Cadre du commentaire général plus visible

Refonte complète du style du commentaire général (qui est la partie personnalisée que les parents lisent en premier) :

  • Bordure gauche colorée (style "citation") en teal accent (6px)
  • Fond crème léger (#fbf6ec)
  • Ombre douce pour détacher le bloc de la page
  • Police passée à 12pt avec line-height 1.65 (lisibilité améliorée)
  • Titre de section avec bordure inférieure teal de 3px

v1.1.1

Ajustements de l'export en lot suite aux retours

  • Sélecteur d'élèves cochables : la modale "Exporter tous les livrets" affiche maintenant la liste de tous les élèves avec une case à cocher chacun. Boutons "Tout cocher" / "Tout décocher", compteur en temps réel "X / N élèves sélectionnés". Tous cochés par défaut.
  • Toujours en ZIP : retrait de la logique File System Access API (qui n'était dispo que sur Chrome/Edge desktop). Tous les PDFs sont systématiquement regroupés dans un fichier ZIP unique téléchargeable. Marche partout (mobile, Firefox, etc.) et reste prévisible.
  • Format de nom de fichier : Livret-Prénom-Nom-Periode-1.pdf (ou Livret-Prénom-Nom-Annee.pdf pour l'année complète). Les accents sont préservés (le ZIP est encodé en UTF-8).

v1.1.0

Refonte de l'export "Tous les livrets" : pas d'aperçu, choix du dossier

Avant : deux boutons « Exporter les derniers livrets » et « Générer en lot personnalisé… ». Chaque livret ouvrait un aperçu individuel que l'utilisateur devait valider l'un après l'autre — fastidieux pour 25 élèves.

Maintenant : un seul bouton « 📚 Exporter tous les livrets… » avec un workflow totalement silencieux :

  1. Modale de sélection : classe (optionnel), période, thème
  2. Si Chrome/Edge desktop : sélecteur de dossier natif (File System Access API). Chaque PDF est écrit directement dans le dossier choisi.
  3. Si Firefox ou mobile : tous les PDFs sont regroupés dans un fichier ZIP unique téléchargeable
  4. Overlay de progression en temps réel : « 12 / 25 — Léa Martin » avec barre de progression
  5. Bilan final avec liste des erreurs éventuelles

Format de nom : Prenom_Nom_P1.pdf (sans accents, sans espaces, compatible avec tous les OS).

Détails techniques :

  • Nouvelle option returnBlob: true sur PdfExport.exporterLivret() qui retourne le PDF en Blob au lieu de le télécharger
  • Mini-encodeur ZIP en pur JS (~120 lignes, mode "stored" sans compression — les PDFs sont déjà compressés en interne)
  • Pas de dépendance externe (zéro CDN, zéro JSZip de 95 Ko)
  • Le picker de dossier est demandé AVANT la génération : si tu annules, rien n'est généré inutilement

v1.0.0 — Première release stable 🎉

Audit complet du code, nettoyage du code mort, sync validée en conditions réelles

Après le marathon 0.7 → 0.9.9 où on a tracé et corrigé une cascade de bugs de synchronisation Nextcloud, l'app passe en version stable. Sync validée par l'utilisateur sur 2 appareils en usage simultané (suppressions propagées correctement, modifications synchronisées dans les deux sens, polling auto fonctionnel sur ordi et tel).

Audit pré-1.0 :

Sécurité :

  • CSP strict (default-src 'self', pas de unsafe-inline sur les scripts, pas d'eval, pas de CDN)
  • 66 utilisations d'esc() / escHtml() pour échapper les interpolations dans les templates (vérifié : aucune interpolation dangereuse non échappée)
  • Modals (confirmModal, infoModal, promptModal) échappent systématiquement leurs paramètres
  • Whitelist regex stricte sur les noms d'archives (path traversal impossible via restaurerArchive)
  • URLs Nextcloud encodées segment par segment avec encodeURIComponent
  • Auth Basic standard, pas de document.write, pas d'eval
  • Limitation connue : mot de passe Nextcloud stocké en clair dans IndexedDB (limitation des PWA, pas de keystore web sécurisé sans demander un mot de passe maître à chaque ouverture)

Code mort supprimé :

  • DB.observations.listByCompetence : méthode jamais appelée (5 lignes, orpheline depuis le retrait des vues "par compétence")
  • 4 classes CSS .badge-acquis/encours/nonacquis/depasse jamais référencées (vestiges d'une ancienne UI de cotation)

Vérifications passées :

  • 0 fonction définie sans être appelée
  • 0 constante orpheline (sur 6 fichiers JS)
  • 0 ID référencé en JS sans correspondance HTML (73 IDs vérifiés)
  • 0 console.log oublié, 0 TODO en suspens
  • Cohérence des versions sur 4 emplacements (index.html app-version, app.js diagnostic + footer, service-worker CACHE_VERSION)

v0.9.9

Polling auto fonctionne aussi quand l'onglet est en arrière-plan

Bug observé après v0.9.8 : sur ordi, la sync auto ne se déclenchait pas régulièrement (alors qu'elle marchait sur le tel). Cause : le polling 30s avait un check if (document.visibilityState !== 'visible') return; qui sautait le tick quand l'onglet n'était pas au premier plan. Sur tel en PWA installée, l'onglet est quasiment toujours "visible" tant que l'app est ouverte, donc ça marchait. Sur ordi, dès que tu changes d'onglet (mails, ENT, autre site web), Livret Scolaire devient "hidden" et le polling cesse → tu ne reçois plus les modifs faites sur le tel jusqu'à revenir manuellement sur l'onglet.

Fix : on retire ce check. Le polling tourne maintenant toutes les 30s quel que soit l'état de visibilité de l'onglet. Le navigateur throttle nativement les timers d'onglets cachés (Chrome ralentit à ~1 fois/min après quelques minutes), donc on reste économe en ressources.

Le check immédiat au visibilitychange (quand tu reviens sur l'onglet) est conservé — utile pour ne pas attendre le prochain tick.

v0.9.8

🎯 BUG RACINE TROUVÉ : exportAll filtrait les tombstones

Voilà la cause profonde de toutes tes suppressions qui ne se propageaient pas. Découverte via le journal de sync (le compteur passait de 7 à 6 enregistrements après une suppression — incohérent avec le système de tombstones qui devait garder l'enregistrement marqué deleted).

Le bug : depuis la v0.9.0, j'avais ajouté un filtre listAll() → !deleted sur tous les stores pour cacher les tombstones de l'UI. Mais exportAll() (qui prépare le JSON envoyé sur Nextcloud) utilisait aussi listAll() !

Conséquence en cascade :

  1. Ordi : suppression d'une obs → tombstone créé en local ✓
  2. Ordi : push vers Nextcloud → exportAll appelait observations.listAll() qui filtrait le tombstone → JSON poussé sans le tombstone ❌
  3. Tel : pull du JSON Nextcloud → ne recevait JAMAIS le tombstone
  4. Tel : merge → l'obs locale du tel ne savait pas qu'elle devait disparaître → restait vivante

C'est exactement le bug observé : "j'efface une compétence sur l'ordi, le tel ne la perd jamais".

Le fix : exportAll() utilise maintenant getAll() direct (sans filtre) pour inclure les tombstones dans le JSON envoyé. Les tombstones sont propagés normalement via la sync.

Bonus : j'ai aussi enrichi le journal pour distinguer les vivants des tombstones :

Push réussi : 5 élèves + 38 obs vivants + 2 tombstone(s) (après pull entrant)

Ça permet de vérifier visuellement que les tombstones sont bien dans le JSON. Si tu vois "0 tombstone(s)" alors que tu viens de supprimer un élément, c'est que le tombstone n'a pas été créé (autre bug). Si tu vois "+ 1 tombstone(s)", c'est qu'il est bien dans le push.

À tester :

  1. Force la mise à jour vers v0.9.8 sur les 2 appareils (encore)
  2. Sur l'ordi : supprime une observation
  3. Vérifie le journal : "Push réussi : N élèves + M obs vivants + 1 tombstone(s)" ← le tombstone doit apparaître
  4. Sur le tel : attends 30s ou recharge → vérifie le journal : "Pull entrant : … + 1 tombstone(s) reçus" ← le tombstone arrive
  5. L'observation supprimée disparaît bien du tel

v0.9.7

Fix de 2 bugs majeurs identifiés à l'audit (v0.9.6)

🔴 Bug 2 : sync au démarrage ne pushait JAMAIS les modifs locales

Le tick immédiat de startAutoPoll posait un verrou sur Nextcloud à T+0ms, et la sync 500ms plus tard se voyait refusée par ce verrou. Conséquence : tes modifications locales en attente n'étaient jamais propagées au démarrage. C'est probablement la cause principale de "modifs faites sur un appareil qui n'arrivent pas sur l'autre".

Fix : on séquence proprement maintenant. Sync initiale d'abord (await), puis démarrage du polling. Plus de race au démarrage.

🔴 Bug 1 : save() ressuscitait silencieusement les tombstones

Quand on save() un élève dont l'id correspond à un tombstone en base (= supprimé sur l'autre appareil avec la suppression déjà arrivée localement), l'ancien code écrasait le tombstone, supprimait le flag deleted et mettait lastModified=now. La suppression était perdue et l'élément ressuscitait sur tous les appareils au prochain pull.

Fix : nouveau helper verifierPasDeTombstone appelé dans chaque save() (eleves, observations, enseignants, classes, competencesPerso). Si un tombstone existe en base pour cet id, on lève une erreur TOMBSTONE_PROTECTED. Un handler global affiche alors un toast d'erreur clair : « Cet élément a été supprimé (peut-être sur un autre appareil). Recharge la liste pour voir l'état à jour. ». L'opération est loguée dans le journal de sync (visible dans Réglages → Diagnostic → 📋 Journal de sync). Pour ressusciter volontairement (cas avancé), passer {_restaurer: true} dans l'objet — pas exposé dans l'UI.

À tester :

  1. Sur les 2 appareils : forcer la mise à jour vers v0.9.7
  2. Faire des modifs sur l'ordi → recharger l'app → vérifier que le push se fait (Journal de sync : ligne « Push réussi »)
  3. Sur le tel : vérifier que les modifs arrivent
  4. Tester le scénario tombstone : ouvrir la même fiche élève sur les 2 appareils, supprimer sur l'un, attendre 30s (sync), modifier sur l'autre → toast d'erreur attendu

v0.9.6

Outils de diagnostic de la sync + verrou anti-race

L'utilisation simultanée des 2 appareils peut générer des races (sync qui se chevauchent, pull qui écrase un push en cours, etc.). Cette version ne refait pas la sync mais ajoute les outils pour voir précisément ce qui se passe et bloquer les races les plus évidentes.

🆕 Journal de sync dans Réglages → 🛠️ Application → 🔧 Diagnostic → 📋 Journal de sync :

  • Liste des 50 dernières opérations Nextcloud sur cet appareil (push, pull, conflits, erreurs)
  • Coloration : vert (OK), gris (rien à faire), orange (conflit ETag retry), rouge (erreur)
  • Horodatage précis à la seconde
  • Boutons « Rafraîchir » et « Vider le journal »

Les opérations loggées : Push réussi (avec nombre d'enregistrements), Pull entrant (avec nombre d'enregistrements fusionnés), Conflit ETag retry, Sync demandée pendant qu'une autre tourne (saut), Pull demandé pendant sync (saut), Abandon après 5 conflits successifs, Erreurs réseau ou JSON corrompu.

🆕 Verrou anti-race global : si une sync ou un pull tourne déjà (par AutoSync, par polling 30s, ou par bouton manuel), tout nouvel appel est ignoré et logué dans le journal. Plus de scénarios où deux opérations Nextcloud se marchent sur les pieds.

Nouvelle migration DB : la base passe en version 3 pour ajouter le store syncLog. Migration automatique au prochain démarrage, données existantes préservées.

Plan post-1.0.0 : reproduis ton bug (suppression qui revient ou modif qui n'arrive pas), ouvre le Journal de sync sur les deux appareils, colle-moi le contenu et on identifie précisément ce qui foire pour le fixer ciblé.

v0.9.5

🐛 FIX : "Tout effacer" + nouveau bouton "Effacer partout"

Le bug : sur le tel, "Tout effacer" effaçait bien IndexedDB localement mais quelques secondes plus tard les données revenaient. Causes possibles : (1) un push AutoSync planifié juste avant le clear envoyait les anciennes données vers Nextcloud, (2) le merge de sync ne supprime pas les enregistrements absents du distant — donc même après push d'un fichier vide, les autres appareils gardaient tout.

Le fix : remplacé le bouton unique "Tout effacer" par deux boutons explicites dans Réglages → 🛠️ Application → ⚠️ Zone dangereuse.

🗑️ Effacer cet appareil

Pour quand tu veux nettoyer un appareil (par ex. avant de le prêter) sans toucher aux autres :

  • Stoppe le polling automatique et annule toute sync planifiée
  • Efface IndexedDB local et la configuration Nextcloud
  • Recharge l'app
  • Tes autres appareils gardent leurs données. Si tu reconfigures Nextcloud sur cet appareil, tu re-pulleras les données du serveur.

🌐 Effacer partout (cet appareil + autres via Nextcloud)

Pour propager la suppression à tous tes appareils :

  • Marque chaque enregistrement vivant comme tombstone (deleted: true, lastModified: now) via les API remove() standards
  • Push le résultat vers Nextcloud (donc le fichier serveur contient maintenant tous les tombstones)
  • Sur tes autres appareils, à leur prochaine sync (ou polling 30s), les tombstones avec lastModified=now gagneront le merge contre leurs versions locales plus anciennes → les données disparaîtront automatiquement
  • Wipe local complet et reload

Bonus : ajout d'une fonction AutoSync.cancelPending() qui annule une sync planifiée. Appelée avant le wipe local pour éviter les races (push tardif qui ressuscite les données).

v0.9.4

Sync auto en réception : les modifs des autres appareils arrivent toutes les 30s

Avant, pour voir les modifs faites sur ton ordi quand tu utilisais ton tel, il fallait recharger l'app ou cliquer sur "Synchroniser" manuellement. Maintenant ça se fait tout seul.

Comment ça marche :

  • Toutes les 30 secondes quand l'app est au premier plan, l'app fait un HEAD léger (~500 octets) sur ton fichier Nextcloud
  • Compare l'ETag (ou Last-Modified en fallback) avec le dernier vu
  • Si différent → pull complet + merge dans IndexedDB → toast vert « Mises à jour reçues de Nextcloud » + rafraîchissement de la vue
  • Quand tu reviens sur l'app après l'avoir mise en arrière-plan (autre onglet, écran verrouillé) → check immédiat sans attendre les 30s

Optimisations :

  • Pause en arrière-plan : si l'app n'est pas visible (page cachée), pas de polling → économie batterie
  • Reprise immédiate au focus : visibilitychange déclenche un check instantané
  • Pull only : on ne push pas inutilement quand il n'y a rien à pousser, juste un merge entrant
  • Si une modale est ouverte (formulaire d'observation, picker compétence…), la vue de fond n'est pas re-rendue pour éviter de faire disparaître ta saisie en cours
  • Backoff silencieux : si le réseau coupe, le polling continue mais sans afficher d'erreur (juste log console)

Limitations :

  • Si l'app n'est pas ouverte du tout, rien ne se passe (les PWA ne tournent pas en arrière-plan, surtout sur iOS). Idem pour Slack, Discord en PWA, etc.
  • Délai jusqu'à 30s entre la modif sur l'autre appareil et l'arrivée sur le tien
  • Coût réseau : ~24 PROPFIND/heure × 500 octets = 12 ko/h en 4G → négligeable

v0.9.3

Picker de compétences : chips de niveaux au lieu d'un toggle binaire

Avant : un toggle « Voir aussi les compétences des autres classes » qui mélangeait toutes les compétences de tous les niveaux dans un seul arbre, avec des badges PS/MS/CM1 pour distinguer celles du niveau courant.

Maintenant : une rangée de chips PS · MS · GS · CP · CE1 · CE2 · CM1 · CM2 au-dessus de l'arborescence. Le niveau de l'élève est sélectionné par défaut. Tu cliques sur un autre chip → l'arbre se rebuild avec uniquement les compétences de ce niveau-là.

  • Plus simple à parcourir : un seul niveau visible à la fois, pas de surcharge cognitive
  • Pratique pour piocher en avance (compétences CM1 pour un CE1 précoce) ou en consolidation (compétences CP pour un CE1 en difficulté)
  • Plus besoin des badges niveaux et du style italique des compétences voisines
  • CSS orphelin (.toggle-voisins, .comp-voisin, .comp-niveau-badge) nettoyé

v0.9.2

Tri et filtre par niveau scolaire au lieu de la classe

Pour les enseignants en multi-niveaux (CE1/CE2/CM1 dans une seule classe), le filtre par classe était inutile : tous les élèves étaient dans le même groupe et on ne pouvait pas filtrer ses CE1 séparément.

  • Chips de filtre : la barre du haut de la page Mes élèves affiche désormais les niveaux présents chez tes élèves (PS, MS, GS, CP, CE1, CE2, CM1, CM2) dans l'ordre scolaire — et pas les noms de classes. Tu cliques sur « CE1 » → tu vois uniquement tes CE1.
  • Tri : le bouton « Classe » est remplacé par « Niveau ». Quand activé, les élèves sont regroupés visuellement par niveau (PS en premier, CM2 en dernier) avec un titre de section et un compteur.
  • Cohérence : le même tri par niveau est appliqué à la page Livrets et à l'export en lot.
  • Compatibilité : si ton réglage stocké était triEleves='classe', il est automatiquement traité comme 'niveau' — tu n'as rien à refaire.
  • Le concept de classe reste : tu peux toujours créer/utiliser des classes (utile pour la couverture du livret PDF, les notes du dossier, etc.). Ce n'est juste plus le critère de filtre/tri.

v0.9.1

Cohérence visuelle PWA mobile

  • theme-color (couleur de la barre de statut Android quand la PWA est installée) passé de l'ancien gris-bleu #2c3e50 au teal d'accent #14746f actuel.
  • background_color (splash screen affiché pendant le chargement initial après le tap sur l'icône) passé de #f5f7fa (gris-bleu pâle) à #fbf6ec (crème), cohérent avec le mode clair de l'app.
  • Les utilisateurs qui installent la PWA après cette version verront la barre de statut teintée teal et le splash crème au lieu du look "premier projet" précédent.

v0.9.0

🐛 FIX MAJEUR : suppressions qui revenaient après une sync Nextcloud

Le bug : tu supprimais une classe ou une compétence personnelle, elle disparaissait localement, puis revenait après que la sync Nextcloud ait tourné — y compris en passant d'un appareil à l'autre.

La cause : architecture de sync sans "tombstones". Quand tu supprimais une donnée, elle disparaissait juste de ta base IndexedDB locale. Mais sur le serveur Nextcloud (et sur ton autre appareil), elle existait toujours. Au prochain pull, le merge détectait : « cet item est sur le distant mais pas en local → à réinsérer ». Donc la suppression était systématiquement annulée par la sync.

Le fix : j'ai introduit un système de tombstones (marqueurs de suppression) qui survivent à la suppression et bloquent la ré-insertion :

  • Soft-delete : on ne supprime plus physiquement, on marque l'enregistrement avec deleted: true, deletedAt: timestamp, lastModified: timestamp. L'enregistrement reste dans la base mais est filtré partout (listAll, get, listByX, etc.).
  • Propagation par la sync : le tombstone est inclus dans le push Nextcloud. À la prochaine sync de l'autre appareil, le tombstone (plus récent que la version locale qui ne sait pas que l'item est supprimé) gagne au merge → la suppression est propagée partout.
  • Nettoyage automatique : les tombstones de plus de 30 jours sont effacés au démarrage de l'app (les autres appareils ont eu largement le temps de propager la suppression à ce stade).

Concerne tous les stores : élèves, observations, enseignants, classes, compétences personnelles, commentaires généraux.

Migration silencieuse : les enregistrements créés avant v0.9.0 n'ont pas de champ deleted, ce qui équivaut à deleted=undefined — donc ils sont traités comme vivants (filtre !deleted = vrai). Pas de migration nécessaire.

v0.8.8

Cartes élèves bien plus visibles en mode clair

Les cartes des élèves se confondaient un peu trop avec le fond crème. Trois modifs combinées pour leur donner une vraie présence :

  • Bordure passée de 1px à 2px (plus marquée mais toujours discrète)
  • Bande d'accent teal de 5px à gauche de chaque carte (signal visuel fort qui structure la liste)
  • Ombre plus profonde : double ombre teintée chaud (proche + lointaine) pour donner du relief, et au hover ombre encore plus marquée

En mode sombre, l'ombre teintée crème serait invisible sur fond brun foncé, donc on bascule automatiquement sur une ombre noire classique avec opacité plus forte. Le rendu reste cohérent dans les deux thèmes.

v0.8.7

Fix doublon de titre dans le PDF

Sur les pages domaines du livret PDF, le nom du domaine apparaissait deux fois : une fois en grand titre (ex. « Sciences et technologie ») puis une seconde fois juste en dessous sous forme de chip étiquette. Cause : programme-adapter.js duplique délibérément domaineNom dans sousDomaineNom faute de hiérarchie réelle dans le référentiel, et pdf-export.js affichait la chip systématiquement quand sousDomaineNom était présent.

Correctif : la chip de sous-domaine n'est plus affichée si elle ferait doublon avec le titre du domaine. Elle reste affichée pour les vraies hiérarchies (compétences personnelles avec un vrai sous-domaine, futur référentiel à 2 niveaux).

v0.8.6

Réglage des bornes de période scolaire

  • Nouvelle section « 📅 Périodes scolaires » dans Réglages → 🏫 Configuration de la classe (entre École et classes et Élèves). Permet de saisir les dates exactes de début et de fin de chacune des 5 périodes pour ta zone scolaire.
  • Avant : les dates étaient codées en dur dans utils.js (zone B 2025-2026), figées et non modifiables sans toucher au code.
  • Maintenant : 5 paires de champs date, validation (toutes remplies + début < fin), bouton « Enregistrer ». Les dates personnalisées sont prioritaires sur les valeurs par défaut.
  • Synchronisé sur Nextcloud — saisi sur ton ordi, retrouvé sur ton tel.
  • Effet immédiat : les filtres P1-P5 sur la page Mes élèves et l'export PDF par période utilisent les nouvelles bornes sans avoir à recharger l'app.
  • Si aucune date n'a été saisie, on retombe sur les valeurs par défaut zone B (1ᵉʳ sept → 4 juillet, ajustées sur les vacances B).

v0.8.5

Réglages réorganisés en 3 grands menus + correction des doubles flèches

  • Fix double flèche sur Firefox : les sections de Réglages affichaient ► ► (deux flèches) sur Firefox parce que le marqueur natif du <summary> n'était pas caché correctement (seul ::-webkit-details-marker l'était, ce qui ne couvre pas Firefox). Ajout de list-style: none sur les summary pour cacher partout. Maintenant tu vois une seule flèche ▶.

  • Réorganisation en 3 groupes thématiques au lieu d'une liste plate de 10 sections :

    • 🏫 Configuration de la classe (ouvert par défaut) — Enseignants, École et classes, Élèves, Texte d'introduction du livret
    • ☁️ Sauvegarde et synchronisation — Synchronisation Nextcloud, Archives Nextcloud, Sauvegarde JSON
    • 🛠️ Application — Apparence (déplacée du début), Diagnostic, Zone dangereuse

    Chaque groupe a un titre en gras avec une bordure d'accent en bas, et peut être replié/déplié indépendamment des sections qu'il contient. Les sections individuelles restent ouvrables une à une comme avant.

v0.8.4

Création des élèves déplacée dans Réglages

  • Nouvelle section « 🎒 Élèves » dans Réglages, juste après École et classes. Affiche le nombre d'élèves dans la base et un bouton « + Ajouter un élève ». Cohérent avec la création de classes qui se fait aussi dans Réglages.
  • Le bouton « + Ajouter » a été retiré de la page Mes élèves. Cette page est désormais purement consultative : tu y vois tes élèves, tu cliques sur 📷 pour une observation rapide, ou sur la carte pour ouvrir la fiche détaillée (où tu peux toujours modifier ou supprimer l'élève).
  • Empty state mis à jour : si tu n'as pas encore d'élèves, le message dit « Aucun élève pour l'instant. Ajoute-en depuis Réglages → 🎒 Élèves ».
  • Comportement après ajout : si tu ajoutes l'élève depuis Réglages, tu restes sur Réglages (compteur mis à jour). Si tu modifies un élève existant depuis Mes élèves, tu reviens sur sa fiche comme avant.

v0.8.3

Suppression du bouton flottant 📷 redondant

Depuis la v0.7.17, chaque carte élève a son propre bouton 📷 d'action rapide. Le bouton flottant central qui demandait de choisir un élève n'avait plus d'intérêt — il faisait le même travail mais avec un clic en plus. Retiré du HTML, du CSS et du JS.

v0.8.2

Page d'accueil simplifiée

  • 2 étapes au lieu de 3 sur la page d'accueil : la création des classes a été retirée (depuis la v0.8.1, les classes sont créées automatiquement quand tu remplis le champ Classe d'un élève). Reste donc :
    1. Créer ton profil enseignant
    2. Ajouter tes élèves (la classe se crée toute seule)
  • Le bouton « 🚀 Commencer la configuration » ouvre directement l'onglet Réglages au lieu de demander le nom de l'enseignant via une modale. Tu accèdes ainsi à la section 👥 Enseignants déjà ouverte (et aussi École et classes, Synchronisation Nextcloud, etc.) pour faire ton setup au calme.
  • Bonus : ajout d'un .gitlab-ci.yml à la racine du zip pour faciliter le déploiement sur GitLab Pages (copie automatiquement les fichiers vers public/).

v0.8.1

Création automatique des classes via la saisie élève

Tu n'as plus besoin de pré-créer les classes dans Réglages avant d'ajouter tes élèves. Maintenant :

  • Le champ Classe est un input texte avec auto-complétion (HTML datalist) qui suggère les classes déjà existantes mais accepte la saisie libre.
  • À l'enregistrement de l'élève, si la classe saisie n'existe pas en base, elle est créée automatiquement et apparaît immédiatement dans le sélecteur des autres élèves, dans les chips de filtre de la page Élèves, et dans la liste des Réglages.
  • Comparaison case-insensitive pour éviter les doublons : si tu tapes « ce1 » alors que « CE1 » existe déjà, l'élève est rangé dans la classe « CE1 » existante (pas de création de doublon).
  • Toast de confirmation « Classe "X" créée automatiquement » quand une nouvelle classe est créée.

Tu peux toujours créer/supprimer manuellement tes classes via Réglages → 🏫 École et classes si tu veux, mais ce n'est plus obligatoire.

v0.8.0

Onboarding repensé + corrections critiques

  • Page d'accueil au premier lancement au lieu d'une modale qui se relançait à chaque démarrage. La page explique l'app et les 3 étapes (créer profil enseignant / ajouter classes / ajouter élèves), avec un seul bouton « 🚀 Commencer la configuration ». Plus important : elle ne s'affiche QUE si l'app est vraiment vide (aucun enseignant ET aucune classe ET aucun élève) — pas de doublon d'enseignant créé en relançant l'app.
  • Correction du bug majeur du tri par classe sur la page Élèves. Avant : le formulaire élève avait un <input type="text"> libre pour la classe, donc on pouvait taper « ce1 » alors que le chip filtre disait « CE1 » → comparaison stricte échouait → aucun élève affiché. Maintenant :
    • Le champ Classe est un <select> qui liste les classes définies dans Réglages → 🏫 École et classes.
    • Si aucune classe n'est définie, un message rouge l'indique dans le formulaire.
    • Le filtre de la page Élèves fait une comparaison tolérante (lowercase + accents normalisés) pour rattraper les anciens élèves saisis en texte libre.
    • Si un élève existant a une classe non listée, elle est gardée sous l'option « (ancienne) » pour ne pas perdre la donnée à l'enregistrement.
  • Création de classes en boucle : « + Ajouter une classe » dans Réglages reste maintenant ouvert tant que tu valides un nom. Saisir CP → entrée → CE1 → entrée → CE2 → entrée → annuler. D'un coup, sans recliquer entre chaque.

v0.7.17

Esthétique adoucie + ergonomie : moins de clics

  • Palette de couleurs plus chaleureuse. Adieu le gris-bleu froid « tech », bonjour les tons crème/teal pédagogiques :

    • Mode clair : fond crème (#fbf6ec au lieu de #f5f7fa), accent teal verdâtre (#14746f au lieu de bleu froid #3498db), ombres légèrement teintées chaud, bordures beiges (#e8dfc8 au lieu de gris #e0e6ed).
    • Mode sombre : fond brun-noir chaud (#1f1c18 au lieu de gris-bleu froid #1a1d23), surface brun clair (#2a2620), texte crème (#f5e9d4 au lieu de gris-bleu pâle), accent teal lumineux (#1abc9c) qui pop sur fond sombre.
    • Adieu le gradient violet de l'avatar : maintenant l'accent teal uni, cohérent avec le reste de l'app.
  • Toggle Apparence dans Réglages → 🎨 Apparence. 3 options en cartes radio :

    • Automatique (défaut) — suit la préférence système (clair le jour, sombre la nuit selon l'OS).
    • Clair — force la palette crème en permanence.
    • Sombre — force la palette brun chaud en permanence.

    Le choix est par appareil (pas synchronisé avec Nextcloud) — chaque téléphone/ordinateur garde sa préférence.

  • Bouton 📷 d'action rapide sur chaque carte élève. Avant : 3 clics pour créer une observation (FAB → modale « Choisir un élève » → tap élève). Maintenant : 1 seul clic sur le bouton 📷 de la carte de l'élève → directement le formulaire d'observation. Le clic sur le reste de la carte continue d'ouvrir la fiche détaillée comme avant.

v0.7.16

Audit de code et nettoyage

  • Code orphelin retiré :
    • uid() dans utils.js (fonction définie + exportée mais jamais utilisée)
    • formatDate importé dans pdf-export.js mais jamais appelé
  • CSS orphelin retiré (résidus de l'ancienne bannière html2pdf) :
    • .chips-row (wrapper jamais utilisé — c'est .filtres-row-eleves qui sert)
    • .pdf-info-banner et .banner-close (la bannière a été supprimée en v0.7.10)
  • Sécurité — défense en profondeur sur restaurerArchive : ajout d'une whitelist regex stricte sur le nom de fichier d'archive (/^livret-scolaire-(\d{4}-\d{2}-\d{2}|CORROMPU-[\d-]+)\.json$/). Évite tout risque de path traversal si la fonction est appelée depuis un contexte non-UI (console, extension, etc.). buildUrl faisait déjà un encodeURIComponent par segment, donc c'est une 2e couche de protection.
  • Reste de l'audit OK : CSP stricte (script-src 'self', pas d'eval/document.write), esc() utilisé sur 87 points pour XSS, encodeURIComponent dans tous les appels Nextcloud, versions cohérentes partout, pas de console.log/TODO oubliés.

v0.7.15

Sécurité avant écrasement du serveur

  • Le bouton « 🚨 Réinitialiser le fichier serveur » affiche maintenant un récap des données locales avant de procéder : nombre d'élèves, observations, photos, enseignants, classes. Tu peux annuler si les chiffres ne correspondent pas à ce que tu attends (par ex. si tu as ouvert l'app dans un nouveau navigateur où IndexedDB est vide). Évite d'écraser le bon fichier serveur avec des données incomplètes par erreur.

v0.7.14

Récupération en cas de fichier Nextcloud corrompu (JSON.parse error)

  • Erreur explicite quand le fichier serveur est corrompu : avant, on avait un cryptique « JSON.parse: unterminated string at line 34 column 163163 » sans aucune indication de quoi faire. Maintenant l'app détecte précisément ce cas (JSON invalide en réponse du serveur) et affiche un message clair avec la marche à suivre.
  • Nouveau bouton « 🚨 Réinitialiser le fichier serveur » dans Réglages → Synchronisation Nextcloud (zone Dépannage). Ce bouton :
    1. Sauvegarde l'ancien fichier corrompu en archive sous le nom livret-scolaire-CORROMPU-AAAA-MM-JJ.json (au cas où on voudrait l'analyser plus tard).
    2. Écrase le fichier livret-scolaire.json sur Nextcloud avec les données locales (qui restent saines en IndexedDB).
    3. Met à jour les marqueurs locaux pour que la prochaine sync soit cohérente.
  • Cause typique du problème : une écriture précédente s'est interrompue (perte de connexion réseau pendant la sauvegarde, ou conflit entre plusieurs appareils qui ont écrit en même temps). Ça ne devrait pas arriver souvent, mais c'est imparable quand ça arrive — d'où ce bouton de récupération.

v0.7.13

Adoption de la stratégie BrevetManager pour l'export PDF — téléchargement direct sans dialogue

Après avoir vu le code source de BrevetManager, j'ai reproduit exactement sa stratégie. Pour résumer :

  • jsPDF 2.5.1 + html2canvas 1.4.1 séparément (pas le bundle html2pdf qui était bogué). Les deux libs sont à embarquer localement dans js/lib/ — un script setup-libs.sh à la racine les télécharge depuis cdnjs en une commande.
  • Container DOM caché (position:fixed; left:-9999px; width:794px) où on injecte le HTML rendu. html2canvas capture l'ensemble en image (scale 2 = haute qualité).
  • Découpage manuel en pages A4 avec les marqueurs .pdf-block placés autour de chaque élément insécable (couverture, intro, sections de domaine, cards d'observation, commentaire). Pas de découpage automatique buggué — chaque coupure est intelligente, jamais au milieu d'une carte ou d'une page.
  • pdf.save(filename) déclenche un téléchargement direct via le mécanisme natif de jsPDF — AUCUNE boîte d'impression, le fichier descend dans Téléchargements en 2-3 secondes.
  • Overlay « Génération du PDF en cours… » pendant le rendu pour que l'utilisateur attende sans cliquer ailleurs.
  • Plus de modale d'aperçu intermédiaire : un clic « Générer le PDF » dans la modale d'export → 2-3 secondes plus tard, le PDF est dans Téléchargements. Direct, comme dans BrevetManager.
  • Fallback de secours : si les libs ne sont pas installées (fichiers absents dans js/lib/), l'app retombe automatiquement sur l'ancien workflow (aperçu + window.print()) sans planter.

Pour activer le téléchargement direct, lance : bash setup-libs.sh à la racine du projet (équivalent à BrevetManager). Ou copie les 2 fichiers depuis brevetmanager/src/lib/ si tu as déjà BrevetManager sur ton ordi.

v0.7.12

Bouton « Forcer la mise à jour »

  • Nouveau bouton « 🔄 Forcer la mise à jour de l'app » dans Réglages → 🔧 Diagnostic. Permet de désenregistrer le service worker, vider tous les caches du navigateur, et recharger la page d'un seul clic. Utile quand le navigateur sert une ancienne version malgré la nouvelle déployée. Les données (élèves, observations, photos) sont préservées car elles sont en IndexedDB, pas en cache.

v0.7.11

Modale d'export : header vraiment fixe en haut

  • Le position: sticky que j'avais utilisé ne fonctionnait pas correctement (notamment avec top: négatif qui pose problème sur certains navigateurs). Remplacé par une structure flex column propre : la racine .modal ne scrolle plus, le header reste toujours en haut (taille naturelle), et un body séparé .modal-body-scrollable gère le scroll. Le bouton « 📄 Générer le PDF » est désormais vraiment toujours visible en haut, peu importe où on est dans le formulaire.

v0.7.10

Retour à window.print() (comme BrevetManager / Mes Cartes) — fini html2pdf

  • Suppression complète de html2pdf.js et de tout son écosystème. Cette lib posait trop de problèmes (page blanche, rendu non fidèle, dépendance externe à installer manuellement) alors que window.print() fait le travail très bien et est éprouvé sur les autres apps de l'écosystème (BrevetManager, Mes Cartes).
  • Approche simplifiée : la modale d'aperçu PDF garde son iframe avec le rendu fidèle, et un seul bouton « 📄 Enregistrer / Imprimer » déclenche window.print() de l'iframe. La boîte d'impression du navigateur s'ouvre, l'utilisateur choisit « Enregistrer comme PDF » comme imprimante (qui est le défaut sur Chrome, Firefox, Safari modernes — un clic) ou imprime sur papier.
  • Avantages : rendu identique à 100% à l'aperçu (c'est le navigateur lui-même qui rend), aucune dépendance, plus de page blanche, plus de bug bizarre.
  • Compromis : la boîte d'impression apparaît brièvement. Pour avoir un PDF sans intermédiaire, il faudrait revenir à une lib comme html2pdf, mais l'expérience montre que c'est trop fragile.
  • Tout le tooling html2pdf est retiré : fichier js/lib/INSTALL_HTML2PDF.md, script install-html2pdf.sh, ligne dans index.html, bannière "installer html2pdf", diagnostic spécialisé, mode silencieux. Code plus simple et plus court (PDF export module passé de ~1100 à ~850 lignes).
  • Diagnostic des Réglages simplifié : ne teste plus html2pdf, juste les capacités du navigateur (Service Worker, IndexedDB, version de l'app).

v0.7.9

Modale d'export ergonomique + diagnostic html2pdf détaillé

  • Bouton « 📄 Générer le PDF » accessible sans scroller : un header sticky reste visible en haut de la modale d'export avec le titre + les boutons (Annuler / Générer). Plus besoin de descendre tout en bas du formulaire pour cliquer Générer. Les actions du bas sont supprimées (le sticky en haut suffit).
  • Diagnostic html2pdf beaucoup plus précis dans Réglages → Diagnostic. Au lieu d'un simple « ✓ chargé / ✗ non chargé », l'app détecte maintenant 4 cas et donne un message d'aide ciblé :
    1. Chargé et fonctionnel ✓
    2. window.html2pdf existe mais n'est pas une fonction → version incompatible installée, re-télécharger
    3. Le tag <script> est bien dans index.html mais le navigateur n'a pas pu le charger → fichier manquant ou erreur réseau, ouvrir F12 pour voir l'erreur précise
    4. Le tag n'est même pas dans index.html → la ligne est encore commentée, retirer les <!-- et -->
  • Nouveau bouton « Réafficher la bannière » dans le diagnostic si tu l'avais masquée par erreur en cliquant sur le ✕.
  • Nouvelle ligne « Bannière "installer html2pdf" » dans le diagnostic qui indique son état (visible/masquée) — facilite le debug.

v0.7.8

PDF page blanche → page rendue (le container est maintenant vraiment visible)

  • Bug du PDF blanc fixé : le container off-screen (left: -10000px) que j'avais utilisé en v0.7.7 n'était pas réellement rendu par html2canvas dans certains cas → bitmap vide → page blanche. Maintenant le container est visible plein écran, mais caché par un overlay opaque "⏳ Génération du PDF en cours…" pendant la capture. L'utilisateur ne voit que l'overlay, html2canvas a un vrai contenu rendu à capturer.
  • Scale réduit de 2 à 1.5 : un scale 2 sur 5+ pages A4 produit un bitmap d'environ 25 millions de pixels qui peut faire planter html2canvas silencieusement (autre cause possible de page blanche). 1.5 reste une bonne qualité tout en gardant un bitmap raisonnable.
  • Toast d'erreur : si html2pdf échoue maintenant, un toast rouge « Erreur génération PDF — voir console » s'affiche, plus de plantage silencieux.

v0.7.7

PDF généré identique à l'aperçu (vrai bug enfin compris)

  • Bug racine identifié : html2canvas (utilisé par html2pdf) ne sait pas capturer correctement le contenu d'une iframe — il perd la quasi-totalité des styles. C'est pour ça que le PDF généré ne ressemblait pas du tout à l'aperçu vu dans la modale (photos en pleine page sans aucun layout, plus de cards).
  • Fix : pour la capture PDF, on ne passe plus par l'iframe. On reconstruit le HTML dans un container DOM caché du document principal (position: fixed; left: -10000px) avec ses styles "scopés" (préfixés avec #pdf-render-wrapper) pour ne pas écraser le CSS de l'app. html2canvas peut alors capturer correctement, en respectant fidèlement la mise en page de l'aperçu.
  • L'iframe d'aperçu reste utilisée pour l'affichage à l'écran (isolation CSS), mais ne sert plus à la capture.
  • Mêmes changements pour le mode silencieux (export en lot) : container DOM principal aussi.

v0.7.6

Diagnostic html2pdf + clarté du cache

  • Nouvelle section « 🔧 Diagnostic » dans les Réglages : affiche en clair si html2pdf.js est chargé ou pas, avec un encart vert ✓ ou rouge ✗ + message d'aide précis. Affiche aussi la version exacte de l'app chargée (utile pour vérifier que le cache n'est pas resté sur une vieille version), le support du Service Worker, IndexedDB, et le User-Agent.
  • Ce diagnostic permet de répondre tout de suite à la question « ai-je vraiment installé html2pdf et est-il bien actif ? » : Réglages → Diagnostic → si la ligne html2pdf est verte, c'est OK ; si elle est rouge, le message indique précisément ce qu'il faut corriger.

v0.7.5

Tri "Classe" amélioré : regroupement visuel + ordre scolaire

  • Ordre scolaire correct : les classes sont maintenant triées dans l'ordre canonique PS → MS → GS → CP → CE1 → CE2 → CM1 → CM2 au lieu d'un tri alphabétique (qui mettait CE1 < CM1 < CP, ce qui est faux pédagogiquement).
  • Multi-niveaux supportés : "GS-CP", "CE2-CM1", "CM1-CM2" sont placés au rang du niveau le plus bas (donc "GS-CP" → entre "GS" et "CP", "CE2-CM1" → entre "CE2" et "CM1"). Les classes nommées librement (ex: "Classe Étoiles") sont placées à la fin par ordre alphabétique.
  • Regroupement visuel par classe : quand le tri est sur "Classe", les élèves d'une même classe sont rassemblés sous un titre coloré (CE1, CE2…) avec un compteur du nombre d'élèves. Plus facile à scanner visuellement quand on a plusieurs classes.
  • L'ordre scolaire s'applique aussi aux chips de filtre au-dessus de la grille (Toutes, PS, MS, GS, CP… au lieu d'alphabétique).
  • Même logique appliquée à la page Livrets.
  • Fonction ordreClasse() exposée par window.Utils pour réutilisation dans d'autres parties de l'app.

v0.7.4

Esthétique page Élèves + PDF identique à l'aperçu

  • Page « Mes élèves » plus dense et lisible :
    • Bug fixé : les noms n'étaient pas capitalisés sur les cartes (« jade h » → « Jade H »)
    • Le bouton « + Ajouter » n'est plus à l'extrême droite mais juste à côté du titre (plus d'espace vide). Une nouvelle classe CSS .vue-header-compact permet ce comportement, sans casser les autres vues qui ont 3 éléments dans le header (fiche élève avec Retour + titre + bouton modifier).
    • Container max-width 1100px (au lieu de 800px) → moins d'espace vide sur écran desktop
    • Cartes plus larges (220px min au lieu de 160px) et plus aérées
    • Surbrillance discrète au survol (bordure colorée)
    • Plus d'affichage du séparateur « · » quand niveau ou classe est vide
    • Séparateur vertical filtres/tris plus visible
  • Export PDF identique à l'aperçu : c'était un bug : le padding .page valait 1.8cm/1.6cm à l'écran mais 1.5cm/1.2cm en impression. Le PDF avait donc une mise en page resserrée différente de l'aperçu. Maintenant un seul padding valide pour les deux médias, plus l'option @page { margin: 0 } qui empêche le navigateur d'ajouter ses propres marges. Le rendu PDF est désormais identique pixel-pour-pixel à l'aperçu.

v0.7.3

Ergonomie formulaire + script d'installation html2pdf

  • Formulaire d'observation : boutons côte à côte (Supprimer compact à gauche, Enregistrer plus large à droite). Avant ils étaient empilés verticalement, ce qui créait des risques de clic accidentel sur Supprimer en visant Enregistrer sur mobile.
  • Script install-html2pdf.sh à la racine du projet : pour installer html2pdf.js en une seule commande (bash install-html2pdf.sh). Le script télécharge la lib (~370 Ko), la place dans js/lib/, et active automatiquement la ligne dans index.html. Compatible Linux/Mac/Git Bash. Pourquoi un script ? Parce que mon environnement d'exécution n'a pas accès à internet pour faire le téléchargement directement.

v0.7.2

Fluidification de la page Élèves + PDF moins intrusif

  • Page Élèves : filtres et tris regroupés sur une seule ligne avec un séparateur visuel discret (barre verticale fine) entre les deux blocs. Le tri "Classe" a été ajouté en complément de "Prénom" et "Nom". Sur écran large, tout tient sur une ligne ; sur mobile, ça wrap proprement.
  • 3 modes de tri distincts :
    • Prénom → trie tous les élèves par prénom alphabétique, ignore la classe
    • Nom → trie tous les élèves par nom alphabétique, ignore la classe
    • Classe (défaut) → groupe par classe, puis prénom à l'intérieur
  • Plus de toast intrusif sur l'export PDF : le message « Pour le PDF direct, installe html2pdf… » qui s'affichait à chaque export devient une bannière discrète dans la modale d'aperçu (visible uniquement si la lib n'est pas installée), avec un bouton × pour la masquer définitivement. Le bouton « 📄 Télécharger PDF » déclenche directement la boîte d'impression sans message intermédiaire.

v0.7.1

Correctifs UX

  • Fix : capitaliseNom is not defined dans renderLivrets() et ouvrirEleve() — la fonction était bien exposée par le module Utils mais n'était pas extraite dans le destructuring en haut de app.js. Désormais ajoutée à la liste des fonctions importées.
  • Sélecteur de tri repensé : remplacement des boutons segmentés (jugés moches) par des chips au même style que le sélecteur de classes au-dessus. Label « Trier par » discret à gauche, puis deux chips « Prénom » / « Nom » avec l'état actif coloré comme la chip « Toutes ».

v0.7.0

Refonte UX (vague 2 — finalisée avec html2pdf et recherche)

  • Migration vers html2pdf.bundle.min.js (1 seul fichier au lieu de jsPDF nu) : plus simple à installer pour avoir le PDF direct sans boîte d'impression. Marche à suivre dans js/lib/INSTALL_HTML2PDF.md (1 minute).
  • Mode silencieux pour l'export en lot : si html2pdf est installée, les boutons « Exporter les derniers livrets » et « Générer en lot personnalisé » génèrent tous les PDFs en arrière-plan sans aucune intervention utilisateur. Toast de progression (« Génération 3/22 : Léa Martin… »). Sans la lib, fallback sur l'aperçu un par un.
  • Recherche textuelle dans les observations : nouveau champ de recherche en haut du panneau de filtres. Cherche dans l'activité, l'observation, l'attendu, le texte de la compétence. Insensible à la casse et aux accents. Debounce 200ms.

v0.6.1

Refonte UX (vague 2)

  • Modal d'aperçu PDF plein écran avant l'export. L'utilisateur voit le rendu exact dans une iframe A4 et peut choisir entre :
    • 🖨 Imprimer → boîte de dialogue d'impression classique
    • 📄 Télécharger PDF → si la lib est embarquée, génère un vrai fichier PDF téléchargé directement (sans aucune boîte d'impression). Sinon fallback sur impression avec un toast pour rappeler de choisir « Enregistrer comme PDF ».
    • Annuler → ferme l'aperçu sans rien faire
  • Page Livrets, deux nouveaux boutons d'export en lot :
    • 📚 Exporter les derniers livrets : enchaîne automatiquement l'aperçu d'un livret pour chaque élève, avec le dernier thème utilisé et l'année complète comme période
    • 📦 Générer en lot personnalisé : assistant qui demande classe + période + thème, puis génère pour tous les élèves de la classe
  • Filtres et tris sur les observations dans la fiche élève (utile quand il y en aura beaucoup) :
    • Filtre par matière (français, maths, sciences…)
    • Filtre par période (P1 → P5)
    • Filtre par statut (acquis, en cours, non acquis, dépassé)
    • Tri par date (plus récent / plus ancien) ou par matière
    • Compteur intelligent : « 12 / 47 observations » quand un filtre est actif
    • Filtres mémorisés par appareil, repliés par défaut s'ils sont neutres
  • Capitalisation des noms étendue à toutes les vues : fiche élève, page Livrets, modale d'export.
  • Tri cohérent partout (Élèves + Livrets) : par classe → puis par prénom (ou nom selon préférence).

v0.6.0

Début de refonte UX (1ère vague — les autres améliorations suivront)

  • Tri des cartes élèves : par classe → puis par ordre alphabétique. Sélecteur « Trier par : Prénom / Nom » (Prénom par défaut) au-dessus de la grille. Le critère choisi est mémorisé localement par appareil (chaque téléphone garde sa préférence sans la pousser sur Nextcloud).
  • Tri en français correct : insensible à la casse et aux accents (Émile et Emile triés ensemble), gestion des particules (« de la Fontaine », « van der Waals »).
  • Capitalisation des noms dans le PDF : sur la couverture du livret et le titre HTML, le nom et prénom sont maintenant en « Première Lettre Capitalisée » au lieu de TOUT EN MAJUSCULES. Gestion correcte des noms composés (Jean-Marc), apostrophes (D'Artagnan) et particules nobiliaires (de la Fontaine, van der Waals reste avec « der » en minuscule).
  • Le letter-spacing du nom de l'élève sur la couverture est ajusté pour un rendu naturel maintenant qu'il n'est plus en majuscules.

v0.5.5

  • JSON formaté lisiblement sur Nextcloud : indentation à 2 espaces. Quand tu ouvres livret-scolaire.json dans l'interface Nextcloud ou un éditeur, c'est maintenant lisible à l'œil nu.
  • Sauvegarde automatique plus réactive : le debounce passe de 30 secondes à 3 secondes. Tu fais une modif → 3 secondes plus tard, c'est sur Nextcloud. Les autres appareils peuvent voir le changement très vite (notamment grâce à la sync au démarrage et au retour au premier plan).
  • Tolérance multi-appareils renforcée : le nombre de retries en cas de conflit passe de 3 à 5. Si 3-4 téléphones sync à la même seconde, le mécanisme "relit, fusionne, retente" supporte plus de collisions sans abandonner.
  • Note multi-appareils : avec 4 téléphones et le fallback Last-Modified (granularité à la seconde), il reste un risque théorique de conflit si plusieurs personnes saisissent EXACTEMENT à la même seconde. À l'usage, si tu observes des "pertes" de saisies dans des cas de simultanéité, dis-le moi : on basculera vers une architecture "un fichier par téléphone" en v0.6.0 qui élimine totalement ce risque.

v0.5.4

  • Robustesse face aux mises à jour partielles du cache : si l'app charge un mélange de fichiers récents et anciens (cache navigateur incomplètement rafraîchi), le bouton « ⟳ Synchroniser » et la sync au démarrage tombent automatiquement sur Nextcloud.sync() direct quand AutoSync.syncNow() n'est pas disponible. Plus de message « AutoSync.syncNow is not a function » bloquant.

v0.5.3

  • 🔒 Sécurité : les identifiants Nextcloud ne sont plus uploadés sur Nextcloud. Avant, le nextcloudConfig (URL + utilisateur + mot de passe en clair) était inclus dans livret-scolaire.json synchronisé sur le serveur. Maintenant, l'export pour synchronisation exclut les paramètres strictement locaux : nextcloudConfig, deviceId, lastSyncDate, lastSyncEtag, lastArchiveDate, migrationDossierFaite, lastExportTheme, enseignantCourantId. L'import distant filtre aussi ces clés à l'arrivée pour qu'un fichier malicieux ne puisse pas pousser ses propres credentials. Le fichier sur Nextcloud sera nettoyé automatiquement à la prochaine sync.
  • Sauvegarde manuelle : pas de changement — le bouton « 📥 Exporter » dans Réglages → Sauvegarde JSON exporte toujours TOUT (y compris nextcloudConfig), parce que c'est destiné à un backup qu'on restaure sur le même appareil.
  • Synchronisation automatique au démarrage de l'app : à chaque ouverture de l'app, si Nextcloud est configuré, l'app récupère immédiatement les modifications faites depuis un autre appareil. Plus besoin de cliquer manuellement sur « ⟳ Synchroniser » pour voir les nouveautés.
  • Re-sync au retour au premier plan : quand tu reviens sur l'app après l'avoir mise en arrière-plan, elle resync automatiquement (avec un petit délai pour ne pas spam).
  • Nouvelle méthode AutoSync.syncNow() qui force une sync immédiate sans attendre le timer ni vérifier le flag dirty. Utilisée au démarrage et par le bouton manuel.

v0.5.2

  • Correctif : MKCOL bloqué par CORS — l'app WebAppPassword n'autorise pas la méthode MKCOL (création de dossier WebDAV) en cross-origin, donc l'app ne pouvait pas créer le dossier livretscolaire/ toute seule. La sync échouait silencieusement. La création du dossier est maintenant best-effort : si elle échoue, on log un warning et on continue. Si le dossier n'existe vraiment pas et que le PUT échoue avec 409, on remonte un message clair demandant à l'utilisateur de créer le dossier manuellement (3 clics dans Nextcloud).
  • Protection multi-appareils restaurée même sans ETag : ajout d'un fallback Last-Modified (en-tête HTTP) avec If-Unmodified-Since. Cet en-tête est généralement exposé par les serveurs Apache/Nginx par défaut, contrairement à ETag. La concurrence optimiste fonctionne donc à nouveau, juste avec une granularité à la seconde au lieu d'un hash unique.
  • Mode force avec jitter : si ni ETag ni Last-Modified ne sont disponibles, on ajoute un délai aléatoire de 0 à 2 secondes avant le PUT pour réduire les collisions multi-appareils. Pas une protection complète mais ça réduit les risques.
  • L'app log maintenant en console (F12 → Console) le mode et la protection utilisés à chaque sync ([Nextcloud] Mode d'écriture : update, protection : Last-Modified).

v0.5.1

  • Correctif : la sync échouait avec « trop de conflits » quand le serveur Nextcloud ne renvoyait pas l'en-tête ETag au navigateur (cas typique : config CORS de WebAppPassword qui n'expose pas ETag dans Access-Control-Expose-Headers). L'app utilise maintenant trois modes d'écriture : create (premier upload), update (concurrence optimiste avec ETag), et force (fallback quand l'ETag n'est pas lisible). En mode force, on perd la protection multi-appareils mais le mono-appareil fonctionne.
  • L'app détecte automatiquement le cas et affiche un avertissement dans la console (F12) pour expliquer le souci, avec la solution recommandée si l'utilisateur veut récupérer la protection multi-appareils.

v0.5.0

  • Sauvegarde automatique vers Nextcloud avec debounce de 30 secondes : à chaque modification (observation, élève, paramètre…), une sync est planifiée 30s plus tard. Si une autre modification arrive entre temps, on réinitialise le timer pour accumuler les modifs en une seule sync. Au passage en arrière-plan ou à la fermeture de l'app, la sync est forcée immédiatement pour ne rien perdre.
  • Indicateur de sauvegarde dans le header (à droite du titre) : ✓ vert (sauvegardé), ⋯ orange (modifs en attente, sync prévue), ⟳ bleu animé (sync en cours), ⚠ rouge (échec, sera retenté). Cliquer dessus force une sync immédiate.
  • Réorganisation du stockage Nextcloud dans un dossier livretscolaire/ avec :
    • livret-scolaire.json : la sauvegarde courante
    • archives/livret-scolaire-AAAA-MM-JJ.json : une archive par jour, les 10 dernières sont conservées (purge automatique des plus anciennes)
  • Migration automatique depuis l'ancien chemin (livret-scolaire.json à la racine) → l'app détecte et déplace l'ancien fichier vers livretscolaire/livret-scolaire.json au premier sync, puis supprime l'original. Aucune donnée perdue pour les utilisateurs qui mettent à jour.
  • Nouvelle section « Archives Nextcloud » dans les Réglages : liste les archives disponibles avec un bouton « Restaurer » par date. La restauration remplace totalement les données locales (rollback complet) — utile si une erreur a corrompu les données du jour.
  • Sous le capot : 5 nouvelles primitives WebDAV (MKCOL, DELETE, PROPFIND avec listing, getJson, putJson avec If-Match/If-None-Match) dans nextcloud.js, + nouveau module auto-sync.js (debounce + flush sur visibilitychange/pagehide/online).

v0.4.1

  • Service worker passé en network-first sur le code de l'app (HTML/JS/CSS) : une nouvelle version déployée est visible immédiatement au prochain chargement, sans avoir besoin de recharger deux fois ni d'attendre l'expiration du cache. Les icônes restent en cache-first (elles ne bougent pas).
  • Le mode hors-ligne reste fonctionnel : si le réseau est indisponible, on tombe automatiquement sur la dernière version mise en cache.

v0.4.0

  • Commentaire général par période : nouveau champ saisissable dans la modale d'export PDF, sauvegardé pour chaque couple (élève, période). Le texte apparaît sur la dernière page du livret. Si rien n'est saisi, la zone reste vide pour écrire à la main après impression.
  • Diagnostic Nextcloud beaucoup plus précis : au lieu d'un message générique, l'app affiche maintenant la cause probable du problème (401 → besoin d'un mot de passe d'application si 2FA activée ; CORS → problème côté serveur Nextcloud ; URL invalide ; etc.) avec une suggestion d'action.
  • Synchronisation multi-appareils sans perte de données : passage à la concurrence optimiste avec ETag (HTTP If-Match). Si deux téléphones synchronisent en même temps, le second détecte le conflit, relit la version distante, fusionne intelligemment et retente automatiquement (jusqu'à 3 essais). Plus aucun risque d'écrasement.
  • Identifiant d'appareil (deviceId) généré localement et inclus dans les métadonnées de chaque sync, pour pouvoir tracer "qui a écrit la dernière version" en cas de besoin.
  • Schéma DB en v2 : nouveau store commentairesGeneraux (avec clé composée eleveId:periode).
  • Bonus UX : infoModal() pour afficher des messages multi-lignes correctement (utilisé pour les diagnostics).
  • Placeholders des paramètres école : Petaouchnok / 44444 (anonymisés).

v0.3.0

  • Référentiels officiels 2025/2026 : remplacement des programmes intégrés par deux fichiers JS autonomes et réutilisables :
    • referentiel-maternelle-2026.js (PS/MS/GS, 286 compétences, BO 31 oct 2024 + annexe 2026)
    • referentiel-competences-2025.js (CP→CM2, 1064 compétences, programmes 2025)
  • 1350 compétences au total à travers les 8 niveaux scolaires
  • Picker de compétences refondu : hiérarchie groupe (Français, Mathématiques, Langage, Activités physiques…) → domaine fin → items
  • Option « Voir aussi les compétences des autres classes » dans le picker, utile pour les classes multi-niveaux ou les anticipations cycle 3
  • Adaptateur programme-adapter.js qui maintient l'API window.PROGRAMME au-dessus des deux référentiels (rétrocompatibilité)
  • Numéro de version affiché discret à côté du titre

v0.2.0

  • Refonte complète du PDF : couverture / intro / sections par domaine / commentaire général
  • 4 statuts d'évaluation : Non acquis / En cours / Acquis / Dépassé
  • 3 champs par observation : Attendu / Activité / Observation
  • 4 styles de fond au choix : Minimal, Géométrique, Couleur par domaine, Bordure décorative
  • Génération par période scolaire (P1-P5) ou année complète, dates ajustables
  • Photo de l'élève sur la couverture du livret
  • Configuration école (nom, code postal, ville)
  • Texte d'introduction personnalisable
  • Désignation d'un enseignant principal pour la couverture

v0.1.0

  • Version initiale : observations, photos, statuts acquis/en cours, sync Nextcloud, export PDF basique

📜 Licence

GPL v3 — voir le fichier LICENSE.

🤝 Contribution

Projet pédagogique pratique — toute remontée bienvenue. Les programmes officiels évoluent (notamment le cycle 3 attendu en 2026), la base de compétences sera mise à jour en conséquence.


Construit avec ❤️ pour la classe coopérative.