Seed from NAS prod snapshot 20260130-190531
All checks were successful
CI / build-and-anchors (push) Successful in 1m25s
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (pull_request) Successful in 1m20s

This commit is contained in:
archicratia
2026-01-31 10:51:38 +00:00
commit 60d88939b0
142 changed files with 33443 additions and 0 deletions

97
docs/CI-BASELINE.md Normal file
View File

@@ -0,0 +1,97 @@
# CI-BASELINE — Gitea Actions + runner Synology (DS220+)
Baseline VALIDÉE :
- runner : container.network = host
- job CI : container Node 22 (conforme engines)
- checkout : sans GitHub, basé sur workflow/event.json
- zéro apt-get dans le workflow
- durcissement DNS Node : NODE_OPTIONS=--dns-result-order=ipv4first
## Runner (DS220+) — configuration de référence
Fichier : /data/config.yaml dans le conteneur runner (ex: gitea-act-runner)
Section container attendue :
container:
network: host
options: >-
--add-host=gitea.archicratie.trans-hands.synology.me:192.168.1.20
-e NODE_OPTIONS=--dns-result-order=ipv4first
Pourquoi : sur cette infra, le DNS du bridge Docker (127.0.0.11) a généré ESERVFAIL / EAI_AGAIN / apt qui ne résout pas.
Le host network stabilise les résolutions (npm registry, deb.debian.org, etc.).
## Smoke test NAS (doit passer)
docker run --rm --network host mcr.microsoft.com/devcontainers/javascript-node:22-bookworm bash -lc "npm ping --registry=https://registry.npmjs.org"
## Symptômes -> cause -> action
- EAI_AGAIN / ESERVFAIL : runner pas en host network -> remettre container.network: host + restart runner
- EBADENGINE : mauvais Node -> container Node 22
- MODULE_NOT_FOUND scripts/check-anchor-aliases.mjs : fichier non commité -> git add/commit/push
______________________________________________
Dernière mise à jour : 2026-01-29
But : définir un minimum “incassable” (local + runner) qui garantit :
- build OK
- anchors cohérents
- aliases injectés
- dist propre (pas dIDs dupliqués)
- pagefind généré
---
## 1) Commande canonique
en bash :
npm test
## 2) Ce que npm test enchaîne
npm run test:aliases
npm run build
npm run audit:dist
node scripts/verify-anchor-aliases-in-dist.mjs
npm run test:anchors
node scripts/check-inline-js.mjs
## 3) Smoke test “prod-like” (Nginx statique)
Sur NAS (ou local), quand on a un service qui expose un port HTTP :
./scripts/smoke.sh 8081
./scripts/smoke.sh 8082
Attendus :
/ répond 200
/pagefind/pagefind.js répond 200
## 4) Note DSM 7.3 (DS220+) — build réseau
Sur Synology, il arrive que docker build ait des soucis DNS/apt/npm.
On force un build stable :
BuildKit :
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
build en réseau host (dans docker-compose.yml) :
build:
network: host
## 5) Avertissement “git was not found”
BuildKit affiche parfois :
buildx: git was not found ... commit information was not captured
Ce warning est sans impact sur le build du site (il concerne juste la capture dinfos de commit).

173
docs/CI-WORKFLOW.md Normal file
View File

@@ -0,0 +1,173 @@
# CI-WORKFLOW — snapshot de .gitea/workflows/ci.yml
name: CI
on:
push:
pull_request:
branches: ["master"]
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
jobs:
build-and-anchors:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
npm ping --registry=https://registry.npmjs.org
# Checkout SANS action externe (pas de github.com)
- name: Checkout (from event.json, no external actions)
run: |
set -euo pipefail
EVENT_JSON="/var/run/act/workflow/event.json"
if [ ! -f "$EVENT_JSON" ]; then
echo "ERROR: missing $EVENT_JSON"
ls -la /var/run/act/workflow || true
exit 1
fi
# 1) Récupère l'URL du repo depuis event.json
REPO_URL="$(node -e '
const fs=require("fs");
const ev=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));
let url = ev.repository?.clone_url || ev.repository?.html_url || "";
if (!url) process.exit(2);
if (!url.endsWith(".git")) url += ".git";
process.stdout.write(url);
' "$EVENT_JSON")"
# 2) Récupère le SHA (push -> after, PR -> pull_request.head.sha)
SHA="$(node -e '
const fs=require("fs");
const ev=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));
const sha =
ev.after ||
ev.pull_request?.head?.sha ||
ev.head_commit?.id ||
"";
process.stdout.write(sha);
' "$EVENT_JSON")"
if [ -z "$SHA" ]; then
echo "ERROR: cannot find SHA in event.json"
node -e 'const ev=require(process.argv[1]); console.log(Object.keys(ev));' "$EVENT_JSON" || true
exit 1
fi
echo "Repo URL: $REPO_URL"
echo "SHA: $SHA"
# 3) Ajoute token si disponible (NE PAS afficher le token)
AUTH_URL="$REPO_URL"
if [ -n "${GITHUB_TOKEN:-}" ] && [[ "$REPO_URL" == https://* ]]; then
AUTH_URL="${REPO_URL/https:\/\//https:\/\/oauth2:${GITHUB_TOKEN}@}"
elif [ -n "${GITEA_TOKEN:-}" ] && [[ "$REPO_URL" == https://* ]]; then
AUTH_URL="${REPO_URL/https:\/\//https:\/\/oauth2:${GITEA_TOKEN}@}"
fi
# 4) Clone minimal + checkout exact du SHA
rm -rf .git || true
git init .
# Optionnel si ton Gitea a un TLS “non standard” (certificat) :
# git config --global http.sslVerify false
git remote add origin "$AUTH_URL"
git fetch --depth=1 origin "$SHA"
git checkout -q FETCH_HEAD
git log -1 --oneline
- name: Anchor aliases schema
run: node scripts/check-anchor-aliases.mjs
- name: NPM harden
run: |
set -euo pipefail
npm config set fetch-retries 5
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set registry https://registry.npmjs.org
npm config get registry
- name: Install deps
run: npm ci
- name: Inline scripts syntax check
run: node scripts/check-inline-js.mjs
- name: Build
run: npm run build
- name: Verify anchor aliases injected
run: node scripts/verify-anchor-aliases-in-dist.mjs
- name: Anchors contract
run: npm run test:anchors
_________________________________________________
Dernière mise à jour : 2026-01-29
Ce document complète `CI-BASELINE.md` et décrit lintention :
- ne pas casser les ancres
- garantir un dist propre
- garder le pipeline simple et déterministe
---
## 1) Principe
Le CI doit exécuter exactement ce que le dev exécute :
- `npm ci`
- `npm test`
Pas de magie, pas de step “inventée”.
---
## 2) Points critiques
### A) Build via npm (pas via astro direct)
Toujours en bash :
npm run build
pour exécuter postbuild :
injection aliases
génération pagefind
### B) Dist “HTML only”
Laudit dist ignore scripts/styles pour détecter les vrais IDs HTML.
## 3) Runner Synology / réseau
En contexte DSM (Docker), si le runner build des images :
activer BuildKit
si besoin, build en network host (comme en prod NAS)
Voir :
DEPLOY_PROD_SYNOLOGY_DS220.md
OPS_COCKPIT.md

29
docs/CONTRAT_TICKETS.md Normal file
View File

@@ -0,0 +1,29 @@
# Contrat de ticket — Proposer / Apply-ticket / Auto-label
Ce document fixe le format minimal et les invariants des tickets dédition.
Objectif : parsing fiable par scripts + workflows.
## Invariants (non négociables)
Doivent toujours exister dans le body du ticket :
- `Chemin: /.../`
- `URL locale: ...#...` (utile pour audit humain)
- `Ancre: #p-...`
- `Type: type/...`
- `State: state/...`
- `Proposition (remplacer par):`
## Texte actuel : best effort
Priorité :
1) `Texte actuel (copie exacte du paragraphe):`
2) sinon `Texte actuel (extrait):` + note de troncature
> Même si le texte actuel est un extrait, lancre + chemin rendent le ticket opposable.
## Catégorie (optionnelle)
- `Category: cat/...` (ou vide)
## Pourquoi ce contrat ?
- `apply-ticket.mjs` dépend de repères textuels stables
- `auto-label-issues` dépend de `Type/State/Category`
- on veut éviter des tickets “illisibles machine” qui cassent lindustrialisation

View File

@@ -0,0 +1,103 @@
# Déploiement production (Synology DS220+ / DSM 7.3) — Astro → Nginx statique
Dernière mise à jour : 2026-01-29
Ce document décrit la mise en place stable sur NAS :
- build Astro dans une image (Node)
- runtime Nginx statique
- bascule blue/green via Reverse Proxy DSM
---
## 1) Arborescence recommandée
Dossier racine :
- `/volume2/docker/archicratie-web/current`
Contenu attendu :
- `Dockerfile`
- `docker-compose.yml`
- `nginx.conf`
- `.env`
- le code du site (package.json, src/, scripts/, etc.)
---
## 2) Pré-requis DSM
- DSM 7.3 avec accès SSH (admin)
- Docker / Container Manager installé
- Reverse Proxy DSM configuré (Portail des applications)
---
## 3) Sécurité réseau (important)
On publie le site via **HTTPS 443** sur DSM Reverse Proxy.
Les ports 8081/8082 :
- bindés en **localhost uniquement** (`127.0.0.1:8081:80`, `127.0.0.1:8082:80`)
- **nont PAS besoin dêtre ouverts** dans le pare-feu WAN
- servent uniquement à DSM Reverse Proxy (loopback)
---
## 4) Variables Gitea (Proposer)
Le site injecte des variables “publiques” au build :
- `PUBLIC_GITEA_BASE` (URL gitea)
- `PUBLIC_GITEA_OWNER` (casse sensible)
- `PUBLIC_GITEA_REPO`
Exemple dans `.env` :
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition
## 5) Reverse Proxy DSM (le point clé)
DSM 7.3 :
Panneau de configuration → Portail des applications → Proxy inversé
# Règle pour le site :
Source : HTTPS / archicratie.trans-hands.synology.me / port 443
Destination : HTTP / 127.0.0.1 / port 8081 (BLUE) ou 8082 (GREEN)
# Certificat :
Sécurité → Certificat : associer le bon certificat au nom de domaine.
## 6) Notes DS220+ “spécificités”
### 6.1 Build réseau (DNS/apt/npm)
Sur DSM, il arrive que apt-get ou des résolutions DNS échouent pendant docker build.
Solution : build avec réseau host (déjà prévu dans compose) :
build: network: host
Et activer BuildKit :
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
### 6.2 Artefacts Mac (PaxHeader / ._ / DS_Store)
Si tu transfères une archive depuis macOS, tu peux embarquer des dossiers/fichiers parasites.
Conséquence possible : Astro “voit” un faux contenu (ex: PaxHeader/...mdx) → erreurs de schema.
Remède : .dockerignore robuste (voir anchors.md section “Artefacts Mac”).
## 7) Cycle blue/green (résumé)
Tu rebuild le slot inactif (ex: GREEN)
Tu valides en local (curl/smoke/health)
Tu bascules DSM vers ce port
Rollback immédiat : tu repasses DSM sur lautre port
Pour lopérationnel minute par minute, voir OPS_COCKPIT.md.

134
docs/HANDOFF-SESSION.md Normal file
View File

@@ -0,0 +1,134 @@
# HANDOFF — Bilan synthèse (passation)
## Mission
Rendre la CI Gitea Actions fiable (Synology) et sécuriser les ancrages de paragraphes :
- mapping oldId -> newId versionné
- injection build-time dans dist pour préserver les liens profonds
## Causes racines identifiées
1) DNS instable dans les conteneurs de job via bridge Docker (127.0.0.11) sur cette infra
2) Checkout GitHub externe impossible/indésirable + variables GITEA_* parfois absentes
3) engines Node imposent >=22 <23 => EBADENGINE si Node 20
## Résolution validée (baseline)
- Runner : container.network = host
- Job : image Node 22
- Checkout : via workflow/event.json (pas actions/checkout)
- Workflow : pas de apt-get
- Anchors :
- src/anchors/anchor-aliases.json (par route)
- scripts/inject-anchor-aliases.mjs injecte <span id="oldId"> avant lélément id="newId"
- scripts/check-anchor-aliases.mjs valide le schéma en CI
## État actuel
- CI passe (host net + Node 22 + checkout event.json + no apt)
- Injection daliases vérifiée localement dans dist/…/index.html
_________________________________________________
Dernière mise à jour : 2026-01-29
Ce document résume *tout le chemin* parcouru, avec les décisions qui évitent de “casser lexistant”.
---
## 1) Décision structurante : ancrages stables, déterministes
Problème initial :
- les IDs dancres peuvent varier (import, réécriture, reflow)
- un fallback “par index” (runtime) peut pointer un mauvais paragraphe si le texte change
Décision :
- aliasing dancres **à la build**
- mapping versionné
- injection dans `dist/**/index.html`
Implémentation :
- `docs/anchor-aliases.json` (ou équivalent)
- `scripts/inject-anchor-aliases.mjs` (executé en `postbuild`)
Résultat :
- liens profonds préservés
- comportement déterministe, auditable
---
## 2) Audit dist : rendre le contrôle “incassable”
Constat :
- une détection naïve des IDs peut ramasser des `id="..."` présents dans des strings JS/CSS
Action :
- durcissement `scripts/audit-dist.mjs` :
- strip `<script>` + `<style>`
- regex exigeant un espace avant `id=` (évite `data-id=`)
Résultat :
- plus de faux positifs
- signal de qualité fiable
---
## 3) Baseline tests (local + CI)
Consolidation :
- `npm test` enchaîne :
- aliases OK
- build
- audit dist
- vérif injection alias dans dist
- check anchors (+ mode update)
- check inline JS
Résultat :
- un “garde-fou” unique, simple, dur à casser
---
## 4) Déploiement production DS220+ (DSM 7.3) — accomplissement majeur
Objectif :
- sortir du “dev local Mac”
- publier une prod stable, rollback ultra rapide
- ne pas casser loutil éditorial (tickets + labels + runner)
Décisions :
- build Astro dans image Docker (Node Debian “robuste”)
- runtime Nginx statique (léger)
- 2 slots blue/green (ports distincts)
- bascule par Reverse Proxy DSM (édition port)
- ports 8081/8082 bindés sur 127.0.0.1 (pas exposés WAN)
Spécificités DSM rencontrées et traitées :
- soucis réseau/DNS possibles pendant build → `build.network: host` + BuildKit
- transferts macOS pouvant injecter artefacts (`PaxHeader`, `._*`) → `.dockerignore` recommandé
Résultat :
- le site répond en HTTPS sur `archicratie.trans-hands.synology.me`
- Pagefind OK
- Proposer OK (création ticket + redirection Gitea)
- exécuteur (labels) OK
---
## 5) Gitea : diagnostic 404 “Proposer”
Cause typique :
- mauvais couple `OWNER/REPO` (casse sensible)
Action :
- vérification via API Gitea
- correction `.env` (ex : OWNER=Archicratia, REPO=archicratie-edition)
- rebuild image pour injecter les bonnes valeurs
Résultat :
- Proposer fonctionne end-to-end
---
## 6) Où sont les procédures
- Setup prod DSM : `DEPLOY_PROD_SYNOLOGY_DS220.md`
- Cockpit opérateur + rollback + dépannage : `OPS_COCKPIT.md`
- Anchors policy : `anchors.md`
- Baseline CI : `CI-BASELINE.md`

