# Décisions techniques — EDCF 52 · Bilan d'activité

Chaque décision indique son contexte, le choix retenu et ses conséquences. Les décisions
« configurables » peuvent être modifiées sans toucher au code.

## D-01 — Contexte d'hébergement constaté (analyse du dépôt)

Le dépôt était vide (seul un vhost Apache + certificat Let's Encrypt `edcf.jeandon.fr` existaient).
Serveur constaté : VPS Ubuntu 24.04, **1,9 Go de RAM** (≈ 500 Mo libres), **2 vCPU**, **≈ 3,5 Go de
disque libre**, Apache 2.4 déjà en frontal de ~50 sites, pas de Docker, pas de ClamAV.
Disponibles : Python 3.12, Tesseract 5 (fra + osd), Poppler (`pdftotext`, `pdftoppm`, `pdfinfo`),
LibreOffice, Node 18. PostgreSQL 16 a été installé (configuration sobre : 32 Mo de buffers).

**Conséquence** : l'architecture « idéale » Next.js + FastAPI + PostgreSQL + Docker + ClamAV ne tient
pas sur ce VPS (Docker et la base de signatures ClamAV demandent à eux seuls plus de mémoire et de disque
que disponible). On retient donc une architecture **équivalente en sécurité et en fonctionnalités, mais
plus légère**, déployée nativement sur le VPS, et livrée **aussi** sous forme Docker Compose pour une
infrastructure cible maîtrisée (DSI).

## D-02 — Architecture

| Couche | Choix | Raison |
|---|---|---|
| API | **FastAPI** (Python 3.12, Pydantic v2) | Écosystème Python nécessaire pour XLSX/ODS/PDF/OCR ; validation stricte des entrées ; OpenAPI générée. |
| Base | **PostgreSQL 16** + SQLAlchemy 2 + **Alembic** | Contraintes d'intégrité, transactions, déclencheurs d'immuabilité, `SELECT … FOR UPDATE SKIP LOCKED` pour la file de tâches. |
| Interface | **Preact + TypeScript strict + Vite** | Même modèle de composants que React (~10 Ko au lieu de ~140 Ko) ; build en quelques secondes et peu de RAM (Next.js a été écarté : rendu serveur inutile pour une application authentifiée, build trop gourmand pour ce VPS). |
| Styles | CSS natif avec jetons de design (variables) | Équivalent fonctionnel de Tailwind sans chaîne de build supplémentaire ; aucune ressource externe. |
| Graphiques | **SVG maison** | Aucune bibliothèque tierce ni CDN ; accessibles (titre, description, tableau alternatif). |
| Icônes | `lucide-preact` (ISC) empaquetées au build | Servies localement, aucune requête externe. |
| Polices | Barlow / Barlow Condensed (OFL) en TTF locaux | Lisibles, chiffres nets, servies par l'application et utilisées par WeasyPrint (même police à l'écran et dans le PDF). |
| PDF | **WeasyPrint** à partir du **même gabarit HTML** que l'aperçu | Fidélité aperçu/export ; nombre de pages connu exactement après mise en page. |
| OCR | **Tesseract 5** local (`fra`), sous-processus limité | Aucun service externe ; confiance par mot ; boîtes englobantes conservées. |
| PDF (import) | Poppler `pdftotext -bbox-layout` / `pdftoppm` | Outils éprouvés, exécutés en sous-processus avec limites ; aucun JavaScript PDF exécuté. |
| Tâches | **File de tâches en base** + processus `edcf-worker` | Pas de Redis nécessaire ; états *en attente / en cours / terminé / échoué* ; redémarrage sûr. |
| Frontal | Apache 2.4 (existant) en reverse proxy, HTTPS Let's Encrypt | Déjà en place sur le VPS ; configuration Caddy fournie pour Docker. |
| Services | systemd durci (utilisateur `edcf` dédié, `ProtectSystem=strict`, `MemoryMax`…) | Isolation et limites de ressources sans Docker. |

## D-03 — Semaines ISO 8601 et fuseau

Semaine du lundi au dimanche, numérotation ISO 8601 (`date.isocalendar()`), fuseau **Europe/Paris** pour
toute notion de « maintenant » (semaine courante, horodatages affichés). Les horodatages sont stockés en
UTC (`timestamptz`). La semaine 40 de 2026 = **lundi 28/09/2026 → dimanche 04/10/2026** (testé).

