[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$feESScPzpXMOuhorp1g0lIujG4A9_iAJaD5eEVuU5YSU":3,"$fk1oB-e5O_BgwF2c-QwDgsx2Y7SMPhf77z9g2mrpoBuM":23},{"chapter":4,"prev":13,"next":18},{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10,"contentHtml":11,"contentHtmlEn":12},"deploy","09","Déploiement & infrastructure","Deployment & infrastructure","Décrit le pipeline de mise en ligne Synedre OS : asymétrie entre .\u002Fdeploy (IA, preprod) et .\u002Fship (Alex, production), le dispatcher YAML, le pattern build-host sans build VPS, et les règles associées (secrets, commit-avant-deploy, drift, smoke).","Describes the Synedre OS release pipeline: asymmetry between .\u002Fdeploy (AI, preprod) and .\u002Fship (Alex, production), the YAML dispatcher, the build-host pattern without build VPS, and the associated rules (secrets, commit-before-deploy, drift, smoke).","\u003Ch2 id=\"deploiement-asymetrie\">Asymétrie \u003Ccode>.\u002Fship\u003C\u002Fcode> vs \u003Ccode>.\u002Fdeploy\u003C\u002Fcode>\u003C\u002Fh2>\n\n\u003Cp>Deux points d'entrée existent à la racine du dépôt. Ils ne font \u003Cstrong>pas\u003C\u002Fstrong> la même chose et n'ont \u003Cstrong>pas\u003C\u002Fstrong> le même propriétaire.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fdeploy\u003C\u002Fcode>\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fship\u003C\u002Fcode>\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Cible\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Pré-production (ou runtime vaisseau-mère \u002F PROD pour les sites fondateurs sans preprod)\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Production\u003C\u002Fstrong>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Propriétaire\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>IA \u002F worker \u002F cascade\u003C\u002Fstrong>, de façon systématique\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Alex uniquement\u003C\u002Fstrong>, geste manuel\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Git\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Auto-commit du dirty + reste sur la branche \u003Ccode>preprod\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>git checkout main\u003C\u002Fcode> → \u003Ccode>git pull\u003C\u002Fcode> → \u003Ccode>git merge preprod -X theirs\u003C\u002Fcode> → \u003Ccode>git push origin main\u003C\u002Fcode>, puis retour \u003Ccode>preprod\u003C\u002Fcode> (trap EXIT)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Garde-fou dirty\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Auto-commit silencieux, sauf si sur \u003Ccode>main\u003C\u002Fcode> (refus dur)\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Bloquant\u003C\u002Fstrong> : refus si working tree sale, sauf \u003Ccode>--allow-dirty\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Vérification de dérive de schéma\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Une seule cible preprod câblée ; tout autre tenant → skip explicite. Bypass \u003Ccode>--skip-drift\u003C\u002Fcode> disponible.\u003C\u002Ftd>\n      \u003Ctd>Vérification du schéma du hub (bloquante) + DB prod tenant si cible connue. \u003Cstrong>Aucun\u003C\u002Fstrong> équivalent \u003Ccode>--skip-drift\u003C\u002Fcode> : la dérive hub est strictement bloquante.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Application automatique de la dérive\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Le moteur de correction de dérive génère et applique en une seule transaction les DDL idempotentes manquantes (\u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, \u003Ccode>ALTER TABLE ADD COLUMN IF NOT EXISTS\u003C\u002Fcode>). \u003Cstrong>Actif par défaut.\u003C\u002Fstrong> Désactivable ponctuellement. Appliqué \u003Cem>avant\u003C\u002Fem> tout build ou rechargement.\u003C\u002Ftd>\n      \u003Ctd>— (dérive appliquée manuellement avant le ship)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Smoke post-déploiement\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Vérification HTTP des tenants, enchaînée avec un smoke visuel non bloquant. Désactivable.\u003C\u002Ftd>\n      \u003Ctd>Vérification de l'environnement de processus (anti-faux-positif) \u003Cstrong>puis\u003C\u002Fstrong> smoke HTTP + contrôles de contenu JSON.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Migrations\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>Affiche les fichiers SQL en attente, \u003Cstrong>non bloquant\u003C\u002Fstrong>, \u003Cstrong>non auto-appliqué\u003C\u002Fstrong> — aucun runner de base de données branché.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Clôture\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>Propose la fermeture des chantiers de type \u003Ccode>test\u003C\u002Fcode> (post-ship).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Doctrine :\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Ccode>.\u002Fship &lt;tenant&gt;\u003C\u002Fcode> = Production, \u003Cstrong>jamais en autonome\u003C\u002Fstrong>, exclusif Alex. L'IA ne déclenche jamais \u003Ccode>.\u002Fship\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>.\u002Fdeploy &lt;tenant&gt;\u003C\u002Fcode> = \u003Cstrong>toujours l'IA\u003C\u002Fstrong>, systématiquement et sans demander — y compris sur le vaisseau-mère où \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> équivaut à un rebuild live du runtime (pas de preprod dédiée).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cpre>\u003Ccode>        ┌─────────────┐   .\u002Fdeploy &lt;tenant&gt;   ┌──────────────┐\n  IA →  │  branche     │ ──────────────────→   │  preprod \u002F    │\n        │  preprod     │   (auto, N fois)        │  runtime live │\n        └─────────────┘                         └──────────────┘\n                │\n                │  Alex review preprod, valide\n                ▼\n        ┌─────────────┐   .\u002Fship &lt;tenant&gt;      ┌──────────────┐\n Alex → │ merge preprod│ ──────────────────→   │  PRODUCTION   │\n        │  → main      │   (manuel uniquement)   │              │\n        └─────────────┘                         └──────────────┘\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Cstrong>Exception site public sans preprod :\u003C\u002Fstrong> \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> est refusé pour ce tenant — le site public n'a pas de preprod. On le modifie uniquement via \u003Ccode>.\u002Fship\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Exception tenants fondateurs sans preprod :\u003C\u002Fstrong> pour certains sites fondateurs, \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> pointe directement la production. La frontière \u003Cem>deploy = preprod\u003C\u002Fem> ne tient pas pour ces cas. La seule opération qui reste manuelle est \u003Ccode>.\u002Fship\u003C\u002Fcode> (fusion preprod→main + cérémonie complète).\u003C\u002Fp>\n\n\u003Ch2 id=\"deploiement-pipeline-ship\">Détail du pipeline \u003Ccode>.\u002Fship\u003C\u002Fcode> (étapes numérotées)\u003C\u002Fh2>\n\n\u003Cp>\u003Ccode>.\u002Fship &lt;tenant&gt;\u003C\u002Fcode> enchaîne, dans l'ordre :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Parsing des flags :\u003C\u002Fstrong> \u003Ccode>--allow-dirty\u003C\u002Fcode> (bypass garde-fou dirty), \u003Ccode>--no-close\u003C\u002Fcode> (skip post-ship), le reste est transmis au dispatcher.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Garde-fou dirty bloquant :\u003C\u002Fstrong> vérification de la propreté du dépôt ; exit si sale, sauf \u003Ccode>--allow-dirty\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Trap EXIT \u003Ccode>cleanup\u003C\u002Fcode> :\u003C\u002Fstrong> retour sur la branche \u003Ccode>preprod\u003C\u002Fcode> garanti même en cas d'erreur ; tout échec de checkout est signalé bruyamment (pour éviter de rester coincé sur \u003Ccode>main\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Nettoyage des fichiers root-owned :\u003C\u002Fstrong> suppression des artefacts laissés par d'anciens déploiements, qui sinon bloquent l'écriture.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Vérification de dérive du hub (bloquante) :\u003C\u002Fstrong> aucun bypass. Suivie de la vérification de dérive DB prod du tenant si la cible est connue.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Fusion \u003Ccode>preprod → main\u003C\u002Fcode> :\u003C\u002Fstrong> checkout main, pull, merge \u003Ccode>preprod -X theirs\u003C\u002Fcode>, push origin main.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Migrations :\u003C\u002Fstrong> affichage des fichiers SQL en attente (voir section suivante). Non bloquant, non auto-appliqué.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Audit des agents :\u003C\u002Fstrong> vérification de l'état des agents actifs (non bloquant).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Déploiement prod :\u003C\u002Fstrong> appel du dispatcher de déploiement avec les paramètres du tenant.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Smoke prod :\u003C\u002Fstrong> vérification de l'environnement de processus \u003Cem>avant\u003C\u002Fem> le smoke HTTP + contrôles de contenu JSON.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Post-ship close :\u003C\u002Fstrong> propose la fermeture des chantiers de type \u003Ccode>test\u003C\u002Fcode> (ignoré via \u003Ccode>--no-close\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Asymétrie \u003Ccode>--skip-drift\u003C\u002Fcode> :\u003C\u002Fstrong> \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> accepte \u003Ccode>--skip-drift\u003C\u002Fcode> pour bypasser la vérification de dérive en preprod (cas de hotfix urgent). \u003Ccode>.\u002Fship\u003C\u002Fcode> n'a \u003Cstrong>aucun\u003C\u002Fstrong> équivalent — la dérive du hub y est strictement bloquante. On ne livre jamais en production sur un schéma divergent.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"deploiement-migrations-ship\">Migrations de base de données lors du ship\u003C\u002Fh2>\n\n\u003Cp>Le ship \u003Cstrong>affiche\u003C\u002Fstrong> les fichiers SQL en attente mais ne les applique \u003Cstrong>jamais\u003C\u002Fstrong> : aucun runner de base de données n'est branché. Le bloc est purement informatif et non bloquant — il imprime les commandes à exécuter manuellement.\u003C\u002Fp>\n\n\u003Cp>Un mapping \u003Cem>tenant → scope de migrations\u003C\u002Fem> évite de mélanger les migrations du vaisseau-mère et celles des tenants :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Tenant\u003C\u002Fth>\n      \u003Cth>Dossier de migrations ciblé\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Vaisseau-mère \u002F runtime OS\u003C\u002Ftd>\n      \u003Ctd>Scope \u003Ccode>mothership\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Un tenant e-commerce (ex. tenant A)\u003C\u002Ftd>\n      \u003Ctd>Scope propre au tenant A\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Un tenant vape (ex. tenant B)\u003C\u002Ftd>\n      \u003Ctd>Scope propre au tenant B\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Sites fondateurs (\u003Cem>client\u003C\u002Fem>, \u003Cem>alexandrecarette\u003C\u002Fem>, \u003Cem>codemyshop\u003C\u002Fem>)\u003C\u002Ftd>\n      \u003Ctd>Scope propre à chaque site\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>all\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Cas spécial : agrège toutes les cibles\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cem>autre\u003C\u002Fem>\u003C\u002Ftd>\n      \u003Ctd>Fallback : scope = nom du tenant\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Cas spécial \u003Ccode>all\u003C\u002Fcode> :\u003C\u002Fstrong> agrège l'ensemble des fichiers SQL de tous les scopes, en excluant les sous-dossiers \u003Ccode>_applied\u002F\u003C\u002Fcode> et \u003Ccode>applied\u002F\u003C\u002Fcode> (migrations déjà passées). Les autres scopes listent simplement les fichiers SQL du dossier correspondant.\u003C\u002Fp>\n\n\u003Ch2 id=\"deploiement-routage\">Routage de \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> et \u003Ccode>.\u002Fship\u003C\u002Fcode> par tenant\u003C\u002Fh2>\n\n\u003Cp>Chaque tenant est routé vers un script dédié ou vers le dispatcher générique de déploiement.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Tenant (argument)\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fdeploy\u003C\u002Fcode> →\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fship\u003C\u002Fcode> →\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Vaisseau-mère (défaut)\u003C\u002Ftd>\n      \u003Ctd>Vérification de santé du verrou de concurrence, puis script de déploiement dédié au runtime OS (\u003Ccode>--prod --all\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Idem script dédié runtime OS (\u003Ccode>--prod --all\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Site public sans preprod\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Refusé\u003C\u002Fstrong> — pas de preprod\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique (\u003Ccode>--all\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Site fondateur synedre.com\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique → \u003Cstrong>PROD direct\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique ⚠️ vérification d'environnement de processus peut bloquer si le tenant n'est pas déclaré dans le vérificateur\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Site fondateur CodeMyShop\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique → \u003Cstrong>PROD direct\u003C\u002Fstrong> (plus d'ansible\u002Fpreprod) ; smoke activé\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Demo CodeMyShop\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique → staging démo\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tenant e-commerce A (preprod)\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique (\u003Ccode>--target=preprod --all\u003C\u002Fcode>) ; smoke désactivé (preprod derrière basic auth)\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique sans \u003Ccode>--target\u003C\u002Fcode> → prod\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tenant vape B\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique ; smoke activé\u003C\u002Ftd>\n      \u003Ctd>⚠️ Tombe dans le cas générique fallback — probablement en erreur si aucun VPS prod n'est défini\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Site fondateur client\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique → PROD ; smoke activé\u003C\u002Ftd>\n      \u003Ctd>Dispatcher générique\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>all\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>5 jobs en parallèle en arrière-plan : vaisseau-mère, CodeMyShop (PROD), tenant e-commerce A (preprod), tenant vape B, client. Smoke post-jobs sur l'ensemble. ⚠️ Le smoke \u003Ccode>all\u003C\u002Fcode> teste la démo CodeMyShop mais le job déploie la prod vitrine — la vitrine prod n'est pas vérifiée dans ce chemin.\u003C\u002Ftd>\n      \u003Ctd>3 jobs en parallèle : runtime OS, site public sans preprod, CodeMyShop. Smoke sur les trois.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--list\u003C\u002Fcode> \u002F \u003Ccode>-l\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Affiche l'inventaire des VPS clients (voir section dédiée)\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cem>autre (inconnu)\u003C\u002Fem>\u003C\u002Ftd>\n      \u003Ctd>Fallback vers le script d'ansible legacy (\u003Ccode>--preprod\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Fallback vers le script d'ansible legacy (\u003Ccode>--prod\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Convergence vers le dispatcher unique :\u003C\u002Fstrong> depuis la standardisation du pattern de déploiement et la purge des scripts legacy, \u003Cstrong>tous les tenants distants sauf le vaisseau-mère\u003C\u002Fstrong> passent par le dispatcher générique, aussi bien pour \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> que pour \u003Ccode>.\u002Fship\u003C\u002Fcode>. Le vaisseau-mère conserve son script dédié car le self-déploiement du runtime est architecturalement distinct : build docker local, sans transfert réseau vers un VPS externe.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cblockquote>\n  \u003Cp>⚠️ \u003Cstrong>Angle mort smoke \u003Ccode>all\u003C\u002Fcode> :\u003C\u002Fstrong> lors d'un \u003Ccode>.\u002Fdeploy all\u003C\u002Fcode>, le déploiement cible la vitrine CodeMyShop prod, mais le smoke suivant vérifie la démo staging. La vitrine de production n'est pas contrôlée dans ce chemin — point à corriger dans la boucle de smoke.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"dispatcher-yaml-driven\">Le dispatcher piloté par YAML\u003C\u002Fh2>\n\n\u003Cp>Le pipeline de déploiement standard — utilisé pour tous les tenants distants, à l'exception du déploiement du vaisseau-mère lui-même — est piloté par un fichier de configuration YAML propre à chaque tenant. Un script de déploiement principal orchestre l'ensemble de la séquence en lisant ce fichier, en le validant, puis en enchaînant les étapes dans un ordre déterministe.\u003C\u002Fp>\n\n\u003Ch3>Résolution du fichier de configuration\u003C\u002Fh3>\n\n\u003Cp>À l'invocation, le dispatcher localise le fichier YAML à appliquer selon deux règles :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>Lorsque la cible désigne le vaisseau-mère, le dispatcher tente de charger un fichier de configuration dédié. Ce fichier est documenté dans le schéma officiel, mais \u003Cstrong>n'existe pas sur disque\u003C\u002Fstrong> : le vaisseau-mère suit un pipeline de self-déploiement distinct, activé en amont. Le branchement correspondant dans le dispatcher est donc inerte en pratique.\u003C\u002Fli>\n  \u003Cli>Pour tout autre tenant, le dispatcher charge le fichier \u003Ccode>deploy.yaml\u003C\u002Fcode> situé dans le répertoire du tenant. Si une cible nommée est précisée (option \u003Ccode>--target=&lt;X&gt;\u003C\u002Fcode>), le fichier \u003Ccode>deploy.&lt;X&gt;.yaml\u003C\u002Fcode> est utilisé à la place.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note :\u003C\u002Fstrong> le nom \u003Ccode>synedre\u003C\u002Fcode> (sans suffixe) identifie le tenant fondateur du site synedre.com ; il suit le chemin tenant standard et n'active pas le branchement vaisseau-mère.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Étapes du dispatcher\u003C\u002Fh3>\n\n\u003Cp>Le dispatcher exécute les étapes suivantes dans l'ordre :\u003C\u002Fp>\n\u003Col>\n  \u003Cli>\u003Cstrong>Analyse des arguments :\u003C\u002Fstrong> l'option \u003Ccode>--target=&lt;X&gt;\u003C\u002Fcode> (le signe \u003Ccode>=\u003C\u002Fcode> est obligatoire), le flag \u003Ccode>--all\u003C\u002Fcode> (obligatoire) et le flag optionnel \u003Ccode>--clean\u003C\u002Fcode> sont extraits et validés.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Résolution et validation YAML :\u003C\u002Fstrong> un validateur Python lit le fichier YAML, vérifie sa conformité au schéma, et émet les variables de configuration sous forme de paires clé-valeur évaluées par le shell. Toute erreur de validation provoque l'arrêt immédiat du pipeline.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Vérification des arguments standard :\u003C\u002Fstrong> les flags \u003Ccode>CLEAN_CACHE\u003C\u002Fcode> et \u003Ccode>HAS_ALL\u003C\u002Fcode> sont consolidés.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Bannière de démarrage :\u003C\u002Fstrong> affichage du contexte et initialisation du chronomètre de déploiement.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Vérification de dérive de schéma (\u003Cem>drift check\u003C\u002Fem>) :\u003C\u002Fstrong> si la section \u003Ccode>drift\u003C\u002Fcode> est présente dans le YAML, le moteur compare l'état réel de la base de données au schéma attendu et signale les écarts.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Application automatique de la dérive (\u003Cem>drift auto-apply\u003C\u002Fem>) :\u003C\u002Fstrong> activé par défaut, ce mécanisme génère et applique en une seule transaction les instructions DDL idempotentes manquantes (\u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, \u003Ccode>ALTER TABLE ADD COLUMN IF NOT EXISTS\u003C\u002Fcode>). Le schéma est mis à jour \u003Cem>avant\u003C\u002Fem> tout build ou rechargement. Cette étape peut être désactivée ponctuellement via une variable d'environnement dédiée.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Lancement des hooks en arrière-plan :\u003C\u002Fstrong> l'auditeur d'agents est démarré en tâche de fond.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Génération des routes i18n (prébuild) :\u003C\u002Fstrong> si la section \u003Ccode>seed_i18n\u003C\u002Fcode> est présente, le dispatcher interroge les tables de catégories et de métadonnées localisées du tenant (via SSH ou conteneur) et écrit le fichier JSON des segments de route localisés consommé par le module de routing international au moment du build. Cette étape est non bloquante : si la base de données est inaccessible, le build se replie sur les valeurs codées en dur.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Build Nuxt :\u003C\u002Fstrong> compilation de l'application front-end.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Seed i18n :\u003C\u002Fstrong> si la section est présente, injection des données de traduction.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Source maps :\u003C\u002Fstrong> si le hook correspondant est activé, envoi des source maps.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Packaging :\u003C\u002Fstrong> création d'une archive compressée du build dans le répertoire temporaire de la machine locale.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Push Git en arrière-plan :\u003C\u002Fstrong> si le hook est activé, poussée du dépôt en tâche de fond.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Upload de l'archive :\u003C\u002Fstrong> transfert sécurisé de l'archive vers le VPS client.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Rechargement distant :\u003C\u002Fstrong> selon la variante déclarée dans le YAML, le dispatcher exécute soit un rechargement gracieux via le gestionnaire de processus (\u003Ccode>pm2\u003C\u002Fcode>), soit un redémarrage du conteneur applicatif (\u003Ccode>docker compose restart\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Installation des automates récurrents (\u003Cem>cron\u003C\u002Fem>) :\u003C\u002Fstrong> si la section \u003Ccode>cron\u003C\u002Fcode> est présente, le dispatcher installe les entrées planifiées dans le crontab de l'utilisateur SSH \u003Cem>sur le VPS client\u003C\u002Fem> (jamais sur le vaisseau-mère). Chaque bloc est marqué et idempotent : l'ancien bloc est retiré avant réécriture. Une liste vide déclenche la suppression du bloc. \u003Cstrong>Doctrine :\u003C\u002Fstrong> les automates récurrents s'exécutent au plus près de la base de données du tenant.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Attente des hooks en arrière-plan :\u003C\u002Fstrong> synchronisation avec les tâches Git push et audit d'agents.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Health check :\u003C\u002Fstrong> boucle de vérification de disponibilité de l'URL cible jusqu'à expiration du délai configuré.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Bannière de fin :\u003C\u002Fstrong> affichage du bilan et du temps total écoulé.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>Schéma du fichier de configuration YAML\u003C\u002Fh3>\n\n\u003Cp>Chaque fichier de configuration est validé par le validateur Python avant tout déploiement. Les sections reconnues sont les suivantes :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Section\u003C\u002Fth>\n      \u003Cth>Obligatoire\u003C\u002Fth>\n      \u003Cth>Description\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>name\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Oui\u003C\u002Ftd>\n      \u003Ctd>Identifiant du déploiement (minuscules, chiffres, tirets, underscores).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>build\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Oui\u003C\u002Ftd>\n      \u003Ctd>Chemin du client local (\u003Ccode>local_client\u003C\u002Fcode> requis) et environnement Node (\u003Ccode>node_env\u003C\u002Fcode>, défaut \u003Ccode>production\u003C\u002Fcode>).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>ssh\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Oui\u003C\u002Ftd>\n      \u003Ctd>Hôte cible requis ; utilisateur SSH (défaut \u003Ccode>ubuntu\u003C\u002Fcode>) et chemin de clé optionnels.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>remote\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Oui\u003C\u002Ftd>\n      \u003Ctd>Variante de déploiement : \u003Ccode>pm2\u003C\u002Fcode> ou \u003Ccode>docker\u003C\u002Fcode>. Voir garde-fous ci-dessous.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>health\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Oui\u003C\u002Ftd>\n      \u003Ctd>URL de vérification requise ; délai maximal d'attente (défaut 45 secondes).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>drift\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Non\u003C\u002Ftd>\n      \u003Ctd>Déclaration des écarts de schéma à détecter et appliquer automatiquement.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>seed_i18n\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Non\u003C\u002Ftd>\n      \u003Ctd>Active la génération prébuild des routes i18n et l'injection des traductions.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>hooks\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Non\u003C\u002Ftd>\n      \u003Ctd>Hooks optionnels : audit d'agents, upload de source maps, push Git.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>cron\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Non\u003C\u002Ftd>\n      \u003Ctd>Liste d'automates récurrents à installer sur le VPS client.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Garde-fous de cohérence par variante\u003C\u002Fh3>\n\n\u003Cp>Le validateur applique des règles de cohérence strictes selon la variante choisie :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>Variante \u003Ccode>pm2\u003C\u002Fcode> :\u003C\u002Fstrong> le nom de l'application de processus (\u003Ccode>pm2_app\u003C\u002Fcode>) est \u003Cstrong>requis\u003C\u002Fstrong> ; les champs propres à Docker (\u003Ccode>container\u003C\u002Fcode>, \u003Ccode>subdir\u003C\u002Fcode>) sont \u003Cstrong>interdits\u003C\u002Fstrong>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Variante \u003Ccode>docker\u003C\u002Fcode> :\u003C\u002Fstrong> le nom du conteneur cible (\u003Ccode>container\u003C\u002Fcode>) est \u003Cstrong>requis\u003C\u002Fstrong> ; les champs propres à PM2 sont \u003Cstrong>interdits\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Validation de la section \u003Ccode>cron\u003C\u002Fcode>\u003C\u002Fh3>\n\n\u003Cp>Chaque entrée de la section \u003Ccode>cron\u003C\u002Fcode> est validée individuellement :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Ccode>key\u003C\u002Fcode> : identifiant en kebab-case, unique dans le fichier.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>schedule\u003C\u002Fcode> : expression crontab en exactement 5 champs.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>command\u003C\u002Fcode> : commande non vide à exécuter.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>comment\u003C\u002Fcode> : description libre (optionnelle).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Une section \u003Ccode>cron\u003C\u002Fcode> déclarée vide (\u003Ccode>[]\u003C\u002Fcode>) est transmise à l'installeur comme signal de nettoyage : le bloc correspondant est retiré du crontab du tenant.\u003C\u002Fp>\n\n\u003Ch3>Exemple de fichier de configuration\u003C\u002Fh3>\n\n\u003Cp>Le fichier ci-dessous illustre une configuration type pour un tenant déployé via le gestionnaire de processus PM2, avec push Git activé :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>name: mon-tenant\nbuild:\n  local_client: un composant interne\n  node_env: production\nssh:\n  host: &lt;adresse du VPS client&gt;\nremote:\n  variant: pm2\n  dir: \u002Fvar\u002Fwww\u002Fun composant interne\n  pm2_app: mon-tenant-nuxt\n  pm2_remote_user: codemyshop\nhealth:\n  url: https:\u002F\u002Fmon-tenant.example.com\nhooks:\n  git_push: mon-tenant\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Rappel :\u003C\u002Fstrong> l'hôte SSH (\u003Ccode>host\u003C\u002Fcode>) doit être renseigné avec l'adresse ou le nom d'hôte réel du VPS client. Aucune adresse IP réelle ne doit être versionnée dans un fichier de configuration partagé publiquement.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"build-host-pattern\">Pattern : compilation sur le vaisseau-mère, déploiement vers le VPS\u003C\u002Fh2>\n\n\u003Cp>L'invariant central pour les déploiements distants est le suivant : \u003Cstrong>le VPS client ne compile jamais\u003C\u002Fstrong>. Le bundle Nuxt est entièrement produit sur le vaisseau-mère, compressé, transféré, extrait sur place, puis le processus applicatif est rechargé. La bannière de départ du pipeline affiche explicitement ce mode de fonctionnement.\u003C\u002Fp>\n\n\u003Ch3>Déroulement en huit étapes\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>  ┌─────────────────── VAISSEAU-MÈRE (compilation) ───────────────────┐\n  │ 1. Installation des dépendances (delta, mode hors-ligne préféré)  │\n  │ 2. Exécution des tests smoke d'invariants d'URL                   │\n  │ 3. Compilation Nuxt du client cible  →  .output\u002F                 │\n  │ 4. Archivage compressé (pigz si disponible, sinon gzip)           │\n  └───────────────────────────┬───────────────────────────────────────┘\n                               │  transfert sécurisé (SCP)\n                               ▼\n  ┌─────────────────── VPS CLIENT ────────────────────────────────────┐\n  │ 5. Rotation  .output → .output_old        (rollback prêt)        │\n  │ 6. Extraction de l'archive compressée                             │\n  │ 7. Rechargement du processus (graceful reload ou restart)         │\n  │ 8. Si en ligne → suppression de .output_old                       │\n  │    Sinon       → affichage de la commande de restauration + stop  │\n  └───────────────────────────┬───────────────────────────────────────┘\n                               │\n                               ▼  sondage HTTP 200 (délai max configurable)\n                         vérification de santé\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Détail des fonctions du pipeline\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Compilation Nuxt\u003C\u002Fstrong> : purge du répertoire de sortie (et du cache incrémental si l'option \u003Ccode>--clean\u003C\u002Fcode> est passée), installation des dépendances en mode hors-ligne, puis \u003Cstrong>test smoke bloquant\u003C\u002Fstrong> sur les invariants de forme des URL produit avant tout \u003Ccode>nuxi build\u003C\u002Fcode>. Si le build incrémental échoue, une tentative avec cache purgé est automatiquement relancée.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Archivage\u003C\u002Fstrong> : le répertoire \u003Ccode>.output\u003C\u002Fcode> du client est empaqueté dans une archive compressée. \u003Ccode>pigz\u003C\u002Fcode> est utilisé si disponible sur le vaisseau-mère, avec repli automatique sur \u003Ccode>gzip\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Transfert\u003C\u002Fstrong> : l'archive est copiée sur le VPS via SCP, puis supprimée localement.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Redémarrage Docker\u003C\u002Fstrong> : connexion SSH au VPS, rotation \u003Ccode>.output → .output_old\u003C\u002Fcode>, extraction de l'archive dans le répertoire cible, suppression d'un module CommonJS superflu identifié lors d'une cicatrice antérieure (tree-shake CJS), redémarrage du conteneur applicatif, vérification que le conteneur est bien en état \u003Ccode>running\u003C\u002Fcode> — sinon rollback.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Rechargement PM2 graceful\u003C\u002Fstrong> : \u003Ccode>pm2 reload\u003C\u002Fcode> avec mise à jour des variables d'environnement — préserve les connexions en cours. Gère optionnellement un utilisateur PM2 dédié sur le VPS. Un lien symbolique optionnel vers le dossier des uploads du back-office peut être (re)créé pour éviter les erreurs de fichiers statiques introuvables.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Redémarrage PM2 hard\u003C\u002Fstrong> : suppression puis recréation complète du processus PM2 (premier déploiement ou configuration explicite via paramètre \u003Ccode>pm2_start\u003C\u002Fcode>), suivi d'une sauvegarde de la liste des processus.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Boucle de vérification de santé\u003C\u002Fstrong> : sondage HTTP répété jusqu'à obtenir un code \u003Ccode>200\u003C\u002Fcode> ou expiration du délai maximum (45 secondes par défaut).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Bannières de départ et de fin\u003C\u002Fstrong> : encadrement visuel du pipeline avec décomposition des durées par phase.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Installateur de tâche planifiée\u003C\u002Fstrong> : installe la tâche cron par client sur le VPS distant (voir §3).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Comportement du rollback\u003C\u002Fh3>\n\n\u003Cp>\u003Ccode>.output_old\u003C\u002Fcode> est conservé pendant toute la durée du rechargement. Il est purgé uniquement si le processus redémarre avec succès. En cas d'échec, le script \u003Cstrong>affiche uniquement\u003C\u002Fstrong> la commande de restauration suggérée (\u003Ccode>mv .output_old .output\u003C\u002Fcode>) et se termine avec un code d'erreur — \u003Cstrong>la restauration reste manuelle\u003C\u002Fstrong>, le déplacement n'est jamais exécuté automatiquement. Pour forcer la conservation de \u003Ccode>.output_old\u003C\u002Fcode> même en cas de succès, la variable \u003Ccode>SHIP_KEEP_ROLLBACK=1\u003C\u002Fcode> peut être positionnée.\u003C\u002Fp>\n\n\u003Ch2 id=\"smoke-post-deploy\">Smoke post-déploiement : trois couches de validation\u003C\u002Fh2>\n\n\u003Cp>Le script de vérification post-déploiement comprend deux sous-niveaux de vérification formels, complétés d'un troisième niveau visuel non-bloquant.\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\n    \u003Cstrong>Vérifications HTTP\u003C\u002Fstrong> : sondage de chaque URL déclarée pour le client ; tout code \u003Ccode>5xx\u003C\u002Fcode> est bloquant, un \u003Ccode>4xx\u003C\u002Fcode> génère un avertissement non-bloquant. Les clients couverts incluent le cockpit principal, la vitrine démo, plusieurs tenants de production et le site institutionnel. Une boucle interne couvre six clients en mode \u003Cem>all\u003C\u002Fem> ; deux autres sont vérifiés individuellement depuis le pipeline de livraison.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Vérifications de contenu \u002F JSON\u003C\u002Fstrong> : valide la \u003Cem>forme\u003C\u002Fem> du JSON retourné par l'API de navigation principale. Cette couche a été introduite après un incident où une page renvoyait HTTP \u003Ccode>200\u003C\u002Fcode> pendant deux jours avec un JSON portant \u003Ccode>{ error: true }\u003C\u002Fcode>, rendant la navigation entièrement vide et le pied de page affiché en clés brutes. Sans ce niveau, les rendus SSR pilotés par la base de données pouvaient échouer silencieusement. Les clients disposant d'une navigation en base de données sont couverts ; les clients sans navigation globale (cockpit headless, vitrine sans navigation configurée) sont volontairement exclus.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Vérification visuelle automatisée\u003C\u002Fstrong> (non-bloquante) : capture d'écran Playwright suivie d'un verdict multimodal. Le module est ignoré proprement si aucun fichier de configuration de vérifications visuelles n'existe pour le client (zéro latence ajoutée). Actif par défaut, désactivable via variable d'environnement.\n  \u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch2 id=\"check-pm2-env\">Anti-faux-positif : vérification des variables d'environnement PM2\u003C\u002Fh2>\n\n\u003Cp>Un code HTTP \u003Ccode>200\u003C\u002Fcode> ne suffit pas à valider un déploiement PM2. Un incident a démontré qu'un client pouvait être redéployé sans aucune variable de connexion à la base de données : toutes les routes SSR pilotées par les données renvoyaient \u003Ccode>500\u003C\u002Fcode>, mais la page servait un \u003Cstrong>squelette vide syntaxiquement valide\u003C\u002Fstrong> — le smoke HTTP retournait donc \u003Ccode>200\u003C\u002Fcode> pendant deux jours.\u003C\u002Fp>\n\n\u003Cp>C'est pourquoi le script de livraison lance un vérificateur d'environnement PM2 \u003Cstrong>avant\u003C\u002Fstrong> la vérification smoke : il se connecte en SSH au VPS distant et inspecte la présence des variables critiques dans l'environnement du processus PM2 actif.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Code retour\u003C\u002Fth>\n      \u003Cth>Signification\u003C\u002Fth>\n      \u003Cth>Comportement\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>0\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Toutes les variables critiques sont présentes\u003C\u002Ftd>\n      \u003Ctd>Pipeline continue\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>1\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Variables manquantes\u003C\u002Ftd>\n      \u003Ctd>Bloquant — arrêt du pipeline\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>2\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Connexion SSH ou processus PM2 inaccessible\u003C\u002Ftd>\n      \u003Ctd>Bloquant — arrêt du pipeline\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Les cibles non-PM2 (vaisseau-mère headless, environnements Docker purs) retournent automatiquement \u003Ccode>0\u003C\u002Fcode> et ne sont pas inspectées.\u003C\u002Fp>\n\n\u003Ch2 id=\"lock-health-check\">Vérification de cohérence du lockfile avant compilation\u003C\u002Fh2>\n\n\u003Cp>Avant tout déploiement du cockpit principal, le pipeline exécute un contrôle de cohérence du lockfile détectant deux décalages bloquants :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Version de minifieur CSS inférieure au seuil requis\u003C\u002Fstrong> dans le lockfile (dépendances PostCSS transitives manquantes → build cassé silencieusement).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Version de Nuxt inférieure au seuil requis\u003C\u002Fstrong> dans le manifeste de dépendances (risque de décalage avec l'outil de bundling).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>Un avertissement non-bloquant est émis si des surcharges de version de Vite sont détectées dans la configuration racine. En cas d'erreur bloquante, le script se termine avec un code \u003Ccode>1\u003C\u002Fcode> et un message correctif explicite — le déploiement est annulé. La correction suggérée consiste à régénérer le lockfile depuis zéro.\u003C\u002Fp>\n\n\u003Ch2 id=\"self-deploy-cockpit\">Déploiement du cockpit principal : auto-déploiement local\u003C\u002Fh2>\n\n\u003Cp>Le cockpit principal ne passe \u003Cstrong>pas\u003C\u002Fstrong> par le pipeline de déploiement distant décrit ci-dessus. C'est un \u003Cstrong>auto-déploiement local\u003C\u002Fstrong> : le vaisseau-mère compile à l'intérieur de son propre conteneur applicatif. Un script dédié orchestre l'ensemble en mode \u003Ccode>--prod --all\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Ch3>Spécificités par rapport aux déploiements distants\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Absence d'environnement de pré-production depuis mi-2026\u003C\u002Fstrong> : \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> sur le cockpit principal est un \u003Cstrong>push direct en production live\u003C\u002Fstrong>, sans la cérémonie de livraison de \u003Ccode>.\u002Fship\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Transfert local par copie Docker\u003C\u002Fstrong> : une archive source minimale (code applicatif + manifestes de dépendances, sans \u003Ccode>.nuxt\u003C\u002Fcode>, \u003Ccode>.output\u003C\u002Fcode> ni \u003Ccode>node_modules\u003C\u002Fcode>) est copiée dans le conteneur via \u003Ccode>docker cp\u003C\u002Fcode> — aucun transfert réseau.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Préservation du cache de build incrémental\u003C\u002Fstrong> : avant l'extraction de l'archive, le cache \u003Ccode>.nuxt\u003C\u002Fcode> existant dans le conteneur est mis de côté puis restauré après extraction. Ce mécanisme est le principal levier de performance : le build reste incrémental et non un cold build complet.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Compilation à l'intérieur du conteneur\u003C\u002Fstrong> : le build Nuxt s'exécute dans le conteneur actif via \u003Ccode>docker exec\u003C\u002Fcode>. L'ancien serveur reste en ligne pendant toute la durée de la compilation. Aucun gestionnaire de processus PM2 n'est utilisé dans ce pipeline : le conteneur démarre directement le serveur Node.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Séquence de rotation\u003C\u002Fstrong> : arrêt gracieux du conteneur → copie du nouveau \u003Ccode>.output\u003C\u002Fcode> vers l'hôte → recréation forcée du conteneur (relit les fichiers d'environnement). Cette étape a été introduite après un incident où les variables d'environnement n'étaient pas rechargées lors d'un simple \u003Ccode>restart\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Base de code autonome\u003C\u002Fstrong> : le cockpit principal n'étend plus le socle commun — l'archive transférée est allégée d'environ 16 MB par rapport à l'ancienne configuration.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Optimisation npm par empreinte\u003C\u002Fstrong> : une empreinte SHA-256 du fichier de verrou des dépendances est conservée dans le conteneur. Si le lockfile n'a pas changé depuis le dernier déploiement, l'étape \u003Ccode>npm install\u003C\u002Fcode> est intégralement sautée.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Vérification de santé par UUID de build\u003C\u002Fstrong> : la vérification ne se limite pas à un HTTP \u003Ccode>200\u003C\u002Fcode>. L'UUID du build attendu (lu depuis le manifeste de build côté hôte) est comparé à celui retourné par le point de terminaison \u003Ccode>\u002Fapi\u002Fhealth\u003C\u002Fcode> du conteneur. Cela garantit que c'est bien le nouveau build qui sert les requêtes, et non l'ancien maintenu par un rechargement partiel.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Variante accélérée\u003C\u002Fstrong> : une option opt-in permet de déporter la compilation sur l'hôte plutôt que dans le conteneur, avec un swap atomique du répertoire de sortie (~35 % de gain de temps).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Cérémonie de livraison \u003Ccode>.\u002Fship\u003C\u002Fcode>\u003C\u002Fstrong> : ajoute la fusion de branche, le push, les audits d'agents, le smoke de production et la clôture de chantier, mais appelle en final le même script de déploiement local.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note\u003C\u002Fstrong> : contrairement aux VPS clients, le conteneur du cockpit principal ne passe jamais par la phase de transfert SCP ni par PM2. La compilation et le service applicatif sont entièrement conteneurisés sur le même hôte physique.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"inventaire-topologie\">Inventaire de la topologie — source unique de vérité\u003C\u002Fh2>\n\n\u003Cp>La topologie complète de l'infrastructure (liste des environnements, accès SSH, bases de données associées) est stockée dans \u003Cstrong>une seule table de référence\u003C\u002Fstrong>, hébergée dans la base de données centrale du vaisseau-mère. Cette table est la \u003Cstrong>source de vérité unique\u003C\u002Fstrong> : aucun fichier d'architecture, aucun script, aucun agent ne doit dupliquer ou coder en dur ces informations — tout autre document peut être obsolète.\u003C\u002Fp>\n\n\u003Cp>La commande de listage de l'inventaire interroge cette table et affiche un tableau aligné en console. La requête filtre les entrées actives, ordonne les résultats par criticité décroissante, et formate les colonnes pour une lecture rapide.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>-- Exemple illustratif (colonnes réelles, valeurs anonymisées)\nSELECT\n  rpad(identifiant_tenant, 14)        AS tenant,\n  rpad(environnement, 11)             AS env,\n  rpad(coalesce(domaine, '-'), 40)    AS domaine,\n  CASE WHEN stack_nuxt THEN 'nuxt' ELSE 'ps' END AS stack\nFROM inventaire_topologie\nWHERE actif = 1\nORDER BY criticite DESC, identifiant_tenant, environnement;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Colonnes notables de la table d'inventaire\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Colonne\u003C\u002Fth>\n      \u003Cth>Rôle\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Identifiant tenant\u003C\u002Ftd>\n      \u003Ctd>Clé logique primaire identifiant l'environnement client\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Environnement\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>production\u003C\u002Fcode> | \u003Ccode>staging\u003C\u002Fcode> | \u003Ccode>infra\u003C\u002Fcode> | \u003Ccode>legacy\u003C\u002Fcode> | \u003Ccode>audit\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Accès SSH\u003C\u002Ftd>\n      \u003Ctd>Adresse du VPS client, utilisateur et chemin de clé SSH\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Base de données\u003C\u002Ftd>\n      \u003Ctd>Références vers le container DB, le nom de base et l'\u003Cem>identifiant\u003C\u002Fem> de la variable d'environnement contenant le mot de passe (jamais la valeur)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Runtime web\u003C\u002Ftd>\n      \u003Ctd>Container applicatif, présence PrestaShop, présence Nuxt\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Criticité business\u003C\u002Ftd>\n      \u003Ctd>Niveau de criticité, revenu mensuel récurrent, offre souscrite\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Ship automatique autorisé\u003C\u002Ftd>\n      \u003Ctd>Booléen autorisant un déploiement automatique — à confirmer avant usage\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Codename de déploiement\u003C\u002Ftd>\n      \u003Ctd>Identifiant utilisé pour composer la commande exacte de déploiement\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Codename client\u003C\u002Ftd>\n      \u003Ctd>Clé étrangère vers la table de fédération client (référentiel centralisé)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>État de l'inventaire actif\u003C\u002Fh3>\n\n\u003Cp>L'inventaire comprend plusieurs catégories d'entrées actives :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Tenants en production active\u003C\u002Fstrong> : environnements critiques hébergeant des boutiques ou sites en service, sur un ou plusieurs VPS clients.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Environnements staging\u003C\u002Fstrong> : copies de recette associées aux tenants principaux.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Infra vaisseau-mère\u003C\u002Fstrong> : l'entrée désignant le VPS central du vaisseau-mère lui-même.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Productions orphelines\u003C\u002Fstrong> : certains tenants portent \u003Ccode>purpose='production'\u003C\u002Fcode> tout en étant en sommeil — il s'agit de projets secondaires sans maintenance active. Ils ne doivent pas être agrégés sous l'étiquette « legacy ».\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Double entrée hétérogène\u003C\u002Fstrong> : un tenant peut apparaître en deux lignes distinctes (ex. une ligne production Nuxt et une ligne legacy sans Nuxt) reflétant deux générations techniques coexistantes. Ne pas les fusionner.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Entrée décommissionnée\u003C\u002Fstrong> : un tenant ayant fait l'objet d'une mission ponctuelle terminée a été passé \u003Ccode>actif=0\u003C\u002Fcode> ; son dossier opérationnel a été retiré, mais son historique est conservé dans le référentiel client centralisé.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Doctrine opérationnelle :\u003C\u002Fstrong> avant chaque déploiement (\u003Ccode>.\u002Fdeploy\u003C\u002Fcode>) ou mise en production (\u003Ccode>.\u002Fship\u003C\u002Fcode>), consulter le cadran de gestion des tenants pour y copier la commande exacte associée au codename cible. Ne jamais recomposer la commande de mémoire.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"doctrine-secrets\">Doctrine de gestion des secrets — cinq niveaux\u003C\u002Fh2>\n\n\u003Cp>Chaque secret possède \u003Cstrong>un seul fichier canonique\u003C\u002Fstrong>. La présence d'une même clé dans deux fichiers distincts est traitée comme un bug, non comme une redondance de sécurité.\u003C\u002Fp>\n\n\u003Ch3>Les cinq niveaux de fichiers de secrets\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>#\u003C\u002Fth>\n      \u003Cth>Fichier\u003C\u002Fth>\n      \u003Cth>Périmètre\u003C\u002Fth>\n      \u003Cth>Suivi git\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>1\u003C\u002Ftd>\n      \u003Ctd>Fichier d'environnement du tenant\u003C\u002Ftd>\n      \u003Ctd>Un seul tenant — lu par son container applicatif et la configuration de déploiement\u003C\u002Ftd>\n      \u003Ctd>Non\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2\u003C\u002Ftd>\n      \u003Ctd>Fichier d'environnement du cœur vaisseau-mère\u003C\u002Ftd>\n      \u003Ctd>Services centraux du vaisseau-mère\u003C\u002Ftd>\n      \u003Ctd>Non\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>3\u003C\u002Ftd>\n      \u003Ctd>Fichier d'environnement hôte\u003C\u002Ftd>\n      \u003Ctd>Scripts hôte : tâches planifiées, automates, accès SSH aux VPS clients\u003C\u002Ftd>\n      \u003Ctd>Non\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>4\u003C\u002Ftd>\n      \u003Ctd>Fichier d'environnement partagé\u003C\u002Ftd>\n      \u003Ctd>Cross-projet : clés API des fournisseurs IA, clé de chiffrement API, configuration SMTP maître\u003C\u002Ftd>\n      \u003Ctd>Non (hors dépôt)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>5\u003C\u002Ftd>\n      \u003Ctd>Fichiers d'exemple (\u003Ccode>*.env.example\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Templates publics open-source — ne contiennent aucune valeur réelle\u003C\u002Ftd>\n      \u003Ctd>Oui\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Ordre de chargement et règle de précédence\u003C\u002Fh3>\n\n\u003Cp>Au démarrage du service central, les fichiers d'environnement sont chargés dans l'ordre suivant : \u003Cstrong>fichier partagé → fichier cœur → fichier hôte\u003C\u002Fstrong>. En cas de clé présente dans le fichier cœur \u003Cem>et\u003C\u002Fem> dans le fichier hôte, \u003Cstrong>le fichier hôte prend la priorité\u003C\u002Fstrong>. Il est donc interdit de déposer la même clé dans ces deux niveaux.\u003C\u002Fp>\n\n\u003Ch3>Règle anti-fuite P0\u003C\u002Fh3>\n\n\u003Cp>Aucun secret en clair ne doit apparaître dans un fichier suivi par git. Lorsqu'un fichier versionné doit référencer un secret, il cite uniquement le \u003Cstrong>nom de la variable d'environnement\u003C\u002Fstrong> et le fichier \u003Ccode>.env*\u003C\u002Fcode> qui la porte — jamais sa valeur. Par exemple, la configuration de déploiement d'un tenant référence le \u003Cem>nom\u003C\u002Fem> de la variable contenant le mot de passe de base de données ; la table d'inventaire topologie stocke de même le nom de la variable, pas le mot de passe lui-même.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note :\u003C\u002Fstrong> la vue résumée en trois niveaux (infra \u002F cœur \u002F tenants) constitue l'entrée rapide dans la documentation principale. La doctrine complète à cinq niveaux — qui ajoute le fichier partagé cross-projet et les templates publics — est la référence faisant foi en cas de divergence.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"commit-avant-deploy\">Règle de commit avant déploiement\u003C\u002Fh2>\n\n\u003Cp>Une modification appliquée localement mais jamais commitée ne survit pas à un déploiement : le mécanisme de synchronisation du dépôt restaure la version commitée et écrase tout changement flottant. Un incident passé (correction d'URL en production restée non commitée, écrasée lors d'un déploiement suivant) a causé plusieurs heures de production cassée. Cette règle est donc classée \u003Cstrong>P0\u003C\u002Fstrong>.\u003C\u002Fp>\n\n\u003Ch3>Comportement du garde-fou de propreté\u003C\u002Fh3>\n\n\u003Cp>Un script de vérification de l'état du dépôt est exécuté avant chaque opération de déploiement. Il retourne trois états : propre, modifié (\u003Cem>dirty\u003C\u002Fem>), ou hors-dépôt. La vérification est scopée par cible : un fichier modifié côté vaisseau-mère ne bloque pas un déploiement tenant, et inversement.\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\n    \u003Cstrong>Lors d'un déploiement preprod (\u003Ccode>.\u002Fdeploy\u003C\u002Fcode>) :\u003C\u002Fstrong>\n    si l'état est \u003Cem>dirty\u003C\u002Fem>, un commit automatique silencieux est généré\n    (\u003Ccode>chore(auto): pre-deploy snapshot &lt;horodatage ISO&gt;\u003C\u002Fcode>) et poussé en arrière-plan.\n    \u003Cstrong>Exception :\u003C\u002Fstrong> si la branche courante est \u003Ccode>main\u003C\u002Fcode>, le déploiement est refusé — aucun commit automatique sur la branche principale.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Lors d'une mise en production (\u003Ccode>.\u002Fship\u003C\u002Fcode>) :\u003C\u002Fstrong>\n    si l'état est \u003Cem>dirty\u003C\u002Fem>, la commande est bloquante et retourne une erreur explicite invitant à commiter d'abord. Un paramètre de contournement existe pour les corrections d'urgence volontaires uniquement.\n  \u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Un hook de fin de session bloque également la clôture de session si le dépôt n'est pas propre. La règle de workflow est la suivante : \u003Cstrong>aucun travail terminé ne reste uncommitted\u003C\u002Fstrong> — l'agent commente et commite, l'opérateur humain ne manipule jamais directement les commandes git de base.\u003C\u002Fp>\n\n\u003Ch2 id=\"fichiers-reference\">Annexe — fichiers de référence\u003C\u002Fh2>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Composant\u003C\u002Fth>\n      \u003Cth>Rôle\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Entrypoint de déploiement preprod\u003C\u002Ftd>\n      \u003Ctd>Point d'entrée racine pour les déploiements de recette et le routage vers les tenants\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Entrypoint de mise en production\u003C\u002Ftd>\n      \u003Ctd>Point d'entrée racine pour les mises en production — opérateur humain uniquement — avec cérémonie complète de validation\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Dispatcher de déploiement\u003C\u002Ftd>\n      \u003Ctd>Lit la configuration de déploiement au format YAML et orchestre les déploiements distants pour tous les tenants (hors vaisseau-mère)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Bibliothèque de helpers de déploiement\u003C\u002Ftd>\n      \u003Ctd>Fonctions réutilisables : build, packaging, upload, rechargement, vérification de santé, bannière, tâches planifiées\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Parseur de configuration de déploiement\u003C\u002Ftd>\n      \u003Ctd>Analyse et valide le fichier de configuration YAML et expose les variables de déploiement (sections : build, SSH, dérive, initialisation i18n, remote, santé, hooks, tâches planifiées)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Script de self-déploiement vaisseau-mère\u003C\u002Ftd>\n      \u003Ctd>Déploie le service central du vaisseau-mère via build dans le container suivi d'un swap atomique stop\u002Fcopie\u002Fstart\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Variante de déploiement accélérée\u003C\u002Ftd>\n      \u003Ctd>Effectue le build sur l'hôte directement puis réalise le swap atomique — plus rapide pour les itérations courtes\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Garde-fou de propreté git\u003C\u002Ftd>\n      \u003Ctd>Vérifie l'état du dépôt avant déploiement ou mise en production (propre \u002F modifié \u002F hors-dépôt)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Garde-fou de cohérence des dépendances\u003C\u002Ftd>\n      \u003Ctd>Vérifie la cohérence des versions de lockfile avant déploiement du vaisseau-mère (versions minimales requises pour les outils CSS et le framework)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Script de smoke test HTTP\u003C\u002Ftd>\n      \u003Ctd>Vérifie la disponibilité et le contenu des endpoints après déploiement — huit branches de cas couverts ; mode \u003Ccode>all\u003C\u002Fcode> couvre l'ensemble des tenants actifs\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Vérificateur d'environnement de processus\u003C\u002Ftd>\n      \u003Ctd>Contrôle la configuration de l'environnement du gestionnaire de processus avant les smoke tests en production — plusieurs tenants couverts ; certains environnements sans gestionnaire de processus sont exclus par conception\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Schéma de configuration de déploiement\u003C\u002Ftd>\n      \u003Ctd>Spécification du format YAML de déploiement avec quatre exemples canoniques\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Runbook d'exploitation VPS\u003C\u002Ftd>\n      \u003Ctd>Procédures opérationnelles SSH, gestionnaire de processus, Docker, renouvellement de certificats\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Doctrine des secrets\u003C\u002Ftd>\n      \u003Ctd>Référence complète des cinq niveaux de gestion des secrets\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Table d'inventaire topologie\u003C\u002Ftd>\n      \u003Ctd>Source unique de vérité pour la topologie de l'infrastructure (voir §6)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Avertissement :\u003C\u002Fstrong> le document de workflow historique est partiellement obsolète. Il décrit encore un environnement de preprod mothership (port dédié, sous-domaine de recette) et un script de ship séparé comme pipeline actif, alors que la preprod vaisseau-mère a été démantelée et que le déploiement passe désormais par le script de self-déploiement décrit ci-dessus. En cas de conflit, \u003Cstrong>le code fait foi\u003C\u002Fstrong>, pas la documentation narrative.\u003C\u002Fp>\n\u003C\u002Fblockquote>","\u003Ch2 id=\"deploiement-asymetrie\">Asymmetry \u003Ccode>.\u002Fship\u003C\u002Fcode> vs \u003Ccode>.\u002Fdeploy\u003C\u002Fcode>\u003C\u002Fh2>\n\n\u003Cp>Two entry points exist at the repository root. They do \u003Cstrong>not\u003C\u002Fstrong> do the same thing and do \u003Cstrong>not\u003C\u002Fstrong> have the same owner.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fdeploy\u003C\u002Fcode>\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fship\u003C\u002Fcode>\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Target\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Pre-production (or mothership runtime \u002F PROD for founder sites without preprod)\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Production\u003C\u002Fstrong>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Owner\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>AI \u002F worker \u002F cascade\u003C\u002Fstrong>, systematically\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Alex only\u003C\u002Fstrong>, manual gesture\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Git\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Auto-commit of dirty state + stays on \u003Ccode>preprod\u003C\u002Fcode> branch\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>git checkout main\u003C\u002Fcode> → \u003Ccode>git pull\u003C\u002Fcode> → \u003Ccode>git merge preprod -X theirs\u003C\u002Fcode> → \u003Ccode>git push origin main\u003C\u002Fcode>, then back to \u003Ccode>preprod\u003C\u002Fcode> (EXIT trap)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Dirty guard\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Silent auto-commit, unless on \u003Ccode>main\u003C\u002Fcode> (hard refusal)\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Blocking\u003C\u002Fstrong>: refusal if working tree is dirty, unless \u003Ccode>--allow-dirty\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Schema drift check\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Single hardwired preprod target; any other tenant → explicit skip. \u003Ccode>--skip-drift\u003C\u002Fcode> bypass available.\u003C\u002Ftd>\n      \u003Ctd>Hub schema check (blocking) + prod tenant DB if target is known. \u003Cstrong>No\u003C\u002Fstrong> \u003Ccode>--skip-drift\u003C\u002Fcode> equivalent: hub drift is strictly blocking.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Automatic drift application\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>The drift correction engine generates and applies missing idempotent DDL statements in a single transaction (\u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, \u003Ccode>ALTER TABLE ADD COLUMN IF NOT EXISTS\u003C\u002Fcode>). \u003Cstrong>Active by default.\u003C\u002Fstrong> Can be disabled on a one-off basis. Applied \u003Cem>before\u003C\u002Fem> any build or reload.\u003C\u002Ftd>\n      \u003Ctd>— (drift applied manually before ship)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Post-deployment smoke\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>HTTP check of tenants, chained with a non-blocking visual smoke. Can be disabled.\u003C\u002Ftd>\n      \u003Ctd>Process environment check (anti-false-positive) \u003Cstrong>then\u003C\u002Fstrong> HTTP smoke + JSON content checks.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Migrations\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>Displays pending SQL files, \u003Cstrong>non-blocking\u003C\u002Fstrong>, \u003Cstrong>not auto-applied\u003C\u002Fstrong> — no database runner wired.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cstrong>Closing\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n      \u003Ctd>Proposes closing \u003Ccode>test\u003C\u002Fcode>-type worksites (post-ship).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Doctrine:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Ccode>.\u002Fship &lt;tenant&gt;\u003C\u002Fcode> = Production, \u003Cstrong>never autonomous\u003C\u002Fstrong>, Alex-exclusive. The AI never triggers \u003Ccode>.\u002Fship\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>.\u002Fdeploy &lt;tenant&gt;\u003C\u002Fcode> = \u003Cstrong>always the AI\u003C\u002Fstrong>, systematically and without asking — including on the mothership where \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> is equivalent to a live rebuild of the runtime (no dedicated preprod).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cpre>\u003Ccode>        ┌─────────────┐   .\u002Fdeploy &lt;tenant&gt;   ┌──────────────┐\n  AI →  │  preprod     │ ──────────────────→   │  preprod \u002F    │\n        │  branch      │   (auto, N times)       │  live runtime │\n        └─────────────┘                         └──────────────┘\n                │\n                │  Alex reviews preprod, validates\n                ▼\n        ┌─────────────┐   .\u002Fship &lt;tenant&gt;      ┌──────────────┐\n Alex → │ merge preprod│ ──────────────────→   │  PRODUCTION   │\n        │  → main      │   (manual only)         │              │\n        └─────────────┘                         └──────────────┘\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Cstrong>Exception — public site without preprod:\u003C\u002Fstrong> \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> is refused for this tenant — the public site has no preprod. It is modified exclusively via \u003Ccode>.\u002Fship\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Exception — founder tenants without preprod:\u003C\u002Fstrong> for certain founder sites, \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> points directly to production. The \u003Cem>deploy = preprod\u003C\u002Fem> boundary does not hold for these cases. The only operation that remains manual is \u003Ccode>.\u002Fship\u003C\u002Fcode> (preprod→main merge + full ceremony).\u003C\u002Fp>\n\n\u003Ch2 id=\"deploiement-pipeline-ship\">Detail of the \u003Ccode>.\u002Fship\u003C\u002Fcode> pipeline (numbered steps)\u003C\u002Fh2>\n\n\u003Cp>\u003Ccode>.\u002Fship &lt;tenant&gt;\u003C\u002Fcode> executes the following steps in order:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>Flag parsing:\u003C\u002Fstrong> \u003Ccode>--allow-dirty\u003C\u002Fcode> (dirty guard bypass), \u003Ccode>--no-close\u003C\u002Fcode> (skip post-ship), the rest is forwarded to the dispatcher.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Blocking dirty guard:\u003C\u002Fstrong> repository cleanliness check; exits if dirty, unless \u003Ccode>--allow-dirty\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>EXIT trap \u003Ccode>cleanup\u003C\u002Fcode>:\u003C\u002Fstrong> return to the \u003Ccode>preprod\u003C\u002Fcode> branch guaranteed even on error; any checkout failure is reported loudly (to avoid getting stuck on \u003Ccode>main\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Root-owned file cleanup:\u003C\u002Fstrong> removal of artifacts left by previous deployments that would otherwise block writes.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Hub drift check (blocking):\u003C\u002Fstrong> no bypass. Followed by the prod tenant DB drift check if the target is known.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>preprod → main\u003C\u002Fcode> merge:\u003C\u002Fstrong> checkout main, pull, merge \u003Ccode>preprod -X theirs\u003C\u002Fcode>, push origin main.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Migrations:\u003C\u002Fstrong> display of pending SQL files (see next section). Non-blocking, not auto-applied.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Agent audit:\u003C\u002Fstrong> active agent status check (non-blocking).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Prod deployment:\u003C\u002Fstrong> call to the deployment dispatcher with tenant parameters.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Prod smoke:\u003C\u002Fstrong> process environment check \u003Cem>before\u003C\u002Fem> HTTP smoke + JSON content checks.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Post-ship close:\u003C\u002Fstrong> proposes closing \u003Ccode>test\u003C\u002Fcode>-type worksites (skipped via \u003Ccode>--no-close\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>\u003Ccode>--skip-drift\u003C\u002Fcode> asymmetry:\u003C\u002Fstrong> \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> accepts \u003Ccode>--skip-drift\u003C\u002Fcode> to bypass the drift check in preprod (urgent hotfix case). \u003Ccode>.\u002Fship\u003C\u002Fcode> has \u003Cstrong>no\u003C\u002Fstrong> equivalent — hub drift is strictly blocking there. Production is never shipped on a divergent schema.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"deploiement-migrations-ship\">Database migrations during ship\u003C\u002Fh2>\n\n\u003Cp>The ship \u003Cstrong>displays\u003C\u002Fstrong> pending SQL files but \u003Cstrong>never\u003C\u002Fstrong> applies them: no database runner is wired. The block is purely informational and non-blocking — it prints the commands to be executed manually.\u003C\u002Fp>\n\n\u003Cp>A \u003Cem>tenant → migration scope\u003C\u002Fem> mapping prevents mixing mothership migrations with tenant migrations:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Tenant\u003C\u002Fth>\n      \u003Cth>Targeted migrations folder\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Mothership \u002F OS runtime\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>mothership\u003C\u002Fcode> scope\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>An e-commerce tenant (e.g. tenant A)\u003C\u002Ftd>\n      \u003Ctd>Tenant A's own scope\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>A vape tenant (e.g. tenant B)\u003C\u002Ftd>\n      \u003Ctd>Tenant B's own scope\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Founder sites (\u003Cem>client\u003C\u002Fem>, \u003Cem>alexandrecarette\u003C\u002Fem>, \u003Cem>codemyshop\u003C\u002Fem>)\u003C\u002Ftd>\n      \u003Ctd>Each site's own scope\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>all\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Special case: aggregates all targets\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cem>other\u003C\u002Fem>\u003C\u002Ftd>\n      \u003Ctd>Fallback: scope = tenant name\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Special case \u003Ccode>all\u003C\u002Fcode>:\u003C\u002Fstrong> aggregates all SQL files from all scopes, excluding \u003Ccode>_applied\u002F\u003C\u002Fcode> and \u003Ccode>applied\u002F\u003C\u002Fcode> subfolders (already-run migrations). Other scopes simply list the SQL files in the corresponding folder.\u003C\u002Fp>\n\n\u003Ch2 id=\"deploiement-routage\">Routing of \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> and \u003Ccode>.\u002Fship\u003C\u002Fcode> by tenant\u003C\u002Fh2>\n\n\u003Cp>Each tenant is routed to a dedicated script or to the generic deployment dispatcher.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Tenant (argument)\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fdeploy\u003C\u002Fcode> →\u003C\u002Fth>\n      \u003Cth>\u003Ccode>.\u002Fship\u003C\u002Fcode> →\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Mothership (default)\u003C\u002Ftd>\n      \u003Ctd>Concurrency lock health check, then dedicated OS runtime deployment script (\u003Ccode>--prod --all\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Same dedicated OS runtime script (\u003Ccode>--prod --all\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Public site without preprod\u003C\u002Ftd>\n      \u003Ctd>\u003Cstrong>Refused\u003C\u002Fstrong> — no preprod\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher (\u003Ccode>--all\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Founder site synedre.com\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher → \u003Cstrong>direct PROD\u003C\u002Fstrong>\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher ⚠️ process environment check may block if the tenant is not declared in the checker\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Founder site CodeMyShop\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher → \u003Cstrong>direct PROD\u003C\u002Fstrong> (no more ansible\u002Fpreprod); smoke enabled\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>CodeMyShop demo\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher → demo staging\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>E-commerce tenant A (preprod)\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher (\u003Ccode>--target=preprod --all\u003C\u002Fcode>); smoke disabled (preprod behind basic auth)\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher without \u003Ccode>--target\u003C\u002Fcode> → prod\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Vape tenant B\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher; smoke enabled\u003C\u002Ftd>\n      \u003Ctd>⚠️ Falls into the generic fallback case — likely to error if no prod VPS is defined\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Founder site client\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher → PROD; smoke enabled\u003C\u002Ftd>\n      \u003Ctd>Generic dispatcher\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>all\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>5 background parallel jobs: mothership, CodeMyShop (PROD), e-commerce tenant A (preprod), vape tenant B, client. Post-jobs smoke across all. ⚠️ The \u003Ccode>all\u003C\u002Fcode> smoke tests the CodeMyShop demo but the job deploys the production storefront — the prod storefront is not verified in this path.\u003C\u002Ftd>\n      \u003Ctd>3 parallel jobs: OS runtime, public site without preprod, CodeMyShop. Smoke on all three.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>--list\u003C\u002Fcode> \u002F \u003Ccode>-l\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Displays the client VPS inventory (see dedicated section)\u003C\u002Ftd>\n      \u003Ctd>—\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Cem>other (unknown)\u003C\u002Fem>\u003C\u002Ftd>\n      \u003Ctd>Fallback to legacy ansible script (\u003Ccode>--preprod\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Fallback to legacy ansible script (\u003Ccode>--prod\u003C\u002Fcode>)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Convergence toward a single dispatcher:\u003C\u002Fstrong> since the deployment pattern was standardized and legacy scripts were purged, \u003Cstrong>all remote tenants except the mothership\u003C\u002Fstrong> go through the generic dispatcher, for both \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> and \u003Ccode>.\u002Fship\u003C\u002Fcode>. The mothership retains its dedicated script because self-deployment of the runtime is architecturally distinct: local Docker build, with no network transfer to an external VPS.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cblockquote>\n  \u003Cp>⚠️ \u003Cstrong>\u003Ccode>all\u003C\u002Fcode> smoke blind spot:\u003C\u002Fstrong> during a \u003Ccode>.\u002Fdeploy all\u003C\u002Fcode>, the deployment targets the CodeMyShop production storefront, but the subsequent smoke checks the staging demo. The production storefront is not verified in this path — a point to fix in the smoke loop.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"dispatcher-yaml-driven\">The YAML-driven dispatcher\u003C\u002Fh2>\n\n\u003Cp>The standard deployment pipeline — used for all remote tenants, except for the mothership deployment itself — is driven by a per-tenant YAML configuration file. A main deployment script orchestrates the entire sequence by reading that file, validating it, and chaining the steps in a deterministic order.\u003C\u002Fp>\n\n\u003Ch3>Configuration file resolution\u003C\u002Fh3>\n\n\u003Cp>At invocation, the dispatcher locates the YAML file to apply according to two rules:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>When the target designates the mothership, the dispatcher attempts to load a dedicated configuration file. This file is documented in the official schema, but \u003Cstrong>does not exist on disk\u003C\u002Fstrong>: the mothership follows a separate self-deployment pipeline activated upstream. The corresponding branch in the dispatcher is therefore inert in practice.\u003C\u002Fli>\n  \u003Cli>For any other tenant, the dispatcher loads the \u003Ccode>deploy.yaml\u003C\u002Fcode> file located in the tenant directory. If a named target is specified (option \u003Ccode>--target=&lt;X&gt;\u003C\u002Fcode>), the file \u003Ccode>deploy.&lt;X&gt;.yaml\u003C\u002Fcode> is used instead.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note:\u003C\u002Fstrong> the name \u003Ccode>synedre\u003C\u002Fcode> (without suffix) identifies the founding tenant of the synedre.com site; it follows the standard tenant path and does not activate the mothership branch.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Dispatcher steps\u003C\u002Fh3>\n\n\u003Cp>The dispatcher executes the following steps in order:\u003C\u002Fp>\n\u003Col>\n  \u003Cli>\u003Cstrong>Argument parsing:\u003C\u002Fstrong> the option \u003Ccode>--target=&lt;X&gt;\u003C\u002Fcode> (the \u003Ccode>=\u003C\u002Fcode> sign is mandatory), the flag \u003Ccode>--all\u003C\u002Fcode> (mandatory) and the optional flag \u003Ccode>--clean\u003C\u002Fcode> are extracted and validated.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>YAML resolution and validation:\u003C\u002Fstrong> a Python validator reads the YAML file, checks its compliance with the schema, and emits the configuration variables as key-value pairs evaluated by the shell. Any validation error causes the pipeline to halt immediately.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Standard argument check:\u003C\u002Fstrong> the \u003Ccode>CLEAN_CACHE\u003C\u002Fcode> and \u003Ccode>HAS_ALL\u003C\u002Fcode> flags are consolidated.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Start banner:\u003C\u002Fstrong> display of context and initialization of the deployment timer.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Schema drift check:\u003C\u002Fstrong> if the \u003Ccode>drift\u003C\u002Fcode> section is present in the YAML, the engine compares the actual state of the database against the expected schema and reports any discrepancies.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Automatic drift apply:\u003C\u002Fstrong> enabled by default, this mechanism generates and applies in a single transaction the missing idempotent DDL statements (\u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, \u003Ccode>ALTER TABLE ADD COLUMN IF NOT EXISTS\u003C\u002Fcode>). The schema is updated \u003Cem>before\u003C\u002Fem> any build or reload. This step can be temporarily disabled via a dedicated environment variable.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Background hook launch:\u003C\u002Fstrong> the agent listener is started as a background task.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>i18n route generation (pre-build):\u003C\u002Fstrong> if the \u003Ccode>seed_i18n\u003C\u002Fcode> section is present, the dispatcher queries the tenant's category and localized metadata tables (via SSH or container) and writes the JSON file of localized route segments consumed by the international routing module at build time. This step is non-blocking: if the database is unreachable, the build falls back to hardcoded values.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Nuxt build:\u003C\u002Fstrong> compilation of the front-end application.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>i18n seed:\u003C\u002Fstrong> if the section is present, injection of translation data.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Source maps:\u003C\u002Fstrong> if the corresponding hook is enabled, upload of source maps.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Packaging:\u003C\u002Fstrong> creation of a compressed archive of the build in the local machine's temporary directory.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Background Git push:\u003C\u002Fstrong> if the hook is enabled, push of the repository as a background task.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Archive upload:\u003C\u002Fstrong> secure transfer of the archive to the client VPS.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Remote reload:\u003C\u002Fstrong> depending on the variant declared in the YAML, the dispatcher performs either a graceful reload via the process manager (\u003Ccode>pm2\u003C\u002Fcode>), or a restart of the application container (\u003Ccode>docker compose restart\u003C\u002Fcode>).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Recurring job installation (\u003Cem>cron\u003C\u002Fem>):\u003C\u002Fstrong> if the \u003Ccode>cron\u003C\u002Fcode> section is present, the dispatcher installs the scheduled entries in the SSH user's crontab \u003Cem>on the client VPS\u003C\u002Fem> (never on the mothership). Each block is tagged and idempotent: the old block is removed before rewriting. An empty list triggers removal of the block. \u003Cstrong>Doctrine:\u003C\u002Fstrong> recurring jobs run as close as possible to the tenant's database.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Background hook wait:\u003C\u002Fstrong> synchronization with the Git push and agent audit tasks.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Health check:\u003C\u002Fstrong> availability polling loop on the target URL until the configured timeout expires.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>End banner:\u003C\u002Fstrong> display of the summary and total elapsed time.\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch3>YAML configuration file schema\u003C\u002Fh3>\n\n\u003Cp>Each configuration file is validated by the Python validator before any deployment. The recognized sections are as follows:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Section\u003C\u002Fth>\n      \u003Cth>Required\u003C\u002Fth>\n      \u003Cth>Description\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>name\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Yes\u003C\u002Ftd>\n      \u003Ctd>Deployment identifier (lowercase letters, digits, hyphens, underscores).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>build\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Yes\u003C\u002Ftd>\n      \u003Ctd>Local client path (\u003Ccode>local_client\u003C\u002Fcode> required) and Node environment (\u003Ccode>node_env\u003C\u002Fcode>, default \u003Ccode>production\u003C\u002Fcode>).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>ssh\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Yes\u003C\u002Ftd>\n      \u003Ctd>Target host required; SSH user (default \u003Ccode>ubuntu\u003C\u002Fcode>) and key path optional.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>remote\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Yes\u003C\u002Ftd>\n      \u003Ctd>Deployment variant: \u003Ccode>pm2\u003C\u002Fcode> or \u003Ccode>docker\u003C\u002Fcode>. See consistency guardrails below.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>health\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Yes\u003C\u002Ftd>\n      \u003Ctd>Check URL required; maximum wait timeout (default 45 seconds).\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>drift\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>No\u003C\u002Ftd>\n      \u003Ctd>Declaration of schema drifts to detect and apply automatically.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>seed_i18n\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>No\u003C\u002Ftd>\n      \u003Ctd>Enables pre-build i18n route generation and translation injection.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>hooks\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>No\u003C\u002Ftd>\n      \u003Ctd>Optional hooks: agent audit, source map upload, Git push.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>cron\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>No\u003C\u002Ftd>\n      \u003Ctd>List of recurring jobs to install on the client VPS.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Per-variant consistency guardrails\u003C\u002Fh3>\n\n\u003Cp>The validator enforces strict consistency rules depending on the chosen variant:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>Variant \u003Ccode>pm2\u003C\u002Fcode>:\u003C\u002Fstrong> the process application name (\u003Ccode>pm2_app\u003C\u002Fcode>) is \u003Cstrong>required\u003C\u002Fstrong>; Docker-specific fields (\u003Ccode>container\u003C\u002Fcode>, \u003Ccode>subdir\u003C\u002Fcode>) are \u003Cstrong>forbidden\u003C\u002Fstrong>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Variant \u003Ccode>docker\u003C\u002Fcode>:\u003C\u002Fstrong> the target container name (\u003Ccode>container\u003C\u002Fcode>) is \u003Cstrong>required\u003C\u002Fstrong>; PM2-specific fields are \u003Cstrong>forbidden\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Validation of the \u003Ccode>cron\u003C\u002Fcode> section\u003C\u002Fh3>\n\n\u003Cp>Each entry in the \u003Ccode>cron\u003C\u002Fcode> section is validated individually:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Ccode>key\u003C\u002Fcode>: kebab-case identifier, unique within the file.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>schedule\u003C\u002Fcode>: crontab expression with exactly 5 fields.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>command\u003C\u002Fcode>: non-empty command to execute.\u003C\u002Fli>\n  \u003Cli>\u003Ccode>comment\u003C\u002Fcode>: free-form description (optional).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>A \u003Ccode>cron\u003C\u002Fcode> section declared empty (\u003Ccode>[]\u003C\u002Fcode>) is passed to the installer as a cleanup signal: the corresponding block is removed from the tenant's crontab.\u003C\u002Fp>\n\n\u003Ch3>Configuration file example\u003C\u002Fh3>\n\n\u003Cp>The file below illustrates a typical configuration for a tenant deployed via the PM2 process manager, with Git push enabled:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>name: mon-tenant\nbuild:\n  local_client: un composant interne\n  node_env: production\nssh:\n  host: &lt;client VPS address&gt;\nremote:\n  variant: pm2\n  dir: \u002Fvar\u002Fwww\u002Fun composant interne\n  pm2_app: mon-tenant-nuxt\n  pm2_remote_user: codemyshop\nhealth:\n  url: https:\u002F\u002Fmon-tenant.example.com\nhooks:\n  git_push: mon-tenant\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Reminder:\u003C\u002Fstrong> the SSH host (\u003Ccode>host\u003C\u002Fcode>) must be filled in with the actual address or hostname of the client VPS. No real IP address should be committed to a publicly shared configuration file.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"build-host-pattern\">Pattern: build on the mother ship, deploy to the VPS\u003C\u002Fh2>\n\n\u003Cp>The central invariant for remote deployments is as follows: \u003Cstrong>the client VPS never compiles\u003C\u002Fstrong>. The Nuxt bundle is produced entirely on the mother ship, compressed, transferred, extracted in place, and then the application process is reloaded. The pipeline's opening banner explicitly states this operating mode.\u003C\u002Fp>\n\n\u003Ch3>Eight-step sequence\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>  ┌─────────────────── MOTHER SHIP (build) ───────────────────────────┐\n  │ 1. Dependency installation (delta, offline mode preferred)        │\n  │ 2. URL invariant smoke tests                                      │\n  │ 3. Nuxt build for the target client  →  .output\u002F                 │\n  │ 4. Compressed archive (pigz if available, otherwise gzip)         │\n  └───────────────────────────┬───────────────────────────────────────┘\n                               │  secure transfer (SCP)\n                               ▼\n  ┌─────────────────── CLIENT VPS ────────────────────────────────────┐\n  │ 5. Rotation  .output → .output_old        (rollback ready)       │\n  │ 6. Extraction of the compressed archive                           │\n  │ 7. Process reload (graceful reload or restart)                    │\n  │ 8. If online → deletion of .output_old                            │\n  │    Otherwise → display of restore command + stop                  │\n  └───────────────────────────┬───────────────────────────────────────┘\n                               │\n                               ▼  HTTP 200 polling (configurable max delay)\n                         health check\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Pipeline function details\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Nuxt build\u003C\u002Fstrong>: purge of the output directory (and incremental cache if the \u003Ccode>--clean\u003C\u002Fcode> flag is passed), offline dependency installation, then a \u003Cstrong>blocking smoke test\u003C\u002Fstrong> on the structural invariants of the produced URLs before any \u003Ccode>nuxi build\u003C\u002Fcode>. If the incremental build fails, a retry with a purged cache is automatically triggered.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Archiving\u003C\u002Fstrong>: the client's \u003Ccode>.output\u003C\u002Fcode> directory is packed into a compressed archive. \u003Ccode>pigz\u003C\u002Fcode> is used if available on the mother ship, with automatic fallback to \u003Ccode>gzip\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Transfer\u003C\u002Fstrong>: the archive is copied to the VPS via SCP, then deleted locally.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Docker restart\u003C\u002Fstrong>: SSH connection to the VPS, \u003Ccode>.output → .output_old\u003C\u002Fcode> rotation, archive extraction into the target directory, removal of a superfluous CommonJS module identified during a prior incident (CJS tree-shake), application container restart, verification that the container is in \u003Ccode>running\u003C\u002Fcode> state — otherwise rollback.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>PM2 graceful reload\u003C\u002Fstrong>: \u003Ccode>pm2 reload\u003C\u002Fcode> with environment variable refresh — preserves active connections. Optionally manages a dedicated PM2 user on the VPS. An optional symlink to the back-office uploads folder may be (re)created to prevent missing static file errors.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>PM2 hard restart\u003C\u002Fstrong>: full deletion and recreation of the PM2 process (first deployment or explicit configuration via the \u003Ccode>pm2_start\u003C\u002Fcode> parameter), followed by a process list save.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Health check loop\u003C\u002Fstrong>: repeated HTTP polling until a \u003Ccode>200\u003C\u002Fcode> status code is received or the maximum timeout expires (45 seconds by default).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Opening and closing banners\u003C\u002Fstrong>: visual framing of the pipeline with per-phase duration breakdown.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Scheduled task installer\u003C\u002Fstrong>: installs the per-client cron job on the remote VPS (see §3).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Rollback behaviour\u003C\u002Fh3>\n\n\u003Cp>\u003Ccode>.output_old\u003C\u002Fcode> is retained for the entire duration of the reload. It is purged only if the process restarts successfully. On failure, the script \u003Cstrong>only displays\u003C\u002Fstrong> the suggested restore command (\u003Ccode>mv .output_old .output\u003C\u002Fcode>) and exits with an error code — \u003Cstrong>the restore remains manual\u003C\u002Fstrong>, the move is never executed automatically. To force retention of \u003Ccode>.output_old\u003C\u002Fcode> even on success, the \u003Ccode>SHIP_KEEP_ROLLBACK=1\u003C\u002Fcode> variable can be set.\u003C\u002Fp>\n\n\u003Ch2 id=\"smoke-post-deploy\">Post-deployment smoke: three validation layers\u003C\u002Fh2>\n\n\u003Cp>The post-deployment verification script comprises two formal sub-levels of verification, supplemented by a third non-blocking visual level.\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\n    \u003Cstrong>HTTP checks\u003C\u002Fstrong>: polling of each URL declared for the client; any \u003Ccode>5xx\u003C\u002Fcode> code is blocking, a \u003Ccode>4xx\u003C\u002Fcode> generates a non-blocking warning. Covered clients include the main cockpit, the demo showcase, several production tenants, and the institutional site. An inner loop covers six clients in \u003Cem>all\u003C\u002Fem> mode; two others are checked individually from the delivery pipeline.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Content \u002F JSON checks\u003C\u002Fstrong>: validates the \u003Cem>shape\u003C\u002Fem> of the JSON returned by the main navigation API. This layer was introduced after an incident where a page returned HTTP \u003Ccode>200\u003C\u002Fcode> for two days with a JSON payload carrying \u003Ccode>{ error: true }\u003C\u002Fcode>, rendering navigation entirely empty and the footer displayed as raw keys. Without this level, database-driven SSR renders could fail silently. Clients with database-backed navigation are covered; clients without global navigation (headless cockpit, showcase without configured navigation) are intentionally excluded.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Automated visual verification\u003C\u002Fstrong> (non-blocking): Playwright screenshot followed by a multimodal verdict. The module is cleanly skipped if no visual verification configuration file exists for the client (zero added latency). Active by default, can be disabled via environment variable.\n  \u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ch2 id=\"check-pm2-env\">False-positive prevention: PM2 environment variable check\u003C\u002Fh2>\n\n\u003Cp>An HTTP \u003Ccode>200\u003C\u002Fcode> status code is not sufficient to validate a PM2 deployment. An incident demonstrated that a client could be redeployed without any database connection variables: all data-driven SSR routes returned \u003Ccode>500\u003C\u002Fcode>, yet the page served a \u003Cstrong>syntactically valid empty skeleton\u003C\u002Fstrong> — the HTTP smoke therefore returned \u003Ccode>200\u003C\u002Fcode> for two days.\u003C\u002Fp>\n\n\u003Cp>For this reason, the delivery script runs a PM2 environment checker \u003Cstrong>before\u003C\u002Fstrong> the smoke check: it connects via SSH to the remote VPS and inspects the presence of critical variables in the active PM2 process environment.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Return code\u003C\u002Fth>\n      \u003Cth>Meaning\u003C\u002Fth>\n      \u003Cth>Behaviour\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>0\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>All critical variables are present\u003C\u002Ftd>\n      \u003Ctd>Pipeline continues\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>1\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Missing variables\u003C\u002Ftd>\n      \u003Ctd>Blocking — pipeline stops\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>2\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>SSH connection or PM2 process unreachable\u003C\u002Ftd>\n      \u003Ctd>Blocking — pipeline stops\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Non-PM2 targets (headless mother ship, pure Docker environments) automatically return \u003Ccode>0\u003C\u002Fcode> and are not inspected.\u003C\u002Fp>\n\n\u003Ch2 id=\"lock-health-check\">Lockfile consistency check before build\u003C\u002Fh2>\n\n\u003Cp>Before any main cockpit deployment, the pipeline runs a lockfile consistency check detecting two blocking discrepancies:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>\u003Cstrong>CSS minifier version below the required threshold\u003C\u002Fstrong> in the lockfile (missing transitive PostCSS dependencies → silently broken build).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Nuxt version below the required threshold\u003C\u002Fstrong> in the dependency manifest (risk of mismatch with the bundling tool).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>A non-blocking warning is emitted if Vite version overrides are detected in the root configuration. On a blocking error, the script exits with code \u003Ccode>1\u003C\u002Fcode> and an explicit corrective message — the deployment is cancelled. The suggested fix is to regenerate the lockfile from scratch.\u003C\u002Fp>\n\n\u003Ch2 id=\"self-deploy-cockpit\">Main cockpit deployment: local self-deployment\u003C\u002Fh2>\n\n\u003Cp>The main cockpit does \u003Cstrong>not\u003C\u002Fstrong> go through the remote deployment pipeline described above. It is a \u003Cstrong>local self-deployment\u003C\u002Fstrong>: the mother ship compiles inside its own application container. A dedicated script orchestrates the entire process in \u003Ccode>--prod --all\u003C\u002Fcode> mode.\u003C\u002Fp>\n\n\u003Ch3>Specifics compared to remote deployments\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>No pre-production environment since mid-2026\u003C\u002Fstrong>: running \u003Ccode>.\u002Fdeploy\u003C\u002Fcode> on the main cockpit is a \u003Cstrong>direct push to live production\u003C\u002Fstrong>, without the delivery ceremony of \u003Ccode>.\u002Fship\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Local transfer via Docker copy\u003C\u002Fstrong>: a minimal source archive (application code + dependency manifests, without \u003Ccode>.nuxt\u003C\u002Fcode>, \u003Ccode>.output\u003C\u002Fcode>, or \u003Ccode>node_modules\u003C\u002Fcode>) is copied into the container via \u003Ccode>docker cp\u003C\u002Fcode> — no network transfer.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Incremental build cache preservation\u003C\u002Fstrong>: before archive extraction, the existing \u003Ccode>.nuxt\u003C\u002Fcode> cache inside the container is set aside and restored after extraction. This mechanism is the primary performance lever: the build remains incremental rather than a full cold build.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Build inside the container\u003C\u002Fstrong>: the Nuxt build runs inside the active container via \u003Ccode>docker exec\u003C\u002Fcode>. The old server remains online throughout the entire build duration. No PM2 process manager is used in this pipeline: the container starts the Node server directly.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Rotation sequence\u003C\u002Fstrong>: graceful container stop → copy of the new \u003Ccode>.output\u003C\u002Fcode> to the host → forced container recreation (re-reads environment files). This step was introduced after an incident where environment variables were not reloaded on a simple \u003Ccode>restart\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Self-contained codebase\u003C\u002Fstrong>: the main cockpit no longer extends the shared base — the transferred archive is approximately 16 MB lighter than the previous configuration.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>npm optimisation by fingerprint\u003C\u002Fstrong>: a SHA-256 fingerprint of the dependency lock file is stored in the container. If the lockfile has not changed since the last deployment, the \u003Ccode>npm install\u003C\u002Fcode> step is skipped entirely.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Build UUID health check\u003C\u002Fstrong>: the health check is not limited to an HTTP \u003Ccode>200\u003C\u002Fcode>. The expected build UUID (read from the build manifest on the host side) is compared against the one returned by the container's \u003Ccode>\u002Fapi\u002Fhealth\u003C\u002Fcode> endpoint. This ensures that the new build — and not the old one kept alive by a partial reload — is actually serving requests.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Accelerated variant\u003C\u002Fstrong>: an opt-in option offloads the build to the host rather than the container, with an atomic swap of the output directory (~35% time saving).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Delivery ceremony \u003Ccode>.\u002Fship\u003C\u002Fcode>\u003C\u002Fstrong>: adds branch merging, push, agent audits, production smoke, and workstream closure, but ultimately calls the same local deployment script.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note\u003C\u002Fstrong>: unlike client VPSes, the main cockpit container never goes through the SCP transfer phase or PM2. The build and application serving are entirely containerised on the same physical host.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"inventaire-topologie\">Topology Inventory — Single Source of Truth\u003C\u002Fh2>\n\n\u003Cp>The complete infrastructure topology (environment list, SSH access, associated databases) is stored in \u003Cstrong>a single reference table\u003C\u002Fstrong>, hosted in the mothership's central database. This table is the \u003Cstrong>single source of truth\u003C\u002Fstrong>: no architecture file, no script, no agent should duplicate or hard-code this information — any other document may be outdated.\u003C\u002Fp>\n\n\u003Cp>The inventory listing command queries this table and displays an aligned table in the console. The query filters active entries, orders results by descending criticality, and formats columns for quick reading.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>-- Illustrative example (actual columns, anonymized values)\nSELECT\n  rpad(tenant_identifier, 14)         AS tenant,\n  rpad(environment, 11)               AS env,\n  rpad(coalesce(domain, '-'), 40)     AS domain,\n  CASE WHEN stack_nuxt THEN 'nuxt' ELSE 'ps' END AS stack\nFROM topology_inventory\nWHERE active = 1\nORDER BY criticality DESC, tenant_identifier, environment;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Notable columns of the inventory table\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Column\u003C\u002Fth>\n      \u003Cth>Role\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Tenant identifier\u003C\u002Ftd>\n      \u003Ctd>Primary logical key identifying the client environment\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Environment\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>production\u003C\u002Fcode> | \u003Ccode>staging\u003C\u002Fcode> | \u003Ccode>infra\u003C\u002Fcode> | \u003Ccode>legacy\u003C\u002Fcode> | \u003Ccode>audit\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>SSH access\u003C\u002Ftd>\n      \u003Ctd>Client VPS address, user account, and SSH key path\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Database\u003C\u002Ftd>\n      \u003Ctd>References to the DB container, database name, and the \u003Cem>identifier\u003C\u002Fem> of the environment variable holding the password (never the value itself)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Web runtime\u003C\u002Ftd>\n      \u003Ctd>Application container, PrestaShop presence, Nuxt presence\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Business criticality\u003C\u002Ftd>\n      \u003Ctd>Criticality level, monthly recurring revenue, subscribed plan\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Automatic ship authorized\u003C\u002Ftd>\n      \u003Ctd>Boolean authorizing automatic deployment — must be confirmed before use\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Deployment codename\u003C\u002Ftd>\n      \u003Ctd>Identifier used to compose the exact deployment command\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Client codename\u003C\u002Ftd>\n      \u003Ctd>Foreign key referencing the client federation table (centralized registry)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Active inventory state\u003C\u002Fh3>\n\n\u003Cp>The inventory includes several categories of active entries:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Active production tenants\u003C\u002Fstrong>: critical environments hosting live shops or sites, on one or more client VPS instances.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Staging environments\u003C\u002Fstrong>: staging copies associated with primary tenants.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Mothership infra\u003C\u002Fstrong>: the entry designating the mothership's own central VPS.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Orphan productions\u003C\u002Fstrong>: some tenants carry \u003Ccode>purpose='production'\u003C\u002Fcode> while being dormant — these are secondary projects with no active maintenance. They must not be aggregated under the \"legacy\" label.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Heterogeneous dual entry\u003C\u002Fstrong>: a tenant may appear as two distinct rows (e.g. one Nuxt production row and one legacy row without Nuxt), reflecting two coexisting technical generations. Do not merge them.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Decommissioned entry\u003C\u002Fstrong>: a tenant that was the subject of a completed one-off engagement has been set to \u003Ccode>active=0\u003C\u002Fcode>; its operational folder has been removed, but its history is retained in the centralized client registry.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Operational doctrine:\u003C\u002Fstrong> before each deployment (\u003Ccode>.\u002Fdeploy\u003C\u002Fcode>) or production release (\u003Ccode>.\u002Fship\u003C\u002Fcode>), consult the tenant management dashboard to copy the exact command associated with the target codename. Never recompose the command from memory.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"doctrine-secrets\">Secrets Management Doctrine — Five Levels\u003C\u002Fh2>\n\n\u003Cp>Each secret has \u003Cstrong>exactly one canonical file\u003C\u002Fstrong>. The presence of the same key in two distinct files is treated as a bug, not as a security redundancy.\u003C\u002Fp>\n\n\u003Ch3>The five levels of secret files\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>#\u003C\u002Fth>\n      \u003Cth>File\u003C\u002Fth>\n      \u003Cth>Scope\u003C\u002Fth>\n      \u003Cth>Git-tracked\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>1\u003C\u002Ftd>\n      \u003Ctd>Tenant environment file\u003C\u002Ftd>\n      \u003Ctd>Single tenant — read by its application container and deployment configuration\u003C\u002Ftd>\n      \u003Ctd>No\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2\u003C\u002Ftd>\n      \u003Ctd>Mothership core environment file\u003C\u002Ftd>\n      \u003Ctd>Mothership central services\u003C\u002Ftd>\n      \u003Ctd>No\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>3\u003C\u002Ftd>\n      \u003Ctd>Host environment file\u003C\u002Ftd>\n      \u003Ctd>Host scripts: scheduled tasks, automation, SSH access to client VPS instances\u003C\u002Ftd>\n      \u003Ctd>No\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>4\u003C\u002Ftd>\n      \u003Ctd>Shared environment file\u003C\u002Ftd>\n      \u003Ctd>Cross-project: AI provider API keys, API encryption key, master SMTP configuration\u003C\u002Ftd>\n      \u003Ctd>No (outside repository)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>5\u003C\u002Ftd>\n      \u003Ctd>Example files (\u003Ccode>*.env.example\u003C\u002Fcode>)\u003C\u002Ftd>\n      \u003Ctd>Public open-source templates — contain no real values\u003C\u002Ftd>\n      \u003Ctd>Yes\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Load order and precedence rule\u003C\u002Fh3>\n\n\u003Cp>At central service startup, environment files are loaded in the following order: \u003Cstrong>shared file → core file → host file\u003C\u002Fstrong>. If a key is present in both the core file \u003Cem>and\u003C\u002Fem> the host file, \u003Cstrong>the host file takes precedence\u003C\u002Fstrong>. It is therefore forbidden to place the same key in both of these levels.\u003C\u002Fp>\n\n\u003Ch3>P0 anti-leak rule\u003C\u002Fh3>\n\n\u003Cp>No plaintext secret must appear in any git-tracked file. When a versioned file needs to reference a secret, it cites only the \u003Cstrong>environment variable name\u003C\u002Fstrong> and the \u003Ccode>.env*\u003C\u002Fcode> file that carries it — never its value. For example, a tenant's deployment configuration references the \u003Cem>name\u003C\u002Fem> of the variable holding the database password; the topology inventory table likewise stores the variable name, not the password itself.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Note:\u003C\u002Fstrong> the three-level summary view (infra \u002F core \u002F tenants) serves as the quick entry point into the main documentation. The full five-level doctrine — which adds the cross-project shared file and the public templates — is the authoritative reference in case of discrepancy.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"commit-avant-deploy\">Pre-Deployment Commit Rule\u003C\u002Fh2>\n\n\u003Cp>A change applied locally but never committed does not survive a deployment: the repository synchronization mechanism restores the committed version and overwrites any floating change. A past incident (a production URL fix left uncommitted, overwritten during a subsequent deployment) caused several hours of broken production. This rule is therefore classified \u003Cstrong>P0\u003C\u002Fstrong>.\u003C\u002Fp>\n\n\u003Ch3>Cleanliness guard behavior\u003C\u002Fh3>\n\n\u003Cp>A repository state verification script is executed before each deployment operation. It returns three states: clean, dirty, or out-of-repository. The check is scoped by target: a modified file on the mothership side does not block a tenant deployment, and vice versa.\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\n    \u003Cstrong>During a preprod deployment (\u003Ccode>.\u002Fdeploy\u003C\u002Fcode>):\u003C\u002Fstrong>\n    if the state is \u003Cem>dirty\u003C\u002Fem>, a silent automatic commit is generated\n    (\u003Ccode>chore(auto): pre-deploy snapshot &lt;ISO timestamp&gt;\u003C\u002Fcode>) and pushed in the background.\n    \u003Cstrong>Exception:\u003C\u002Fstrong> if the current branch is \u003Ccode>main\u003C\u002Fcode>, the deployment is refused — no automatic commit on the main branch.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>During a production release (\u003Ccode>.\u002Fship\u003C\u002Fcode>):\u003C\u002Fstrong>\n    if the state is \u003Cem>dirty\u003C\u002Fem>, the command is blocking and returns an explicit error prompting the operator to commit first. A bypass parameter exists for intentional emergency fixes only.\n  \u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>A session-end hook also blocks session closure if the repository is not clean. The workflow rule is as follows: \u003Cstrong>no completed work remains uncommitted\u003C\u002Fstrong> — the agent stages and commits, the human operator never directly runs low-level git commands.\u003C\u002Fp>\n\n\u003Ch2 id=\"fichiers-reference\">Appendix — Reference Files\u003C\u002Fh2>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Component\u003C\u002Fth>\n      \u003Cth>Role\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Preprod deployment entrypoint\u003C\u002Ftd>\n      \u003Ctd>Root entry point for staging deployments and tenant routing\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Production release entrypoint\u003C\u002Ftd>\n      \u003Ctd>Root entry point for production releases — human operator only — with full validation ceremony\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Deployment dispatcher\u003C\u002Ftd>\n      \u003Ctd>Reads the YAML deployment configuration and orchestrates remote deployments for all tenants (excluding the mothership)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Deployment helper library\u003C\u002Ftd>\n      \u003Ctd>Reusable functions: build, packaging, upload, reload, health check, banner, scheduled tasks\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Deployment configuration parser\u003C\u002Ftd>\n      \u003Ctd>Parses and validates the YAML configuration file and exposes deployment variables (sections: build, SSH, drift, i18n initialization, remote, health, hooks, scheduled tasks)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Mothership self-deployment script\u003C\u002Ftd>\n      \u003Ctd>Deploys the mothership central service via an in-container build followed by an atomic stop\u002Fcopy\u002Fstart swap\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Accelerated deployment variant\u003C\u002Ftd>\n      \u003Ctd>Performs the build directly on the host then executes the atomic swap — faster for short iteration cycles\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Git cleanliness guard\u003C\u002Ftd>\n      \u003Ctd>Verifies repository state before deployment or production release (clean \u002F dirty \u002F out-of-repository)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Dependency consistency guard\u003C\u002Ftd>\n      \u003Ctd>Verifies lockfile version consistency before mothership deployment (minimum required versions for CSS tooling and the framework)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>HTTP smoke test script\u003C\u002Ftd>\n      \u003Ctd>Verifies endpoint availability and content after deployment — eight case branches covered; \u003Ccode>all\u003C\u002Fcode> mode covers all active tenants\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Process environment checker\u003C\u002Ftd>\n      \u003Ctd>Validates process manager environment configuration before production smoke tests — several tenants covered; some environments without a process manager are excluded by design\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Deployment configuration schema\u003C\u002Ftd>\n      \u003Ctd>Specification of the YAML deployment format with four canonical examples\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>VPS operations runbook\u003C\u002Ftd>\n      \u003Ctd>Operational procedures for SSH, process manager, Docker, and certificate renewal\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Secrets doctrine\u003C\u002Ftd>\n      \u003Ctd>Complete reference for the five-level secrets management policy\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Topology inventory table\u003C\u002Ftd>\n      \u003Ctd>Single source of truth for infrastructure topology (see §6)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Warning:\u003C\u002Fstrong> the historical workflow document is partially outdated. It still describes a mothership preprod environment (dedicated port, staging subdomain) and a separate ship script as the active pipeline, whereas the mothership preprod has been decommissioned and deployment now goes through the self-deployment script described above. In case of conflict, \u003Cstrong>the code is authoritative\u003C\u002Fstrong>, not the narrative documentation.\u003C\u002Fp>\n\u003C\u002Fblockquote>",{"slug":14,"chapterNum":15,"title":16,"titleEn":17},"memory","08","Mémoire & apprentissage — architecture à trois niveaux","Memory & Learning — Three-Level Architecture",{"slug":19,"chapterNum":20,"title":21,"titleEn":22},"facades","10","catalogue des façades et points d'entrée du harness","harness facade and entry-point catalogue",{"chapters":24},[25,32,39,46,53,60,67,74,77,78,81],{"slug":26,"chapterNum":27,"title":28,"titleEn":29,"summary":30,"summaryEn":31},"overview","01","Vue d'ensemble du harness agentique","Agentic harness overview","Le harness transforme une demande (courriel, console, cron) en action déployée en enchaînant classification, spawn LLM via pseudo-TTY, validation qualité à deux niveaux et boucle d'apprentissage, le tout sans état métier hors base de données.","The harness transforms a request (email, console, cron) into a deployed action by chaining intent classification, pseudo-TTY LLM spawn, two-level quality validation, and a learning loop — with no business state stored outside the database.",{"slug":33,"chapterNum":34,"title":35,"titleEn":36,"summary":37,"summaryEn":38},"data-layer","02","La couche données","The Data Layer","Décrit l'architecture données de Synedre OS : une base PostgreSQL unique (un composant interne\u002Fun composant interne), les trois chemins d'accès (Nuxt\u002FNitro, Python agentique, Drizzle ORM DDL), les familles de tables par préfixe et les conventions de nommage associées.","Describes the Synedre OS data architecture: a single PostgreSQL database (un composant interne\u002Fun composant interne), the three access paths (Nuxt\u002FNitro, agentic Python, Drizzle ORM DDL), the table families by prefix, and the associated naming conventions.",{"slug":40,"chapterNum":41,"title":42,"titleEn":43,"summary":44,"summaryEn":45},"agentic-core","03","Le cœur agentique : Atlas et les agents","The Agentic Core: Atlas and the Agents","Décrit l'architecture du moteur agentique : classification LLM des emails entrants, spawn headless de Claude Code via pseudo-TTY, injection des personas agents et orchestration post-spawn (deploy, QA, récap).","Describes the architecture of the agentic engine: LLM classification of incoming emails, headless spawning of Claude Code via pseudo-TTY, injection of agent personas, and post-spawn orchestration (deploy, QA, recap).",{"slug":47,"chapterNum":48,"title":49,"titleEn":50,"summary":51,"summaryEn":52},"chantiers","04","Chantiers, travaux & tâches — modèle de données et API","Worksites, Jobs & Tasks — Data Model and API","Décrit la hiérarchie chantier\u002Ftravail\u002Ftâche de Synedre OS, le modèle de données DB, les statuts\u002Fscopes canoniques et l'API Python associée.","Describes the worksite\u002Fjob\u002Ftask hierarchy in Synedre OS, the DB data model, canonical statuses\u002Fscopes, and the associated Python API.",{"slug":54,"chapterNum":55,"title":56,"titleEn":57,"summary":58,"summaryEn":59},"automates","05","Automates, crons & runs","Automations, crons & runs","Décrit la couche d'exécution non-conversationnelle de Synedre OS : les 209 façades Python, leur enrobage cron, le registre un composant interne et la frontière entre doctrine run et unité agent.","Describes the non-conversational execution layer of Synedre OS: the 209 Python facades, their cron wrapper, the un composant interne registry, and the boundary between run doctrine and agent unit.",{"slug":61,"chapterNum":62,"title":63,"titleEn":64,"summary":65,"summaryEn":66},"hub","06","Le Hub (\u002Fhub\u002F*)","The Hub (\u002Fhub\u002F*)","Présentation de l'interface d'administration privée Synedre OS (mothership-app), son architecture en couches Nuxt, la carte de ses modules et pages, ainsi que le système d'authentification et de session.","Presentation of the Synedre OS private administration interface (mothership-app), its Nuxt layered architecture, the map of its modules and pages, as well as the authentication and session management system.",{"slug":68,"chapterNum":69,"title":70,"titleEn":71,"summary":72,"summaryEn":73},"email","07","Inbox hub & Atlas Inbox — deux pipelines email","Inbox hub & Atlas Inbox — two email pipelines","Décrit les deux pipelines IMAP→DB du harness (contact@ trié pour le hub, atlas@ déclencheur d'actions agentiques), la façade d'envoi sortant et les doctrines bloquantes associées (scan AV, zéro contact client direct).","Describes the two IMAP→DB pipelines of the harness (contact@ sorted for the hub, atlas@ triggering agentic actions), the outbound sending facade, and the associated blocking doctrines (AV scan, zero direct client contact).",{"slug":14,"chapterNum":15,"title":16,"titleEn":17,"summary":75,"summaryEn":76},"Ce chapitre décrit les trois niveaux de mémoire du harness Synedre OS (réflexe, Zettelkasten, vectoriel pgvector), les automates d'indexation associés et la boucle de capitalisation des erreurs en règles réutilisables.","This chapter describes the three memory levels of the Synedre OS harness (reflex, Zettelkasten, vector pgvector), the associated indexing automata, and the loop for capitalizing errors into reusable rules.",{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10},{"slug":19,"chapterNum":20,"title":21,"titleEn":22,"summary":79,"summaryEn":80},"comment le harness synedre organise ses centaines d'automates en familles fonctionnelles, avec les garde-fous en temps réel qui encadrent chaque action.","how the synedre harness organizes its hundreds of automations into functional families, with the real-time guard-rails that frame every action.",{"slug":82,"chapterNum":83,"title":84,"titleEn":85,"summary":86,"summaryEn":87},"skills-hooks","11","Skills, agents & hooks — harness agentique Synedre OS","Skills, agents & hooks — Synedre OS agentic harness","Décrit le câblage complet du harness Claude Code de synedre-os : skills disponibles, sous-agents délégables et hooks d'événements qui imposent la doctrine commit-en-flux, garde-fous prod\u002Femail et injection mémoire.","Describes the complete wiring of the synedre-os Claude Code harness: available skills, delegatable sub-agents, and event hooks that enforce the commit-in-flow doctrine, prod\u002Femail guardrails, and memory injection."]