Seed from NAS prod snapshot 20260130-190531
This commit is contained in:
97
docs/CI-BASELINE.md
Normal file
97
docs/CI-BASELINE.md
Normal 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 d’IDs 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 d’infos de commit).
|
||||
173
docs/CI-WORKFLOW.md
Normal file
173
docs/CI-WORKFLOW.md
Normal 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 l’intention :
|
||||
- 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”
|
||||
|
||||
L’audit 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
29
docs/CONTRAT_TICKETS.md
Normal 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, l’ancre + 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 l’industrialisation
|
||||
103
docs/DEPLOY_PROD_SYNOLOGY_DS220.md
Normal file
103
docs/DEPLOY_PROD_SYNOLOGY_DS220.md
Normal 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`)
|
||||
- **n’ont 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 l’autre port
|
||||
|
||||
Pour l’opérationnel minute par minute, voir OPS_COCKPIT.md.
|
||||
|
||||
134
docs/HANDOFF-SESSION.md
Normal file
134
docs/HANDOFF-SESSION.md
Normal 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 d’aliases 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 l’existant”.
|
||||
|
||||
---
|
||||
|
||||
## 1) Décision structurante : ancrages stables, déterministes
|
||||
|
||||
Problème initial :
|
||||
- les IDs d’ancres peuvent varier (import, réécriture, reflow)
|
||||
- un fallback “par index” (runtime) peut pointer un mauvais paragraphe si le texte change
|
||||
|
||||
Décision :
|
||||
- aliasing d’ancres **à 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 l’outil é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
392
docs/MANUEL_REFERENCE.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# Manuel de référence — Archicratie Web Edition (Site + Gitea + Runner)
|
||||
|
||||
Ce manuel explique comment utiliser et maintenir l’outil d’édition :
|
||||
- Site Astro “Archicratie – Web Edition”
|
||||
- Proposer (tickets Gitea pré-remplis)
|
||||
- Apply-ticket (application semi-automatique dans le contenu)
|
||||
- Contrat d’ancres (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 d’une branche vers `master`.
|
||||
- **PAT** (Personal Access Token) : jeton d’accès Gitea utilisé comme “mot de passe” pour API/Git.
|
||||
- **Anchor / Ancre** : identifiant stable d’un paragraphe (ex: `p-8-0e65838d`) utilisable dans une URL `#...`.
|
||||
- **Churn** : variation des ancres d’une 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 d’ensemble : 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>` (d’abord `--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 d’ancre), “Citer”, “Proposer”
|
||||
- construit l’URL de création d’issue 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 l’issue 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 d’embarquer “copie exacte”
|
||||
- **URL_HARD_LIMIT** : si l’URL 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 c’est volontaire)
|
||||
|
||||
> Important : `dist/` est un artefact ; la baseline sert à mesurer, pas à “éditer dist”.
|
||||
|
||||
---
|
||||
|
||||
## 7) Garde-fou JS inline : `check-inline-js`
|
||||
Pourquoi : éviter qu’un 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” : c’est 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 l’onglet 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 d’ancres.
|
||||
- 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 d’ancrage (header + bandeau)
|
||||
Objectif : quand on va vers `#p-…`, `#h2…`, `#h3…`, l’élément visé **n’est 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` s’aligne 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 l’ancre
|
||||
- `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 l’ancre 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 qu’attribut `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 l’issue en **nouvel onglet** (sans remplacer la page)
|
||||
|
||||
Garde-fou URL :
|
||||
- `URL_HARD_LIMIT` (modal) empêche d’exploser la taille d’URL quand on tente d’injecter 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 l’onglet **Console**
|
||||
- Tu peux exécuter du JS directement (comme tu l’as 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 d’injection côté paragraphes n’a pas ajouté les attributs,
|
||||
- ou le selector diffère (`data-propose` absent / renommé),
|
||||
- ou tu n’es 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 s’ouvre (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 l’interception,
|
||||
- soit le script modal n’est pas exécuté (erreur JS en console).
|
||||
|
||||
### 14.5 — Check “offsets d’ancrage”
|
||||
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 l’inspecteur, 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 qu’on n’a 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
385
docs/OPS-DEPLOYMENT.md
Normal 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 l’orthographe 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 d’or “safe prod”
|
||||
• On ne touche jamais au slot live (celui pointé par DSM).
|
||||
• On construit/teste sur l’autre 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 n’est pas “blue” ou “green” qui décide : c’est le Reverse Proxy DSM qui pointe vers 8081 ou 8082.
|
||||
➡️ Donc :
|
||||
• blue et green sont deux environnements identiques
|
||||
• l’un est “live”, l’autre est “next”
|
||||
• après bascule, les rôles s’inversent
|
||||
|
||||
## 2) Arborescence NAS (standard à stabiliser)
|
||||
Chemin racine :
|
||||
• /volume2/docker/archicratie-web/
|
||||
Sous-dossiers recommandés :
|
||||
• incoming/ : dépôt d’archives 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 d’exploitation (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 à l’extraction
|
||||
➡️ Ce n’est pas bloquant, mais c’est 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 “n’a pas les fichiers ops”
|
||||
Tu as eu un tar “inner” (archicratie-web.tar.gz) qui n’embarque pas docker-compose.yml/Dockerfile/nginx.conf/.env.
|
||||
Règle pro : ces fichiers doivent être dans la release.
|
||||
Fix immédiat si c’est absent (tu l’as 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 n’est 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 n’a 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 n’est pas forcément une erreur.
|
||||
Ce qui compte : est-ce qu’il finit par télécharger / réussir.
|
||||
On s’alarme 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 l’ancien port (8081 ou 8082)
|
||||
• refresh navigateur
|
||||
Option pro : ne stoppe pas l’autre 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
|
||||
C’est quasiment toujours l’un 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 → c’est 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 d’auth / droits Gitea
|
||||
Si tu es loggé mais “pas autorisé”, vérifier que tu as accès au repo et que l’URL 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 n’indexe 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 n’est 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, c’est 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
205
docs/OPS_COCKPIT.md
Normal 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 (c’est *ça* la bascule)
|
||||
|
||||
💡 Les couleurs n’ont pas de “sens métier”.
|
||||
**Le slot pointé par DSM = PRODUCTION.** L’autre = 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 n’existent 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 c’est bon côté Gitea mais Proposer continue à viser l’ancien 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
184
docs/QUICKSTART.md
Normal 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 s’ouvre 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 d’or
|
||||
|
||||
Ne jamais éditer dist/ à la main.
|
||||
|
||||
Toujours garder Chemin + Ancre + Proposition dans le ticket.
|
||||
|
||||
Déplacer/ajouter les deux docs que je t’ai 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 l’outil 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 l’URL 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 d’ancres)
|
||||
|
||||
pagefind --site dist (index de recherche)
|
||||
|
||||
## 5) Tests (incassable)
|
||||
npm test
|
||||
|
||||
# Ce test enchaîne notamment :
|
||||
|
||||
vérif aliases d’ancres
|
||||
|
||||
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
221
docs/ROADMAP.md
Normal 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 d’aliases 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 l’infra DNS bridge n’est 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 d’ancrages **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 l’ancien `id` avant l’élément ciblé
|
||||
|
||||
---
|
||||
|
||||
## 3) Prochains jalons (ordre recommandé)
|
||||
|
||||
### Jalons A — Verrouillage qualité (court terme, “béton”)
|
||||
A1) CI : prouver l’injection (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 d’attribut `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 l’alias automatiquement
|
||||
- si conflit : message clair “oldId déjà mappé / newId introuvable”
|
||||
|
||||
B2) `check-anchors.mjs` : suggestion d’aliases
|
||||
- lorsqu’il 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 l’image du job (Node 22)
|
||||
|
||||
### CI échoue “MODULE_NOT_FOUND scripts/...”
|
||||
- fichier non commité
|
||||
- `git status --porcelain` puis `git add/commit/push`
|
||||
|
||||
### Injection d’alias 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 d’IDs
|
||||
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 d’ancres 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 d’ancres à 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 l’hygiè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 2–3 jours, rotation simple (optionnel)
|
||||
|
||||
### 4) Sécurité reverse proxy
|
||||
- confirmer headers (HSTS si voulu, CSP si nécessaire)
|
||||
- s’assurer que 8081/8082 restent local-only (127.0.0.1)
|
||||
|
||||
Fin.
|
||||
0
docs/SESSION_BILAN_CI_RUNNER_DNS_2026-01.md
Normal file
0
docs/SESSION_BILAN_CI_RUNNER_DNS_2026-01.md
Normal file
278
docs/TOPO_LOGIQUE_METIER.md
Normal file
278
docs/TOPO_LOGIQUE_METIER.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Topo – Logique métier de l’outil éditorial (Web Edition ↔ Gitea ↔ apply-ticket ↔ CI)
|
||||
|
||||
Ce document explique **ce que fait réellement l’outil**, 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 `#...` d’une URL (ex : `#p-8-0e65838d`). Le navigateur saute à l’élément HTML portant cet `id`.
|
||||
- **Paragraphe citable** : un `<p id="p-...">...</p>` dans l’article.
|
||||
- **Ticket / Issue** : demande de correction sur Gitea (ex : #14).
|
||||
- **PR (Pull Request)** : “demande d’ajout / fusion” : une proposition de merge d’une 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 d’ensemble : 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 s’ouvre 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 l’API 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 l’infra (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 l’offset 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” n’envoie pas directement sur Gitea :
|
||||
1) Choix du type (Correction / Fact-check)
|
||||
2) Choix optionnel d’une 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 d’or :** 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 l’ID dépend du contenu, modifier un paragraphe peut changer l’ID.
|
||||
Donc une citation historique `...#ancien` casse.
|
||||
|
||||
### 6.2 La solution robuste (“web native”)
|
||||
On ne “résout” pas l’ancre : **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 d’injection 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 n’existent pas ⇒ fallback JS peut s’activer.
|
||||
|
||||
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 d’or (invariants non négociables)
|
||||
|
||||
Ne jamais éditer dist/ à la main (artefact).
|
||||
|
||||
Ne jamais sacrifier l’ancre : Chemin + Ancre doivent exister.
|
||||
|
||||
Tickets toujours “parsables” (clés stables ligne-par-ligne).
|
||||
|
||||
apply-ticket doit pouvoir refuser quand c’est 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 d’interception 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
157
docs/anchors.md
Normal 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 d’IDs : 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 d’une 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 d’un é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 l’ID exact n’existe plus :
|
||||
- on cherche le premier élément dont l’id commence par `p-<index>-`
|
||||
- puis scroll avec offset.
|
||||
|
||||
But : éviter les “liens morts” historiques quand une régénération d’IDs a eu lieu.
|
||||
|
||||
Limite : c’est un fallback de dernier recours (moins déterministe qu’un 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 d’ancres générés (ou dérivés) peuvent changer :
|
||||
- réécriture
|
||||
- insertion/suppression de paragraphes
|
||||
- re-slug d’un titre
|
||||
|
||||
➡️ Donc : on ne “devine” pas un fallback par index en runtime.
|
||||
➡️ On fait un aliasing **déterministe à la build**, versionné.
|
||||
|
||||
---
|
||||
|
||||
## 2) Le mapping d’alias
|
||||
|
||||
- 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 qu’on n’a pas d’IDs 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 d’ancres :
|
||||
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.
|
||||
Reference in New Issue
Block a user