Un bilan est identifié par le couple (année ISO, numéro de semaine ISO). La notion de « période » est
portée par le bilan (`period_start`, `period_end`) : les périodes mensuelles/annuelles sont **calculées**,
jamais stockées, pour éviter toute incohérence.

## D-04 — Règle d'agrégation mensuelle et annuelle (configurable)

Un bilan hebdomadaire ne contient pas de ventilation par jour. Une semaine qui chevauche deux mois ne
peut donc pas être répartie exactement. Deux règles sont proposées, la règle active étant **toujours
affichée** dans le tableau de bord :

1. **Règle du jeudi (par défaut)** — la semaine est rattachée au mois (et à l'année) qui contient son
   jeudi. C'est la convention de la norme ISO 8601 pour rattacher une semaine à une année ; elle est
   étendue aux mois. Les valeurs restent des entiers.
2. **Prorata des jours (estimation)** — chaque valeur est répartie au prorata du nombre de jours de la
   semaine tombant dans le mois. Les résultats peuvent être décimaux et sont signalés « estimation ».

Dans les deux cas, la liste des semaines à cheval est affichée avec un avertissement. Exemple : S40 2026
(28/09 → 04/10) a son jeudi le 01/10 : elle est comptée en **octobre** avec la règle du jeudi.

La vue « Année » utilise l'année ISO (même règle). La vue « Période personnalisée » inclut les semaines
dont le jeudi est dans l'intervalle (ou au prorata), avec le même avertissement.

## D-05 — Données officielles vs brouillons dans les statistiques

Par défaut, le tableau de bord n'utilise que la **dernière version validée** de chaque bilan (un bilan
rouvert continue d'afficher sa version validée tant qu'il n'a pas été revalidé). Une option « inclure les
brouillons » existe ; les chiffres concernés sont alors marqués « provisoire ».

## D-06 — Vide ≠ zéro

`value IS NULL` = non renseigné ; `value = 0` = aucune infraction relevée. Aucune conversion automatique
n'est faite, ni à la saisie, ni à l'import, ni à l'export (cellule vide dans XLSX/ODS/CSV, « — » dans le
PDF). Les agrégats ignorent les valeurs non renseignées et affichent le nombre de semaines renseignées.
Une agrégation sans aucune valeur renseignée vaut « — », jamais 0.

## D-07 — Indicateurs configurables et libellés historiques

Les 14 indicateurs initiaux sont créés par la migration de données (ordre et libellés exacts du cahier
des charges, acronymes AFD/ESI non développés). Chaque valeur conserve un **instantané du libellé**
(`label_snapshot`) et chaque version validée stocke l'instantané complet : un ancien bilan affiche
toujours le libellé en vigueur au moment de sa saisie. Désactiver un indicateur le retire des nouveaux
bilans sans toucher à l'historique ; la suppression n'existe pas.

