diff --git a/docs/diagrams/START-HERE.md b/docs/diagrams/START-HERE.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/diagrams/archicratie-web-edition-blue-green-runbook-verbatim-v2.svg b/docs/diagrams/archicratie-web-edition-blue-green-runbook-verbatim-v2.svg new file mode 100644 index 0000000..6d12de1 --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-blue-green-runbook-verbatim-v2.svg @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + Archicratie — Runbook Blue/Green (v2, verbatim) + Mise à jour 2026-02-20 — release-pack → releases/<ts>/app → current → docker compose web_blue/web_green + + 0) Pré-requis + main protégé → travail via branches + PR + CI doit rester source de vérité + Éviter d'éditer une release en prod (hotfix = exception) + Si hotfix: on le re-synchronise ensuite dans Git (cf. étape 5) + + 1) Préparer une release (atelier DEV) + npm ci && npm run build + release-pack.sh → tarball/artefact + inclut dist/ + pagefind + indexes + build stamp + ouvrir PR → merge → CI + + 2) Déposer sur NAS + /volume2/docker/archicratie-web/releases/<ts>/app + current → pointe vers la release active + build context docker = current OU release/app (selon compose) + + 3) Build images + sudo env DOCKER_API_VERSION=1.43 \ + docker compose -f docker-compose.yml build --no-cache \ + web_blue web_green + les 2 images doivent builder OK + + 4) Switch trafic (blue ↔ green) + déterminer couleur active (proxy / conf) + mettre à jour routing vers l'autre couleur + reload proxy, vérifier 200/302 + curl -sSI -H 'Host: staging.*' http://127.0.0.1:18080/ + rollback = revenir à l'ancienne couleur + + 5) Hotfix de release → re-synchroniser Git (step8) + A) NAS: find src -mtime -3 → liste fichiers + B) NAS: tar -czf /tmp/hotfix.tgz -T liste + C) sha256 + manifest, puis scp vers Mac + D) Mac: tar -xzf → rsync --checksum vers repo + E) commit sur branche dédiée → push → PR vers main + + Arborescence NAS (rappel) + /volume2/docker/archicratie-web/ + releases/ + 20260219-103222/ + app/ (ctx build) + current -> releases/…/app + compose/ docker-compose.yml + compose.expanded.yml t'indique le build.context effectif + + → NAS + build + + images OK + + si hotfix + + Règle d'or + La release doit être reproductible depuis Git. Toute modif manuelle en prod doit finir: (a) re-sync dans une branche, (b) PR, (c) merge, (d) prochaine release propre. + diff --git a/docs/diagrams/archicratie-web-edition-blue-green-runbook-verbatim.svg b/docs/diagrams/archicratie-web-edition-blue-green-runbook-verbatim.svg new file mode 100644 index 0000000..d918b6a --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-blue-green-runbook-verbatim.svg @@ -0,0 +1,551 @@ + + + + + + + + + + + + Archicratie – Web Edition : Blue/Green Runbook visuel (VERBATIM) + Cible : déployer une nouvelle version sur le slot inactif (8081/8082), basculer via Traefik (dynamic/20-archicratie-backend.yml), vérifier (smoke tests), rollback en 30s si besoin. + + + + + Invariants (ce qui évite de casser la prod) + • Les 2 slots existent en parallèle : archicratie-web-blue = 127.0.0.1:8081 et archicratie-web-green = 127.0.0.1:8082. + • Traefik edge écoute :18080 et choisit le slot LIVE via /volume2/docker/edge/config/dynamic/20-archicratie-backend.yml. + • Une seule cible active dans Traefik (pas de load-balance non déterministe). Rollback = remettre l’URL précédente dans le même fichier. + + + • RIGUEUR ABSOLUE : STAGING = slot INACTIF (opposé au LIVE). LIVE = 20-archicratie-backend.yml ; STAGING = 21-archicratie-staging.yml. + + + + Étape 1 — Build & déployer + But + Mettre la nouvelle version sur le slot inactif + (sans toucher au slot LIVE actuel). + + + + /volume2/docker/archicratie-web/current/ + Compose : docker-compose.yml + Slots : + web_blue → 127.0.0.1:8081 + web_green → 127.0.0.1:8082 + Le build injecte aussi PUBLIC_GITEA_* via build args + (déjà dans ton compose). + + + Commandes (safe) + 1) Choisir le slot cible (inactif) + Astuce : le LIVE = ce que pointe Traefik dans 20-archicratie-backend.yml + 2) Build + redémarrer uniquement ce slot + cd /volume2/docker/archicratie-web/current + sudo docker compose build web_green + sudo docker compose up -d --no-deps web_green + Remplace web_green par web_blue selon la cible. + Ne pas toucher l’autre service. + + + + + Étape 2 — Switch Traefik (LIVE) + But + Basculer le LIVE en modifiant 1 fichier + et laisser Traefik recharger automatiquement. + + + Fichier canonique (LIVE switch) + /volume2/docker/edge/config/dynamic/20-archicratie-backend.yml + Contient : + http.services.archicratie_web.loadBalancer.servers[0].url + Ex : http://127.0.0.1:8082 (green) + ou http://127.0.0.1:8081 (blue) + + + Procédure (anti-casse) + 1) Backup horodaté du fichier + cd /volume2/docker/edge/config/dynamic + sudo cp 20-archicratie-backend.yml20-archicratie-backend.yml.bak.$(date +%F-%H%M%S) + 2) Éditer l’URL (un seul backend) + sudo vi 20-archicratie-backend.yml + Changer uniquement la valeur url : 8081 ↔ 8082 + 2bis) Mettre à jour 21-archicratie-staging.yml sur l’autre port(opposé au LIVE) + 3) Traefik recharge (watch=true) + Pas de restart requis si provider file watch=true(ton traefik.yml). + + + + + Étape 3 — Smoke tests + But + Prouver que le nouveau LIVE répond, + et que l’auth (Authelia/whoami) est OK. + + + Tests “slot direct” (preuve build) + Le slot construit doit répondre en 200 : + curl -sS -I http://127.0.0.1:8081/ | head -n 12 + curl -sS -I http://127.0.0.1:8082/ | head -n 12 + L’un des deux peut rester l’ancien LIVE. + L’objectif est que le slot cible soit OK. + + + Tests “edge” (preuve routage + auth) + Host rules : tester AVEC Host header : + curl -sS -I -H 'Host:archicratie.trans-hands.synology.me'http://127.0.0.1:18080/ | head -n 20 + + + diff --git a/docs/diagrams/archicratie-web-edition-edge-routing-verbatim-v2.svg b/docs/diagrams/archicratie-web-edition-edge-routing-verbatim-v2.svg new file mode 100644 index 0000000..8271368 --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-edge-routing-verbatim-v2.svg @@ -0,0 +1,437 @@ + + + + + + + + + + + + + + + + + + + + + + + + Archicratie — Edge routing (v2, verbatim) + Mise à jour 2026-02-20 — Host routing + Authelia + Blue/Green web_* + + Client (navigateur) + https://archicratie.* / https://staging.archicratie.* + cookies authelia_session + HEAD/GET → 302 si non auth + + HTTPS 443 + + Reverse-proxy (Nginx / DSM) + Routage par Host + auth_request → Authelia + proxy_pass → service web_blue ou web_green + headers: X-Forwarded-* + Host + en local: curl -H 'Host: ...' http://127.0.0.1:18080/ + + auth_request + + Auth stack + Authelia (portal login) + LLDAP (backend LDAP) + Redis (sessions / storage) + auth.* domain + + 302 → auth.*?rd=... + + Service web_blue (container) + nginx static (dist/) + /pagefind/*, /assets/* + + Service web_green (container) + nginx static (dist/) + build identique, couleur swap + + une seule couleur active + + Atelier DEV (local) + astro dev : http://localhost:4321 + pas d'authelia + predev génère: + /annotations-index.json + /para-index.json + 404 = index manquant (relancer predev/dev) + + requête + + auth_request + + + proxy_pass (active) + + callback + cookie + + Note importante (debug) + Si tu testes via loopback (127.0.0.1:18080), la directive Host détermine la vhost. Sans Host correct, tu peux tomber sur une autre conf. + diff --git a/docs/diagrams/archicratie-web-edition-edge-routing-verbatim.svg b/docs/diagrams/archicratie-web-edition-edge-routing-verbatim.svg new file mode 100644 index 0000000..b545add --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-edge-routing-verbatim.svg @@ -0,0 +1,324 @@ + + + + + + + + + + Edge Traefik (verbatim) — routers Host(...) + middlewares + services + Source : /volume2/docker/edge/config/dynamic/10-core.yml + 20-archicratie-backend.yml + 21-archicratie-staging.yml + 30-lldap-ui.yml + + Traefik : entryPoint web = :18080 — provider file (dynamic/) watch=true + + + Middlewares (10-core.yml) + sanitize-remote : purge Remote-* + force X-Forwarded-Proto/Port + authelia : forwardAuth → http://127.0.0.1:9091/api/authz/forward-auth + chain-auth : [sanitize-remote, authelia] + + + Routers + archicratie + Host(archicratie.trans-hands.synology.me) + chain-auth → service archicratie_web + archicratie-authinfo + Host(archicratie…) PathPrefix(/_auth/whoami) + chain-auth → whoami + gitea + Host(gitea.archicratie.trans-hands.synology.me) + sanitize-remote → gitea_web + archicratie-staging + Host(staging.archicratie.trans-hands.synology.me) + chain-auth → archicratie_blue + lldap-ui + Host(lldap.archicratie.trans-hands.synology.me) + chain-auth → lldap_ui + + Services (loadBalancer → url) + whoami + http://127.0.0.1:18081 (edge-whoami) + gitea_web + http://127.0.0.1:3000 (Gitea) + archicratie_web + → défini par 20-archicratie-backend.yml + • actuel : http://127.0.0.1:8082 (green) + archicratie_blue + http://127.0.0.1:8081 (staging) + lldap_ui + http://127.0.0.1:17170 (LLDAP UI) + + Interprétation debug (safe) + • Si tu testes sans Host header sur :18080 → 404 (normal) + • Si archicratie → 302 auth.* : Authelia forward-auth OK + • Si /_auth/whoami → 302 auth.* : gate OK (non-auth) + • Pour basculer blue/green : modifier 20-archicratie-backend.yml (8081 ↔ 8082) + But : une seule cible active (évite load-balance non déterministe). + diff --git a/docs/diagrams/archicratie-web-edition-git-ci-workflow-v1.svg b/docs/diagrams/archicratie-web-edition-git-ci-workflow-v1.svg new file mode 100644 index 0000000..90d696a --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-git-ci-workflow-v1.svg @@ -0,0 +1,870 @@ + + + + + + + + + + + + + + + + Archicratie — Workflow Git “pro” (main protégé) + CI + Release + Blue/Green + + Objectif : partir d’un hotfix appliqué (si besoin), le remettre proprement sous Git (branche → PR → CI → merge), + puis produire une release packagée et déployer sans régression. + + + + + + + Atelier DEV (Mac Studio) + Travail local, build, commit, push de branche + Gitea (remote) + main verrouillé · PR obligatoire · historique canon + CI (CI.yaml) + build checks · gate de merge + NAS (Prod) + release-pack + blue/green + + + 1) Se baser sur main (canon) + Synchroniser le dépôt local sur le dernier état validé. + git checkout main git pull --ff-only + INFO main est protégé : pas de commit direct. + + 2) Créer une branche dédiée “hotfix sync” + Nom explicite + date. Toute la synchro se fait ici. + git checkout -b chore/step8-sync-hotfix-YYYYMMDD + MANUEL Optionnel : appliquer un pack hotfix (tar/sha/rsync)si prod a bougé. + + 3) Appliquer les changements vérifier + Copier/merge les fichiers (rsync/checksum), puis tester build/dev. + rm -rf .astro node_modules/.vite + npm i npm run build + npm run dev + OK On ne push que si build + postbuild passent. + + 4) Commit propre + diff lisible + Inspecter, puis commiter en message clair (hotfix étape X). + git status git diff + git add -A git commit -m "step8: sync hotfix(SidePanel/reading)" + TIP Garder le commit “gros” mais unique si c’est un backport prod. + + 5) Push de branche vers Gitea + git push -u origin chore/step8-sync-hotfix-YYYYMMDD + + + 6) Ouvrir une PR vers main + main protégé → PR obligatoire. Décrire : “backport hotfix prod”. + MANUEL Ajouter contexte : fichiers touchés, risque, checks attendus. + INFO La PR déclenche CI.yaml (pipeline de validation). + + 7) Review + décisions + Lecture diff, vérif logique, pas de secrets, pas de régressions UI. + STOP Si CI rouge : corriger sur la branche, push → CI relancé. + OK Si CI vert + review OK : merge autorisé. + + 8) Merge PR → main (canon) + main devient l’unique source officielle.La prod se recale dessus. + Merge (UI Gitea) → origin/main updated + INFO Optionnel : tagger une release (vX.Y / date). + + 9) Préparer une release packagée + Générer un paquet de release reproductible(sources + scripts + config). + ./release-pack.sh + MANUEL Le pack sert au déploiement sur NAS (blue/green). + TIP Conserver checksum + manifest (traçabilité). + + + CI : checks + npm ci + astro build + postbuild scripts + pagefind + PASS → merge autorisé + FAIL → corriger branche + + Artefacts + Logs + traces + Optionnel : build artefact + INFO Sert au diagnostic rapide. + + + Déploiement + Importer release + docker build + web_blue / web_green + switch proxy + OK rollback possible + + Runbook + healthchecks + logs + validation UI + MANUEL staging d’abord + + Hotfix prod + À éviter si possible + Si nécessaire : + pack (tar+sha) + → rapatrier DEV + RISK Toujours backportervia PR. + + + Règle d’or + Le NAS n’est pas le dépôt source.Même si un hotfix a été fait en prod, + l’état final “vrai” doit être : branche→ PR → CI → merge main → release→ deploy. + BUT Passation étape 9 = base Git propre+ reproductible. + + + + + + + + + + + + Légende : INFO invariant / contexte · MANUEL action humaine · + OK attendu · STOP bloquant. + + diff --git a/docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg b/docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg new file mode 100644 index 0000000..5be4cb5 --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg @@ -0,0 +1,537 @@ + + + + + + + + + + + + + + + + + + + + + + + + Archicratie — Vue globale (v2, verbatim) + Étape 8 (hotfix UI + sync Git) — mise à jour 2026-02-20 — Astro static + Pagefind + Authelia + Blue/Green + + Atelier DEV (Mac Studio) + repo git (branches, PR vers main) + npm run dev → http://localhost:4321 + npm run build → dist/ (static) + postbuild: + inject-anchor-aliases.mjs + dedupe-ids-dist.mjs + build-para-index.mjs → dist/para-index.json + build-annotations-index.mjs → dist/annotations-index.json + pagefind → dist/pagefind/ + predev: build public/para-index.json + public/annotations-index.json + + dist/ + pagefind + indexes + + public/*-index.json + + UI Lecture / Édition (dans le site) + EditionLayout.astro (globals + meta) + SidePanel.astro (reading-follow + annotations + propose) + LevelToggle.astro (Niveaux) + global.css (UX lecture + TOC-local sync) + SidePanel consomme para-index + annotations-index + ProposeModal ouvre une issue Gitea (direct ou via bridge) + env publics: PUBLIC_GITEA_* + PUBLIC_ISSUE_BRIDGE_PATH + + Gitea (sur NAS) — source of truth + main protégé (push direct interdit) + branches de travail → PR → merge + CI (workflow) : build + checks + artefacts + issues + labels (tickets) + + PR → CI.yaml + + Hotfix de release → re-sync Git (méthode step8) + 1) lister fichiers modifiés sur NAS + 2) tar + sha256 → transfert + 3) rsync --checksum vers repo local + 4) commit sur branche dédiée + push + PR + + NAS DS220+ — runtime Blue/Green + /volume2/docker/archicratie-web/ + releases/<timestamp>/app (build context) + current → release active + docker compose: web_blue / web_green + une seule couleur sert le trafic (reverse-proxy) + + docker compose build --no-cache web_* + + Edge & Auth + Reverse-proxy (Nginx) protège le site + Authelia (SSO) + LLDAP (LDAP) + Redis + 302 vers auth.* si non authentifié + Host header: staging.* / archicratie.* + /_auth/whoami utilisé en check côté client (peut 404 en local) + + git push (branche) + PR + + CI/artefact→ déploiement release + + HTTPS → navigateur + + Propose → issue + + + tar+sha256 → scp → rsync --checksum + + Légende + Bleu = flux Git/CI · Vert = flux de re-sync hotfix · Orange = build runtime · Le reste = navigation/HTTP + diff --git a/docs/diagrams/archicratie-web-edition-global-verbatim.svg b/docs/diagrams/archicratie-web-edition-global-verbatim.svg new file mode 100644 index 0000000..a603ae9 --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-global-verbatim.svg @@ -0,0 +1,828 @@ + + + + + + + + + + + Archicratie – Web Edition : schéma global VERBATIM (Mac Studio ↔ NAS Synology DS220+) + Factuel (capturé sur ton NAS) : DSM (TLS) → Traefik :18080 (file provider) → routers Host(...) → (Authelia forward-auth) → backends (blue/green). Gitea via Traefik sans chain-auth. + + + LOCAL — Mac Studio (atelier) + + Repo site (Astro) + • build statique → dist/ + • postbuild : inject aliases + dedupe IDs + indexes + pagefind + + Tooling (scripts/) + • scripts/inject-anchor-aliases.mjs + • scripts/apply-ticket.mjs --alias + • scripts/check-anchor-aliases.mjs + verify-anchor-aliases-in-dist.mjs + + Déploiement (release pack) + • build Docker avec ARG/ENV : PUBLIC_GITEA_BASE/OWNER/REPO + • pousse/maj sur NAS (containers web_blue/web_green) + + Repères “vrais” côté site + • whoami runtime : /_auth/whoami + • variables injectées : PUBLIC_GITEA_BASE/OWNER/REPO + • anchors canon : src/anchors/anchor-aliases.json + • injection build-time : scripts/inject-anchor-aliases.mjs + + + DISTANT — NAS Synology DS220+ (DSM + Container Manager) + + + Utilisateurs + • Web (public) + • Éditeurs (groupe LDAP) + + + DSM Reverse Proxy (TLS terminé ici) + • Host archicratie.trans-hands.synology.me → 127.0.0.1:18080 + • Host gitea.archicratie.trans-hands.synology.me → 127.0.0.1:18080 + • (idem staging.*, lldap.* si routés via Traefik) + + + edge-traefik (traefik:v2.11) — network_mode: host + • entryPoint web : :18080 + • provider file : /etc/traefik/dynamic (watch: true) + • Host rules (routers) + middlewares (chain-auth / sanitize-remote) + Tes 404 initiaux venaient d’un test sans Host: les routers utilisent Host(...) + + + Fichiers dynamiques (edge) + /volume2/docker/edge/config/dynamic/ + • 10-core.yml (routers + chain-auth) + • 20-archicratie-backend.yml (slot actif) + • 21-archicratie-staging.yml(staging→8081) + • 30-lldap-ui.yml (lldap UI) + + + Auth stack (auth) + auth-authelia (authelia:4.39.13) — host + • forward-auth :http://127.0.0.1:9091/api/authz/forward-auth + auth-lldap (lldap:stable) + • LDAP : 127.0.0.1:3890 • UI : 127.0.0.1:17170 + auth-redis (redis:7-alpine) + • exposé : 127.0.0.1:6380 + Traefik injecte Remote-* via forward-auth,et purge l’entrée (sanitize-remote). + + + edge-whoami (traefik/whoami) + • exposé : 127.0.0.1:18081 → 80 + • router Traefik : PathPrefix('/_auth/whoami') + • protégé par chain-auth (302 login si non auth) + + + archicratie-web-blue + • 127.0.0.1:8081 → 80 + • Nginx sert dist/ + slot blue (staging cible 8081) + + archicratie-web-green + • 127.0.0.1:8082 → 80 + • Nginx sert dist/ + slot green (backend actuel) + + Bascule blue/green (Traefik) :modifier dynamic/20-archicratie-backend.yml→ url 8081/8082 (un seul backend actif) + Actuellement (d’après ton dump) :archicratie_web → http://127.0.0.1:8082 + + + Gitea (actuel) + • conteneur : gitea-old-2026-02-09-105211 + • port : 0.0.0.0:3000 (Traefik route aussi vers 127.0.0.1:3000) + • router Traefik : Host(gitea.archicratie...)+ middleware sanitize-remote (pas chain-auth) + “Proposer” dépend de PUBLIC_GITEA_* corrects(owner casse sensible). + + gitea-act-runner + • image : gitea/act_runner:0.2.11 + CI : labels / checks (anchors, aliases, etc.). + + + + + + + + forward-auth + + + + + + + + + + + + + + Lecture (opérationnelle) + 1) Web public : DSM → Traefik :18080 → Host(archicratie...)→ chain-auth → backend (8081/8082) + 2) Gate éditeurs : site appelle /_auth/whoami→ Traefik route vers edge-whoami (protégé) + 3) Gitea : Host(gitea...) → Traefik → 127.0.0.1:3000(sanitize-remote) + 4) Blue/green : changer dynamic/20-archicratie-backend.yml(un seul backend actif) + NB : un test sans Host sur :18080 renvoie 404 (normal, Host rules). + diff --git a/docs/diagrams/archicratie-web-edition-machine-editoriale-v2.svg b/docs/diagrams/archicratie-web-edition-machine-editoriale-v2.svg new file mode 100644 index 0000000..cb51eed --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-machine-editoriale-v2.svg @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + Archicratie — Machine éditoriale (v2) + De la source au site (lecture + annotations + propositions) — 2026-02-20 + + Sources (repo) + Contenu : src/content/** (MD/MDX) + Annotations : src/annotations/** (YAML) + UI : src/layouts + src/components + global.css + Plugin paragraph-ids ajoute des ids stables sur paragraphes + + Build (Astro static) + astro build → dist/**/index.html + meta Pagefind: edition/level/status/version + Layout : EditionLayout + SiteLayout + data-pagefind-body = zone indexée + + Postbuild (qualité + recherche + indexes) + Aliases d'ancres (backward compat) + Dédoublonnage d'IDs (anti-régression) + Index des paragraphes (para-index) + Index des annotations (annotations-index) + Pagefind (recherche full-text) + + Artefacts (dist/) + HTML statique + assets + dist/pagefind/** + dist/para-index.json + dist/annotations-index.json + (en dev) recopiés dans public/*-index.json + + Runtime navigateur (lecture) + LocalToc sync (H2/H3) + banner-follow + reading-follow__inner + SidePanel: niveaux + annotations + propose + Comportement lecture: H2/H3 unifiés (plus d’accordéon gênant) + + Flux “Proposer” (tickets) + UI collecte: page + paragraphe + type + message + Création d'issue Gitea (labels) + Lien retour: issue → page + id + Option: bridge same-origin pour éviter CORS/auth + + + + + + issues + + Conseil de maintenance + Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR. + diff --git a/docs/diagrams/archicratie-web-edition-machine-editoriale-v3.svg b/docs/diagrams/archicratie-web-edition-machine-editoriale-v3.svg new file mode 100644 index 0000000..28d2f83 --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-machine-editoriale-v3.svg @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + Archicratie — Machine éditoriale (synthèse “exploitation + onboarding”) + Inclut : DEV vs PROD (indices), ordre exact du postbuild, et fork “Proposer” (direct vs bridge same-origin). + + + + + + 1) Sources & vérité éditoriale + Ce qui est versionné (canon) et transforme l’édition + 2) Build Astro (static) + Rendu HTML + UI d’édition (EditionLayout) + 3) Postbuild (ordre exact) + Anti-régressions + index + recherche + 4) Runtime & feedback + DEV (public) / PROD (dist) + Proposer + + + Sources amont (traçabilité) + Fichiers “sources/” (docx/pdf) + historiques. + sources/** (non servi tel quel) + → import/pipeline vers le contenu canon. + + Contenu canon (site) + Pages : MD/MDX (Astro content) + src/content/** + Annotations : YAML + src/annotations/** + Ces deux entrées alimentent l’UI (SidePanel, highlights, etc.). + + Scripts d’import / qualité + Import DOCX, contrôle d’IDs, aliases, etc. + scripts/*.mjs + Objectif : build reproductible + pas de régression d’ancres. + + + EditionLayout (UI d’édition) + SiteNav + TOC global + TOC local + reading-follow+ SidePanel. + src/layouts/EditionLayout.astro + src/components/SidePanel.astro + Globals boot (flags/env)+ interactions “Propos / Réfs / Illus / Com”. + + Build statique + Astro génère HTML & assets. + npm run build → astro build + Sortie : + dist/** + + + Postbuild : ordre exact (fixe) + 1) inject-anchor-aliases.mjs + 2) dedupe-ids-dist.mjs + 3) build-para-index.mjs + 4) build-annotations-index.mjs + 5) pagefind + Sorties PROD : + dist/para-index.json + dist/annotations-index.json + dist/pagefind/** + + + DEV vs PROD : indices + PROD (statique) lit dans : + dist/*.json + DEV (astro dev) sert depuis : + public/*.json + DEV : predev copie/génèreles index pour éviter les 404. + + “Proposer” : 2 modes + A) Direct client → Gitea API + ⚠️ CORS/auth/token (déconseillé) + B) Bridge same-origin (reco) + PUBLIC_ISSUE_BRIDGE_PATH + UI → bridge → Gitea(secrets côté serveur) + + Gitea (Issues) + Création + suivi + /issues/new + Labels/assignee selon règles d’équipe. + + + Rappel “pro” (anti régression) + • L’ordre du postbuild ne doit pas changer sans raison : il garantit ancres stables + index cohérents. + • DEV sert des index dans public/ ; PROD lit dans dist/. + • Pour “Proposer”, préférer le bridge same-origin : pas de token côté navigateur. + + + + + + + + Astuce : si Inkscape affichait “noir”, c’était très souvent des CSS variables. Ici : couleurs explicites + fond explicite. + diff --git a/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim-v2.svg b/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim-v2.svg new file mode 100644 index 0000000..84494ec --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim-v2.svg @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + Archicratie — Machine éditoriale (v2, verbatim) + Détails scripts/fichiers — 2026-02-20 + + Sources (repo) + Contenu : src/content/** (MD/MDX) + Annotations : src/annotations/** (YAML) + UI : src/layouts + src/components + global.css + Plugin paragraph-ids ajoute des ids stables sur paragraphes + + rehype-paragraph-ids.js + + Build (Astro static) + astro build → dist/**/index.html + meta Pagefind: edition/level/status/version + Layout : EditionLayout + SiteLayout + data-pagefind-body = zone indexée + + npm run build + + Postbuild (qualité + recherche + indexes) + Aliases d'ancres (backward compat) + Dédoublonnage d'IDs (anti-régression) + Index des paragraphes (para-index) + Index des annotations (annotations-index) + Pagefind (recherche full-text) + + inject-anchor-aliases.mjs + + dedupe-ids-dist.mjs + + build-para-index.mjs + + build-annotations-index.mjs + + Artefacts (dist/) + HTML statique + assets + dist/pagefind/** + dist/para-index.json + dist/annotations-index.json + (en dev) recopiés dans public/*-index.json + + Runtime navigateur (lecture) + LocalToc sync (H2/H3) + banner-follow + reading-follow__inner + SidePanel: niveaux + annotations + propose + Comportement lecture: H2/H3 unifiés (plus d’accordéon gênant) + + SidePanel.astro + + LevelToggle.astro + + Flux “Proposer” (tickets) + UI collecte: page + paragraphe + type + message + Création d'issue Gitea (labels) + Lien retour: issue → page + id + Option: bridge same-origin pour éviter CORS/auth + + PUBLIC_GITEA_* + + PUBLIC_ISSUE_BRIDGE_PATH + + + + + + issues + + Conseil de maintenance + Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR. + diff --git a/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim-v3.svg b/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim-v3.svg new file mode 100644 index 0000000..00e51d9 --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim-v3.svg @@ -0,0 +1,613 @@ + + + + + + + + + + + + + + Archicratie — Machine éditoriale (verbatim technique) + 3 ajouts “pro” inclus : (1) indices DEV vs PROD, (2) fork Proposer direct vs bridge, (3) ordre postbuild exact. + + + + + A) Entrées & canon + Ce qui est versionné et alimente la build + B) Build + postbuild + Astro (static) + scripts (ordre fixe) + C) Runtime (DEV/PROD) + “Proposer” + Indices servis, UI, et création d’issues + + + Contenu canon (pages) + Astro Content : MD / MDX (pages, chapitres, etc.) + src/content/** + Layouts/TOC/reading-follow consomment ces pages. + Les IDs de paragraphes doivent rester stables (anti-régression). + + Annotations (surcouche) + YAML : notes, refs, illus, commentaires par paragraphe. + src/annotations/** + Indexé en JSON pour le SidePanel : + dist/annotations-index.json (PROD) + public/annotations-index.json (DEV) + + Scripts (import & qualité) + Import DOCX, checks d’IDs, aliases d’ancres, etc. + scripts/import-docx.mjs + scripts/check-anchors.mjs + Objectif : build reproductible + compat backward (ancres). + + + Build Astro (static) + Génère le HTML + assets + routes (output: static). + npm run build + → astro build → dist/** + UI d’édition (côté pages) : + src/layouts/EditionLayout.astro + src/components/SidePanel.astro + + Postbuild (ordre exact = contrat) + À conserver tel quel pour éviter les régressions. + 1) node scripts/inject-anchor-aliases.mjs + 2) node scripts/dedupe-ids-dist.mjs + 3) node scripts/build-para-index.mjs--in dist --out dist/para-index.json + 4) node scripts/build-annotations-index.mjs--in src/annotations --out dist/annotations-index.json + 5) npx pagefind --site dist (→ dist/pagefind/**) + Sorties PROD (statique) : + dist/para-index.json + dist/annotations-index.json + dist/pagefind/** + Mini-règle : inject (aliases) AVANT dedupe, et indices AVANT pagefind. + + DEV server : indices “public/” (mini-ajout #1) + En DEV, l’UI lit via HTTP depuis public/ (pas dist/). + predev: build-annotations-index → public/annotations-index.json + predev: build-para-index (depuis dist) → public/para-index.json + Si ces fichiers manquent : 404 (normal) → relancer npm run dev (predev). + + + Runtime PROD + Site statique servi depuis dist/** + dist/*.html + dist/para-index.json + dist/annotations-index.json + dist/pagefind/** + Ces artefacts sont reproductibles via CI. + + “Proposer” (mini-ajout #2) + Depuis SidePanel / ProposeModal (UI). + Mode A — Direct navigateur → Gitea API + ⚠️ CORS + auth + token : fragile / déconseillé. + Mode B — Bridge same-origin (recommandé) + PUBLIC_ISSUE_BRIDGE_PATH (ex: /bridge/issues) + Le serveur/proxy ajoute l’auth (secrets) et appelle Gitea côté backend. + Résultat : pas de secret exposé au client + moins de soucis CORS. + + Gitea : Issues + Création de tickets (PR/CI séparés du flux éditorial) + /issues/new + labels / assignee / templates + Le workflow Git/PR/CI reste la source canon (pas la prod). + + + Mini-ajout #3 — ordre postbuild + inject-anchor-aliases → dedupe-ids → para-index→ annotations-index → pagefind + C’est le “contrat” anti-régression : si tu changes l’ordre, tu re-tests tout. + + + + + + + Inkscape-safe : couleurs & fond explicites (zéro var()). + diff --git a/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim.svg b/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim.svg new file mode 100644 index 0000000..3952e9f --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-machine-editoriale-verbatim.svg @@ -0,0 +1,634 @@ + + + + + + + + + + + Archicratie – Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket) + Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH="/_auth/whoami", GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias. + + + A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea + + + Utilisateur (lecteur/éditeur) + • lit une page + • sur un paragraphe : Citer / Proposer / Marque-page + • Proposer visible uniquement si “editors” + (le gate est runtime via whoami) + + + Site Astro statique — EditionLayout + src/layouts/EditionLayout.astro + Variables publiques injectées + PUBLIC_GITEA_BASE, PUBLIC_GITEA_OWNER, PUBLIC_GITEA_REPO + • si une manque : giteaReady=false → Proposer désactivé + Outils paragraphe + • Citer : copie une citation structurée (titre + URL#ancre) + • Proposer : modal 2 étapes → ouvre /issues/new?... + Gate “editors” (whoami) + WHOAMI_PATH="/_auth/whoami" + fetch same-origin + • lit header Remote-Groups → affiche/retire Proposer du DOM + + + Traefik edge + • Host(archicratie…) + • middleware : chain-auth + • route /_auth/whoami + forward-auth vers Authelia + + Authelia + LLDAP + • forward-auth : :9091 + • groupes via LDAP + • injecte headers Remote-* + non auth ⇒ 302 vers auth.* + + + Gitea (UI) + Issue préremplie : + BASE/OWNER/REPO/issues/new + Contenu typique : + • URL page + #p-… + • Type / State / Category + • proposition / commentaire + Résultat : + • issue = backlog éditorial + • labels (CI/bot) pour tri + + + + + + + Contrat runtime (robuste) + • Citer marche sans droits (copie + lien) + • Proposer n’existe pas si non “editors” + • whoami renvoie 302 login si non auth + • si PUBLIC_GITEA_* faux → 404/login loop + + + B — Automatisation : issue → labels + checks qualité + + Gitea Actions (workflows) + • triggers : issues opened / edited (labels) + • checks build : anchors / aliases / inline-js / dist audit + token API requis côté job (ex : FORGE_TOKEN) pour écrire labels + + Runner + • conteneur : gitea-act-runner (gitea/act_runner) + • exécute jobs (souvent en conteneur) + • appelle API Gitea pour labels + + Gitea (API) + labels + • labels = tri natif (type/state/cat) + • backlog propre, opérable + si 401 : token manquant/mauvais droits + + + + + C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time) + + Apply-ticket + scripts/apply-ticket.mjs <issue_number> --alias + • applique patch dans src/content/… + • écrit alias old→new dans src/anchors/anchor-aliases.json + + Aliases canon + injection + src/anchors/anchor-aliases.json + postbuild : node scripts/inject-anchor-aliases.mjs + • injecte <span id="oldId" class="para-alias"> avant newId dans dist/**/index.html + + Preuves (tests) + scripts/check-anchor-aliases.mjs + scripts/verify-anchor-aliases-in-dist.mjs + scripts/check-anchors.mjs + + + + + diff --git a/docs/diagrams/archicratie-web-edition-machine-editoriale.svg b/docs/diagrams/archicratie-web-edition-machine-editoriale.svg new file mode 100644 index 0000000..845c930 --- /dev/null +++ b/docs/diagrams/archicratie-web-edition-machine-editoriale.svg @@ -0,0 +1,631 @@ + + + + + + + + + + + Archicratie – Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket) + Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH="/_auth/whoami", GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias. + + + A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea + + + Utilisateur (lecteur/éditeur) + • lit une page + • sur un paragraphe : ¶ / Citer / Proposer + • Proposer visible uniquement si “editors” + (le gate est runtime via whoami) + + + Site Astro statique — EditionLayout + src/layouts/EditionLayout.astro + Variables publiques injectées + PUBLIC_GITEA_BASE, PUBLIC_GITEA_OWNER, PUBLIC_GITEA_REPO + • si une manque : giteaReady=false → Proposer désactivé + Outils paragraphe + • ¶ : lien d’ancre vers #p-… + • Citer : copie une citation structurée (titre + URL#ancre) + • Proposer : modal 2 étapes → ouvre /issues/new?... + Gate “editors” (whoami) + WHOAMI_PATH="/_auth/whoami" + fetch same-origin + • lit header Remote-Groups → affiche/retire Proposer du DOM + + + Traefik edge + • Host(archicratie…) + • middleware : chain-auth + • route /_auth/whoami + forward-auth vers Authelia + + Authelia + LLDAP + • forward-auth : :9091 + • groupes via LDAP + • injecte headers Remote-* + non auth ⇒ 302 vers auth.* + + + Gitea (UI) + Issue préremplie : + BASE/OWNER/REPO/issues/new + Contenu typique : + • URL page + #p-… + • Type / State / Category + • proposition / commentaire + Résultat : + • issue = backlog éditorial + • labels (CI/bot) pour tri + + + + + + + Contrat runtime (robuste) + • Citer marche sans droits (copie + lien) + • Proposer n’existe pas si non “editors” + • whoami renvoie 302 login si non auth + • si PUBLIC_GITEA_* faux → 404/login loop + + + B — Automatisation : issue → labels + checks qualité + + Gitea Actions (workflows) + • triggers : issues opened / edited (labels) + • checks build : anchors / aliases / inline-js / dist audit + token API requis côté job (ex : FORGE_TOKEN) pour écrire labels + + Runner + • conteneur : gitea-act-runner (gitea/act_runner) + • exécute jobs (souvent en conteneur) + • appelle API Gitea pour labels + + Gitea (API) + labels + • labels = tri natif (type/state/cat) + • backlog propre, opérable + si 401 : token manquant/mauvais droits + + + + + C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time) + + Apply-ticket + scripts/apply-ticket.mjs <issue_number> --alias + • applique patch dans src/content/… + • écrit alias old→new dans src/anchors/anchor-aliases.json + + Aliases canon + injection + src/anchors/anchor-aliases.json + postbuild : node scripts/inject-anchor-aliases.mjs + • injecte<span id="oldId" class="para-alias"> avant newId dans dist/**/index.html + + Preuves (tests) + scripts/check-anchor-aliases.mjs + scripts/verify-anchor-aliases-in-dist.mjs + scripts/check-anchors.mjs + + + + + diff --git a/docs/diagrams/diagram.svg b/docs/diagrams/diagram.svg new file mode 100644 index 0000000..a91c386 --- /dev/null +++ b/docs/diagrams/diagram.svg @@ -0,0 +1,618 @@ + + + + + + + + + + + Archicratie – Web Edition : schéma global (Local Mac Studio vs NAS Synology DS220+) + Lecture : (1) utilisateur → DSM → Traefik/Authelia → site ; (2) dev → package → slot green/blue → switch Traefik (provider file) ; (3) proposer → issues → runner → labels. + + + LOCAL — Mac Studio (atelier de dev) + + Repo Astro (édition) + • src/content/ (contenu web) + • scripts tooling (anchors, import docx, apply-ticket) + + Build statique + npm run build → dist/ (site statique) + + Release pack + archive .tar.gz + .sha256 + destination NAS : /volume2/docker/archicratie-web/incoming/ + + Centre de vérité + Tu commits/push vers Gitea (NAS) : + • code + docs + diagrammes (SVG) + • issues = backlog éditorial + + + DISTANT — NAS Synology DS220+ (DSM + Container Manager) + + + Utilisateurs (web) + • visiteurs + • éditeurs (accès protégé) + + + DSM Reverse Proxy (HTTPS public → Traefik) + • pointe vers 127.0.0.1:18080 (Traefik edge) + • bascule/rollback = switch Traefik (reload) ; DSM reste stable + + + Traefik (edge) — écoute 127.0.0.1:18080 + • entrée unique derrière DSM + • middlewares : sanitize-remote + forward-auth Authelia + • un seul backend site actif (blue OU green) via 20-archicratie-backend.yml + + + Auth stack + Authelia + • forward-auth : /api/authz/forward-auth + LLDAP + • annuaire LDAP “source of truth” + Redis + • sessions / cache (selon config) + Objectif : SSO/MFA + anti lock-outdéploiement progressif) + + + web_blue (slot A) + 127.0.0.1:8081 → container:80 + sert dist/ (Nginx/HTTP) + ne jamais modifier si LIVE + + web_green (slot B) + 127.0.0.1:8082 → container:80 + sert dist/ (Nginx/HTTP) + slot “next” (staging) + + + switch-archicratie.sh : bascule blue/green en réécrivant 20-archicratie-backend.ymlpuis reload Traefik (provider file) + + + Gitea (forge web + API) + • repo = centre de vérité + • issues = backlog + • labels = tri natif (type/state/cat) + Note : parfois laissé sans forward-auth (runner/accès API) selon réglage + + + Gitea Actions Runner (act_runner) + • exécute les workflows + • doit monter /var/run/docker.sock + • jobs en conteneur (ex : python:3.12-slim) + • applique labels via API avec PAT (FORGE_TOKEN)— sinon 401 + + + + + + + + + + + + + + + + + + + + + Légende / invariants (ce qui casse “pour de vrai”) + • Prod safe : on ne touche jamais au slot LIVE ;build/test sur l’autre ; switch Traefik ; DSM ne change pas. + • Runner : sans docker.sock → aucun job ;sans FORGE_TOKEN → 401 (labels). + • Edge : Traefik :18080 derrière DSM ; sanitize-remote+ forward-auth Authelia. + • Blue/green : 8081/8082 ; Traefik décide le LIVE(DSM pointe toujours sur :18080). + Astuce : exporte en PNG/PDF pour lecture “grand public”,garde SVG comme source éditable. + diff --git a/scripts/build-para-index.mjs b/scripts/build-para-index.mjs new file mode 100644 index 0000000..08dfa5f --- /dev/null +++ b/scripts/build-para-index.mjs @@ -0,0 +1,148 @@ +// scripts/build-para-index.mjs +import fs from "node:fs/promises"; +import path from "node:path"; + +function parseArgs(argv) { + const out = { inDir: "dist", outFile: "dist/para-index.json" }; + + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + + if (a === "--in" && argv[i + 1]) { + out.inDir = argv[++i]; + continue; + } + if (a.startsWith("--in=")) { + out.inDir = a.slice("--in=".length); + continue; + } + + if (a === "--out" && argv[i + 1]) { + out.outFile = argv[++i]; + continue; + } + if (a.startsWith("--out=")) { + out.outFile = a.slice("--out=".length); + continue; + } + } + + return out; +} + +async function exists(p) { + try { + await fs.access(p); + return true; + } catch { + return false; + } +} + +async function walk(dir) { + const out = []; + const ents = await fs.readdir(dir, { withFileTypes: true }); + for (const e of ents) { + const p = path.join(dir, e.name); + if (e.isDirectory()) out.push(...(await walk(p))); + else out.push(p); + } + return out; +} + +function stripTags(html) { + return String(html || "") + .replace(//gi, " ") + .replace(//gi, " ") + .replace(/<[^>]+>/g, " "); +} + +function decodeEntities(s) { + // minimal, volontairement (évite dépendances) + return String(s || "") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'"); +} + +function normalizeSpaces(s) { + return decodeEntities(s).replace(/\s+/g, " ").trim(); +} + +function relPageFromIndexHtml(inDirAbs, fileAbs) { + const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/"); + if (!/index\.html$/i.test(rel)) return null; + + // dist//index.html -> "//" + const page = "/" + rel.replace(/index\.html$/i, ""); + return page; +} + +async function main() { + const { inDir, outFile } = parseArgs(process.argv.slice(2)); + const CWD = process.cwd(); + + const inDirAbs = path.isAbsolute(inDir) ? inDir : path.join(CWD, inDir); + const outAbs = path.isAbsolute(outFile) ? outFile : path.join(CWD, outFile); + + // ✅ antifragile: si dist/ (ou inDir) absent -> on SKIP proprement + if (!(await exists(inDirAbs))) { + console.log(`ℹ️ para-index: skip (input missing): ${inDir}`); + process.exit(0); + } + + const files = (await walk(inDirAbs)).filter((p) => /index\.html$/i.test(p)); + + if (!files.length) { + console.log(`ℹ️ para-index: skip (no index.html found in): ${inDir}`); + process.exit(0); + } + + const items = []; + const byId = Object.create(null); + + //