392
docs/MANUEL_REFERENCE.md Normal file
View File

@@ -0,0 +1,392 @@
# Manuel de référence — Archicratie Web Edition (Site + Gitea + Runner)
Ce manuel explique comment utiliser et maintenir loutil dédition :
- Site Astro “Archicratie Web Edition”
- Proposer (tickets Gitea pré-remplis)
- Apply-ticket (application semi-automatique dans le contenu)
- Contrat dancres (citabilité) + tests
- CI Gitea Actions + ds220-runner (DS220+)
> Objectif : une boucle éditoriale reproductible, opposable, sûre, et testée.
---
## 0) Glossaire minimal (anti-jargon)
- **PR** (Pull Request) : demande de fusion dune branche vers `master`.
- **PAT** (Personal Access Token) : jeton daccès Gitea utilisé comme “mot de passe” pour API/Git.
- **Anchor / Ancre** : identifiant stable dun paragraphe (ex: `p-8-0e65838d`) utilisable dans une URL `#...`.
- **Churn** : variation des ancres dune page entre deux builds (mesure de stabilité).
- **dist/** : sortie buildée (artefact), jamais la “source de vérité”.
- **CI** : automatisation (build + tests) à chaque push/PR.
---
## 1) Pré-requis (poste local)
### Outils
- Node.js + npm
- Git
- (Optionnel) Python3 pour scripts de debug ponctuels
### Accès
- Compte Gitea avec droits sur le repo
- Un **PAT** Gitea
- Accès au site local (dev) et/ou build (dist)
---
## 2) Vue densemble : la boucle éditoriale (rituel)
### Boucle standard (la “cadence courte”)
1) Sur le site, cliquer **Proposer** sur un paragraphe.
2) Choisir Type (Correction / Fact-check), puis Category.
3) Gitea ouvre un ticket pré-rempli (Chemin/URL/Ancre + texte actuel).
4) Rédiger la **Proposition (remplacer par)** + Justification.
5) En local :
- `node scripts/apply-ticket.mjs <num>` (dabord `--dry-run`)
- `npm run test:anchors`
- `npm run build`
6) Commit + push + PR si nécessaire.
---
## 3) Le site “Proposer / Citer / ¶” (para-tools)
### Où ça vit ?
- `src/layouts/EditionLayout.astro` injecte un script qui :
- ajoute “¶” (lien dancre), “Citer”, “Proposer”
- construit lURL de création dissue Gitea
- embarque si possible le **paragraphe exact** (sinon un extrait)
- `src/components/ProposeModal.astro` gère la modal 2 étapes :
- Type (correction/fact-check)
- Category (cat/style, cat/lexique, etc.)
- ouvre ensuite lissue en **nouvel onglet** sans quitter le site
### Invariants (non négociables)
- **Ancre + URL + Chemin** doivent toujours être présents.
- Le mode “texte exact” est *best effort* :
- si trop long → fallback extrait + note
- priorité absolue : rester robuste et cliquable.
### Limites / règles de taille
- **FULL_TEXT_SOFT_LIMIT** : taille max pour tenter dembarquer “copie exacte”
- **URL_HARD_LIMIT** : si lURL devient trop longue → repasse en extrait
> But : éviter des issues amputées + éviter des URLs trop longues / fragiles.
---
## 4) Format des tickets (machine-readable)
Le parsing des tickets est fait par `scripts/apply-ticket.mjs` + workflow auto-label.
Donc le contenu doit rester “parsable”.
Sections attendues (au minimum) :
- `Chemin: /.../`
- `Ancre: #p-...`
- `Texte actuel (...)` (idéalement “copie exacte”, sinon “extrait”)
- `Proposition (remplacer par): ...`
- `Justification: ...` (peut être vide, mais le champ doit exister si possible)
> Voir aussi : docs/CONTRAT_TICKETS.md
---
## 5) Appliquer un ticket en local : `apply-ticket`
### Commandes
- Dry run (recommandé) :
- `node scripts/apply-ticket.mjs <N> --dry-run`
- Application réelle :
- `node scripts/apply-ticket.mjs <N>`
### Comportements de sûreté (guardrails)
- `--dry-run` :
- **aucun fichier écrit**
- **aucun backup créé**
- affiche BEFORE/AFTER (extrait)
- Mode écriture :
- crée un backup `.bak.issue-<N>` uniquement si écriture
- match “best effort” mais refuse si score insuffisant
### Après application
- `git diff -- <fichier>`
- `git add <fichier>`
- `git commit -m "edit: apply ticket #N (...)"`
---
## 6) Contrat de citabilité : ancres + churn test
### Pourquoi ?
Les ancres sont le “socle de citabilité”. On veut prévenir les liens morts.
### Scripts
- `npm run test:anchors`
- compare les ancres de `dist/` avec une baseline
- calcule le churn et signale les pages modifiées
- `npm run test:anchors:update`
- met à jour la baseline (à faire seulement quand cest volontaire)
> Important : `dist/` est un artefact ; la baseline sert à mesurer, pas à “éditer dist”.
---
## 7) Garde-fou JS inline : `check-inline-js`
Pourquoi : éviter quun JS inline cassé supprime les boutons et fasse disparaître “Proposer/Citer”.
- `node scripts/check-inline-js.mjs`
- intégré dans `npm test`
---
## 8) CI (Gitea Actions)
### Objectif
Sur chaque push/PR : vérifier automatiquement que :
- build OK
- ancres OK
- JS inline OK
### Workflow
- `.gitea/workflows/ci.yml`
### Commande “contrat”
- `npm test`
- exécute : build + anchors + inline-js
---
## 9) ds220-runner (DS220+ / Gitea act-runner)
### Comportement normal
Le runner lance des jobs dans des conteneurs éphémères qui démarrent/stop.
DSM peut notifier “conteneur arrêté de manière inattendue” : cest souvent un faux positif “bruyant”.
### À surveiller vraiment
- Jobs en échec côté Gitea Actions
- logs mentionnant :
- auth API (401)
- labels manquants
- npm ci/build en échec
---
## 10) Dépannage (symptômes → causes fréquentes)
### “Proposer” ouvre deux onglets / remplace longlet courant
Cause : double handler click / manque de `stopPropagation` / mauvais fallback.
Fix : le flux doit ouvrir **nouvel onglet uniquement**, et sinon prompt.
### “Proposer/Citer/¶” disparaissent
Cause : JS inline cassé → exception ou erreur syntaxe.
Fix : `node scripts/check-inline-js.mjs` + vérifier `dist/.../index.html` console.
### apply-ticket : “Proposition introuvable”
Cause : ticket sans section “Proposition (remplacer par):”.
Fix : respecter le contrat de ticket.
### apply-ticket : “Match trop faible”
Cause : texte actuel absent / trop tronqué / mismatch.
Fix :
- privilégier “Texte actuel (copie exacte du paragraphe)”
- sinon le script récupère via `dist` (si possible).
---
## 11) Règles de contribution (discipline de repo)
- Toute modif éditoriale passe par ticket → apply-ticket → tests.
- Toute modif structurelle (layouts, rendu, MDX) doit être suivie de :
- `npm test`
- éventuellement `npm run test:anchors:update` si changement volontaire dancres.
- Ne jamais éditer `dist/` à la main.
---
## 12) Commandes utiles (raccourcis)
- Dev : `npm run dev`
- Build : `npm run build`
- Tests : `npm test`
- Anchors : `npm run test:anchors`
- Apply ticket : `node scripts/apply-ticket.mjs <N> --dry-run`
## 13) UI de lecture outillée (EditionLayout / Reading)
Cette section documente les comportements “front” qui ont été verrouillés et qui peuvent casser si on refactorise le layout, le CSS ou les scripts inline.
### 13.1 — Structure DOM (contrat)
Le layout dédition repose sur cette structure (simplifiée) :
- `src/layouts/EditionLayout.astro`
- `<aside class="page-aside">`
- `<div class="page-aside__scroll">`
- `<slot name="aside" />` (TOC global + TOC local)
- `<article class="reading" data-pagefind-body>`
- contenu (paragraphes + headings)
- `<BuildStamp />`
- `<div id="reading-follow" class="reading-follow">` (bandeau H1/H2/H3)
- `<ProposeModal />` (dialog)
Important :
- Les styles `.page-aside` / `.page-aside__scroll` sont **dans EditionLayout.astro** (balise `<style>`), pas dans `global.css`.
- `global.css` contient les styles transverses (reading, offsets, reading-follow, tools boutons…).
### 13.2 — Scrollbar “left-side” (TOC global + TOC local)
But : **un seul scroll** qui englobe tout le panneau gauche.
Contrat :
- `.page-aside` est sticky (collé sous le header).
- Le scroll est **uniquement** sur `.page-aside__scroll` :
- `max-height: calc(100vh - (...))`
- `overflow: auto`
- Sur mobile (`max-width: 860px`) :
- aside redevient “dans le flux” (plus sticky)
- plus de scroll interne
Symptôme si ça casse :
- deux scrollbars (aside + un sous-bloc), ou
- le TOC local défile mais pas le global (ou inversement).
### 13.3 — Offsets dancrage (header + bandeau)
Objectif : quand on va vers `#p-…`, `#h2…`, `#h3…`, lélément visé **nest jamais caché** derrière le header ou le bandeau.
Contrat CSS (dans `src/styles/global.css`) :
- `--sticky-offset = calc(--sticky-header-h + --followbar-h)`
- `scroll-margin-top: var(--sticky-offset)` sur :
- `.reading p[id]`
- `.reading h1`, `.reading h2[id]`, `.reading h3[id]`
- `.details-anchor`, `.para-alias` (ancres techniques)
Contrat JS (inline dans `EditionLayout.astro`) :
- mesure `header` et met à jour `--sticky-header-h`
- mesure la hauteur réelle du bandeau et met à jour :
- `--followbar-h` (CSS)
- `--sticky-offset-px` (utilisé par les scroll JS `scrollToElWithOffset()`)
### 13.4 — Bandeau “reading-follow” (H1/H2/H3)
But : afficher un rappel de contexte (H1/H2/H3) aligné sur la largeur du reading, avec navigation.
Contrat :
- CSS : `.reading-follow` saligne via
- `left: var(--reading-left)`
- `width: var(--reading-width)`
- JS :
- calcule `--reading-left` / `--reading-width` à partir du `getBoundingClientRect()` de `article.reading`
- maintient létat courant H2/H3 (y compris dans des `<details>` ouverts/fermés)
- expose 2 actions :
- “haut du chapitre” (H1)
- “haut de la section” (H2 courant)
Note : une micro-hystérésis (`HYST`) évite le clignotement quand on scroll à la limite.
### 13.5 — Outils par paragraphe (¶ / Citer / Proposer / Marque-page)
Le script inline injecte, pour chaque `.reading p[id^="p-"]`, une UI de tools.
Boutons :
- `¶` : lien direct vers lancre
- `Citer` : copie une citation structurée : `Titre (vX) — URL#ancre`
- `Proposer` : si Gitea configuré, ouvre un ticket pré-rempli (via modal)
- `Marque-page` : épingle un “pinned bookmark”
Stockage local :
- pinned : `archicratie:bookmark:pinned`
- dernier lu (par page) : `archicratie:bookmark:last:<pathname>`
Le bouton header “Reprendre la lecture” (`#resume-btn`) :
- se base sur pinned, sinon sur last
- si on est déjà sur la même page, il scroll vers lancre au bon offset (sans recharger)
### 13.6 — Boucle “Proposer” + modal (2 étapes)
Fichiers :
- génération du lien : `src/layouts/EditionLayout.astro` (inline script)
- modal + logique : `src/components/ProposeModal.astro`
Contrat côté lien “Proposer” :
- le lien DOIT porter :
- `data-propose="1"` (ou présent en tant quattribut `data-propose`)
- `data-url="<URL Gitea issues/new?...>"`
- optionnel : `data-full="<texte complet>"` (permet upgrade du body)
- fallback “sans JS” :
- le lien a `target="_blank"` et ouvre quand même Gitea
Contrat modal :
- Interception par délégation : `document.addEventListener("click", ...)` sur `a[data-propose]`
- Step 1 : choix du type (`correction` / `fact`)
- met à jour `Type:` et `State:` dans le body
- préfixe le title (`[Correction]` / `[Fact-check]`)
- Step 2 : choix de `Category:` (ou vide)
- ajoute/retire la ligne `Category: ...`
- ouvre lissue en **nouvel onglet** (sans remplacer la page)
Garde-fou URL :
- `URL_HARD_LIMIT` (modal) empêche dexploser la taille dURL quand on tente dinjecter le texte complet dans `body=`.
---
## 14) Dépannage rapide (Firefox DevTools) — “tout comprendre” sans magie
### 14.1 — Ouvrir la console correctement (Firefox)
- Ouvre les DevTools : `F12` (ou `Ctrl+Shift+I`)
- Va dans longlet **Console**
- Tu peux exécuter du JS directement (comme tu las fait).
### 14.2 — Check “ProposeModal est là et vivant”
À exécuter dans la console :
- Le dialog existe :
- `document.getElementById("propose-modal")`
- Le navigateur supporte bien les dialogs :
- `typeof document.getElementById("propose-modal")?.showModal`
- attendu : `"function"`
- Test affichage :
- `document.getElementById("propose-modal")?.showModal()`
- puis : `document.getElementById("propose-modal")?.close()`
### 14.3 — Check “les liens Proposer sont bien marqués”
- Combien de liens interceptables :
- `document.querySelectorAll('a[data-propose]').length`
- Inspecter un exemple :
- `document.querySelector('a[data-propose]')?.dataset`
- attendu : au minimum `propose` et `url`
- optionnel : `full`
Si `a[data-propose]` = 0 :
- le script dinjection côté paragraphes na pas ajouté les attributs,
- ou le selector diffère (`data-propose` absent / renommé),
- ou tu nes pas sur une page “reading” (pas de `.reading p[id^="p-"]`).
### 14.4 — Check “le click est bien intercepté”
Tu peux provoquer un click “manuel” via :
- `document.querySelector('a[data-propose]')?.click()`
Résultat attendu :
- le modal souvre (step 1),
- puis après choix step1 + step2 : nouvel onglet Gitea.
Si ça ouvre directement Gitea sans modal :
- soit `data-propose` / `data-url` manquent,
- soit un autre handler stoppe linterception,
- soit le script modal nest pas exécuté (erreur JS en console).
### 14.5 — Check “offsets dancrage”
Pour lire les variables calculées :
- `getComputedStyle(document.documentElement).getPropertyValue("--sticky-header-h")`
- `getComputedStyle(document.documentElement).getPropertyValue("--followbar-h")`
- `getComputedStyle(document.documentElement).getPropertyValue("--sticky-offset")`
- `getComputedStyle(document.documentElement).getPropertyValue("--sticky-offset-px")`
Symptôme si mauvais :
- clic sur une ancre -> heading/para caché derrière le header.
### 14.6 — Check “scroll du panneau gauche”
Dans linspecteur, sélectionne `.page-aside__scroll` et vérifie les styles calculés :
- `overflow: auto`
- `max-height: ...`
Si le scroll est ailleurs :
- `.page-aside` a récupéré `overflow` / `max-height` (mauvais endroit)
- ou une règle CSS globale a repris la main.
### 14.7 — Commandes terminal “sanity checks”
Toujours lancer depuis la racine du site (là où est `package.json`), ex :
- Rechercher les patterns sensibles :
- `rg -n --hidden --glob '!node_modules/**' 'page-aside__scroll|reading-follow|data-propose|sticky-offset' src`
- Après build : vérifier quon na pas de règles “dangereuses” injectées dans `dist/` :
- `npm run build`
- `rg -n 'scroll-margin-top:\s*var\(--scroll-margin-top|overflow:\s*visible\s*!important|max-height:\s*none\s*!important' dist || echo "OK"`

385
docs/OPS-DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,385 @@
OPS — Déploiement Archicratie Web Edition (Mac Studio → DS220+)
Objectif : déployer une nouvelle version du site sur le NAS (DS220+) sans jamais casser la prod, en utilisant un schéma blue/green piloté par DSM Reverse Proxy, avec une procédure robuste même quand docker compose build est instable sur le NAS.
## 0) Repères essentiels
Noms & domaines
• Site public (prod) : https://archicratie.trans-hands.synology.me
• Gitea public : https://gitea.archicratie.trans-hands.synology.me
Attention : tu as eu un “piège” de typo (trans-hands-synology.me ≠ trans-hands.synology.me). Toujours vérifier lorthographe exacte.
Ports locaux NAS
• web_blue : 127.0.0.1:8081 → container:80
• web_green : 127.0.0.1:8082 → container:80
• DSM Reverse Proxy pointe soit vers 8081 soit vers 8082.
Règle dor “safe prod”
• On ne touche jamais au slot live (celui pointé par DSM).
• On construit/teste sur lautre slot.
• On bascule DSM (10 secondes).
• On rollback DSM (10 secondes) si besoin.
## 1) Blue/Green : qui fait quoi ?
Convention simple et pro
• blue = slot A (souvent “actif” par défaut) → 8081
• green = slot B (staging/next) → 8082
Mais : la vérité = DSM.
Ce nest pas “blue” ou “green” qui décide : cest le Reverse Proxy DSM qui pointe vers 8081 ou 8082.
➡️ Donc :
• blue et green sont deux environnements identiques
• lun est “live”, lautre est “next”
• après bascule, les rôles sinversent
## 2) Arborescence NAS (standard à stabiliser)
Chemin racine :
• /volume2/docker/archicratie-web/
Sous-dossiers recommandés :
• incoming/ : dépôt darchives venant du Mac (upload File Station / scp)
• releases/ : releases dépliées, horodatées
• current : symlink vers la release “courante” (le code utilisé pour builder)
• ops/ : scripts dexploitation (smoke, which-live, helpers)
• current__backup_before_cleanup/ : backup historique (optionnel)
Exemple sain :
/volume2/docker/archicratie-web/
incoming/
archicratie-web-20260130-104937.tar.gz
archicratie-web-20260130-104937.tar.gz.sha256
releases/
20260130-104937/
app/ (code déplié prêt)
current -> releases/20260130-104937/app
ops/
smoke.sh
which-live.sh
## 3) Permissions NAS (ce qui est “propre”)
Tu as eu des problèmes de release dépliée “root:root” → impossible à lire en user.
Politique simple
• propriétaire : archicratia:users
• dossiers : 750
• fichiers : 640
• incoming peut être 770 si tu y upload souvent via File Station.
Commandes (à lancer à la racine) :
BASE="/volume2/docker/archicratie-web"
sudo chown -R archicratia:users "$BASE"
sudo chmod 750 "$BASE" "$BASE/ops" "$BASE/releases" "$BASE/current__backup_before_cleanup" 2>/dev/null || true
sudo chmod 770 "$BASE/incoming" 2>/dev/null || true
# Important : options find AVANT les tests
find "$BASE/releases" -maxdepth 3 -type d -exec chmod 750 {} \;
find "$BASE/releases" -maxdepth 3 -type f -exec chmod 640 {} \;
⚠️ Éviter chmod 700 * “au hasard” : ça peut te bloquer toi-même + bloquer des outils DSM.
Le trio 750/640 est ton bon compromis.
## 4) Variables Gitea (les 3 variables qui conditionnent “Proposer”)
Dans .env sur le NAS (dans current/ ou dans la release) :
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition
Point critique : la casse du OWNER
Tu as déjà vu le symptôme :
• archicratia/archicratie-edition → peut renvoyer 404
• Archicratia/archicratie-edition → OK
Donc : OWNER doit être exactement la casse que Gitea attend.
Test rapide (NAS) :
BASE="https://gitea.archicratie.trans-hands.synology.me"
curl -kI "$BASE/Archicratia/archicratie-edition/" | head -n 8
✅ attendu : HTTP/2 200
## 5) Côté Mac Studio : préparer une release “propre” (sans scories macOS)
Pourquoi
Tu as eu :
• fichiers ._*
• xattrs LIBARCHIVE.xattr.com.apple.*
• warnings à lextraction
➡️ Ce nest pas bloquant, mais cest sale et ça te pollue les releases.
Script Mac : release-pack.sh (sans Git, ultra simple)
À placer à la racine du repo site/ sur Mac.
#!/bin/zsh
set -euo pipefail
TS="${1:-$(date +%Y%m%d-%H%M%S)}"
NAME="archicratie-web-$TS"
OUT_DIR="_release_out"
ARCHIVE="$OUT_DIR/$NAME.tar.gz"
SHA="$ARCHIVE.sha256"
mkdir -p "$OUT_DIR"
### Évite les AppleDouble (._*) + certaines métadonnées
export COPYFILE_DISABLE=1
export COPY_EXTENDED_ATTRIBUTES_DISABLE=1
### Nettoyages “safe”
find . -name ".DS_Store" -delete 2>/dev/null || true
### Crée un dossier staging
STAGE="$(mktemp -d)"
mkdir -p "$STAGE/$NAME"
### Copie le repo SANS déchets
rsync -a --delete \
--exclude ".git/" \
--exclude "node_modules/" \
--exclude "dist/" \
--exclude ".astro/" \
--exclude "_release_out/" \
--exclude ".DS_Store" \
--exclude "__MACOSX/" \
--exclude "._*" \
./ "$STAGE/$NAME/"
### Tar propre
tar -czf "$ARCHIVE" -C "$STAGE" "$NAME"
### Checksum
shasum -a 256 "$ARCHIVE" > "$SHA"
echo "OK: $ARCHIVE"
echo "OK: $SHA"
Usage :
chmod +x release-pack.sh
./release-pack.sh 20260130-104937
Résultat :
• _release_out/archicratie-web-20260130-104937.tar.gz
• _release_out/archicratie-web-20260130-104937.tar.gz.sha256
## 6) Transfert vers le NAS
Option A — DSM File Station (simple)
• Upload les 2 fichiers dans :
◦ /volume2/docker/archicratie-web/incoming/
Option B — scp (si SSH)
Depuis le Mac :
scp _release_out/archicratie-web-20260130-104937.tar.gz* \
archicratia@192.168.1.20:/volume2/docker/archicratie-web/incoming/
## 7) NAS : vérifier + déplier une release (procédure canonique)
BASE="/volume2/docker/archicratie-web"
TS="20260130-104937"
cd "$BASE"
### 1) checksum (IMPORTANT)
sha256sum -c "incoming/archicratie-web-$TS.tar.gz.sha256"
### 2) créer dossier release
rm -rf "releases/$TS"
mkdir -p "releases/$TS"
### 3) extraire
tar -xzf "incoming/archicratie-web-$TS.tar.gz" -C "releases/$TS"
### 4) pointer APP vers le dossier extrait
APP="$BASE/releases/$TS/archicratie-web-$TS"
### option: normaliser en "app"
mv "$APP" "$BASE/releases/$TS/app"
APP="$BASE/releases/$TS/app"
### 5) ownership + perms
sudo chown -R archicratia:users "$BASE/releases/$TS"
find "$BASE/releases/$TS" -type d -exec chmod 750 {} \;
find "$BASE/releases/$TS" -type f -exec chmod 640 {} \;
### 6) basculer current (symlink)
ln -sfn "$APP" "$BASE/current"
### 7) sanity check : compose présent ?
ls -la "$BASE/current/docker-compose.yml" "$BASE/current/Dockerfile" "$BASE/current/nginx.conf" "$BASE/current/.env"
Cas réel déjà rencontré : la release “na pas les fichiers ops”
Tu as eu un tar “inner” (archicratie-web.tar.gz) qui nembarque pas docker-compose.yml/Dockerfile/nginx.conf/.env.
Règle pro : ces fichiers doivent être dans la release.
Fix immédiat si cest absent (tu las déjà fait) :
OPS_SRC="$BASE/releases/20260129-174516_clean"
cp -a "$OPS_SRC/docker-compose.yml" "$APP/"
cp -a "$OPS_SRC/Dockerfile" "$APP/"
cp -a "$OPS_SRC/nginx.conf" "$APP/"
cp -a "$OPS_SRC/.env" "$APP/.env"
## 8) Build GREEN sur NAS (robuste) — IMPORTANT
Pourquoi docker compose build nest PAS fiable sur DS220+
Tu as vu :
• Temporary failure resolving 'deb.debian.org'
• apt-get update qui part en Ign: puis Err:
➡️ En pratique sur ton NAS : le builder na pas toujours le bon réseau, même si build: network: host est déclaré.
Procédure “qui marche vraiment” (golden path)
➡️ On build en host network avec docker build, puis on déploie avec compose sans rebuild.
cd /volume2/docker/archicratie-web/current
### charge les env vars du .env
set -a
. ./.env
set +a
### BUILD image green (host network)
sudo docker build --no-cache --network host \
--build-arg PUBLIC_GITEA_BASE="$PUBLIC_GITEA_BASE" \
--build-arg PUBLIC_GITEA_OWNER="$PUBLIC_GITEA_OWNER" \
--build-arg PUBLIC_GITEA_REPO="$PUBLIC_GITEA_REPO" \
-t archicratie-web:green \
-f Dockerfile .
Voir Ign: pendant apt-get nest pas forcément une erreur.
Ce qui compte : est-ce quil finit par télécharger / réussir.
On salarme uniquement si on voit Err: ... Temporary failure resolving + exit code non-zéro.
## 9) Démarrer GREEN (sans impacter BLUE)
cd /volume2/docker/archicratie-web/current
# ne jamais lancer "up" sans préciser le service
sudo docker rm -f archicratie-web-green 2>/dev/null || true
sudo docker compose up -d --force-recreate --no-build web_green
Smoke test (port 8082) :
/volume2/docker/archicratie-web/ops/smoke.sh 8082
## 10) DSM : basculer le Reverse Proxy (10 secondes)
DSM → Panneau de configuration → Portail de connexion (ou “Portail des applications”) → Proxy inversé
Trouve la règle :
• Source : https://archicratie.trans-hands.synology.me (443)
Destination :
• pour BLUE : http://127.0.0.1:8081
• pour GREEN : http://127.0.0.1:8082
Tu changes seulement le port, tu “Appliques”.
Test immédiat :
curl -kI https://archicratie.trans-hands.synology.me/ | head -n 8
## 11) Rollback (10 secondes)
Si ça ne va pas :
• DSM Reverse Proxy → remettre lancien port (8081 ou 8082)
• refresh navigateur
Option pro : ne stoppe pas lautre container tout de suite (tu peux comparer / investiguer calmement).
## 12) Ops scripts (à garder dans /volume2/docker/archicratie-web/ops)
smoke.sh (version “complète”)
#!/bin/sh
set -eu
PORT="${1:-8081}"
BASE="http://127.0.0.1:${PORT}"
echo "Smoke test ${BASE}"
curl -fsSI "${BASE}/" | head -n 5
curl -fsSI "${BASE}/pagefind/pagefind.js" | head -n 5
curl -fsSI "${BASE}/pagefind/pagefind-ui.js" | head -n 5 || true
curl -fsSI "${BASE}/pagefind/pagefind-ui.css" | head -n 5 || true
curl -fsSI "${BASE}/pagefind/pagefind-entry.json" | head -n 5 || true
echo "OK"
which-live.sh (déduire quel port est live)
Idée : on compare les Last-Modified ou ETag entre le domaine public et les ports locaux.
#!/bin/sh
set -eu
DOMAIN="https://archicratie.trans-hands.synology.me"
A="http://127.0.0.1:8081"
B="http://127.0.0.1:8082"
etag() { curl -ksI "$1/" | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r'; }
lm() { curl -ksI "$1/" | awk -F': ' 'tolower($1)=="last-modified"{print $2}' | tr -d '\r'; }
E_D="$(etag "$DOMAIN")"
E_A="$(etag "$A")"
E_B="$(etag "$B")"
echo "DOMAIN ETag: $E_D"
echo "8081 ETag: $E_A"
echo "8082 ETag: $E_B"
if [ -n "$E_D" ] && [ "$E_D" = "$E_A" ]; then
echo "LIVE=8081 (blue slot probable)"
elif [ -n "$E_D" ] && [ "$E_D" = "$E_B" ]; then
echo "LIVE=8082 (green slot probable)"
else
echo "LIVE=INCONNU (ETag mismatch) — check Last-Modified:"
echo "DOMAIN: $(lm "$DOMAIN")"
echo "8081 : $(lm "$A")"
echo "8082 : $(lm "$B")"
fi
## 13) Pannes & diagnostics (les vrais cas rencontrés)
### A) 504 via DSM Reverse Proxy
Symptôme : HTTP/2 504 côté domaine.
Check :
1. le container répond en local :
curl -I http://127.0.0.1:8081/
curl -I http://127.0.0.1:8082/
2. DSM Reverse Proxy destination = http://127.0.0.1:808X (pas https)
3. ports bindés en loopback (recommandé) : 127.0.0.1:808X:80
4. logs container :
sudo docker logs --tail=80 archicratie-web-blue
sudo docker logs --tail=80 archicratie-web-green
### B) “Proposer” ouvre Gitea mais tombe sur 404
Cest quasiment toujours lun de ces trois points :
1. OWNER/REPO incorrects (casse)
Test :
BASE="https://gitea.archicratie.trans-hands.synology.me"
curl -kI "$BASE/Archicratia/archicratie-edition/" | head
• Si 404 → cest ton .env / build args.
• La build a embarqué une ancienne config
Vérifie dans le HTML servi (dans le container) :
sudo docker exec -it archicratie-web-green sh -lc \
'grep -Rin "const GITEA_BASE" /usr/share/nginx/html | head -n 20'
1. Tu dois voir https://gitea.archicratie.trans-hands.synology.me + bon OWNER/REPO.
2. Problème dauth / droits Gitea
Si tu es loggé mais “pas autorisé”, vérifier que tu as accès au repo et que lURL pointe au bon repo.
### C) “Proposer” échoue avant Gitea (pas de pop-up, pas de 2 choix)
Check :
• console navigateur (JS)
• vérifier que le script “Proposer” est bien injecté dans la page (view source)
• tester une page simple (ex: /archicratie/)
### D) Pagefind nindexe plus
Ce que tu as déjà observé :
• Pagefind ignore les pages sans data-pagefind-body
• Tu avais bien ~12 pages indexées (log Pagefind)
Check dans le container :
sudo docker exec -it archicratie-web-green sh -lc \
'ls -la /usr/share/nginx/html/pagefind | head'
Note :
• GET /pagefind/ peut renvoyer 403 (listing interdit) : ce nest pas un bug
• tu testes plutôt :
◦ /pagefind/pagefind.js
◦ /pagefind/pagefind-entry.json
### E) docker compose build casse sur apt-get update (DNS)
Symptôme :
• Temporary failure resolving 'deb.debian.org'
Diagnostic rapide :
sudo docker run --rm --network host node:22-bookworm-slim sh -lc \
'apt-get -o Acquire::ForceIPv4=true update -qq && echo OK_APT'
✅ si OK_APT : ton NAS sait sortir, cest le builder compose qui est instable → utiliser la golden path : docker build --network host.
## 14) Dockerfile : version robuste “NAS host-network”
Dans ton Dockerfile, garde :
RUN --network=host apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
Option “anti-fragile” (retries + IPv4) :
RUN --network=host apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
## 15) Important : ne pas se faire piéger par le docker.sock
Sur ton DS220+, /var/run/docker.sock est root:root avec 660 → ton user ne peut pas accéder Docker sans sudo.
➡️ Règle :
• commandes Docker = sudo docker ...
• compose = sudo docker compose ...
(Optionnel : alias)
alias d='sudo docker'
alias dc='sudo docker compose'
## 16) Workflow résumé “cockpit” (le plus important)
Depuis le Mac :
1. ./release-pack.sh 20260130-104937
2. upload _release_out/*.tar.gz* → NAS incoming/
Sur le NAS :
1. vérifier checksum
2. déplier releases/$TS + chown/chmod
3. ln -sfn releases/$TS/app current
4. build green :
◦ sudo docker build --network host ... -t archicratie-web:green .
5. run green :
◦ sudo docker compose up -d --force-recreate --no-build web_green
6. smoke :
◦ /volume2/docker/archicratie-web/ops/smoke.sh 8082
7. switch DSM Reverse Proxy 8081 ↔ 8082
8. si souci → rollback DSM
Post-scriptum : ton problème local Mac (Astro “Cannot find module astro/config”)
Ce symptôme arrive typiquement quand :
• node_modules est incomplet/corrompu
• tu as basculé de dossier (ex: un _release_out/... a été pris pour le vrai repo)
• ou le process dev a reload sur un tsconfig “nouveau” mais pas les deps
Fix standard (dans le vrai dossier site/) :
rm -rf node_modules .astro dist
npm ci
npm run dev

205
docs/OPS_COCKPIT.md Normal file
View File

@@ -0,0 +1,205 @@
# OPS_COCKPIT — Exploitation “cockpit” (DS220+ / DSM 7.3)
Dernière mise à jour : 2026-01-29
Objectif : déployer en **blue/green** (2 slots), basculer via **DSM Reverse Proxy**, rollback en ~10 secondes.
---
## 1) Repères (à ne pas confondre)
- Dossier NAS : `/volume2/docker/archicratie-web/current`
- Domaine site : `https://archicratie.trans-hands.synology.me`
- Gitea public : `https://gitea.archicratie.trans-hands.synology.me`
- Slot BLUE : conteneur `archicratie-web-blue``127.0.0.1:8081 -> 80`
- Slot GREEN : conteneur `archicratie-web-green``127.0.0.1:8082 -> 80`
- DSM Reverse Proxy pointe **soit** vers 8081 **soit** vers 8082 (cest *ça* la bascule)
💡 Les couleurs nont pas de “sens métier”.
**Le slot pointé par DSM = PRODUCTION.** Lautre = pré-prod / candidat.
---
## 2) Commandes “état des lieux” (30 secondes)
en bash
cd /volume2/docker/archicratie-web/current
docker compose ps
docker inspect -f '{{.State.Health.Status}}' archicratie-web-blue
docker inspect -f '{{.State.Health.Status}}' archicratie-web-green
# Test local direct (depuis le NAS)
curl -I http://127.0.0.1:8081/ | head
curl -I http://127.0.0.1:8082/ | head
# Pour voir quelle version est SERVIE par DSM :
curl -kI https://archicratie.trans-hands.synology.me/ | head
## 3) Déploiement standard (build sur slot inactif)
Hypothèse : DSM pointe actuellement sur 8081 (BLUE) → on déploie sur GREEN (8082).
### 3.1 Pré-check (obligatoire)
cd /volume2/docker/archicratie-web/current
ls -la docker-compose.yml Dockerfile nginx.conf .env
cat .env
### 3.2 Build GREEN (image + dist + pagefind)
cd /volume2/docker/archicratie-web/current
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
docker compose build --no-cache web_green
### 3.3 Démarrer GREEN
docker compose up -d --force-recreate web_green
docker compose ps
### 3.4 Smoke test (GREEN)
./scripts/smoke.sh 8082
### 3.5 Health + logs (GREEN)
docker inspect -f '{{.State.Health.Status}}' archicratie-web-green
docker compose logs -f --tail=80 web_green
## 4) Basculer PROD (DSM) — 10 secondes
# Dans DSM 7.3 :
Panneau de configuration → Portail des applications → Proxy inversé
# Ouvre la règle du host :
Source : https / archicratie.trans-hands.synology.me / port 443
# Destination :
Protocole : http
Hôte : 127.0.0.1
Port : 8082 (pour basculer vers GREEN)
Enregistre.
# Validation rapide :
Dans ton navigateur : recharge le site (Ctrl+F5)
# Ou en shell :
curl -kI https://archicratie.trans-hands.synology.me/ | head
## 5) Rollback (si souci) — 10 secondes
Dans DSM → même écran → repasse Destination Port à 8081.
Optionnel (si GREEN est “pollué” / à arrêter) :
docker compose stop web_green
docker compose logs --tail=200 web_green
## 6) Dépannage “scénarios typiques”
### A) DSM renvoie HTTP 504 (timeout)
# Symptôme : curl -kI https://archicratie... → 504, et DSM affiche “noindex”.
# Causes probables
Le conteneur nécoute pas / est down
Le port DSM ne correspond pas au bon slot
Bind local absent (ports mal écrits)
Santé KO (mais DSM bascule quand même et tombe sur un truc mort)
# Checklist
docker compose ps
curl -I http://127.0.0.1:8081/ | head
curl -I http://127.0.0.1:8082/ | head
docker compose logs --tail=200 web_blue
docker compose logs --tail=200 web_green
Dans DSM : vérifie Destination = http://127.0.0.1:808X
⚠️ Important : ne pas ouvrir 8081/8082 au WAN.
Le reverse proxy DSM accède en loopback. Les ports restent en local (127.0.0.1:...).
### B) Gitea : 404 sur la redirection “Proposer”
Ca arrive si OWNER/REPO nexistent pas, ou si la casse est mauvaise.
# 1) Vérifier les variables injectées
cat .env
# 2) Vérifier que le repo existe vraiment
BASE="https://gitea.archicratie.trans-hands.synology.me"
OWNER="Archicratia"
REPO="archicratie-edition"
curl -kI "$BASE/$OWNER/$REPO/" | head -n 12
# 3) Vérifier via API
curl -ks "$BASE/api/v1/users/$OWNER" | head -c 250; echo
curl -ks "$BASE/api/v1/users/$OWNER/repos?limit=100" | grep -o '"full_name":"[^"]*"' | head
# 4) Si cest bon côté Gitea mais Proposer continue à viser lancien repo
→ tu as juste une image web construite avec les anciennes valeurs. Rebuild le slot.
docker compose build --no-cache web_green
docker compose up -d --force-recreate web_green
./scripts/smoke.sh 8082
# 5) Contrôle dans le HTML servi
docker exec -it archicratie-web-green sh -lc \
'grep -Rin "const GITEA_BASE" /usr/share/nginx/html | head -n 20'
### C) “Proposer” échoue (pas de ticket / pas de redirection)
# Cas typiques :
JS inline bloqué / non injecté
la page a été rebuild sans les variables publiques
erreur CORS / cookies / mauvais domaine (http vs https)
# Check rapide
Ouvre la console navigateur → onglet Network
Vérifie que la redirection pointe bien vers :
https://gitea.archicratie.../Archicratia/archicratie-edition/...
# Check côté build
docker exec -it archicratie-web-green sh -lc \
'grep -Rin "PUBLIC_GITEA" /usr/share/nginx/html | head -n 50'
### D) Pagefind ne marche plus (recherche vide / pagefind.js absent)
# Check HTTP
curl -I http://127.0.0.1:8082/pagefind/pagefind.js | head
# Check fichier dans le conteneur
docker exec -it archicratie-web-green sh -lc 'ls -la /usr/share/nginx/html/pagefind | head'
# Causes
build lancé via astro build au lieu de npm run build (donc postbuild non exécuté)
pagefind a échoué pendant postbuild
dist non généré / non copié dans nginx
# Remède
docker compose build --no-cache web_green
docker compose up -d --force-recreate web_green
./scripts/smoke.sh 8082
## 7) Logs “propres”
docker compose logs --tail=200 web_blue
docker compose logs --tail=200 web_green
# Si tu veux suivre en live :
docker compose logs -f web_green

184
docs/QUICKSTART.md Normal file
View File

@@ -0,0 +1,184 @@
# Quickstart — 10 minutes (Proposer → Ticket → Apply → Tests)
## 1) Pré-requis
- Node.js + npm
- Accès Gitea (compte) + PAT si usage API (apply-ticket)
## 2) Lancer le site
en bash :
npm install
npm run dev
Dans le navigateur, url : http://localhost:4321
## 3) Proposer une correction
Ouvre une page (ex: Prologue).
Sur un paragraphe : clique Proposer.
Choisis :
Type : Correction / Fact-check
Category (optionnel)
Un nouvel onglet Gitea souvre sur une issue pré-remplie.
Rédige :
Proposition (remplacer par):
Justification:
## 4) Appliquer le ticket en local (<NUMERO> = numéro du ticket non pas id-paragraphe)
En bash :
# dry-run (recommandé)
node scripts/apply-ticket.mjs <NUMERO> --dry-run
# appliquer
node scripts/apply-ticket.mjs <NUMERO>
git diff
git add <fichier>
git commit -m "edit: apply ticket #<NUMERO> (...)"
## 5) Vérifier avant push
npm test
## 6) Règle dor
Ne jamais éditer dist/ à la main.
Toujours garder Chemin + Ancre + Proposition dans le ticket.
Déplacer/ajouter les deux docs que je tai donnés
- `docs/MANUEL_REFERENCE.md`
- `docs/CONTRAT_TICKETS.md`
*(Tu peux reprendre mes versions telles quelles.)*
---
## Commandes terminal P0 (copier-coller)
En bash
mkdir -p docs
# crée/édite les fichiers avec ton éditeur habituel
# README.md
# docs/QUICKSTART.md
# docs/MANUEL_REFERENCE.md
# docs/CONTRAT_TICKETS.md
git status -sb
npm test
git add docs/QUICKSTART.md docs/MANUEL_REFERENCE.md docs/CONTRAT_TICKETS.md
git commit -m "docs: add quickstart + reference manual + ticket contract"
git push
## Checklist express (60 secondes) — valider loutil dédition
Après `npm run dev` :
1) Ouvrir un chapitre (page “reading”).
2) Hover un paragraphe : les tools apparaissent à droite.
3) Cliquer `Citer` : une citation est copiée (ou prompt fallback).
4) Cliquer `Marque-page` puis vérifier le bouton `Reprendre la lecture` en haut.
5) Cliquer `Proposer` :
- attendu : modal 2 étapes
- puis ouverture Gitea en nouvel onglet
### Si “Proposer” ne déclenche pas le modal (Firefox)
Console :
- `document.querySelectorAll('a[data-propose]').length` doit être > 0
- `typeof document.getElementById("propose-modal")?.showModal` doit retourner `"function"`
______________________________________
# Dernière mise à jour : 2026-01-29
Ce quickstart couvre :
- dev local (Mac / Linux)
- build/test (anchors + aliases + pagefind)
- pointeur vers déploiement production DS220+
---
## 1) Pré-requis
- Node : `>=22 <23`
- npm : `>=10 <11`
Vérifie en bash :
node -v
npm -v
## 2) Installer / lancer en local
Dans le dossier projet :
npm ci
npm run dev
Puis ouvrir lURL affichée (souvent http://localhost:4321
).
## 3) Variables Gitea (fonction “Proposer”)
La fonction “Proposer” construit des liens vers Gitea à partir de :
PUBLIC_GITEA_BASE
PUBLIC_GITEA_OWNER (⚠️ casse sensible)
PUBLIC_GITEA_REPO
En local : tu peux les mettre dans .env.local (ou .env.development) :
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition
## 4) Build “édition web” (avec Pagefind + aliases)
Important : toujours builder via npm (pour exécuter postbuild) :
npm run build
Ce qui se passe :
astro build génère dist/
postbuild exécute :
scripts/inject-anchor-aliases.mjs (aliases dancres)
pagefind --site dist (index de recherche)
## 5) Tests (incassable)
npm test
# Ce test enchaîne notamment :
vérif aliases dancres
build
audit dist (IDs dupliqués)
présence effective des aliases dans dist
check anchors (et option update)
check inline JS
## 6) Import éditorial (docx → mdx)
npm run import
Puis publication via build.
## 7) Production sur Synology DS220+
Le déploiement “propre” (Docker multi-stage + Nginx statique + DSM Reverse Proxy + blue/green) est documenté ici :
DEPLOY_PROD_SYNOLOGY_DS220.md
OPS_COCKPIT.md

221
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,221 @@
# ROADMAP — CI (Gitea Actions Synology) + Ancrages (aliases build-time)
But : permettre à un successeur de reprendre sans rien deviner.
Ce document décrit :
- létat stable actuel (baseline)
- les invariants à ne pas casser
- les prochaines étapes “mission principale” (ancrages primaires robustes + CI durable)
- la méthode de debug rapide
---
## 0) État actuel (baseline VALIDÉE)
### CI (Gitea Actions)
- ✅ Job dans un container Node 22 (conforme `engines`)
- ✅ Checkout **sans actions GitHub**, depuis `workflow/event.json`
- ✅ Zéro `apt-get` dans le workflow
-`npm ci` + build + tests anchors + validation schema aliases
- ✅ Injection daliases au postbuild confirmée en logs
### Runner (DS220+)
-`container.network: host` dans `/data/config.yaml` du runner
-`NODE_OPTIONS=--dns-result-order=ipv4first` passé aux containers de job
-`--add-host=gitea.archicratie.trans-hands.synology.me:192.168.1.20`
Raison : le DNS du bridge Docker (127.0.0.11) est instable sur cette infra → EAI_AGAIN / ESERVFAIL (npm, debian).
Référence : `docs/CI-BASELINE.md` + `docs/CI-WORKFLOW.md` + `docs/HANDOFF-SESSION.md`.
---
## 1) Invariants (NE PAS “optimiser”)
Ces points sont des garde-fous. Si on les retire, on revient aux mêmes pannes.
1) Runner :
- garder `container.network: host` (tant que linfra DNS bridge nest pas corrigée)
- garder `-e NODE_OPTIONS=--dns-result-order=ipv4first`
2) Workflow :
- ne pas réintroduire `apt-get`
- ne pas dépendre de `actions/checkout@...`
- garder un container Node 22 tant que `package.json engines` impose `>=22 <23`
3) Ancrages :
- le fichier canonique : `src/anchors/anchor-aliases.json`
- injection build-time : `scripts/inject-anchor-aliases.mjs`
- test anchors : `scripts/check-anchors.mjs`
- validation schema aliases : `scripts/check-anchor-aliases.mjs`
---
## 2) Mission principale (raccrochage)
Objectif “métier” :
- préserver les liens profonds (ancrages) malgré lédition (déplacements, insertions, corrections)
- éviter les résolutions “par index” (fragiles)
- rendre la migration dancrages **déterministe, versionnée, testée**
Traduction technique :
- quand un `newId` remplace un `oldId`, on versionne `oldId -> newId` **par page**
- au build, on injecte un alias DOM invisible portant lancien `id` avant lélément ciblé
---
## 3) Prochains jalons (ordre recommandé)
### Jalons A — Verrouillage qualité (court terme, “béton”)
A1) CI : prouver linjection (pas seulement “build ok”)
- ajouter un test qui parcourt `src/anchors/anchor-aliases.json` et vérifie dans `dist/<route>/index.html` :
- présence de `<span id="oldId" ...>`
- présence de lélément `id="newId"`
- et idéalement : alias placé “juste avant” la cible (proximité)
A2) CI : interdire les IDs en double (risque SEO/DOM)
- dans les pages `dist`, détecter les doublons dattribut `id="..."`
A3) CI : artefacts / logs actionnables
- quand un test échoue : afficher `route`, `oldId`, `newId`, extrait HTML et ligne
### Jalons B — Ergonomie éditeur (moyen terme)
B1) `apply-ticket.mjs` : renforcer le mode `--alias`
- si un paragraphe est remplacé : écrire lalias automatiquement
- si conflit : message clair “oldId déjà mappé / newId introuvable”
B2) `check-anchors.mjs` : suggestion daliases
- lorsquil détecte “removed X / added Y” avec même préfixe `p-8-...`
- générer une proposition, option `--write-aliases` (ou sortie patch)
### Jalons C — Robustesse long terme (ops)
C1) Runner : réduire le risque “host network”
- isoler le runner sur LAN (réseau dédié/pare-feu)
- limiter les labels/queues aux repos nécessaires
- documenter comment restaurer `/data/config.yaml`
C2) Versionner les décisions
- tout changement CI/runner : documenté dans `docs/` + commit (pas de “magic fix” non tracé)
---
## 4) Procédure standard (dev -> PR -> merge)
### Ajouter/modifier du contenu
1) modifier les sources (docx/import etc.)
2) si des IDs de paragraphes changent :
- appliquer `scripts/apply-ticket.mjs --alias` si possible
- sinon éditer `src/anchors/anchor-aliases.json` (par route)
### Vérifier en local
- `npm test`
- ou au minimum :
- `npm run build`
- vérifier injection : `grep -n "para-alias" dist/<route>/index.html`
### PR & merge
- une PR = un ticket logique
- CI doit passer
- merge seulement quand anchors + aliases sont cohérents
---
## 5) Debug express (quand ça casse)
### CI échoue “DNS / npm”
Symptômes typiques :
- `EAI_AGAIN`, `ESERVFAIL`, `Temporary failure resolving`
Actions :
1) vérifier runner config : `/data/config.yaml` contient bien `network: host`
2) vérifier job container : logs montrent `network="host"`
3) smoke test NAS :
- `docker run --rm --network host mcr.microsoft.com/devcontainers/javascript-node:22-bookworm bash -lc "npm ping --registry=https://registry.npmjs.org"`
### CI échoue “EBADENGINE”
- Node pas 22 → corriger limage du job (Node 22)
### CI échoue “MODULE_NOT_FOUND scripts/...”
- fichier non commité
- `git status --porcelain` puis `git add/commit/push`
### Injection dalias absente
- vérifier que `postbuild` appelle bien `inject-anchor-aliases.mjs`
- vérifier que `src/anchors/anchor-aliases.json` respecte le schéma (par route)
---
## 6) Définition de “DONE” (quand on peut dire “mission accomplie”)
1) CI stable sur 30+ runs consécutifs (push + PR + merge)
2) Toute modification de paragraphes qui casse des anchors produit :
- soit un alias automatique via tooling
- soit un échec CI explicite (avec patch proposé)
3) Aliases injectés testés (preuve dans dist) + pas de doublons dIDs
4) Documentation à jour (baseline + décisions + procédures)
---
## P2 — UX / Front “lecture outillée” (verrouillé)
- [x] Scroll unique dans le panneau gauche (TOC global + local).
- [x] Bandeau H1/H2/H3 aligné au reading + offsets dancres cohérents.
- [x] Boucle “Proposer” rétablie : modal 2 étapes (type + category) puis ouverture Gitea en nouvel onglet.
- [x] Bookmarks + reprise de lecture + citation copiable.
- [x] Procédure de diagnostic (terminal + dist + DevTools Firefox).
______________________________________
Dernière mise à jour : 2026-01-29
---
## ✅ Réalisé (socle solide)
### A) Ancrages stables (anti-casse)
- Aliasing dancres à la build (déterministe) :
- mapping versionné (ex: `docs/anchor-aliases.json`)
- injection dans `dist/**/index.html` via `scripts/inject-anchor-aliases.mjs`
- Audit dist : détection des IDs dupliqués (sans faux positifs JS/CSS) via `scripts/audit-dist.mjs`
- Vérification “aliases réellement présents dans dist” via `scripts/verify-anchor-aliases-in-dist.mjs`
### B) Qualité CI
- Suite de tests `npm test` (build + audit + anchors + inline JS)
### C) ✅ Production DS220+ (DSM 7.3) — Blue/Green
- Build Astro en image Docker (Node Debian) + runtime Nginx statique
- 2 slots :
- `web_blue` → 127.0.0.1:8081
- `web_green` → 127.0.0.1:8082
- Reverse Proxy DSM :
- bascule port 8081/8082 = bascule prod (rollback ~10s)
- Variables `PUBLIC_GITEA_*` injectées au build → “Proposer” fonctionne end-to-end
- Smoke test + healthcheck opérationnels
Docs :
- `DEPLOY_PROD_SYNOLOGY_DS220.md`
- `OPS_COCKPIT.md`
---
## ⏭️ Prochaines actions “futées” (à très forte valeur)
### 1) Automatiser le cycle blue/green (sans complexifier)
- Script `scripts/deploy-slot.sh green` :
- build --no-cache
- up --force-recreate
- smoke + health
- imprime “OK pour bascule DSM vers 8082”
- Script `scripts/which-live.sh` :
- compare 8081/8082 avec le domaine public
### 2) Durcir lhygiène “transfert macOS → NAS”
- `.dockerignore` contre `PaxHeader/`, `._*`, `.DS_Store`
- (optionnel) script `scripts/clean-macos-artifacts.sh`
### 3) Observabilité minimale
- page “/healthz” (statique) ou check `/pagefind/pagefind.js`
- journalisation Nginx : garder 23 jours, rotation simple (optionnel)
### 4) Sécurité reverse proxy
- confirmer headers (HSTS si voulu, CSP si nécessaire)
- sassurer que 8081/8082 restent local-only (127.0.0.1)
Fin.

278
docs/TOPO_LOGIQUE_METIER.md Normal file
View File

@@ -0,0 +1,278 @@
# Topo Logique métier de loutil éditorial (Web Edition ↔ Gitea ↔ apply-ticket ↔ CI)
Ce document explique **ce que fait réellement loutil**, pourquoi il existe, et comment le maintenir **sans casser** la promesse centrale : **citabilité durable + édition traçable + intégration sûre**.
> Public visé : “commun des mortels” (nouveau contributeur, mainteneur occasionnel, moi dans 6 mois).
---
## 0) La promesse (le “pourquoi”)
On veut un site (Astro) où chaque paragraphe est :
- **citable** (URL + ancre stable),
- **corrigeable** (ouvrir un ticket pré-rempli depuis le paragraphe),
- **réparable de manière sûre** (appliquer la proposition dans les sources sans magie),
- **vérifié automatiquement** (tests + build + garde-fous en CI).
En bref :
**Lecture → proposition → ticket → application → tests → build → publication**
…sans perdre la traçabilité, ni casser les citations historiques.
---
## 1) Glossaire (pas de jargon sans définition)
- **Ancre** : partie `#...` dune URL (ex : `#p-8-0e65838d`). Le navigateur saute à lélément HTML portant cet `id`.
- **Paragraphe citable** : un `<p id="p-...">...</p>` dans larticle.
- **Ticket / Issue** : demande de correction sur Gitea (ex : #14).
- **PR (Pull Request)** : “demande dajout / fusion” : une proposition de merge dune branche vers `master`.
- **CI** : tests automatiques exécutés sur le serveur à chaque push/PR.
- **Runner** : machine/conteneur qui exécute la CI (chez nous : DS220+ / ds220-runner).
- **DEV** : `npm run dev` (serveur Astro à la volée, pas de `dist/`).
- **BUILD / PROD-like** : `npm run build` puis servir `dist/` (ce qui ressemble à la prod).
---
## 2) Vue densemble : le pipeline (la “machine éditoriale”)
### 2.1 Le flux humain (ce que fait léditeur)
1) Lire une page sur “Archicratie Web Edition”.
2) Sur un paragraphe : cliquer **Proposer**.
3) Choisir **Type** (Correction / Fact-check) + **Category** (lexique, style, etc.).
4) Gitea souvre en **nouvel onglet** avec un ticket pré-rempli.
5) Léditeur écrit la proposition, justifie, et valide le ticket.
### 2.2 Le flux technique (ce que fait la machine)
6) `apply-ticket` récupère le ticket via lAPI Gitea.
7) Il retrouve le bon fichier source (MDX) + le bon paragraphe (matching sûr).
8) Il applique la modification (ou refuse si trop incertain).
9) On lance `npm test` → build + test ancres + check JS inline.
10) On commit / push.
11) La CI revalide automatiquement sur le serveur.
## Lecture outillée (UI) — ce qui est “verrouillé” côté ergonomie
Au-delà de linfra (CI/anchors/tickets), lédition web repose sur un contrat UX précis :
### A) Panneau gauche : sommaires (TOC global + TOC local)
- Le panneau gauche reste sticky sous le header.
- Un seul scroll interne englobe :
- TOC global (navigation chapitre)
- TOC local (sections du chapitre courant)
- Implémentation : scroll porté par `.page-aside__scroll` (pas par des sous-blocs).
### B) Bandeau H1/H2/H3 (“reading-follow”)
- Un bandeau suit la lecture et rappelle le contexte courant.
- Il est aligné sur la largeur du reading via des variables CSS mesurées.
- Il pilote aussi loffset réel des ancres (scroll-margin-top) pour éviter que les titres soient masqués.
### C) Outillage “par paragraphe”
Chaque paragraphe (ID `p-…`) expose :
- un lien direct (¶),
- une citation copiable,
- un bouton “Proposer” (si Gitea configuré),
- un marque-page (pinned) + un checkpoint automatique (dernier lu).
### D) Boucle “Proposer” : modal 2 étapes
Le click “Proposer” nenvoie pas directement sur Gitea :
1) Choix du type (Correction / Fact-check)
2) Choix optionnel dune Category
Puis ouverture du ticket en nouvel onglet avec body/title mis à jour.
---
## 3) Composant A — Le site (Astro) et les outils de paragraphe
Dans le rendu final, chaque paragraphe important reçoit un `id` de forme :
- `p-<index>-<hash8>`
ex : `p-8-0e65838d`
Ensuite, un script ajoute des **outils de paragraphe** (“para-tools”) :
- **¶** : lien direct sur le paragraphe (ancre)
- **Citer** : copie une citation (titre + version + URL sans query-string)
- **Proposer** : ouvre la modale (2 étapes), puis ouvre le ticket Gitea
### Point crucial : progressive enhancement
- Si JS marche : UX optimale.
- Si JS casse : le site reste lisible, et le lien de proposition reste utilisable au minimum via `href`.
---
## 4) Composant B — Contrat des tickets (machine-readable)
Pour que `apply-ticket` puisse travailler, le ticket doit rester **structuré**.
Le ticket contient des lignes “clé: valeur” (une par ligne), par ex :
- `Chemin: /archicratie/prologue/`
- `URL locale: http://localhost:4321/archicratie/prologue/#p-...`
- `Ancre: #p-8-...`
- `Version: ...`
- `Type: type/correction` (ou type/fact-check)
- `State: state/recevable` (ou state/a-sourcer)
- `Category: cat/lexique` (optionnel)
**Règle dor :** ces clés doivent rester simples, stables, sur une ligne chacune.
Sinon, scripts + CI + auto-labeling deviennent fragiles.
---
## 5) Composant C — apply-ticket (appliquer sans magie)
`apply-ticket` fait 4 choses :
1) **Fetch** : récupère le ticket via API (FORGE_TOKEN requis).
2) **Parse** : extrait Chemin/Ancre/Proposition/(Texte actuel si présent).
3) **Match** : retrouve le paragraphe dans le fichier source (MDX) :
- idéal : “Texte actuel (copie exacte…)” → match fort
- sinon : match par score → si score trop faible, **refus** (sécurité)
4) **Apply** : modifie le fichier source, sans casser le reste.
### Mode sûr
- `--dry-run` : montre BEFORE/AFTER, **nécrit rien**
- mode normal : écrit + propose ensuite `git diff`, `git add`, `git commit`
---
## 6) Composant D — Citabilité P0 : ancres, churn test, aliases
### 6.1 Le problème
Si lID dépend du contenu, modifier un paragraphe peut changer lID.
Donc une citation historique `...#ancien` casse.
### 6.2 La solution robuste (“web native”)
On ne “résout” pas lancre : **on la fait exister**.
On maintient un fichier :
- `src/anchors/anchor-aliases.json`
Exemple (par page/chemin) :
en json
{
"/archicratie/prologue/": {
"p-8-e7075fe3": "p-8-0e65838d"
}
}
Au build, un script injecte dans dist/.../index.html :
<span class="para-alias" id="p-8-e7075fe3"></span>
<p id="p-8-0e65838d">...</p>
Résultat :
le navigateur résout #p-8-e7075fe3 sans JS.
### 6.3 Pourquoi un fallback JS existe encore ?
En mode npm run dev, on ne passe pas par dist/, donc pas dinjection build-time.
Le fallback JS est un filet :
utile en DEV,
utile si un alias manque,
mais pas la solution principale.
## 7) Tests & garde-fous (qualité automatique)
### 7.1 npm test doit rester “simple et vrai”
Il exécute :
npm run build (génère dist)
npm run test:anchors (stabilité des ancres)
node scripts/check-inline-js.mjs (évite les scripts inline invalides)
### 7.2 test:anchors (baseline + churn)
tests/anchors-baseline.json = snapshot de référence.
check-anchors compare dist à la baseline.
Si trop de churn → échec (donc on voit la casse).
On met à jour la baseline uniquement quand on accepte consciemment la nouvelle réalité :
npm run test:anchors:update
## 8) DEV vs BUILD : comprendre sans se tromper
npm run dev → serveur Astro “live”, pas de dist/
⇒ les alias build-time nexistent pas ⇒ fallback JS peut sactiver.
npm run build && npx serve dist → rendu final “prod-like”
⇒ alias injectés ⇒ citations historiques fonctionnent sans JS.
Repère immédiat :
port 4321 = DEV
port 3000 (serve dist) = PROD-like
## 9) Rituels opérationnels (les 3 recettes)
### Recette 1 — Créer un ticket propre
Cliquer “Proposer”
Choisir Type + Category
Dans Gitea : compléter “Proposition (remplacer par)” + “Justification”
Ne pas détruire les lignes Chemin / Ancre / Type / State / Category
### Recette 2 — Appliquer un ticket
en bash :
node scripts/apply-ticket.mjs <ID> --dry-run
node scripts/apply-ticket.mjs <ID>
git diff
npm test
git add ...
git commit -m "edit: apply ticket #<ID> (<chemin>#<ancre>)"
git push
### Recette 3 — Quand un ID change (citabilité)
trouver ancien id + nouveau id
ajouter alias dans src/anchors/anchor-aliases.json
npm run build
vérifier ...#ancien → scroll sur le bon paragraphe
npm test
## 10) Règles dor (invariants non négociables)
Ne jamais éditer dist/ à la main (artefact).
Ne jamais sacrifier lancre : Chemin + Ancre doivent exister.
Tickets toujours “parsables” (clés stables ligne-par-ligne).
apply-ticket doit pouvoir refuser quand cest ambigu.
npm test doit rester “le bouton rouge” fiable.
## 11) Dépannage rapide
401 invalid token : FORGE_TOKEN absent ou mal exporté.
Header invalid value : token collé avec des caractères parasites (espaces, retours, texte autour).
Proposer ouvre 2 onglets / remplace la page : bug dinterception click → vérifier preventDefault + stopPropagation et l“openInNewTab”.
Ancien #id ne marche plus :
en PROD-like : vérifier alias injecté + JSON
en DEV : normal que fallback JS soit requis
## 12) Où lire quoi (docs)
docs/QUICKSTART.md : démarrer vite
docs/MANUEL_REFERENCE.md : usage complet + procédures
docs/CONTRAT_TICKETS.md : format strict des issues
(ce doc) docs/TOPO_LOGIQUE_METIER.md : la logique qui relie tout