Par indicateur, l'administrateur configure : libellé, ordre, actif, type de détail (`taux`, `vitesse`
ou aucun), **unité du détail (vide par défaut pour les taux : aucune unité réglementaire n'est imposée)**,
sens d'évolution (`hausse favorable`, `baisse favorable` ou *non défini* → couleur neutre), seuil
d'alerte haut, inclusion dans le total.

## D-08 — Valeurs inhabituelles (avertissements, jamais bloquants)

Aucune valeur métier n'est modifiée automatiquement. Avertissements configurables :
- seuil haut par indicateur (vide par défaut : aucune règle métier inventée) ;
- variation par rapport à la semaine précédente : au-delà de ±`variation_pct` % **et** d'au moins
  `variation_min_abs` unités (défauts techniques : 100 % et 5, modifiables) ;
- plus de détails (taux/vitesses) saisis que d'infractions comptées (cohérence de saisie).

Erreurs bloquantes : nombre négatif, nombre décimal, texte dans « Nombre », valeur > 100 000.

## D-09 — Cycle de vie d'un bilan

`brouillon` → (`à vérifier`) → `validé` → `rouvert` (motif obligatoire) → `validé` (nouvelle version)
… → `archivé`. La sauvegarde automatique n'écrit **que** le brouillon. Chaque validation crée une ligne
immuable dans `bilan_versions` (instantané JSON + empreinte SHA-256) ; un déclencheur PostgreSQL interdit
toute modification ou suppression de ces lignes. La restauration d'une version recopie son instantané
dans le brouillon (le bilan doit être rouvert) — elle ne réécrit jamais l'historique.

Concurrence : chaque bilan porte un numéro de révision ; une sauvegarde envoyée avec une révision
périmée est refusée (HTTP 409) et l'interface propose de recharger.

## D-10 — Document final sur une page

Le document est produit par **un seul gabarit HTML/CSS** (Jinja2, échappement automatique) :
- l'aperçu affiche ce HTML dans une feuille aux dimensions exactes du format (mm) ;
- le PDF est produit par WeasyPrint à partir du même HTML, avec les mêmes polices locales.

L'option « Ajuster sur une page » essaie, dans l'ordre et en s'arrêtant dès que le document tient :
1. resserrer les espacements verticaux ; 2. réduire les marges (15 → 10 mm) ; 3. élargir la colonne
Observations ; 4. réduire le texte (10,5 → 9,5 → **9 pt minimum**) ; 5. passer en paysage (si autorisé).
Le A3 et l'export sur deux pages ne sont jamais imposés : ils sont **proposés**. Le nombre de pages est
mesuré par WeasyPrint (pas estimé) et revérifié sur le PDF produit. Les observations responsables du
dépassement sont identifiées par la hauteur réelle de leur ligne. Rien n'est tronqué ni masqué.

## D-11 — Imports

Tous les imports passent par la quarantaine (`/var/lib/edcf/imports/<uuid>/`, hors racine web, droits
0600) puis par une analyse **dans un sous-processus isolé** (limites mémoire/CPU/taille de fichier,
délai maximum, environnement vidé, sans accès à la base). Le résultat est une liste de **propositions**
que l'utilisateur accepte, corrige ou ignore. Rien n'est écrit dans le bilan sans confirmation ; même
après application, le bilan reste un brouillon à valider.

- Type réel déterminé par signature binaire et structure interne ; l'extension doit concorder.
- XLSX/ODS : inspection de l'archive avant lecture (nombre d'entrées, taille décompressée, ratio de
  compression, chemins, `DOCTYPE`/`ENTITY` interdits, macros/objets incorporés/liens externes).
- **`.xls` refusé** dans cette version : format binaire OLE2 historiquement vecteur d'attaques, que l'on
  ne peut pas inspecter aussi sûrement. L'utilisateur est invité à l'enregistrer en `.xlsx` ou `.ods`.
- Images : réencodées en PNG (métadonnées et contenus polyglottes éliminés) ; seule la version réencodée
  est affichée. Limite de 40 mégapixels (protection contre les « decompression bombs »).
- PDF : refus si `/JavaScript`, `/Launch` ou `/EmbeddedFile` ; limite de pages ; OCR si pas de texte.
- Doublons : même empreinte SHA-256 déjà importée, indicateur présent deux fois dans un fichier, bilan
  identique à celui de la semaine précédente.

## D-12 — Antivirus