...

+ // (regex volontairement stricte sur l'id pour éviter faux positifs) + const reP = /]*\bid\s*=\s*["'](p-\d+-[^"']+)["'][^>]*)>([\s\S]*?)<\/p>/gi; + + for (const f of files) { + const page = relPageFromIndexHtml(inDirAbs, f); + if (!page) continue; + + const html = await fs.readFile(f, "utf8"); + + let m; + while ((m = reP.exec(html))) { + const id = m[2]; + const inner = m[3]; + + if (byId[id] != null) continue; // protège si jamais doublons + + const text = normalizeSpaces(stripTags(inner)); + if (!text) continue; + + byId[id] = items.length; + items.push({ id, page, text }); + } + } + + const out = { + schema: 1, + generatedAt: new Date().toISOString(), + items, + byId, + }; + + await fs.mkdir(path.dirname(outAbs), { recursive: true }); + await fs.writeFile(outAbs, JSON.stringify(out), "utf8"); + + console.log(`✅ para-index: items=${items.length} -> ${path.relative(CWD, outAbs)}`); +} + +main().catch((e) => { + console.error("FAIL: build-para-index crashed:", e); + process.exit(1); +}); diff --git a/scripts/check-annotations.mjs b/scripts/check-annotations.mjs new file mode 100644 index 0000000..6dd0718 --- /dev/null +++ b/scripts/check-annotations.mjs @@ -0,0 +1,173 @@ +// scripts/check-annotations.mjs +import fs from "node:fs/promises"; +import path from "node:path"; +import YAML from "yaml"; + +const CWD = process.cwd(); +const ANNO_DIR = path.join(CWD, "src", "annotations"); +const DIST_DIR = path.join(CWD, "dist"); +const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json"); + +async function exists(p) { + try { await fs.access(p); return true; } catch { return false; } +} + +async function walk(dir) { + const out = []; + const ents = await fs.readdir(dir, { withFileTypes: true }); + for (const e of ents) { + const p = path.join(dir, e.name); + if (e.isDirectory()) out.push(...(await walk(p))); + else out.push(p); + } + return out; +} + +function escRe(s) { + return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function inferPageKeyFromFile(fileAbs) { + const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/"); + return rel.replace(/\.(ya?ml|json)$/i, ""); +} + +function normalizePageKey(s) { + return String(s || "").replace(/^\/+/, "").replace(/\/+$/, ""); +} + +function isPlainObject(x) { + return !!x && typeof x === "object" && !Array.isArray(x); +} + +async function loadAliases() { + if (!(await exists(ALIASES_PATH))) return {}; + try { + const raw = await fs.readFile(ALIASES_PATH, "utf8"); + const json = JSON.parse(raw); + return isPlainObject(json) ? json : {}; + } catch { + return {}; + } +} + +function parseDoc(raw, fileAbs) { + if (/\.json$/i.test(fileAbs)) return JSON.parse(raw); + return YAML.parse(raw); +} + +function getAlias(aliases, pageKey, oldId) { + // supporte: + // 1) { "": { "": "" } } + // 2) { "": "" } + const a1 = aliases?.[pageKey]?.[oldId]; + if (a1) return a1; + const a2 = aliases?.[oldId]; + if (a2) return a2; + return ""; +} + +async function main() { + if (!(await exists(ANNO_DIR))) { + console.log("✅ annotations: aucun dossier src/annotations — rien à vérifier."); + process.exit(0); + } + + if (!(await exists(DIST_DIR))) { + console.error("FAIL: dist/ absent. Lance d’abord `npm run build` (ou `npm test`)."); + process.exit(1); + } + + const aliases = await loadAliases(); + const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p)); + + let pages = 0; + let checked = 0; + let failures = 0; + const notes = []; + + for (const f of files) { + const rel = path.relative(CWD, f).replace(/\\/g, "/"); + const raw = await fs.readFile(f, "utf8"); + + let doc; + try { + doc = parseDoc(raw, f); + } catch (e) { + failures++; + notes.push(`- PARSE FAIL: ${rel} (${String(e?.message ?? e)})`); + continue; + } + + if (!isPlainObject(doc) || doc.schema !== 1) { + failures++; + notes.push(`- INVALID: ${rel} (schema must be 1)`); + continue; + } + + const pageKey = normalizePageKey(inferPageKeyFromFile(f)); + + if (doc.page != null && normalizePageKey(doc.page) !== pageKey) { + failures++; + notes.push(`- PAGE MISMATCH: ${rel} (page="${doc.page}" != path="${pageKey}")`); + continue; + } + + if (!isPlainObject(doc.paras)) { + failures++; + notes.push(`- INVALID: ${rel} (missing object key "paras")`); + continue; + } + + const distFile = path.join(DIST_DIR, pageKey, "index.html"); + if (!(await exists(distFile))) { + failures++; + notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`); + continue; + } + + pages++; + const html = await fs.readFile(distFile, "utf8"); + + for (const paraId of Object.keys(doc.paras)) { + checked++; + + if (!/^p-\d+-/i.test(paraId)) { + failures++; + notes.push(`- INVALID ID: ${rel} (${paraId})`); + continue; + } + + const re = new RegExp(`\\bid=["']${escRe(paraId)}["']`, "g"); + if (re.test(html)) continue; + + const alias = getAlias(aliases, pageKey, paraId); + if (alias) { + const re2 = new RegExp(`\\bid=["']${escRe(alias)}["']`, "g"); + if (re2.test(html)) { + notes.push(`- WARN alias used: ${pageKey} ${paraId} -> ${alias}`); + continue; + } + } + + failures++; + notes.push(`- MISSING ID: ${pageKey} (#${paraId})`); + } + } + + const warns = notes.filter((x) => x.startsWith("- WARN")); + + if (failures > 0) { + console.error(`FAIL: annotations invalid (pages=${pages} checked=${checked} failures=${failures})`); + for (const n of notes) console.error(n); + process.exit(1); + } + + for (const w of warns) console.log(w); + console.log(`✅ annotations OK: pages=${pages} checked=${checked} warnings=${warns.length}`); +} + +main().catch((e) => { + console.error("FAIL: annotations check crashed:", e); + process.exit(1); +}); diff --git a/scripts/seed-gitea-labels.mjs b/scripts/seed-gitea-labels.mjs new file mode 100644 index 0000000..54bddb1 --- /dev/null +++ b/scripts/seed-gitea-labels.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/** + * seed-gitea-labels — crée les labels attendus (idempotent) + * + * Usage: + * FORGE_TOKEN=... FORGE_API=http://192.168.1.20:3000 node scripts/seed-gitea-labels.mjs + * (ou FORGE_BASE=https://gitea... si pas de FORGE_API) + * + * Optionnel: + * GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote origin) + */ + +import { spawnSync } from "node:child_process"; + +function getEnv(name, fallback = "") { + return (process.env[name] ?? fallback).trim(); +} + +function inferOwnerRepoFromGit() { + const r = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }); + if (r.status !== 0) return null; + const u = (r.stdout || "").trim(); + const m = u.match(/[:/](?[^/]+)\/(?[^/]+?)(?:\.git)?$/); + if (!m?.groups) return null; + return { owner: m.groups.owner, repo: m.groups.repo }; +} + +async function apiReq(base, token, method, path, payload = null) { + const url = `${base.replace(/\/+$/, "")}/api/v1${path}`; + const headers = { + Authorization: `token ${token}`, + Accept: "application/json", + "User-Agent": "archicratie-seed-labels/1.0", + }; + const init = { method, headers }; + + if (payload != null) { + init.headers["Content-Type"] = "application/json"; + init.body = JSON.stringify(payload); + } + + const res = await fetch(url, init); + const text = await res.text().catch(() => ""); + let json = null; + try { json = text ? JSON.parse(text) : null; } catch {} + if (!res.ok) throw new Error(`HTTP ${res.status} ${method} ${url}\n${text}`); + return json; +} + +async function main() { + const token = getEnv("FORGE_TOKEN"); + if (!token) throw new Error("FORGE_TOKEN manquant"); + + const inferred = inferOwnerRepoFromGit() || {}; + const owner = getEnv("GITEA_OWNER", inferred.owner || ""); + const repo = getEnv("GITEA_REPO", inferred.repo || ""); + if (!owner || !repo) throw new Error("Impossible de déterminer owner/repo (GITEA_OWNER/GITEA_REPO ou git remote)"); + + const base = getEnv("FORGE_API") || getEnv("FORGE_BASE"); + if (!base) throw new Error("FORGE_API ou FORGE_BASE manquant"); + + const wanted = [ + // type/* + { name: "type/comment", color: "1d76db", description: "Commentaire éditorial (site)" }, + { name: "type/media", color: "1d76db", description: "Media à intégrer (image/audio/video)" }, + { name: "type/correction", color: "1d76db", description: "Correction proposée" }, + { name: "type/fact-check", color: "1d76db", description: "Vérification / sourçage" }, + + // state/* + { name: "state/a-trier", color: "0e8a16", description: "À trier" }, + { name: "state/recevable", color: "0e8a16", description: "Recevable" }, + { name: "state/a-sourcer", color: "0e8a16", description: "À sourcer" }, + + // scope/* + { name: "scope/readers", color: "5319e7", description: "Signalé par lecteur" }, + { name: "scope/editors", color: "5319e7", description: "Signalé par éditeur" }, + ]; + + const labels = (await apiReq(base, token, "GET", `/repos/${owner}/${repo}/labels?limit=1000`)) || []; + const existing = new Set(labels.map((x) => x?.name).filter(Boolean)); + + let created = 0; + for (const L of wanted) { + if (existing.has(L.name)) continue; + await apiReq(base, token, "POST", `/repos/${owner}/${repo}/labels`, { + name: L.name, + color: L.color, + description: L.description, + }); + created++; + console.log("✅ created:", L.name); + } + + if (created === 0) console.log("ℹ️ seed: nothing to do (all labels already exist)"); + else console.log(`✅ seed done: created=${created}`); +} + +main().catch((e) => { + console.error("💥 seed-gitea-labels:", e?.message || e); + process.exit(1); +}); diff --git a/src/pages/archicrat-ia/[...slug].astro b/src/pages/archicrat-ia/[...slug].astro new file mode 100644 index 0000000..e6bd039 --- /dev/null +++ b/src/pages/archicrat-ia/[...slug].astro @@ -0,0 +1,35 @@ +--- +import EditionLayout from "../../layouts/EditionLayout.astro"; +import { getCollection } from "astro:content"; +import EditionToc from "../../components/EditionToc.astro"; +import LocalToc from "../../components/LocalToc.astro"; + +export async function getStaticPaths() { + const entries = await getCollection("archicratie"); + return entries.map((entry) => ({ + params: { slug: entry.slug }, + props: { entry }, + })); +} + +const { entry } = Astro.props; +const { Content, headings } = await entry.render(); +--- + + + + + + + +

{entry.data.title}

+ +