Apparence
03. Architecture applicative
Comment le code est découpé, où passent les frontières, et les choix de conception qui contraignent le travail futur.
Deux applications, deux origines
Le SPA et l'API sont servis sur deux noms d'hôte distincts, donc sur deux origines au sens du navigateur. Ce n'est pas un accident de configuration mais une décision, et elle a un coût qu'il faut connaître :
- l'URL de l'API est compilée dans le build du SPA (
VITE_API_BASE_URL) : changer d'environnement demande de reconstruire l'image du frontend, pas seulement de redémarrer un conteneur ; - l'origine du front doit être déclarée côté serveur, dans
DJANGO_CORS_ALLOWED_ORIGINSetDJANGO_ALLOWED_HOSTS. Un oubli se manifeste comme une erreur CORS dans le navigateur, dont la cause réelle est ailleurs.
Ce qu'on achète en échange : les deux applications se déploient, se dimensionnent et se journalisent séparément, et le back-office Django n'est jamais servi depuis l'origine du SPA — une session d'administration compromise n'emporte pas l'application, et l'inverse non plus.
Le backend : un domaine par application Django
backend/config/ ne contient que le câblage du projet — réglages, URL racine, sonde de santé, application Celery. Le domaine vit sous backend/apps/ :
| Application | Responsabilité |
|---|---|
accounts | Authentification : jeton, compte courant, réinitialisation de mot de passe |
roles | Habilitations : rôles, attributions datées par établissement |
establishment | Référentiel de l'organisation |
contract_types | Référentiel des types de contrat |
employee | Dossier du personnel : salariés, contrats, affectations |
counters | Compteurs de congés, CET, annualisation |
holidays | Référentiel des jours fériés et sa synchronisation |
data_imports | Collecte SFTP, parsing CEGI, import atomique et journal de reprise |
common | Ce qui est transversal et non métier : fenêtres de validité, amorçage |
devtools | Outillage de développement : introspection alimentant la documentation |
Les routes que chaque application monte sont en annexe C.
Décision : l'intégration CEGI est une frontière applicative dédiée.data_imports contient le connecteur SFTP, le parseur et l'orchestration, puis écrit par les modèles des domaines employee, contract_types et establishment. Le transport est derrière une interface remplacée par une fausse implémentation dans les tests ; la logique métier ne dépend donc pas d'un serveur SFTP réel. Celery Beat déclenche la collecte périodique et le worker exécute la tâche comme les autres travaux planifiés du projet.
Décision : roles réutilise auth.Group de Django comme rôle. Un modèle Role propre aurait dupliqué une table qui existe déjà, qui porte ses permissions et qui est administrable. Ce que Django ne fournit pas, c'est l'attribution datée et cantonnée à des établissements : c'est le seul modèle ajouté (RoleAssignment), une table de liaison qui porte des dates et une liste d'établissements.
Décision : les cinq référentiels sont montés à la racine de /api/. Monter chaque application sous son propre préfixe aurait donné /api/establishments/establishments/. Le routeur utilisé est SimpleRouter et non DefaultRouter, pour qu'aucune application n'installe une vue « racine d'API » sur le /api/ que toutes partagent.
Décision : pas de jeton de rafraîchissement. L'authentification repose sur un jeton d'accès seul, de durée de vie longue mais bornée. Une session se termine donc par une reconnexion, sans mécanisme de rotation à maintenir. Le § 05 dit ce que ce choix implique.
Le frontend : atomic design
Les composants sont classés par responsabilité, du plus inerte au plus lié à l'application : atoms/ (aucune logique métier), molecules/, organisms/ (qui peuvent appeler des hooks), templates/ (mise en page), pages/ (niveau route).
Trois règles tiennent le découpage :
- L'accès aux données passe par
src/api/(axios) puissrc/hooks/(TanStack Query). Un composant n'appelle jamais axios directement. Ce n'est pas une préférence de style : la mise en cache, l'invalidation et la gestion d'erreur vivent dans les hooks, et un appel direct les contourne toutes les trois silencieusement. - L'état client partagé est dans
src/store/(zustand), et se limite aujourd'hui à l'authentification. Tout le reste est de l'état serveur, donc du ressort de TanStack Query. - Le responsive est écrit en CSS, pas en JavaScript. Les styles de base visent le mobile et le bureau est ajouté par-dessus au point de rupture 768 px. Une mise en page pilotée par JavaScript provoque un saut visuel au premier rendu.
Le SPA est une PWA (vite-plugin-pwa, mise à jour automatique) : installable, avec un manifeste et un service worker. Il n'y a pas de mode hors ligne fonctionnel — le service worker sert le shell applicatif, pas les données.
Aucune bibliothèque d'internationalisation
Manque délibéré. Les chaînes visibles par l'utilisateur sont écrites en français directement dans le composant ou le sérialiseur. Il n'y a ni fichier de traduction, ni bibliothèque d'i18n, et il n'en sera pas ajouté tant que l'application reste monolingue.
La raison est le coût comparé : une couche d'i18n impose une clé par chaîne, un fichier de ressources à tenir en parallèle du code et une indirection à chaque libellé, contre un bénéfice nul pour une association qui travaille dans une seule langue. Le jour où une seconde langue est demandée, le travail consiste à extraire les chaînes — mécanique, et outillé — plutôt qu'à avoir maintenu une abstraction pendant des années sans l'utiliser.
Ce que cela impose en contrepartie : le code source est en anglais — identifiants, commentaires, messages de commit — et seules les chaînes destinées à l'utilisateur sont en français. La frontière est explicite, sans quoi les deux langues se mélangent dans le même fichier sans règle.
Ce que la documentation lit dans le code
Une contrainte de structure mérite d'être connue, parce qu'elle explique un découpage qui surprend : le conteneur backend ne monte que ./backend. Un test pytest ne voit donc ni docs/, ni frontend/, ni l'historique git.
D'où trois étages pour la documentation générée :
manage.py dump_docs_factsintrospecte Django et écrit des faits en JSON sur la sortie standard — aucun accès à la base, aucun fichier écrit ;- les moteurs de rendu, en Node sur l'hôte, lisent ces faits et ce que seul l'hôte voit (git,
docker-compose.yml,.env.example, les suites de tests) ; - les invariants documentaires tournent au même endroit, puisqu'ils croisent les deux.
Le français vit dans les moteurs de rendu ; Python n'émet que des données. Les règles sont dans references/documentation.md.