ClamAV ne peut pas tourner sur ce VPS (≈ 1 Go de RAM pour les signatures). L'application intègre un
client `clamd` (protocole INSTREAM) activé par `EDCF_CLAMAV_SOCKET`, avec trois modes :
`required` (import refusé si l'antivirus est indisponible — **valeur recommandée en production**),
`optional` (import accepté, rapport marqué « antivirus non exécuté ») et `disabled`.
L'instance de démonstration tourne en `optional` ; c'est affiché dans chaque rapport d'import.

## D-13 — Authentification

Comptes individuels uniquement, aucune inscription publique, premier administrateur créé par la
commande `edcf create-admin`. Mots de passe **Argon2id** (argon2-cffi, paramètres RFC 9106 « low memory »),
politique configurable (12 caractères minimum par défaut, liste de mots de passe triviaux refusés).
**TOTP** (RFC 6238) avec secret chiffré en AES-256-GCM (clé dérivée de `EDCF_SECRET_KEY`), obligatoire
pour les administrateurs (configurable), 10 codes de récupération à usage unique (hachés).
Sessions côté serveur : jeton aléatoire de 256 bits dans un cookie `HttpOnly; Secure; SameSite=Strict`,
seule son empreinte SHA-256 est stockée ; expiration d'inactivité (30 min par défaut) et absolue (12 h) ;
rotation à la connexion et après MFA ; révocation unitaire ou globale. Protection CSRF par jeton
synchroniseur (en-tête `X-CSRF-Token`) + vérification de l'en-tête `Origin`.
Limitation des tentatives par identifiant et par IP, délai progressif, verrouillage temporaire,
message unique « Identifiant ou mot de passe incorrect » (pas d'énumération).

## D-14 — Autorisations

RBAC côté serveur, refus par défaut : chaque route déclare la permission requise (`require(perm)`).
Rôles `admin` (gestionnaire) et `reader` (lecture seule). L'export est une permission distincte
(`export.create`) accordée aux lecteurs uniquement si l'option « peut exporter » est cochée sur leur
compte. Les identifiants d'objets sont vérifiés à chaque requête (un import n'est lisible que par un
gestionnaire, une version n'est servie que pour son bilan, etc.).

## D-15 — Journal d'audit

Table `audit_log` **en ajout seul** (déclencheur interdisant UPDATE/DELETE) et **chaînée** : chaque ligne
contient l'empreinte SHA-256 de la précédente, ce qui rend détectable toute altération
(`edcf verify-audit`). Ne contient jamais de mot de passe, secret MFA, cookie ou jeton.
Les modifications de valeurs sont en plus historisées champ par champ dans `value_history`
(ancienne/nouvelle valeur, auteur, source, motif), avec regroupement des frappes successives d'un même
utilisateur sur un même champ pendant 5 minutes en brouillon.

## D-16 — Exports bureautiques

XLSX via openpyxl, ODS généré directement (format OpenDocument écrit à la main, sans dépendance),
CSV UTF-8 avec BOM et séparateur `;`. Nombres stockés comme nombres, observations comme texte,
cellules vides laissées vides. Neutralisation des formules : tout texte commençant par `=`, `+`, `-`, `@`,
tabulation ou retour chariot est préfixé d'une apostrophe (recommandation OWASP) — conséquence assumée :
une observation commençant par un tiret apparaît précédée d'une apostrophe dans le tableur.

## D-17 — Rondache

Aucun emblème n'est téléchargé. Un **placeholder neutre** (`backend/edcf/assets/branding/placeholder.svg`,
simple pastille « EDCF 52 ») est utilisé tant que le fichier officiel n'est pas fourni. Le fichier officiel
(SVG, PNG ou WebP) se dépose via l'écran Paramètres (contrôlé et réencodé pour PNG/WebP, SVG assaini)
ou dans `/var/lib/edcf/branding/`. Proportions conservées (`object-fit: contain`).

## D-18 — Mention de diffusion

Champ libre configurable, **vide par défaut**. L'application ne crée aucune classification ni mention
de protection officielle d'elle-même.

## D-19 — Instance de démonstration

L'instance publiée sur `edcf.jeandon.fr` est une **instance de démonstration** (`EDCF_ENV=demo`) :
bandeau permanent « Instance de démonstration — données fictives uniquement ». Elle ne doit recevoir
aucune donnée opérationnelle réelle tant que la DSI / les référents sécurité n'ont pas validé
l'hébergement (voir `docs/MISE_EN_PRODUCTION.md`). Les commandes `edcf demo-load` / `edcf demo-clear`
refusent de s'exécuter si `EDCF_ENV=production`.

## D-20 — Ce qui n'est pas (encore) couvert

Liste tenue à jour dans le README, section « État d'avancement ». Aucune fonctionnalité incomplète n'est
présentée comme terminée.

## D-21 — Semaine affichée à la connexion

Le bilan d'une semaine ne peut être complet qu'une fois la semaine terminée. À la connexion, la page de
saisie ouvre donc : (1) le brouillon en cours le plus récent ; sinon (2) la dernière semaine écoulée si
son bilan n'est pas validé ; sinon (3) la semaine courante. La semaine affichée est toujours indiquée en
toutes lettres (« Semaine 40 — du 28/09/2026 au 04/10/2026 ») avec la raison du choix. Le tableau de bord
s'ouvre sur la dernière semaine écoulée (S40 2026 le 07/10/2026, conforme à la période de référence).