157
docs/anchors.md Normal file
View File

@@ -0,0 +1,157 @@
# Contrat des ancres (paragraphes opposables)
## Source de vérité du sélecteur
Le site garantit la citabilité des paragraphes via des IDs injectés sur les balises `<p>`.
**Sélecteur contractuel :**
- `.reading p[id^="p-"]`
Tout outillage (scripts, tests, docs) doit utiliser ce sélecteur comme référence.
## Ce que le test vérifie
Le test compare, page par page, la liste des IDs de paragraphes présents dans `dist/` contre une baseline versionnée.
- Ajouts dIDs : généralement OK (nouveaux paragraphes).
- Suppressions / churn élevé : alerte (risque de casser des citations existantes).
## Fichier baseline
- `tests/anchors-baseline.json`
## Commandes
1) Générer / mettre à jour la baseline (cas intentionnel) :
- `npm run build`
- `npm run test:anchors:update`
2) Vérifier sans changer la baseline (cas normal) :
- `npm run build`
- `npm run test:anchors`
## Politique déchec (pragmatique)
Le test échoue si le churn dune page dépasse un seuil (défaut : 20%) sur une page “suffisamment grande”.
## Aliases build-time
- `src/anchors/anchor-aliases.json`
- `scripts/inject-anchor-aliases.mjs`
- `scripts/check-anchor-aliases.mjs`
- et rappelle : *alias = compat rétro de liens historiques sans JS*
## Ancres de section (H2) quand on utilise `<details>`
Quand un chapitre est structuré en sections repliables (`<details>`), on utilise une ancre “technique” dédiée pour garantir :
- une cible stable même si le H2 est dans un bloc replié,
- un scroll-offset correct (header + bandeau),
- une détection fiable pour le bandeau “reading-follow”.
Contrat côté HTML rendu :
- présence dun élément de type :
- `<span class="details-anchor" id="..."></span>`
- puis, dans le même `<details>`, un H2 visible dans le body.
Contrat côté CSS :
- `.details-anchor` a `scroll-margin-top: var(--sticky-offset)`.
Contrat côté JS (EditionLayout) :
- `openDetailsIfNeeded(el)` ouvre le `<details>` parent si nécessaire avant de scroller.
## Compat “legacy hash” : `#p-<idx>-<hash>`
Un hotfix de compat existe pour les anciennes ancres de paragraphes au format :
- `#p-<index>-<8 hex>`
Si lID exact nexiste plus :
- on cherche le premier élément dont lid commence par `p-<index>-`
- puis scroll avec offset.
But : éviter les “liens morts” historiques quand une régénération dIDs a eu lieu.
Limite : cest un fallback de dernier recours (moins déterministe quun alias explicite).
Le mécanisme recommandé reste : `docs/anchor-aliases.json` + injection au build.
_______________________________________
Dernière mise à jour : 2026-01-29
Ce document formalise comment on évite de casser les liens profonds (URLs avec `#ancre`).
---
## 1) Principe
Les IDs dancres générés (ou dérivés) peuvent changer :
- réécriture
- insertion/suppression de paragraphes
- re-slug dun titre
➡️ Donc : on ne “devine” pas un fallback par index en runtime.
➡️ On fait un aliasing **déterministe à la build**, versionné.
---
## 2) Le mapping dalias
- Fichier versionné (ex) : `docs/anchor-aliases.json`
- Format : `oldId -> newId` par page
Ex en json :
{
"/archicratie/archicrat-ia/chapitre-4/": {
"p-8-ancien": "p-8-nouveau"
}
}
## 3) Injection à la build (dans dist)
Script : scripts/inject-anchor-aliases.mjs
Moment : postbuild (après astro build, avant pagefind)
Ce script injecte dans dist/**/index.html un <span id="oldId"></span> juste avant lélément portant id="newId".
## 4) Contrôles (incassables)
### A) Vérifier quon na pas dIDs dupliqués en HTML
Script : scripts/audit-dist.mjs
Robustesse :
ignore <script> et <style> (évite faux positifs)
exige un espace avant id= (évite data-id=)
Exécution :
npm run audit:dist
### B) Vérifier que les aliases sont vraiment présents dans dist
Script : scripts/verify-anchor-aliases-in-dist.mjs
Exécuté dans npm test
## 5) Mise à jour assistée (si besoin)
Pour générer/mettre à jour des aliases à partir des deltas dancres :
npm run test:anchors:update
## 6) ⚠️ Artefacts macOS (PaxHeader / ._ / DS_Store)
Quand on transfère une archive depuis macOS, on peut importer des dossiers/fichiers parasites :
PaxHeader/
._*
.DS_Store
Ces artefacts peuvent polluer le build (Astro croit voir un “vrai” contenu MDX).
➡️ Recommandation : .dockerignore strict :
# macOS / archives
.DS_Store
._*
**/._*
PaxHeader
**/PaxHeader
# Node
node_modules
dist
# (Optionnel) Avant build sur NAS : supprimer ces dossiers si présents.