Apparence
06. Exploitation
Comment la plateforme est construite, contrôlée, déployée, certifiée et surveillée.
Environnements
| Environnement | Ce qui y tourne | Frontend | TLS | Portail de documentation |
|---|---|---|---|---|
| Poste de développement | La stack entière en Docker | Serveur Vite, rechargement à chaud | Non, et volontairement (voir plus bas) | Servi, sans garde |
Serveur de développement (sirh-dev.apajh81.org) | La stack entière, déployée automatiquement depuis develop | Build statique | Oui, Let's Encrypt | Servi, derrière auth_basic |
Serveur de préproduction (sirh-preprod.apajh81.org) | La stack entière, déployée automatiquement depuis preprod | Build statique | Oui, Let's Encrypt | Servi, derrière auth_basic |
Il n'y a pas d'environnement de production à ce jour. La branche preprod est contrôlée et déployée automatiquement sur son serveur dédié. La branche main reste contrôlée par l'intégration continue, sans déploiement.
Chaque environnement possède son propre projet Firebase. Les opérations dans Google Cloud et Firebase ne sont pas automatisées par ce dépôt : suivez la procédure manuelle des notifications Firebase, puis installez uniquement les valeurs obtenues dans le .env de l'environnement.
Jeu de données de développement
La seed prépare une cohorte synthétique sur l'année précédente et l'année courante : trois établissements, six équipes, neuf salariés et deux managers aux périmètres FAM/MAS et MAS/IME. Le compte Admin technique ne porte aucune fiche salariée. La première seed d'une base neuve fixe la date de référence (--reference-date, la date locale au démarrage) ; les réexécutions conservent les essais de recette, les scénarios déjà créés et leurs suppressions. Un registre interne DevSeedScenario garde la référence initiale par année et les identifiants des absences et événements créés. Une correction de commentaire, de titre ou de décision ne change donc pas l'identité du scénario. Cette table n'a ni endpoint ni écran d'administration ; sa migration est appliquée sur la nouvelle base avant la seed.
Décision : cette cohorte remplace l'ancienne sur une base neuve, préparée séparément après intégration. Il n'y a ni purge automatique ni conversion de la base existante : une base antérieure (des salariés, aucun DevSeedScenario) est laissée telle quelle, la seed le signale et le backend démarre quand même. La préparation et les tests ne redémarrent pas la dev actuelle. Le prochain démarrage sur la nouvelle base appelle seed_all, puis prepare_demo_calculations. Les sources paie sont explicitement synthétiques (DEV-SEED) et les résultats viennent des services métier, pas de soldes annuels inventés. Les commandes de seed et de calcul refusent le mode hors dev.
Les comptes, cycles, alertes et instructions de replay sont dans les prérequis de recette.
Intégration continue
Trois travaux tournent sur chaque pull request visant develop, preprod ou main, et de nouveau avant tout déploiement :
| Travail | Ce qu'il vérifie |
|---|---|
| Backend | ruff (lint), mypy en mode strict, la suite pytest avec base et cache réels |
| Frontend | Vérification de types et build Vite, contrôle de formatage |
| Documentation | Régénère les documents dérivés et échoue sur une dérive ; joue les invariants documentaires ; construit le portail ; lint et formatage des scripts |
Le travail de documentation mérite une explication, parce que son échec surprend : il régénère puis compare aux octets commités. Un document généré modifié à la main est donc écrasé et le contrôle passe ; ce qu'il attrape est le cas inverse — du code changé sans que le document soit régénéré. Il a aussi besoin de l'historique complet et des tags, que le changelog lit.
Il construit également le portail, et c'est ce qui attrape les liens morts et les pages qu'aucune barre latérale ne cite. Un lien interne cassé est le défaut le plus fréquent d'une pull request documentaire, et il ne se voit pas à la relecture ; l'exclusion configurée ne couvre que les liens qui sortent de docs/ vers un fichier du dépôt.
La même séquence tourne en local par make check (sans stack) et make check-full (avec, pour les tests de bout en bout Playwright).
Déploiement
Un push sur develop ou preprod déclenche l'intégration continue puis le déploiement du commit contrôlé. Les deux chaînes divergent après cette étape : seul develop publie une version.
Décision : le déploiement porte sur le commit qui l'a déclenché, pas sur la pointe de la branche au moment où il s'exécute. La nuance a l'air théorique et ne l'est pas : entre le début d'un déploiement et sa fin, quelqu'un peut fusionner. En déployant la pointe, la plateforme mettrait en ligne un commit dont l'intégration continue n'est peut-être pas terminée, et le tag posé juste après désignerait un commit différent de l'arbre réellement parti — deux mensonges que rien ne signalerait ensuite. Chaque travail qui touche un serveur nomme donc un commit précis : celui du déclencheur pour les deux déploiements, celui du changelog pour la republication du portail develop.
Décision : la construction des images se fait sur le serveur. Il n'y a ni registre d'images, ni archive transférée. Le déploiement consiste à mettre à jour le clone du dépôt présent sur le serveur et à laisser Docker Compose reconstruire et recréer ce qui a changé. C'est ce qui rend le déploiement identique à ce qu'un développeur fait en local, au prix d'une construction à chaque mise en ligne.
Décision : les déploiements passent par des runners auto-hébergés. Le pare-feu du serveur de développement n'accepte que des adresses françaises, ce que les runners hébergés par GitHub n'ont pas. Develop utilise le runner étiqueté apt ; préprod a son runner dédié, étiqueté preprod, et ses propres secrets de connexion dans l'environnement GitHub du même nom. Le travail develop qui ne parle qu'à GitHub — la pose du tag et la publication de la Release — tourne, lui, sur un runner hébergé : il n'a aucune raison de dépendre d'une machine de l'association.
Il n'y a pas d'étape de migration. L'entrée du conteneur backend applique les migrations à chaque démarrage. Un déploiement qui recrée ce conteneur migre donc de lui-même. La conséquence à connaître : une migration longue retarde la disponibilité du service, et une migration qui échoue laisse le conteneur en échec de démarrage — ce qui est visible, contrairement à une migration oubliée.
Le déploiement écarte explicitement la surcouche de développement et applique celle du TLS avec le profil tls. C'est nécessaire dans les deux sens : sans cela, le proxy serait réconcilié contre le fichier de base seul et perdrait silencieusement la publication du port 443 à chaque déploiement, cassant HTTPS même après une correction manuelle.
Versions et changelog
Aucun numéro de version n'est décidé à la main : un déploiement develop réussi est la livraison. Le travail qui suit ce déploiement pose un tag v<AAAA.MM.JJ>.<n>, régénère le changelog à partir des tags, le commite sur develop et publie une GitHub Release dont le corps est la section qui vient d'être rendue. Préprod ne crée ni tag, ni section de changelog, ni GitHub Release : elle valide un commit déjà passé par develop sans ouvrir une seconde numérotation concurrente.
Deux conséquences :
- Le sujet d'un commit est une surface de documentation. Il est publié tel quel à un lecteur extérieur. Le format attendu est décrit dans
references/documentation.md. - Le fuseau utilisé est celui de l'exploitation : un déploiement à 23 h 30 appartient au jour où l'équipe l'a lancé, pas au lendemain en UTC.
Le portail de documentation, et la boucle qu'il fallait couper autrement
Le portail est déployé par le même mécanisme que le reste : le service docs fait partie du fichier Compose que chaque déploiement reconstruit. Il n'y a rien de plus à câbler, et surtout aucune seconde chaîne de publication où la documentation pourrait prendre du retard sur le code. La republication décrite ci-dessous ne concerne que develop, car préprod ne produit aucun commit de changelog.
Cela a une conséquence sur le déclencheur. Le travail de version commite docs/changelog.md sur develop ; si ce commit relance un déploiement, la boucle « déploie → tague → régénère → commite » ne s'arrête jamais. Elle était coupée par un paths-ignore sur docs/**, qui ne peut plus tenir : depuis que la documentation est servie par un conteneur, un changement documentaire doit déclencher un déploiement, sans quoi le portail en ligne diverge du dépôt.
La coupure porte donc sur le commit et non sur les chemins : le déclencheur ignore un push dont le sujet est celui que le travail de version écrit. C'est une condition explicite, lisible dans le workflow à côté de la ligne qui produit ce sujet.
Cette coupure a une conséquence qu'il faut traiter, sans quoi elle se paye sur le portail. Le site est construit depuis l'arbre déployé, donc depuis un commit qui précède sa propre note de version : le changelog en ligne serait en retard d'exactement une version, en permanence, et personne ne le verrait puisque la page existe et paraît complète. Un quatrième travail suit donc celui de version et republie le portail — le seul service docs, puisque le commit de changelog ne diffère du commit déployé que par un fichier. Il est sauté quand le changelog n'a pas bougé, ce qui est le cas d'un redéploiement sans nouveau commit.
C'est aussi la réponse à une question qu'un lecteur se pose ici : pourquoi ne pas générer le changelog avant de déployer, ce qui éviterait le va-et-vient ? Parce que le changelog se construit à partir du tag, et que le tag n'est posé qu'une fois le déploiement réussi. L'inverse voudrait dire publier une version avant de savoir si elle est en ligne.
Pourquoi une condition explicite alors que GitHub coupe déjà la boucle. Un push authentifié par le jeton par défaut ne déclenche aucun workflow, donc la boucle est déjà coupée aujourd'hui sans rien écrire. Mais cette protection disparaît le jour où quelqu'un passe à un jeton personnel ou d'application pour contourner une règle de branche — un changement qui n'a aucun rapport apparent avec ce fichier, fait par quelqu'un qui n'a aucune raison de connaître cette conséquence. Ne compter que sur un comportement de plateforme, c'est laisser une boucle de déploiement infinie derrière une modification sans lien avec elle.
TLS
La terminaison TLS est en option, pas absente : la surcouche docker-compose.tls.yml publie le port 443 et le profil tls démarre le service certbot, qui obtient un certificat Let's Encrypt puis le renouvelle deux fois par jour. Les serveurs de développement et de préproduction tournent dans cette configuration.
Sur chaque environnement, un seul certificat couvre tous les hôtes — celui du SPA, celui de l'API, et celui du portail de documentation quand DOCS_HOST est renseignée — et l'ordre des noms compte : la configuration du proxy lit toujours le répertoire du certificat au nom de FRONTEND_HOST, qui doit donc être demandé en premier.
Chaque serveur possède son propre volume certbot_certs. Develop et préprod ont donc deux lignées et deux certificats distincts, même si leur triplet de noms suit la même convention. Les noms sirh-dev.apajh81.org, api.sirh-dev.apajh81.org, docs.sirh-dev.apajh81.org et leurs équivalents sous sirh-preprod.apajh81.org sont publiquement résolvables pour le défi HTTP-01.
Pas de TLS en développement local, et ce n'est pas un oubli
Let's Encrypt ne certifie pas un nom en .localhost, qui n'est pas résolvable publiquement — le défi HTTP-01 ne peut pas aboutir. Le développement local reste donc en HTTP simple, et doit y rester : une redirection forcée vers HTTPS casse les appels d'API entre les deux origines dès la requête de pré-vol, ce que le navigateur signale comme une erreur CORS alors que CORS est correctement configuré. Une heure perdue à chercher au mauvais endroit, d'où cette note.
Le piège de l'ajout d'un nom d'hôte
À lire avant d'ajouter un -d à la demande de certificat. L'entrée du conteneur certbot ne demande un certificat que s'il n'existe pas déjà de fichier de renouvellement pour FRONTEND_HOST. C'est ce qui l'empêche de redemander un certificat à chaque démarrage et de consommer le quota de Let's Encrypt.
Conséquence sur un serveur déjà déployé : ajouter un nom d'hôte à la liste -d du script ne suffit pas. Le fichier de renouvellement existe, la branche de demande n'est pas prise, et le certificat continue de ne couvrir que les hôtes d'origine — sans aucune erreur, le nouveau nom se contentant de servir un certificat invalide.
Deux façons de s'en sortir, à faire une fois, à la main sur le serveur :
étendre la lignée existante :
bashdocker compose run --rm --entrypoint certbot certbot certonly \ --webroot -w /var/www/certbot \ --expand --cert-name "$FRONTEND_HOST" \ -d "$FRONTEND_HOST" -d "$API_HOST" -d "<le nouveau nom>"ou supprimer la lignée (
/etc/letsencrypt/live/$FRONTEND_HOST, sonarchive/et sonrenewal/) et laisser l'entrée du conteneur redemander un certificat au prochain démarrage. Plus simple, mais consomme un certificat neuf du quota.
L'option --cert-name est ce qui garantit que le certificat reste écrit dans le répertoire que la configuration du proxy lit. Sans elle, certbot crée une lignée -0001 que le proxy ne regarde jamais.
Surveillance
Ce qui existe :
- une sonde de santé HTTP publique sur
/api/health/, utilisée par Docker et par le proxy ; - des sondes de santé Docker sur la base et le cache, dont le démarrage du backend dépend ;
- les journaux des conteneurs (
make logs), qui reçoivent aussi les courriels en développement.
Ce qui n'existe pas : pas de collecte centralisée des journaux, pas de métriques et pas d'alerte générale. La collecte CEGI est le premier traitement ciblé : ses rejets et ses erreurs terminales préviennent les destinataires configurés par courriel. Un service arrêté n'avertit toujours personne.
Le cas le plus difficile à remarquer est le service beat : son arrêt suspend les traitements planifiés sans qu'aucune requête n'échoue. Les signes observables sont l'écran Jours fériés, dont la date du dernier enregistrement cesse d'avancer, la liste Imports de données › Lots d'import, dont le dernier succès n'évolue plus malgré les dépôts attendus, et l'absence de nouvelles écritures Système sur les compteurs après une clôture. Aucun de ces retards ne déclenche d'alerte : le courriel d'import suppose que Beat ait lancé la tâche et que le transport Microsoft Graph fonctionne.
Activer et reprendre l'acquisition des compteurs
Le déploiement livre COUNTERS_ACQUISITION_ENABLED=false. Pour la première mise en service, fixez COUNTERS_ACQUISITION_START_DATE=2026-01-01, puis :
- lancez
make acquire-counters ARGS="--from 2026-01-01 --as-of <date> --dry-run"; - rapprochez le détail des valeurs manuelles et corrigez les écarts validés avec la paie ;
- relancez sans
--dry-runet contrôlez les compteurs ainsi que l'audit Système ; - activez le drapeau et redémarrez
workeretbeat.
La tâche quotidienne passe à 02 h 17. Elle repart toujours de la date configurée, mais le journal des périodes rend ce balayage idempotent. La commande manuelle accepte une autre plage pour un rattrapage ciblé ; sa prévisualisation exécute les mêmes transactions puis les annule.
Surveiller et reprendre l'import CEGI
La liste Imports de données ▸ Lots d'import du back-office affiche le dernier succès, le dernier échec et le nombre de lots en cours ou à reprendre. Un fichier rejeté conserve son code d'erreur, sa ligne et sa colonne. Après correction du référentiel, un administrateur disposant de la permission dédiée peut lancer Rejouer les imports sélectionnés ; la commande équivalente est make retry-import ID=<UUID>.
La collecte planifiée envoie un courriel par fichier rejeté. Le message indique le nom du fichier, l'erreur et son horodatage, sans recopier son contenu. Une indisponibilité SFTP ou de la base n'alerte qu'après les cinq reprises automatiques. La tâche est alors marquée en échec. Les reprises lancées depuis le back-office et les commandes manuelles restent synchrones et n'envoient pas cette alerte. Les destinataires sont décrits avec les autres variables en annexe B.
Exploiter l'envoi Microsoft Graph
Tous les courriels de production partent de la boîte partagée alertes-sirh@apajh81.org au moyen de l'application Entra APAJH SIRH Mailer. Exchange lui accorde le seul rôle Application Mail.Send, limité au périmètre APAJH SIRH Mailer - Sender. Les ID réels ne sont pas versionnés : le tenant et le client sont dans le fichier d'environnement et le coffre de secrets ; l'Object ID de l'application d'entreprise, l'identité Exchange, l'empreinte et l'échéance du certificat sont dans l'inventaire d'exploitation privé.
Avant un déploiement ou après une modification RBAC, contrôlez la boîte autorisée :
powershell
Test-ServicePrincipalAuthorization `
-Identity "<ApplicationId>" `
-Resource "alertes-sirh@apajh81.org" |
Format-Table RoleName, GrantedPermissions, AllowedResourceScope, ScopeType, InScopeInScope doit valoir True. Une exécution sans -Resource affiche Not Run : elle inventorie le rôle mais ne teste aucune boîte. Rejouez la commande avec une autre boîte du tenant et vérifiez False. Si la boîte expéditrice retourne False, inspectez le filtre sans envoyer de message :
powershell
$Scope = Get-ManagementScope "APAJH SIRH Mailer - Sender"
$Scope | Format-List Name, RecipientFilter
Get-Recipient -RecipientPreviewFilter $Scope.RecipientFilter |
Format-Table Name, PrimarySmtpAddressLe dernier résultat doit contenir exclusivement alertes-sirh@apajh81.org. Une permission applicative globale Mail.Send dans Entra est interdite : les droits Entra et Exchange se cumulent, elle ouvrirait toutes les boîtes malgré ce filtre.
Une réponse Graph 202 Accepted confirme la prise en charge, pas la livraison. Contrôlez le destinataire final avec Exchange Message Trace. Le transport ne réessaie pas automatiquement un appel incertain, afin de ne pas produire de doublon. Une panne Graph, Entra ou Exchange fait donc échouer l'envoi ; aucun canal SMTP de secours n'est activé.
Renouveler le certificat chaque année
L'inventaire privé porte l'empreinte, l'échéance et le responsable du certificat. Déclenchez son renouvellement à J-30 ; programmez des rappels à J-60, J-30 et J-7.
- Générez une nouvelle paire certificat/clé sur un poste sécurisé, hors du dépôt.
- Ajoutez le certificat public dans
APAJH SIRH Mailersans retirer l'ancien. - Encodez les deux PEM en Base64 et remplacez les secrets de l'environnement.
- Recréez
backend,workeretbeatpour charger les nouvelles valeurs. - Envoyez un courriel technique, demandez une réinitialisation de mot de passe, puis provoquez un rejet CEGI contrôlé.
- Vérifiez les trois messages avec Exchange Message Trace.
- Retirez l'ancien certificat après la période de retour arrière.
- Reportez la nouvelle empreinte, l'échéance et le responsable dans l'inventaire.
Ne copiez jamais dans un ticket, un journal ou cette documentation la clé privée, sa valeur Base64, le certificat encodé ou un jeton OAuth.
make poll-sftp-imports lance une collecte immédiate, utile après une intervention sur le service. make purge-import-archives applique immédiatement les durées de conservation. Le fonctionnement normal repose sur Celery Beat : collecte quotidienne à 04 h 37, puis purge quotidienne. Les valeurs exactes sont en annexe D.
L'interface SFTPGo s'ouvre depuis un poste d'administration avec un tunnel, par exemple ssh -L 8088:127.0.0.1:8088 <serveur>, puis http://127.0.0.1:8088/web/admin. Le pare-feu de l'hôte n'ouvre pas ce port. À la première installation :
- connectez-vous avec l'administrateur d'amorçage défini dans
.env; - créez un administrateur nommé, activez son TOTP et supprimez le compte d'amorçage ;
- créez le déposant, chrooté dans
/srv/sftpgo/data/imports/incoming, avec le seul droit de téléversement ; - créez l'importeur, chrooté dans
/srv/sftpgo/data/imports, avec les droits de lecture, création de répertoire, déplacement et suppression nécessaires ; - relevez
/var/lib/sftpgo/id_ed25519.pubdans le conteneur, placez sa clé publique dansSFTP_HOST_PUBLIC_KEY, puis redémarrez le backend et le worker.
Une rotation se fait compte par compte : changez d'abord le secret dans SFTPGo, mettez à jour le .env consommateur, redémarrez et contrôlez une collecte avant de révoquer l'ancien secret. Une rotation de clé hôte exige de distribuer la nouvelle clé épinglée avant de retirer l'ancienne instance ; sinon toute collecte est volontairement refusée.
Sauvegarde et restauration
Le service db-backup sauvegarde MySQL dès son démarrage, puis chaque nuit à 03 h 00 dans le fuseau Europe/Paris. Il réalise un snapshot transactionnel avec le compte applicatif, le compresse à la volée et ne publie le fichier qu'après validation de l'archive. Un échec laisse les sauvegardes antérieures intactes et n'exécute aucune purge.
Les fichiers suivent la forme backup_<environnement>_<base>_AAAAMMJJ_HHMMSS.sql.gz. Le répertoire est limité au mode 0700 et chaque dump au mode 0600. docker compose logs db-backup affiche l'heure, l'environnement, la base, le résultat, la taille produite et les fichiers purgés. docker compose ps db-backup devient unhealthy lorsque le dernier succès remonte à plus de 30 heures.
La configuration attendue est :
| Environnement | DB_BACKUP_ENVIRONMENT | DB_BACKUP_RETENTION_DAYS | DB_BACKUP_HOST_PATH |
|---|---|---|---|
| Develop | develop | 7 | /var/backups/apajh/mysql |
| Préproduction | preprod | 7 | /var/backups/apajh/mysql |
| Production future | production | 30 | /var/backups/apajh/mysql |
Avant le premier déploiement, l'exploitant crée ce répertoire sur l'hôte, le réserve à root avec le mode 0700, puis renseigne ces variables dans le .env non suivi. make db-backup déclenche un dump supplémentaire sans attendre le prochain horaire.
Restaurer MySQL
Une restauration remplace les données présentes. L'exploitant :
- arrête
backend,workeretbeatpour empêcher toute écriture concurrente ; - vérifie l'archive avec
gzip -t <fichier.sql.gz>; - lance
make db-restore FILE=<fichier.sql.gz>et confirme l'opération ; - redémarre les trois services, contrôle
/api/health/et les journaux ; - vérifie un parcours métier représentatif avant de rendre le service.
Un exercice de restauration dans une base isolée est réalisé après la mise en place sur chaque environnement, puis au moins une fois par trimestre. Un backup non restauré n'est pas considéré comme vérifié.
Limites de la couverture
Le service db-backup couvre uniquement MySQL ; Veeam assure la sauvegarde des serveurs :
| À sauvegarder | Volume | Si perdu |
|---|---|---|
| Données applicatives | mysql_data | Couvertes par les dumps de db-backup |
| Fichiers joints | media | Veeam, couverture et restauration à vérifier par environnement |
| Certificats | certbot_certs | Reconstructible, dans la limite du quota Let's Encrypt |
| Dépôts et archives CEGI | sftpgo_data | Reprise impossible pour les fichiers non expirés |
| Comptes, clés hôte et configuration SFTPGo | sftpgo_config | Accès de dépôt et clé épinglée à reprovisionner |
Les dumps restent sur la même VM. Ils protègent d'une corruption, d'une mauvaise manipulation ou d'une migration défaillante, mais pas de la perte de l'hôte ou de son disque. La sauvegarde Veeam des serveurs doit couvrir ce sinistre ; sa couverture et sa restauration doivent être vérifiées avant la bascule du stockage. Le .env, également local et non suivi, doit être conservé dans l'inventaire privé de l'exploitation sans être copié dans les dumps.
Bascule du stockage privé et sauvegarde Veeam
Les serveurs develop et preprod sont sauvegardés par Veeam. Aucun second planificateur de sauvegarde des documents n’est ajouté. Avant chaque bascule, l’exploitant vérifie que le disque contenant le volume Docker media est inclus et restaure un point de sauvegarde sur une machine isolée. Relever l’emplacement réel avec docker volume inspect <projet>_media : ne pas présumer du répertoire de données Docker. Consigner environnement, point restauré, résultat et responsable.
La restauration doit remettre MySQL et media au même point cohérent, écritures suspendues. Restaurer une base ancienne avec un volume récent peut laisser des références vers des fichiers supprimés. Contrôler avec python manage.py verify_media puis consulter un justificatif de chaque type. Les modes attendus sont 0600 pour les fichiers et 0700 pour les dossiers. Le démarrage du backend exécute également cette vérification après les migrations et refuse de servir si un justificatif manque. Les sauvegardes SQL existantes restent en place.
Migration ponctuelle depuis le bucket historique
Cette procédure s’exécute séparément sur develop puis preprod, en maintenance. Elle ne supprime aucun objet source ni volume historique et ne modifie pas les chemins en base. Suspendre les déploiements automatiques jusqu’à validation.
Avant le changement d’image, conserver l’image backend actuelle :
shmigration_image=$(docker inspect "$(docker compose ps -q backend)" --format '{{.Image}}') docker image tag "$migration_image" apajh-storage-migration:localArrêter
backend,workeretbeat; maintenir MySQL, Redis et l’ancien serveur objet disponibles. Installer le code de cette PR, sans encore lancer la nouvelle application. Garder les anciens identifiants dans le.envprivé le temps de la copie. Ne pas les passer sur la ligne de commande.Relever le nom Compose existant et le placer dans
storage_project. Créer le volume cible, puis lancer la simulation avec l’ancienne image, qui contient encore boto3 :shstorage_project=apajh-gta docker volume create "${storage_project}_media" docker run --rm --network "${storage_project}_internal" --env-file .env \ --mount "type=bind,source=$PWD/backend,target=/app,readonly" \ --mount "type=volume,source=${storage_project}_media,target=/app/media" \ --entrypoint python apajh-storage-migration:local \ manage.py migrate_media --destination /app/media --dry-runRejouer la même commande sans
--dry-run. Elle parcourt toutes les pages du bucket, conserve les chemins, compare les tailles et les empreintes SHA-256, et vérifie les références des deux modèles. Une collision de contenu, une référence absente ou un chemin sortant du volume interrompt la commande. Une interruption nettoie le fichier temporaire ; relancer vérifie les objets déjà copiés sans les écraser. La simulation lit la source mais n’écrit rien.Configurer
DJANGO_MEDIA_ROOT=/app/media, retirer les anciennes variablesMINIO_*etS3_*de la configuration courante après la copie. Conserver leur version antérieure dans l’inventaire privé pendant la fenêtre de retour arrière.Construire et démarrer la nouvelle stack avec la surcouche TLS habituelle. Ne pas utiliser
down -vni--remove-orphans. Vérifierverify_media, la consultation des deux types, un dépôt et une suppression sur des dossiers de recette. Arrêter explicitement les anciens conteneurs de stockage après validation, en conservant leurs volumes.Rouvrir l’application et réactiver les déploiements. L’exploitant valide la couverture Veeam du nouveau volume et conserve le compte rendu de restauration.
Retour arrière avant réouverture : remettre le code, l’image et la configuration antérieurs, puis redémarrer les anciens services sur leurs volumes conservés. Après reprise des écritures, le bucket n’est plus à jour : un retour simple est interdit. Il exige une resynchronisation contrôlée ou une restauration cohérente de la base et des fichiers. Ne retirer les volumes historiques qu’au terme de la période de validation définie par l’exploitation.
Manques d'exploitation
| Manque | Conséquence |
|---|---|
| Aucune supervision ni alerte générale | Un arrêt de service n'est vu que par un usager |
| Pas de séparation des configurations par environnement | Les réglages de durcissement HTTPS du § 05 ne peuvent pas être activés sans casser le développement local |
| Aucun environnement de production | main est contrôlée, mais pas déployée |
Ces trois points sont des travaux identifiés, non des oublis. La séparation des configurations est celle qui débloque les autres : c'est elle qui permettrait d'activer la redirection HTTPS, HSTS et les cookies Secure en production sans toucher au développement local.
Initialiser les droits CA paie
- Rapprocher les totaux acquis extraits par
scripts/cegi/sql/leave.sql, ancienneté comprise, avec la paie. Valider les périodes d'acquisition et leur état dansLeavePeriods; la finalisation reste explicite et ne dépend pas de l'horodatage de l'export. Vérifier un cas de droit réduit après maladie. Le raccordement à CEGI et la validation paie restent des préalables externes. - Exécuter
make db-backupet vérifier l'archive avant d'appliquer les migrations. - Relever par salarié et période les consommations CA + ANC, puis appliquer la reprise. Rapprocher pris et planifiés et contrôler les contre-passations ; un second passage ne doit ajouter aucun débit. Les objectifs clos restent conservés.
- Déposer le CSV d'initialisation au format congés avec
final, pour la période de prise courante et les périodes futures connues. Lancermake poll-sftp-imports. - Contrôler le lot Congés annuels, l'acquis et le restant, puis une ancienne annulation ANC. Les consommations doivent être identiques avant et après import.
- Rejouer le contenu et simuler la finalisation d'un cumul N+1 inchangé : aucun double crédit. Le flux mensuel suffit ensuite ; ne programmer aucun import annuel.
Une erreur de reprise demande une restauration de la sauvegarde : les mouvements append-only ne sont pas réécrits par un retour de migration. Aucun chargement réel ne doit précéder la validation de l'extraction et ces rapprochements.
Exploiter la file des liens de mot de passe
Les créations avec email professionnel préparent une invitation. Les comptes CEGI n'en reçoivent pas automatiquement : le format ne fournit aucun email professionnel. Une sélection dans le back-office permet de préparer les liens après saisie de cette adresse. Aucun rattrapage des comptes existants n'est réalisé par la migration.
ACCOUNT_INVITATION_SENDING_ENABLED autorise le dépilage ; sa valeur initiale est false. La préparation reste active pendant une suspension. Les paramètres ACCOUNT_INVITATION_BATCH_SIZE (5 par défaut, de 1 à 10) et ACCOUNT_INVITATION_INTERVAL_SECONDS (60 par défaut, minimum 60) sont communs aux invitations, réinitialisations administratives et reprises. Beat vérifie la file toutes les quinze secondes : l'envoi peut donc commencer jusqu'à quinze secondes après l'échéance, selon la disponibilité du worker. Aucun worker ne dort entre les lots. python manage.py dispatch_password_emails applique le même verrou et la même cadence lors d'une intervention manuelle.
L'activation avec un transport console, fichier ou factice est refusée, même en développement : le premier écrit les liens dans les logs, le deuxième sur le disque, le dernier enregistrerait « accepté » sans rien envoyer. Utilisez un transport SMTP, Graph ou, dans les tests, le transport en mémoire.
Un verrou de session MySQL propre à la base protège le lot entier. Chaque résultat est persisté séparément. Le lot s'arrête entre deux mails après cinq minutes ; le worker le tue trente secondes plus tard, pour qu'un envoi déjà engagé puisse finir. Une réservation de cette durée plus la temporisation est enregistrée avant les appels réseau. Après une fin normale, l'échéance devient la fin du lot plus la temporisation. Un lot vide ne reporte pas cette échéance. Les transports SMTP et Graph ont des délais réseau explicites de dix secondes.
Une demande garde l'email professionnel de la fiche au moment où elle est préparée, et le back-office l'affiche avant confirmation. Si la fiche change d'adresse avant l'envoi, la demande est annulée (« Email professionnel modifié depuis la demande ») plutôt que d'envoyer le lien à la nouvelle adresse : il faut la préparer de nouveau. Un superutilisateur n'est jamais visé par la préparation administrative.
Une demande dispose de trois tentatives, avec reprises après cinq puis trente minutes. Chaque échec enregistre le nom de l'erreur du transport (par exemple SMTPAuthenticationError), et le journal du worker en ajoute le code SMTP. Un traitement interrompu est repris après expiration de la réservation et du délai de reprise. L'historique conserve une erreur sûre : aucun token, uid, lien ni corps du mail n'est stocké ou journalisé. Un arrêt après acceptation par le fournisseur peut provoquer un doublon lors de la reprise automatique. Accepté par le fournisseur ne prouve pas la livraison : utilisez Exchange Message Trace pour la vérifier. Le transport Graph lui-même ne réessaie pas ; cette file est le seul endroit qui décide les reprises de ces liens.
Pour déployer : laissez le dépilage désactivé, appliquez les migrations puis reconstruisez et recréez backend, worker, beat, frontend et proxy. Les nouvelles configurations nginx retirent les paramètres et le référent des journaux d'accès de l'hôte frontend — où vit le lien — et protègent les erreurs de la page du lien. Les hôtes API et documentation gardent leurs paramètres de requête dans les journaux : ils ne portent aucun lien, et c'est ce qu'un diagnostic doit pouvoir relire. Vérifiez la file sur quelques comptes de test, activez le dépilage et contrôlez deux lots : cinq acceptations au plus par défaut, au moins soixante secondes entre la fin du premier et le début du suivant. Une panne se suspend en remettant le drapeau à false et en recréant les services ; les demandes restent conservées. Un renvoi explicite ouvre un nouveau cycle tout en conservant l'historique.