Compare commits

...

32 Commits

Author SHA1 Message Date
e2468be522 fix(etape8): resync hotfix edition depuis NAS (2026-02-19)
All checks were successful
CI / build-and-anchors (push) Successful in 1m50s
SMOKE / smoke (push) Successful in 18s
2026-02-19 23:00:58 +01:00
dc2826df08 Merge pull request 'build: fix astro mdx/rehype config + dedupe duplicate ids in dist' (#82) from chore/fix-astro-ts-mdx-types into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m6s
SMOKE / smoke (push) Successful in 23s
Reviewed-on: #82
2026-02-15 15:19:57 +01:00
3e4df18b88 build: fix astro mdx/rehype config + dedupe duplicate ids in dist
All checks were successful
CI / build-and-anchors (push) Successful in 2m25s
SMOKE / smoke (push) Successful in 23s
2026-02-15 15:19:05 +01:00
a2d1df427d Merge pull request 'docs: RUNBOOK-PR-AUTO-BITEA' (#81) from docs/RUNBOOK-PR-AUTO-GITEA into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m30s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #81
2026-02-13 20:11:46 +01:00
ab63511d81 docs: RUNBOOK-PR-AUTO-BITEA
All checks were successful
CI / build-and-anchors (push) Successful in 1m39s
SMOKE / smoke (push) Successful in 16s
2026-02-13 20:10:05 +01:00
e5d831cb61 Merge pull request 'docs: add auth stack + main protected PR workflow' (#80) from docs/runbooks-sync-2026-02-13-FIX into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m37s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #80
2026-02-13 19:31:36 +01:00
cab9e9cf2d docs: add auth stack + main protected PR workflow
All checks were successful
CI / build-and-anchors (push) Successful in 1m27s
SMOKE / smoke (push) Successful in 16s
2026-02-13 19:17:35 +01:00
c7704ada8a Merge pull request 'docs: add runbooks (proposer/whoami gate, blue-green deploy, gitea PR workflow)' (#79) from docs/runbooks-sync-2026-02-13-FIX into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m28s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #79
2026-02-13 19:06:14 +01:00
010601be63 docs: add runbooks (proposer/whoami gate, blue-green deploy, gitea PR workflow)
All checks were successful
CI / build-and-anchors (push) Successful in 1m30s
SMOKE / smoke (push) Successful in 15s
2026-02-13 17:33:33 +01:00
add688602a Merge pull request 'Fix: Proposer gate non-destructive + show/hide consistent' (#75) from fix/proposer-non-destructif into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m29s
SMOKE / smoke (push) Successful in 14s
Reviewed-on: #75
2026-02-12 15:37:28 +01:00
3f3c717185 Fix: Proposer gate non-destructive + show/hide consistent
All checks were successful
CI / build-and-anchors (push) Successful in 1m39s
SMOKE / smoke (push) Successful in 11s
2026-02-12 15:28:58 +01:00
b5f32da0c8 Merge pull request 'Gate 'Proposer' to editors via /_auth/whoami' (#74) from feat/proposer-editors-only into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m29s
SMOKE / smoke (push) Successful in 14s
Reviewed-on: #74
2026-02-12 09:47:03 +01:00
6e7ed8e041 Gate 'Proposer' to editors via /_auth/whoami
All checks were successful
CI / build-and-anchors (push) Successful in 1m57s
SMOKE / smoke (push) Successful in 13s
2026-02-12 09:46:30 +01:00
90f79a7ee7 Merge pull request 'Supprimer docs/SESSION_BILAN_CI_RUNNER_DNS_2026-01.md' (#71) from archicratia-patch-1 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m36s
SMOKE / smoke (push) Successful in 24s
Reviewed-on: #71
2026-02-02 12:13:12 +01:00
b5663891a1 Supprimer docs/SESSION_BILAN_CI_RUNNER_DNS_2026-01.md
All checks were successful
CI / build-and-anchors (push) Successful in 1m56s
SMOKE / smoke (push) Successful in 14s
2026-02-02 12:12:53 +01:00
a74b95e775 Merge pull request 'docs: normalisation md + diagnostics dedup + LEGACY strict' (#70) from docs/normalisation2-md into main
Some checks failed
CI / build-and-anchors (push) Has been cancelled
SMOKE / smoke (push) Has been cancelled
Reviewed-on: #70
2026-02-02 12:10:03 +01:00
b78eb4fc7b docs: normalisation md + diagnostics dedup + LEGACY strict
All checks were successful
CI / build-and-anchors (push) Successful in 1m52s
SMOKE / smoke (push) Successful in 11s
2026-02-02 12:08:53 +01:00
80c047369f Merge pull request 'docs(ops): clarify canonical deploy doc + mark runbook/legacy' (#69) from docs/ops-sync-20260201 into main
Some checks failed
SMOKE / smoke (push) Successful in 33s
CI / build-and-anchors (push) Failing after 13m44s
Reviewed-on: #69
2026-02-01 17:33:33 +01:00
d7c158a0fc docs(ops): clarify canonical deploy doc + mark runbook/legacy
All checks were successful
CI / build-and-anchors (push) Successful in 1m39s
SMOKE / smoke (push) Successful in 12s
2026-02-01 17:32:50 +01:00
30f0ef4164 Merge pull request 'chore(security): stop tracking .env files (keep .env.example)' (#68) from docs/ops-sync-20260201-165524 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m24s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #68
2026-02-01 17:06:40 +01:00
d2963673c9 chore(security): stop tracking .env files (keep .env.example)
All checks were successful
CI / build-and-anchors (push) Successful in 1m41s
SMOKE / smoke (push) Successful in 21s
2026-02-01 17:05:31 +01:00
d59e10dfc6 Merge pull request 'docs(ops): add triple-source sync + troubleshooting + proposer spec' (#67) from docs/ops-missing2-20260201 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m28s
SMOKE / smoke (push) Successful in 21s
Reviewed-on: #67
2026-02-01 14:47:48 +01:00
214f930e56 docs(ops): add triple-source sync + troubleshooting + proposer spec
All checks were successful
CI / build-and-anchors (push) Successful in 1m37s
SMOKE / smoke (push) Successful in 12s
2026-02-01 14:47:22 +01:00
9e903607bb Merge pull request 'docs(ops): add triple-source sync + troubleshooting + proposer spec' (#66) from docs/ops-missing-20260201 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m35s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #66
2026-02-01 14:30:45 +01:00
8b7cfdfd48 docs(ops): add triple-source sync + troubleshooting + proposer spec
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 17s
2026-02-01 14:30:18 +01:00
c12b6015ab Merge pull request 'docs: unwrap markdown blocks (render as real docs)' (#65) from docs/unwrap-20260201 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m26s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #65
2026-02-01 13:46:49 +01:00
f7f6b8f770 docs: unwrap markdown blocks (render as real docs)
All checks were successful
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 12s
2026-02-01 13:45:43 +01:00
ae8ec42349 Merge pull request 'docs/ops-reference-20260201' (#64) from docs/ops-reference-20260201 into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m25s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #64
2026-02-01 13:36:44 +01:00
2f296f4fe4 docs(runbook): add archicratie-web operations runbook
All checks were successful
CI / build-and-anchors (push) Successful in 2m26s
SMOKE / smoke (push) Successful in 17s
2026-02-01 13:31:57 +01:00
ccaa8029fb docs(ops): add reference + env config + git workflow 2026-02-01 13:31:43 +01:00
770dad30fb docs(deploy): update DS220+ production guide (2026-02-01) 2026-02-01 13:31:27 +01:00
0d79d1aa78 chore(security): ignore env files, backups and macOS artifacts 2026-02-01 13:31:10 +01:00
31 changed files with 4253 additions and 505 deletions

3
.env
View File

@@ -1,3 +0,0 @@
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition

View File

@@ -1,4 +0,0 @@
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition
PUBLIC_SITE=https://archicratie.trans-hands.synology.me

View File

@@ -1,5 +0,0 @@
FORGE_API=http://192.168.1.20:3000
FORGE_BASE=https://gitea.archicratie.trans-hands.synology.me
FORGE_TOKEN=aW73wpfJ4MiN2!3UU69qL*vWF9$9V7f@2
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition

View File

@@ -1,6 +0,0 @@
PUBLIC_SITE=https://archicratie.trans-hands.synology.me
PUBLIC_RELEASE=0.1.0
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition

13
.gitignore vendored
View File

@@ -1,7 +1,20 @@
# --- secrets / env ---
.env
.env.*
!.env.example
# --- local backups ---
*.bak
*.bak.*
Dockerfile.bak.*
*.swp
*~
node_modules/
dist/
.astro/
/_release_out/
# --- macOS ---
.DS_Store
._*

View File

@@ -10,41 +10,101 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs";
import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
const must = (name, fn) => {
if (typeof fn !== "function") {
throw new Error(`[astro.config] rehype plugin "${name}" is not a function (export default vs named?)`);
}
return fn;
};
/**
* Cast minimal pour satisfaire @ts-check sans dépendre de types internes Astro/Unified.
* @param {unknown} x
* @returns {any}
*/
const asAny = (x) => /** @type {any} */ (x);
/**
* @param {any} node
* @param {string} cls
* @returns {boolean}
*/
function hasClass(node, cls) {
const cn = node?.properties?.className;
if (Array.isArray(cn)) return cn.includes(cls);
if (typeof cn === "string") return cn.split(/\s+/).includes(cls);
return false;
}
/**
* Rehype plugin: retire les ids dupliqués en gardant en priorité:
* 1) span.details-anchor
* 2) h1..h6
* 3) sinon: premier rencontré
* @returns {(tree: any) => void}
*/
function rehypeDedupeIds() {
/** @param {any} tree */
return (tree) => {
/** @type {Map<string, Array<{node:any, pref:number, idx:number}>>} */
const occ = new Map();
let idx = 0;
/** @param {any} node */
const walk = (node) => {
if (!node || typeof node !== "object") return;
if (node.type === "element") {
const id = node.properties?.id;
if (typeof id === "string" && id) {
let pref = 2;
if (node.tagName === "span" && hasClass(node, "details-anchor")) pref = 0;
else if (/^h[1-6]$/.test(String(node.tagName || ""))) pref = 1;
const arr = occ.get(id) || [];
arr.push({ node, pref, idx: idx++ });
occ.set(id, arr);
}
const children = node.children;
if (Array.isArray(children)) for (const c of children) walk(c);
} else if (Array.isArray(node.children)) {
for (const c of node.children) walk(c);
}
};
walk(tree);
for (const [id, items] of occ.entries()) {
if (items.length <= 1) continue;
items.sort((a, b) => (a.pref - b.pref) || (a.idx - b.idx));
const keep = items[0];
for (let i = 1; i < items.length; i++) {
const n = items[i].node;
if (n?.properties?.id === id) delete n.properties.id;
}
// safety: on s'assure qu'un seul garde bien l'id
if (keep?.node?.properties) keep.node.properties.id = id;
}
};
}
export default defineConfig({
output: "static",
trailingSlash: "always",
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
integrations: [
mdx(),
// Important: MDX hérite du pipeline markdown (ids p-… + autres plugins)
mdx({ extendMarkdownConfig: true }),
sitemap({
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"),
}),
],
// ✅ Plugins appliqués AU MDX
mdx: {
// ✅ MDX hérite déjà de markdown.rehypePlugins
// donc ici on ne met QUE le spécifique MDX
rehypePlugins: [
must("rehype-details-sections", rehypeDetailsSections),
],
},
// ✅ Plugins appliqués au Markdown non-MDX
markdown: {
rehypePlugins: [
must("rehype-slug", rehypeSlug),
[must("rehype-autolink-headings", rehypeAutolinkHeadings), { behavior: "append" }],
must("rehype-paragraph-ids", rehypeParagraphIds),
asAny(rehypeSlug),
[asAny(rehypeAutolinkHeadings), { behavior: "append" }],
asAny(rehypeDetailsSections),
asAny(rehypeParagraphIds),
asAny(rehypeDedupeIds),
],
},
});

74
docs/CONFIG-ENV.md Normal file
View File

@@ -0,0 +1,74 @@
# CONFIG-ENV — variables, priorités, injection build
## 0) Ce que la prod doit garantir
Le bouton “Proposer” doit ouvrir :
- `PUBLIC_GITEA_BASE` (domaine Gitea)
- `PUBLIC_GITEA_OWNER=Archicratia`
- `PUBLIC_GITEA_REPO=archicratie-edition`
Si un seul de ces 3 paramètres est faux → on obtient :
- 404 / redirect login inattendu
- ou un repo/owner incorrect
# Diagnostic — “Proposer” (résumé)
**Symptôme :** clic “Proposer” → 404 / login / mauvais repo
**Cause la plus fréquente :** `PUBLIC_GITEA_OWNER` (casse sensible) ou `PUBLIC_GITEA_REPO` faux.
➡️ Procédure complète (pas-à-pas + commandes) : voir `docs/TROUBLESHOOTING.md#proposer-404`.
## 1) Variables utilisées (publique, côté build Astro)
- `PUBLIC_GITEA_BASE`
- `PUBLIC_GITEA_OWNER`
- `PUBLIC_GITEA_REPO`
Elles sont consommées via `import.meta.env.PUBLIC_*` dans `EditionLayout.astro`.
## 2) Où elles vivent (en pratique)
Sur NAS, vous avez vu des divergences entre :
- `.env`
- `.env.local`
- `.env.production`
Règle :
- **prod build docker** doit être piloté par **`.env`** (ou `.env.production` si vous standardisez ainsi),
- mais on évite que `.env.local` “corrige en douce” un `.env` faux.
## 3) Vérification rapide (NAS)
en sh
for f in .env .env.local .env.production .env.production.local; do
[ -f "$f" ] && echo "---- $f" && grep -nE '^PUBLIC_GITEA_(BASE|OWNER|REPO)=' "$f" || true
done
## 4) Injection Docker (ce qui doit être vrai)
Le build passe bien les build-args :
--build-arg PUBLIC_GITEA_BASE=...
--build-arg PUBLIC_GITEA_OWNER=...
--build-arg PUBLIC_GITEA_REPO=...
et le Dockerfile expose via ENV pour Astro build.
## 5) Contrôle “résultat dans le HTML”
Après build, vérifier que la page contient le bon chemin /issues/new vers le bon owner/repo :
curl -fsS http://127.0.0.1:8082/archicratie/archicrat-ia/chapitre-4/ > /tmp/page.html
grep -nE 'issues/new|archicratie-edition|Archicratia' /tmp/page.html | head -n 40
## 6) Secrets (à ne pas committer)
GITEA_TOKEN : jamais dans le repo.
Le token peut être utilisé ponctuellement pour push non-interactif, mais doit rester dans lenvironnement local/CI.
## 7) Docker BuildKit / API (si besoin)
Si BuildKit/buildx est instable sur la machine, vous avez déjà un protocole “fonctionnel”.
Ne documenter ici que le “standard validé” (voir DEPLOY_PROD).

View File

@@ -1,18 +1,29 @@
# Déploiement production (Synology DS220+ / DSM 7.3) — Astro → Nginx statique
Dernière mise à jour : 2026-01-29
> ✅ **CANONIQUE** — Procédure de référence “prod DS220+ / DSM 7.3”.
Ce document décrit la mise en place stable sur NAS :
> Toute modif de déploiement doit être faite **ici**, via PR sur Gitea/main (pas dédition à la main en prod).
> Périmètre : build Docker (Node→Nginx), blue/green 8081/8082, Reverse Proxy DSM, smoke, rollback.
> Dépendances critiques : variables PUBLIC_GITEA_* (sinon “Proposer” part en 404/login loop).
> Voir aussi : OPS-REFERENCE.md (index), OPS_COCKPIT.md (checklist), TROUBLESHOOTING.md (incidents).
Dernière mise à jour : 2026-02-01
Ce document décrit une mise en place stable sur NAS :
- build Astro dans une image (Node)
- runtime Nginx statique
- bascule blue/green via Reverse Proxy DSM
- tests fonctionnels (dont “Proposer” → Gitea)
---
## 1) Arborescence recommandée
Dossier racine :
- `/volume2/docker/archicratie-web/current`
Dossier racine “courant” :
- `/volume2/docker/archicratie-web/current` (symlink vers la release active)
Contenu attendu :
- `Dockerfile`
@@ -20,6 +31,10 @@ Contenu attendu :
- `nginx.conf`
- `.env`
- le code du site (package.json, src/, scripts/, etc.)
- `ops/` (scripts de smoke, etc.)
Releases :
- `/volume2/docker/archicratie-web/releases/<YYYYmmdd-HHMMSS>/app`
---
@@ -28,6 +43,7 @@ Contenu attendu :
- DSM 7.3 avec accès SSH (admin)
- Docker / Container Manager installé
- Reverse Proxy DSM configuré (Portail des applications)
- Sortie Internet OK depuis le NAS (résolution DNS + accès registry)
---
@@ -42,12 +58,12 @@ Les ports 8081/8082 :
---
## 4) Variables Gitea (Proposer)
## 4) Variables Gitea (FEATURE “Proposer)
Le site injecte des variables “publiques” au build :
- `PUBLIC_GITEA_BASE` (URL gitea)
- `PUBLIC_GITEA_OWNER` (casse sensible)
- `PUBLIC_GITEA_OWNER` (**casse sensible**)
- `PUBLIC_GITEA_REPO`
Exemple dans `.env` :
@@ -55,49 +71,221 @@ PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition
### 4.1 Piège validé : mauvais owner/repo ⇒ 404/redirect
Symptômes typiques :
- nouvel onglet “Proposer” ouvre `/archicratia/archicratie-web/...` (mauvais)
- Gitea renvoie 404 “page inexistante ou non autorisé”
- ou 303 `/user/login` (si non loggé)
Contrôle rapide :
en sh
for f in .env .env.local .env.production .env.production.local; do
[ -f "$f" ] && echo "---- $f" && grep -nE '^PUBLIC_GITEA_(BASE|OWNER|REPO)=' "$f" || true
done
En cas déchec :
- 404 / login loop / mauvais repo → `docs/TROUBLESHOOTING.md#proposer-404`
- double onglet → `docs/TROUBLESHOOTING.md#proposer-double-onglet`
## Diagnostic — “Proposer” (résumé)
**Symptôme :** clic “Proposer” → 404 / login / mauvais repo
**Cause la plus fréquente :** `PUBLIC_GITEA_OWNER` (casse sensible) ou `PUBLIC_GITEA_REPO` faux.
➡️ Procédure complète (pas-à-pas + commandes) : voir `docs/TROUBLESHOOTING.md#proposer-404`.
## 5) Reverse Proxy DSM (le point clé)
DSM 7.3 :
### DSM 7.3 :
Panneau de configuration → Portail des applications → Proxy inversé
# Règle pour le site :
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 :
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)
### 6.1 Build réseau (DNS/apt/npm) : mode robuste
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) :
Sur DSM, il arrive que apt-get/npm aient des soucis réseau pendant docker build.
Mesures robustes validées :
build: network: host
build en --network host
Et activer BuildKit :
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
apt-get avec retries + ForceIPv4
### 6.2 Artefacts Mac (PaxHeader / ._ / DS_Store)
Exemple (dans Dockerfile) :
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.
apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true ...
Remède : .dockerignore robuste (voir anchors.md section “Artefacts Mac”).
### 6.2 BuildKit / buildx / API Docker (DSM)
## 7) Cycle blue/green (résumé)
Certains environnements DSM peuvent :
Tu rebuild le slot inactif (ex: GREEN)
exiger BuildKit pour --network=host au build
Tu valides en local (curl/smoke/health)
avoir des incompatibilités dAPI (client/daemon)
Tu bascules DSM vers ce port
Standard “chez nous” :
Rollback immédiat : tu repasses DSM sur lautre port
activer BuildKit au build
Pour lopérationnel minute par minute, voir OPS_COCKPIT.md.
utiliser DOCKER_API_VERSION="$API" si requis sur la machine
Remarque : la valeur de $API est celle validée localement sur le NAS.
### 6.3 Artefacts Mac (PaxHeader / ._ / DS_Store)
Si tu transfères une archive depuis macOS, tu peux embarquer des fichiers parasites.
Conséquence possible : Astro “voit” un faux contenu → erreurs de schema.
Remède :
.dockerignore robuste (voir docs anchors / ops)
éviter les copies “tar” non nettoyées
## 7) Cycle blue/green (procédure protocolaire)
### 7.1 Identifier quel slot est actuellement “en prod”
Comparer lETag/Last-Modified public avec 8081/8082 :
curl -kI https://archicratie.trans-hands.synology.me/ | egrep -i 'etag:|last-modified:|server:'
curl -fsSI http://127.0.0.1:8081/ | egrep -i 'etag:|last-modified:'
curl -fsSI http://127.0.0.1:8082/ | egrep -i 'etag:|last-modified:'
Le port qui “match” est le slot actif.
### 7.2 Déployer sur le slot inactif (ex: GREEN 8082)
# Se placer sur le checkout courant :
cd /volume2/docker/archicratie-web/current || exit 1
# Charger .env dans le shell (pour les build-args) :
set -a
. ./.env
set +a
# Rebuild image (exemple green) :
sudo env DOCKER_BUILDKIT=1 DOCKER_API_VERSION="$API" 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 .
# Recreate conteneur (important) :
sudo docker compose up -d --force-recreate --no-build web_green
# Smoke local :
/volume2/docker/archicratie-web/ops/smoke.sh 8082
# Sanity check (fichiers clés) :
curl -fsSI http://127.0.0.1:8082/ | head -n 20
curl -fsSI http://127.0.0.1:8082/favicon.ico | head -n 20
curl -fsSI http://127.0.0.1:8082/favicon.svg | head -n 20
# Contrôle “Proposer” (validation fonctionnelle)
Ouvrir une page chapitre sur le domaine public (ou sur le slot direct si tu testes en local)
Clic Proposer → choix 1 → choix 2
Attendus :
un seul onglet
URL sur .../Archicratia/archicratie-edition/issues/new?...
formulaire visible (si loggé sur Gitea)
issue créée et CI/runner OK (si configuré)
### 7.3 Bascule DSM
Dans DSM Reverse Proxy :
destination 8081 ↔ 8082 (BLUE ↔ GREEN)
### 7.4 Rollback (immédiat)
repointer DSM sur lancien port (1 clic)
analyser ensuite le slot inactif
## 8) Favicon (et faux positifs navigateur)
### 8.1 Symptom: Firefox affiche un 504 sur /favicon.ico
Cause fréquemment observée : cache navigateur (Firefox).
Diagnose :
curl -kI https://.../favicon.ico renvoie 200
mais la console navigateur montre encore une erreur
Fix :
désactiver le cache dans longlet Réseau, puis reload
ou vider le cache site
### 8.2 Permissions fichiers (si 403)
Si /favicon.svg renvoie 403 :
vérifier droits dans limage nginx
standard : fichiers 644, répertoires 755 (validé via RUN find ... chmod)
## 9) Exploitation : log driver “db” et erreur “database is locked”
# Erreur observée :
failed to initialize logging driver : database is locked
# Contexte validé :
docker info → Logging Driver: db
# Remède validé :
redémarrer Container Manager (centre de paquets DSM)
# vérifier lespace disque (df -h) et /tmp
Commandes :
sudo docker info 2>/dev/null | egrep -i 'Logging Driver|Docker Root Dir|Server Version' || true
df -h
df -i
## 10) Références
Pour lopérationnel minute par minute + tableau de bord : OPS_COCKPIT.md
Pour la synchro Mac/Gitea/NAS : OPS-SYNC-TRIPLE-SOURCE.md
Pour le workflow branches/PR/tags : WORKFLOW-GIT.md
Pour lENV : CONFIG-ENV.md
Pour les pannes connues : TROUBLESHOOTING.md
## Depuis quelle machine tu fais ça ?
- **Édition des docs** : **Mac Studio** (éditeur confortable, git propre)
- **Commit/push** : **Mac Studio** (idéal), NAS seulement en “urgence”
- **Déploiement** : **NAS** (évidemment)
- **Gitea docker** : juste lUI/Actions, pas nécessaire pour écrire les fichiers.
---
## Et maintenant (ordre propre)
1) Sur Mac : crée/complète ce doc (ci-dessus) + les autres docs.
2) Commit “docs: ops runbooks (2026-02-01)” sur `main`.
3) Push → CI verte.
4) Sur NAS : `reset --hard origin/main` (via alpine/git si besoin).
5) Rebuild/deploy **uniquement si** tu as touché Dockerfile/nginx/src/public (pas si docs only).

57
docs/FEATURE-PROPOSER.md Normal file
View File

@@ -0,0 +1,57 @@
# FEATURE — “Proposer” (édition par paragraphe → issue Gitea)
Dernière mise à jour : 2026-02-01
Cette feature permet à un lecteur de proposer une correction/amélioration dun paragraphe, en générant une issue pré-remplie dans Gitea.
---
## 0) Objectif fonctionnel
Depuis une page chapitre :
1) clic sur **Proposer** sur un paragraphe
2) choix #1 (type)
3) choix #2 (state/catégorie selon UI)
4) ouverture dun seul onglet vers Gitea : `/issues/new?...`
5) issue pré-remplie avec :
- chemin / URL / ancre
- texte actuel (citation)
- champs “Proposition / Justification”
6) lutilisateur valide, et le runner/CI traite.
---
## 1) Dépendances de configuration (critique)
Le lien Gitea est construit à partir de variables publiques injectées au build Astro :
- `PUBLIC_GITEA_BASE` (ex: `https://gitea.archicratie.trans-hands.synology.me`)
- `PUBLIC_GITEA_OWNER` (**casse sensible**, ex: `Archicratia`)
- `PUBLIC_GITEA_REPO` (ex: `archicratie-edition`)
### Symptômes si mauvais
- mauvais repo → 404
- redirect login inattendu
- création dissues impossible
---
## 2) Contrat “une seule ouverture donglet”
Le flow ne doit jamais ouvrir deux onglets.
### Contrat
- pas de `window.open(...)`
- un seul `a.target="_blank"` (ou équivalent) déclenché
- sur click : handler doit neutraliser les propagations parasites
## Diagnostic (canonique)
Le diagnostic détaillé est centralisé dans `docs/TROUBLESHOOTING.md` pour éviter les doublons.
- 404 / non autorisé / redirect login :
- voir : `TROUBLESHOOTING.md#proposer-404`
- cause la plus fréquente : `PUBLIC_GITEA_OWNER/REPO` faux (souvent casse)
- Double onglet :
- voir : `TROUBLESHOOTING.md#proposer-double-onglet`
- cause la plus fréquente : double handler (bubbling) ou `window.open` + `a.click()`

View File

@@ -1,6 +1,46 @@
OPS — Déploiement Archicratie Web Edition (Mac Studio → DS220+)
# 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.
> 🟧 **LEGACY / HISTORIQUE** — Ce document nest plus la source de vérité.
> Référence actuelle : docs/DEPLOY_PROD_SYNOLOGY_DS220.md (canonique).
> Statut : gelé (on nédite plus que pour ajouter un lien vers le canonique, si nécessaire).
> Raison : doublon → risque de divergence → risque derreur en prod.
> Si tu lis ceci pour déployer : stop → ouvre le canonique.
> ⚠️ LEGACY — Ne pas suivre pour déployer.
> Doc conservé pour historique.
> Canon : `DEPLOY_PROD_SYNOLOGY_DS220.md` + `OPS-SYNC-TRIPLE-SOURCE.md`.
## Pourquoi ce doc existe encore
- Historique : il capture des repères (domaines, ports, logique blue/green) tels quils ont été consolidés pendant la phase dimplémentation.
- Sécurité : éviter la divergence documentaire (un seul pas-à-pas officiel).
- Maintenance : si tu dois déployer, tu suis le canonique ; ici tu ne viens que pour comprendre “doù ça vient”.
## Ce quil faut faire aujourdhui (canonique)
➡️ Déploiement = `docs/DEPLOY_PROD_SYNOLOGY_DS220.md` (procédure détaillée, à jour).
## Schéma (résumé, sans commandes)
- Ne jamais toucher au slot live.
- Construire/tester sur lautre slot.
- Smoke test.
- Bascule DSM Reverse Proxy (8081 ↔ 8082).
- Rollback DSM si besoin.
<details>
> 🚫 NE PAS UTILISER POUR PROD — ARCHIVE UNIQUEMENT
<summary>Archive — ancien pas-à-pas (NE PAS SUIVRE)</summary>
> ⚠️ Archive. Ce contenu est conservé pour mémoire.
## 0) Repères essentiels
Noms & domaines
• Site public (prod) : https://archicratie.trans-hands.synology.me
@@ -383,3 +423,5 @@ Fix standard (dans le vrai dossier site/) :
rm -rf node_modules .astro dist
npm ci
npm run dev
</details>

30
docs/OPS-REFERENCE.md Normal file
View File

@@ -0,0 +1,30 @@
# OPS-REFERENCE — Archicratie Édition
Document “pivot” : liens, invariants, conventions, commandes réflexes.
## 0) Invariants (à ne pas casser)
- **Source de vérité Git** : origin/main (repo Archicratia/archicratie-edition sur Gitea).
- **Prod** : conteneur `archicratie-web-*` (nginx) derrière reverse proxy DSM.
- **Config “Proposer”** : dépend de `PUBLIC_GITEA_BASE`, `PUBLIC_GITEA_OWNER`, `PUBLIC_GITEA_REPO` injectés au build.
- **Branches** : `main` = travail ; `master` = legacy/compat (alignée mais protégée).
- **NAS** : shell sans `git` → usage standard : `alpine/git` en conteneur.
## 1) Repères (chemins & objets)
### NAS (DS220+)
- Checkout/release courant : `/volume2/docker/archicratie-web/current` (symlink vers releases/*/app)
- Releases : `/volume2/docker/archicratie-web/releases/<timestamp>/app`
- Conteneur green (exemple) : `archicratie-web-green``127.0.0.1:8082->80`
- Conteneur blue (exemple) : `archicratie-web-blue``127.0.0.1:8081->80`
- Smoke : `/volume2/docker/archicratie-web/ops/smoke.sh <port>`
### Mac Studio
- Repo de travail : `site/` (ou équivalent)
- Git natif : OK (branches, PR, tags, etc.)
## 2) Commandes réflexes (diagnostic)
### “Quelle version sert le domaine public ?”
```sh
curl -kI https://archicratie.trans-hands.synology.me/ | egrep -i 'etag:|last-modified:|server:'

View File

@@ -0,0 +1,79 @@
# OPS Runbook — Archicratie Web (NAS Synology DS220 + Gitea)
> 🟦 **ALIAS (résumé)** — Runbook 1-page pour opérer vite sans se tromper.
> La procédure détaillée **canonique** est : docs/DEPLOY_PROD_SYNOLOGY_DS220.md.
> Source Git : Gitea/main ; déploiement = rebuild depuis main ; pas de hotfix non versionné.
> Déploiement : build sur slot inactif → smoke → bascule DSM → rollback si besoin.
> Incidents connus : voir docs/TROUBLESHOOTING.md.
## 0. Objectif
Ce document décrit la procédure **exacte** pour :
- maintenir un état cohérent entre **Local (Mac Studio)**, **Gitea**, **NAS (prod)** ;
- déployer proprement une release Docker (build, smoke, bascule) ;
- éviter les régressions (branches protégées, CI bloquante, rollback).
## 1. Invariants (à ne jamais violer)
1) **La branche `main` sur Gitea = vérité canonique.**
2) La prod NAS doit être **rebuild** depuis `main` (pas de modifications “à la main” non versionnées).
3) Les variables `PUBLIC_GITEA_*` doivent pointer vers **le bon owner/repo** (sinon 404 / login loop).
4) En solo : PR requise OK, mais **0 approval** (sinon blocage). CI doit rester bloquante.
5) Interdiction : token/secret commité dans le repo.
## 2. Topologie (NAS)
- Arborescence :
- `/volume2/docker/archicratie-web/releases/<timestamp>/app`
- `/volume2/docker/archicratie-web/current` (symlink vers release active)
- Services :
- `web_green` → port local typique `127.0.0.1:8082` (smoke)
- (optionnel) `web_blue``127.0.0.1:8081` (ancienne)
- Script :
- `/volume2/docker/archicratie-web/ops/smoke.sh <port>`
## 3. Standard Git (Gitea)
### 3.1 Branches
- `main` : branche de travail et de déploiement (par défaut)
- `master` : branche legacy (archivée / conservée pour compat si nécessaire)
- Stratégie : `master` est soit supprimée, soit alignée sur `main`, mais **pas** utilisée comme axe actif.
### 3.2 Protection branches (solo recommandé)
- `main` :
- ✅ Exiger PR (Require pull request)
- ✅ 0 approval (solo)
- ✅ Bloquer merge si CI KO (Prevent merge if checks fail)
- ✅ Désactiver force-push
- (optionnel) désactiver push direct (selon UI Gitea)
- `master` :
- ✅ Interdire push direct
- ✅ Désactiver force-push
- ✅ Mention “legacy” (si UI le permet)
## 4. Variables denvironnement (cause n°1 des 404)
### 4.1 Variables publiques injectées au build
- `PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me`
- `PUBLIC_GITEA_OWNER=Archicratia`
- `PUBLIC_GITEA_REPO=archicratie-edition`
### 4.2 Règle dor
- Le site génère des liens `.../<OWNER>/<REPO>/issues/new?...`
- Si OWNER/REPO est faux (ex: `archicratia/archicratie-web`) :
- `/issues` peut marcher
- MAIS `/issues/new` finit en 404 / redirect login / droits
- => corriger `.env` et rebuild
## 5. Déploiement NAS (procédure protocolaire)
### 5.1 Pré-check
Sur NAS :
- Vérifier quon est bien dans `/volume2/docker/archicratie-web/current`
- Charger `.env` (pour build args)
Commandes :
```sh
cd /volume2/docker/archicratie-web/current || exit 1
set -a
. ./.env
set +a
grep -nE '^PUBLIC_GITEA_(BASE|OWNER|REPO)=' .env

View File

@@ -0,0 +1,122 @@
# OPS-SYNC-TRIPLE-SOURCE — Mac Studio / Gitea / NAS (prod)
Dernière mise à jour : 2026-02-01
Ce document décrit la synchronisation **sans ambiguïté** entre :
- **Local (Mac Studio)** : édition / écriture / préparation PR
- **Gitea** : **vérité canonique** (branche `main`)
- **NAS Synology DS220+** : déploiement (blue/green) à partir de `main`
---
## 0) Invariants (à ne jamais violer)
1) **Gitea `main` = source de vérité.**
2) Le NAS ne doit pas “inventer” du code : pas dédition manuelle non versionnée en prod (sauf hotfix temporaire immédiatement reporté dans une PR).
3) Le bouton “Proposer” dépend de `PUBLIC_GITEA_*` : une valeur fausse → 404 / redirect login / mauvais repo.
4) Les secrets (tokens) **ne doivent jamais** entrer dans le repo : `.env*` ignorés, token injecté uniquement via variable denvironnement locale/CI.
---
## 1) Topologie réelle (ce que nous avons)
### 1.1 Local (Mac Studio)
- Dev et documentation.
- Git complet.
- On fait : branches, commits, push, PR, merge.
### 1.2 Gitea
- Repo canonique : `Archicratia/archicratie-edition`.
- `main` = défaut + protégée.
- Toute modif arrive via PR.
### 1.3 NAS (prod)
- Chemin canonique :
- `/volume2/docker/archicratie-web/releases/<timestamp>/app`
- `/volume2/docker/archicratie-web/current` → symlink vers la release active
- Blue/Green :
- `web_blue` sur `127.0.0.1:8081`
- `web_green` sur `127.0.0.1:8082`
- Reverse proxy DSM : bascule 8081 ↔ 8082.
---
## 2) Règle dor : qui écrit quoi, où ?
### 2.1 Toute écriture “source” se fait sur Mac Studio
- Code Astro
- Scripts
- Docs `docs/*.md`
- `.gitignore`
### 2.2 Gitea ne reçoit que via PR
- Push sur branche feature/docs
- PR → CI → merge
### 2.3 NAS ne fait que :
- `git reset --hard origin/main` (alignement)
- build image + restart slot blue/green
- smoke test
- bascule reverse proxy DSM
---
## 3) Procédure standard (la seule à utiliser)
### Étape A — Mac Studio → Gitea (PR)
1) `git checkout -b feat/...` ou `docs/...`
2) commits propres et atomiques
3) `git push -u origin <branch>`
4) PR dans Gitea → CI OK → merge dans `main`
### Étape B — NAS : aligner `current` sur `origin/main`
Sur NAS, git nest pas forcément installé : on utilise un conteneur git.
en sh :
APP="/volume2/docker/archicratie-web/current"
U_ID="$(id -u)"; G_ID="$(id -g)"
sudo docker run --rm --network host \
-u "$U_ID:$G_ID" -e HOME=/tmp \
-v "$APP":/repo -w /repo \
--entrypoint sh alpine/git -lc '
set -eu
git config --global --add safe.directory /repo
git config http.sslVerify false
git fetch origin --prune
git checkout -B main
git reset --hard origin/main
git status -sb
'
### Étape C — NAS : rebuild du slot inactif + smoke + bascule
Rebuild de limage (slot inactif recommandé).
docker compose up -d --force-recreate --no-build web_green (ou blue)
smoke test via script ou curl
bascule DSM vers le port du slot actif
## 4) Checkpoints rapides (sanity)
### 4.1 Vérifier que NAS = origin/main
git rev-parse --short HEAD sur NAS (via alpine/git)
doit égaler origin/main.
### 4.2 Vérifier “Proposer” (points minimum)
PUBLIC_GITEA_OWNER=Archicratia (casse sensible)
PUBLIC_GITEA_REPO=archicratie-edition
Flow : Proposer → choix 1 → choix 2 → onglet Gitea /issues/new?... OK
## 5) Rollback
DSM reverse proxy : repasser sur lautre port (8081/8082).
En cas de code cassé : réaligner NAS sur origin/main précédent (tag/release) ou repointer /current vers une release précédente.

View File

@@ -0,0 +1,122 @@
# RUNBOOK — Créer une Demande dajout (PR) “automatique” depuis un push (Gitea)
## Objectif
Pousser une branche depuis le Mac vers Gitea et obtenir le workflow standard :
1) branche dédiée
2) push
3) suggestion “Nouvelle demande dajout” (bandeau vert) OU lien terminal
4) création PR via UI
5) merge (main protégé)
> Important : Gitea ne crée pas une PR automatiquement.
> Il affiche une *suggestion* (bandeau vert) ou imprime un lien “Create a new pull request” lors du push.
---
## Pré-check (obligatoire, 10 secondes)
en bash :
git status -sb
git fetch origin --prune
git branch --show-current
Procédure standard (zéro surprise)
### 1) Se remettre propre sur main
git checkout main
git pull --ff-only
### 2) Créer une branche AVANT de modifier / ajouter des fichiers
git switch -c docs/<YYYY-MM-DD>-<sujet-court>
### 3) Ajouter/modifier tes fichiers dans docs/
Exemple :
docs/auth-stack.md
docs/runbook-....
### 4) Vérifier ce qui va partir
git status -sb
git diff
### 5) Commit
git add docs/
git commit -m "docs: <résumé clair>"
### 6) Vérifier que ta branche a bien des commits “devant” main (SINON pas de PR possible)
git fetch origin
git log --oneline origin/main..HEAD
Si ça naffiche rien : tu nas rien à proposer (branche identique à main).
### 7) Push (méthode la plus robuste)
git push -u origin HEAD
### 8) Créer la PR (2 chemins fiables)
# Chemin A — le plus simple : utiliser le lien imprimé dans le terminal
Après le push, Gitea affiche généralement :
“Create a new pull request for '<ta-branche>': <URL>”
➡️ Ouvre cette URL, clique “Créer la demande dajout”.
# Chemin B — via lUI Gitea (si tu veux le bandeau vert)
Va sur le dépôt
Onglet “Demandes dajout”
Clique “Nouvelle demande dajout”
Source branch = ta branche, Target = main
Créer
## Pourquoi le bandeau vert peut ne PAS apparaître (et ce que ça signifie)
Ta branche est identique à main
# Diagnostic :
git fetch origin
git diff --name-status origin/main..HEAD
Si vide => normal, pas de suggestion.
Tu nes pas sur la bonne branche
# Diagnostic :
git branch --show-current
Tu regardes lUI au mauvais endroit
Solution : utilise le bouton “Nouvelle demande dajout” ou le lien du terminal (chemin A).
Anti-bêtise (optionnel mais recommandé)
Empêcher de commit sur main par erreur (hook local)
# Créer .git/hooks/pre-commit :
#!/bin/sh
b="$(git branch --show-current)"
if [ "$b" = "main" ]; then
echo "❌ Refus: commit interdit sur main. Crée une branche."
exit 1
fi
Puis :
chmod +x .git/hooks/pre-commit
## Rappel : main protégé
Si main est protégé, tu ne merges PAS par git push origin main.
Tu merges via la PR (UI), après CI verte.

217
docs/TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,217 @@
# TROUBLESHOOTING — Archicratie Web / NAS / Gitea
Dernière mise à jour : 2026-02-01
Ce document liste les symptômes rencontrés et les remèdes **concrets**.
---
## 0) Réflexe unique
Toujours isoler : **Local**, **Gitea**, **NAS**, **Navigateur**.
- Si ça marche sur `127.0.0.1:8082` mais pas sur le domaine → proxy/cache.
- Si ça marche après login Gitea mais pas via “Proposer” → variables `PUBLIC_GITEA_*`.
- Si push refusé → branch protection (normal).
---
<a id="proposer-404"></a>
## 1) “Proposer” ouvre Gitea mais retourne 404 / non autorisé
### Symptôme
Nouvel onglet :
- 404 Not Found / “nexiste pas ou pas autorisé”
- ou redirect `/user/login`
### Cause la plus fréquente
URL pointe vers **mauvais owner/repo** (casse sensible) :
- `archicratia/archicratie-web` au lieu de `Archicratia/archicratie-edition`
### Diagnostic
Sur NAS (ou dans le HTML généré), vérifier lURL ouverte :
- doit contenir : `/Archicratia/archicratie-edition/issues/new`
### Fix
Dans `.env` de build prod (NAS) :
- `PUBLIC_GITEA_OWNER=Archicratia`
- `PUBLIC_GITEA_REPO=archicratie-edition`
Puis rebuild + restart du container + smoke.
---
<a id="proposer-double-onglet"></a>
## 2) Double onglet à la validation du flow “Proposer”
### Symptôme
Deux onglets souvrent au moment de valider (après choix 1 / choix 2).
### Causes possibles
- handler JS déclenché deux fois (bubbling)
- présence dun `window.open` + `a.click()` simultanément
- bouton “Proposer” est un `<a target=_blank>` et un autre handler ouvre aussi.
### Diagnostic rapide (devtools navigateur)
Chercher `window.open` dans la page générée :
- la commande doit retourner 0 lignes.
Sur NAS :
en sh :
curl -fsS http://127.0.0.1:8082/archicratie/archicrat-ia/chapitre-4/ > /tmp/page.html
grep -n "window.open" /tmp/page.html | head
Fix
garder un seul mécanisme douverture
sur click : preventDefault() + stopImmediatePropagation()
<a id="Favicon-504-erreurs"></a>
## 3) Favicon 504 / erreurs console sur favicon
# Symptôme
Console navigateur : GET /favicon.ico 504
# Cause fréquente
Cache du navigateur (ancienne erreur conservée).
# Diagnostic
Comparer :
curl -I http://127.0.0.1:8082/favicon.ico
curl -kI https://<domaine>/favicon.ico
Si curl = 200 et navigateur = 504 → cache.
# Fix
Désactiver cache dans longlet Réseau (devtools)
hard refresh
vérifier droits fichiers dans dist/
## 4) Sur NAS : git: command not found
# Symptôme
git fetch impossible sur le NAS.
# Cause
Git non installé sur DSM shell.
# Fix standard (recommandé)
Utiliser un conteneur git :
APP="/volume2/docker/archicratie-web/current"
U_ID="$(id -u)"; G_ID="$(id -g)"
sudo docker run --rm --network host \
-u "$U_ID:$G_ID" -e HOME=/tmp \
-v "$APP":/repo -w /repo \
--entrypoint sh alpine/git -lc '
set -eu
git config --global --add safe.directory /repo
git config http.sslVerify false
git fetch origin --prune
git status -sb
'
## 5) Git : “dubious ownership in repository”
# Symptôme
fatal: detected dubious ownership
# Fix
Dans le conteneur git (ou machine locale) :
git config --global --add safe.directory /repo
## 6) Git : non-fast-forward au push
# Symptôme
rejected (non-fast-forward)
# Cause
Ta branche locale est en retard vs remote.
# Fix
En général :
on fait une PR depuis une branche
ou on rebase/merge origin/main avant push
Sur une branche de travail :
git fetch origin
git rebase origin/main
# ou
git merge origin/main
## 7) Gitea : “Not allowed to push to protected branch main”
# Symptôme
pre-receive hook declined
# Cause
Protection de branche (normal/attendu).
# Fix
Push sur une branche
Ouvrir PR
Merger via UI Gitea
## 8) Docker build : BuildKit / buildx / API version
# Symptômes typiques
the --network option requires BuildKit
BuildKit is enabled but the buildx component is missing
client version ... too new. Maximum supported API version ...
# Fix “robuste” (principe)
installer buildx si nécessaire
si DSM/docker API ancienne : définir DOCKER_API_VERSION=<compatible> (selon ton environnement)
garder le build en --network host si nécessaire
## 9) Container Manager / Docker : “database is locked” (logging driver db)
# Symptôme
failed to initialize logging driver : database is locked
# Cause
Le driver de logs Docker est db (Synology) et sa DB est verrouillée.
# Fix rapide
Redémarrer “Container Manager” depuis le centre de paquets DSM.
Vérifier que le conteneur redémarre ensuite.
## 10) Checklist “tout marche”
curl -I http://127.0.0.1:8082/ => 200
curl -kI https://<domaine>/ => 200
PUBLIC_GITEA_* corrects
“Proposer” : 1 onglet, pas de 404, issue pré-remplie
CI passe sur PR merge

71
docs/WORKFLOW-GIT.md Normal file
View File

@@ -0,0 +1,71 @@
# WORKFLOW-GIT — branches, PR, tags (standard “main”)
## 0) Objectif
- Tout le monde (toi compris) suit un protocole unique.
- Zéro ambigüité entre “ce qui tourne” et “ce qui est versionné”.
## 1) Branches : politique
### main (branche de travail)
- Branche par défaut du repo.
- Tout changement passe idéalement par PR (même si tu es seul).
### master (legacy/compat)
- Doit rester alignée sur main (si vous gardez master).
- Protégée contre push accidentel (pas de force-push, pas de push direct).
- Nest **pas** un axe de travail.
### branches de feature
Conventions recommandées :
- `fix/<sujet>` : correctif
- `feat/<sujet>` : nouvelle fonction
- `ops/<sujet>` : infra, déploiement, scripts, docs ops
## 2) PR (même si tu es seul)
- Créer une PR → vérifier CI (Actions) → merge.
- Règle : “Prevent merge if checks fail” activée (bonne pratique).
- Approvals : 0 (si tu es seul).
## 3) Tags
- Tags darchives : `archive-master-<YYYYmmdd-HHMMSS>` (déjà fait).
- Snapshots prod : `prod-snapshot-<YYYYmmdd-HHMMSS>` si besoin.
## 4) Commandes standard (Mac)
### Se mettre parfaitement à jour
en sh :
git checkout main
git fetch origin --prune --tags
git reset --hard origin/main
git clean -fd # optionnel : supprime untracked
### Publier un changement
git checkout -b fix/<slug>
# edits...
git add -A
git commit -m "fix: <message>"
git push -u origin HEAD
# PR via UI, merge
## 5) Remote case-sensitive (piège déjà rencontré)
Le repo peut exister avec Owner en casse différente (ex: Archicratia vs archicratia).
Standard : remote doit être :
https://.../Archicratia/archicratie-edition.git
Vérifier :
git remote -v
## 6) NAS : pas de git natif
Sur NAS, on ninstalle pas forcément git dans le shell.
Standard : utiliser alpine/git en conteneur (cf. OPS-SYNC-TRIPLE-SOURCE).
## 7) Interdits (pour éviter lenfer)
Éditer des fichiers du repo via FileStation “pour aller vite” (sauf hotfix durgence, puis backport immédiat Git).
Pousser sur master (sauf opération contrôlée dalignement).
Force-push sur main.

View File

@@ -63,7 +63,7 @@ Si lID exact nexiste plus :
But : éviter les “liens morts” historiques quand une régénération dIDs a eu lieu.
Limite : cest un fallback de dernier recours (moins déterministe quun alias explicite).
Le mécanisme recommandé reste : `docs/anchor-aliases.json` + injection au build.
Le mécanisme recommandé reste : `src/anchors/anchor-aliases.json` + injection au build.
_______________________________________
@@ -87,7 +87,7 @@ Les IDs dancres générés (ou dérivés) peuvent changer :
## 2) Le mapping dalias
- Fichier versionné (ex) : `docs/anchor-aliases.json`
- Fichier versionné (ex) : `src/anchors/anchor-aliases.json`
- Format : `oldId -> newId` par page
Ex en json :

201
docs/auth-stack.md Normal file
View File

@@ -0,0 +1,201 @@
# Auth Stack — LLDAP + Authelia + Redis (DSM 7.3 / Synology DS220+)
## Objectif
Fournir une pile dauthentification robuste (anti-lockout) pour protéger des services web via reverse-proxy :
- Annuaire utilisateurs : **LLDAP**
- Portail / SSO / MFA : **Authelia**
- Cache/sessions (optionnel selon config) : **Redis**
- Exposition publique : **Reverse proxy** (Synology / Nginx / Traefik) vers Authelia
---
## Architecture
### Composants
- **LLDAP**
- UI admin (HTTP) : `127.0.0.1:17170`
- LDAP : `127.0.0.1:3890`
- Base : sqlite dans `/volume2/docker/auth/data/lldap`
- **Authelia**
- API/portal : `127.0.0.1:9091`
- Stockage : sqlite dans `/volume2/docker/auth/data/authelia/db.sqlite3`
- Accès externe : via reverse proxy -> `https://auth.<domaine>`
- **Redis**
- Local uniquement : `127.0.0.1:6379`
- (peut servir plus tard à sessions/rate-limit selon config)
### Exposition réseau (principe de sécurité)
- Tous les services **bindés sur 127.0.0.1** (loopback NAS)
- Seul le **reverse proxy** expose `https://auth.<domaine>` vers `127.0.0.1:9091`
---
## Fichiers de référence
### 1) docker-compose.auth.yml
- Déploie redis + lldap + authelia.
- Recommandation DSM : **network_mode: host** + bind sur localhost.
- Supprime les aléas “bridge + DNS + subnets”
- Évite les timeouts LDAP sporadiques.
### 2) /volume2/docker/auth/compose/.env
Variables attendues :
#### LLDAP
- `LLDAP_JWT_SECRET=...` (random 32+)
- `LLDAP_KEY_SEED=...` (random 32+)
- `LLDAP_LDAP_USER_PASS=...` (mot de passe admin LLDAP)
#### Authelia
- `AUTHELIA_JWT_SECRET=...` (utilisé ici comme source pour reset_password)
- `AUTHELIA_SESSION_SECRET=...`
- `AUTHELIA_STORAGE_ENCRYPTION_KEY=...`
> Ne jamais committer `.env`. Stocker dans DSM / secrets.
### 3) /volume2/docker/auth/config/authelia/configuration.yml
- LDAP address en mode robuste : `ldap://127.0.0.1:3890`
- Cookie domain : `archicratie.trans-hands.synology.me`
- `authelia_url` : `https://auth.archicratie.trans-hands.synology.me`
- `default_redirection_url` : service principal (ex: gitea)
---
## Procédures opératoires
### Restart safe (redémarrage propre)
en bash :
cd /volume2/docker/auth/compose
sudo docker compose --env-file .env -f docker-compose.auth.yml down --remove-orphans
sudo docker compose --env-file .env -f docker-compose.auth.yml up -d --force-recreate
### Tests santé (sans dépendances DSM)
curl -fsS http://127.0.0.1:17170/ >/dev/null && echo "LLDAP UI OK"
curl -fsS http://127.0.0.1:9091/api/health && echo "AUTHELIA LOCAL OK"
curl -kfsS https://auth.archicratie.trans-hands.synology.me/api/health && echo "AUTHELIA HTTPS OK"
### Test TCP LDAP :
sudo docker run --rm --network host nicolaka/netshoot:latest sh -lc 'nc -vz -w2 127.0.0.1 3890'
### Rotate secrets (rotation)
# Principes :
Rotation = redémarrage forcé dAuthelia (sessions invalidées)
Rotation de LLDAP_KEY_SEED est sensible : peut affecter chiffrement des mots de passe.
# Procédure conseillée :
Sauvegarder DBs :
/volume2/docker/auth/data/lldap/users.db
/volume2/docker/auth/data/authelia/db.sqlite3
Changer dabord secrets Authelia (AUTHELIA_SESSION_SECRET, AUTHELIA_STORAGE_ENCRYPTION_KEY)
docker compose up -d --force-recreate authelia
Vérifier /api/health + login.
Reset admin LLDAP (break-glass)
# Si tu perds le mot de passe admin :
Activer temporairement LLDAP_FORCE_LDAP_USER_PASS_RESET=true dans lenvironnement LLDAP
Redémarrer LLDAP une seule fois
Désactiver immédiatement après.
⚠️ Ne jamais laisser ce flag en permanence : il force le reset à chaque boot.
## Checklist anti-lockout (indispensable)
### 1) Accès direct local (bypass)
LLDAP UI accessible en local : http://127.0.0.1:17170
Authelia health local : http://127.0.0.1:9091/api/health
### 2) Règle Authelia : domaine auth en bypass
Dans configuration.yml :
access_control:
rules:
- domain: "auth.<domaine>"
policy: bypass
But : pouvoir charger le portail même si les règles des autres domaines cassent.
### 3) Route de secours reverse-proxy
Prévoir une route non protégée (ou protégée différemment) pour pouvoir corriger :
ex: https://admin.<domaine>/ ou un vhost interne LAN-only.
### 4) Fenêtre privée pour tester
Toujours tester login/authelia dans un onglet privé pour éviter cookies “fantômes”.
## Troubleshooting (ce quon a rencontré et résolu)
### A) YAML/Compose cassé (tabs, doublons)
# Symptômes :
mapping key "ports" already defined
found character that cannot start any token
# Fix :
supprimer tabs
supprimer doublons (volumes/ports/networks)
valider : docker compose ... config
### B) Substitution foireuse des variables dans healthcheck
# Problème :
$VAR évalué par compose au parse-time
# Fix :
utiliser $$VAR dans CMD-SHELL si nécessaire.
### C) /config monté read-only
# Symptômes :
chown: /config/... Read-only file system
# Fix :
monter /config en :rw si Authelia doit écrire des backups/keys.
### D) Timeouts LDAP aléatoires en bridge
# Symptômes :
dial tcp <ip>:3890: i/o timeout
IP Docker “surprise” (subnet 192.168.32.0/20 etc.)
# Fix robuste DSM :
passer en network_mode: host + bind 127.0.0.1
Authelia -> ldap://127.0.0.1:3890
### E) “Authelia OK mais Gitea redemande login”
# Normal :
tant que Gitea nest pas configuré en OIDC vers Authelia, ce nest pas du SSO.
Authelia protège laccès, mais ne crée pas de session Gitea.

View File

@@ -0,0 +1,67 @@
# Workflow Git/Gitea — main protégé (PR only)
## Objectif
Éviter toute casse de `main` : on travaille **toujours** via branche + Pull Request.
## 1) Démarrer propre (local)
en bash :
git fetch origin --prune
git checkout main
git reset --hard origin/main
git clean -fd
## 2) Créer une branche
git checkout -b fix/ma-modif
## 3) Modifier, tester, commit
npm test
git add -A
git commit -m "Mon changement"
## 4) Push (création branche distante)
git push -u origin fix/ma-modif
## 5) Créer la Pull Request (UI Gitea)
Gitea → repository → Pull Requests → New Pull Request
base : main
compare : fix/ma-modif
Si “je ne vois pas de PR”
Vérifie dabord quil y a un diff réel :
git log --oneline origin/main..HEAD
Si la commande ne sort rien : ta branche ne contient aucun commit différent → PR inutile/invisible.
## 6) Conflits
Ne merge pas en local vers main (push refusé si main protégé).
On met à jour la branche de PR :
Option A (simple) : merge main dans la branche
git fetch origin
git merge origin/main
# résoudre conflits
npm test
git push
Option B (plus propre) : rebase
git fetch origin
git rebase origin/main
# résoudre conflits, puis:
npm test
git push --force-with-lease
## 7) Merge
Toujours depuis lUI de la Pull Request (ou via un mainteneur).

View File

@@ -0,0 +1,69 @@
# “Proposer” protégé par groupe (whoami / editors)
## But
Le bouton **Proposer** (création dissue Gitea pré-remplie) doit être :
- visible **uniquement** pour les membres du groupe `editors`,
- **absent** pour les autres utilisateurs,
- robuste (fail-closed), mais **non-collant** (pas de “bloqué” après un échec transitoire).
## Pré-requis (build-time)
Les variables publiques Astro doivent être injectées au build :
- `PUBLIC_GITEA_BASE`
- `PUBLIC_GITEA_OWNER`
- `PUBLIC_GITEA_REPO`
Si une seule manque → `giteaReady=false` → Proposer est désactivé.
### Vérification NAS (slots blue/green)
Exemple :
- blue : http://127.0.0.1:8081/...
- green : http://127.0.0.1:8082/...
Commande (ex) :
`curl -sS http://127.0.0.1:8081/archicratie/archicrat-ia/chapitre-4/ | grep -n "const GITEA_" | head`
## Signal dauth (runtime) : `/_auth/whoami`
Le site appelle `/_auth/whoami` (same-origin) pour récupérer :
- `Remote-User`
- `Remote-Groups`
Ces headers sont injectés par la chaîne edge (Traefik → Authelia forward-auth).
### Appel robuste
- cache-bust : `?_=${Date.now()}`
- `cache: "no-store"`
- `credentials: "include"`
### Critère
`groups.includes("editors")`
## Comportement attendu (UX)
- utilisateur editors : le bouton “Proposer” est visible, ouvre la modal, puis ouvre Gitea.
- utilisateur non editors : le bouton “Proposer” nexiste pas (retiré du DOM).
## Pièges connus
1) Tester en direct 8081/8082 ne reflète pas toujours la chaîne Traefik+Authelia.
2) Un gate “collant” peut rester OFF si léchec est mis en cache trop agressivement.
3) Si “Proposer” est caché via `style.display="none"`, il faut le réafficher via `style.display=""` (pas via `hidden=false`).
## Debug rapide (console navigateur)
en js :
(async () => {
const r = await fetch("/_auth/whoami?_=" + Date.now(), {
credentials: "include",
cache: "no-store",
redirect: "follow",
});
const t = await r.text();
const groups = (t.match(/^Remote-Groups:\s*(.*)$/mi)?.[1] || "")
.split(",").map(s => s.trim()).filter(Boolean);
console.log({ ok: r.ok, status: r.status, groups, raw: t.slice(0, 220) + "..." });
})();
## Définition “done”
Archicratia (editors) voit Proposer et peut ouvrir un ticket.
s-FunX (non editors) ne voit pas Proposer.
Les deux slots blue/green injectent les constantes Gitea dans le HTML.

View File

@@ -0,0 +1,82 @@
# Runbook — Déploiement Archicratie Web Édition (Blue/Green)
## Arborescence NAS (repère)
- `/volume2/docker/archicratie-web/current/` : état courant (Dockerfile, docker-compose.yml, dist buildé en image)
- `/volume2/docker/archicratie-web/releases/` : historiques éventuels
- `/volume2/docker/edge/` : Traefik + config dynamique
> Important : les commandes `docker compose -f ...` doivent viser le **docker-compose.yml présent dans `current/`**.
---
## Pré-requis Synology
Sur NAS, les commandes ont été exécutées avec :
en bash
sudo env DOCKER_API_VERSION=1.43 docker ...
(contexte DSM / compat API)
### 1) Variables de build (Gitea)
Dans /volume2/docker/archicratie-web/current créer/maintenir :
cat > .env <<'EOF'
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition
EOF
### 2) Build images (blue + green) — méthode robuste
cd /volume2/docker/archicratie-web/current
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml build --no-cache web_blue web_green
Puis recréer les conteneurs sans rebuild :
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml up -d --force-recreate --no-build web_blue web_green
### 3) Vérifier que les deux slots sont OK
curl -sS -D- http://127.0.0.1:8081/ | head -n 12
curl -sS -D- http://127.0.0.1:8082/ | head -n 12
Attendu :
HTTP/1.1 200 OK
Server: nginx/...
### 4) Traefik : sassurer quun seul backend est actif
Fichier :
/volume2/docker/edge/config/dynamic/20-archicratie-backend.yml
Attendu : une seule URL (8081 OU 8082)
http:
services:
archicratie_web:
loadBalancer:
servers:
- url: "http://127.0.0.1:8081"
### 5) Smoke via Traefik (entrée réelle)
curl -sS -H 'Host: archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ | head -n 20
Attendu :
si non loggé : 302 vers Authelia
si loggé : HTML du site
### 6) Piège classique : conflit de nom de conteneur
Si :
Conflict. The container name "/archicratie-web-blue" is already in use...
Faire :
sudo docker rm -f archicratie-web-blue
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml up -d --force-recreate --no-build web_blue

View File

@@ -0,0 +1,71 @@
# Runbook — Gitea : Branches, PR, Merge (sans se faire piéger)
## Règle n°1 (hyper importante)
Une PR napparaît dans Gitea que si la branche contient **au moins 1 commit différent de `main`**.
Symptôme typique :
- `git push -u origin fix/xxx`
- et tu vois : `Total 0 ...`
→ ça veut dire : **aucun nouveau commit** → la branche est identique à main → pas de vraie PR à proposer.
---
## Workflow “propre” (pas à pas)
### 1) Remettre `main` propre
en bash
git checkout main
git pull --ff-only
### 2) Créer une branche de travail
git checkout -b fix/mon-fix
### 3) Faire un changement réel
Modifier le fichier (ex : src/layouts/EditionLayout.astro)
Vérifier :
git status -sb
→ doit montrer un fichier modifié.
### 4) Tester
npm test
### 5) Commit
git add src/layouts/EditionLayout.astro
git commit -m "Fix: ..."
### 6) Push
git push -u origin fix/mon-fix
### 7) Créer la PR dans lUI Gitea
# Aller dans Pull Requests
# New Pull Request
Base : main
Compare : fix/mon-fix
Branch protection (si “Not allowed to push to protected branch main”)
# Cest normal si main est protégé :
On ne pousse jamais directement sur main.
On merge via PR (UI), avec un compte autorisé.
Si Gitea refuse de merger automatiquement :
soit tu actives le réglage côté Gitea “manual merge detection” (admin),
soit tu fais le merge localement MAIS tu ne pourras pas pousser sur main si la protection linterdit.
Conclusion : la voie “pro” = PR + merge UI.

View File

@@ -0,0 +1,67 @@
# Runbook — Bouton “Proposer” (site → Gitea issue) + Gate Authelia
## Objectif
Permettre une proposition de correction éditoriale depuis un paragraphe du site, en créant une *issue* Gitea pré-remplie, uniquement pour les membres du groupe `editors`.
---
## Pré-requis
- Traefik (edge) en front
- Authelia (forwardAuth) opérationnel
- Router `/_auth/whoami` exposé (whoami)
- Variables `PUBLIC_GITEA_*` injectées au build du site
---
## Vérification rapide (navigateur)
### 1) Qui suis-je ? (groupes)
Dans la console :
en js :
await fetch("/_auth/whoami?_=" + Date.now(), {
credentials: "include",
cache: "no-store",
}).then(r => r.text());
Attendu (extraits) :
Remote-User: <login>
Remote-Groups: ...,editors,... pour un éditeur
### 2) Le bouton existe ?
document.querySelectorAll(".para-propose").length
> 0 si editors
0 si non-editor
## Vérification côté NAS (build vars)
### 1) Blue et Green contiennent les constantes ?
P="/archicratie/archicrat-ia/chapitre-4/"
curl -sS "http://127.0.0.1:8081$P" | grep -n "const GITEA_BASE" | head -n 2
curl -sS "http://127.0.0.1:8082$P" | grep -n "const GITEA_BASE" | head -n 2
### 2) Si une des deux est vide → rebuild propre
Dans /volume2/docker/archicratie-web/current :
cat > .env <<'EOF'
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition
EOF
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml build --no-cache web_blue web_green
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml up -d --force-recreate --no-build web_blue web_green
## Dépannage (si Proposer “disparaît”)
Vérifier groupes via /_auth/whoami
Vérifier const GITEA_BASE via curl sur le slot actif
Vérifier que Traefik sert bien le slot actif (grep via curl -H Host: ... http://127.0.0.1:18080/...)
Ouvrir la console : vérifier quaucune erreur JS nempêche linjection des outils paragraphe

View File

@@ -12,7 +12,7 @@
"build": "astro build",
"build:clean": "npm run clean && npm run build",
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && npx pagefind --site dist",
"import": "node scripts/import-docx.mjs",
"apply:ticket": "node scripts/apply-ticket.mjs",

134
scripts/dedupe-ids-dist.mjs Normal file
View File

@@ -0,0 +1,134 @@
import { promises as fs } from "node:fs";
import path from "node:path";
const DIST_DIR = path.resolve("dist");
/** @param {string} dir */
async function walkHtml(dir) {
/** @type {string[]} */
const out = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...(await walkHtml(p)));
else if (e.isFile() && p.endsWith(".html")) out.push(p);
}
return out;
}
/** @param {string} attrs */
function getClass(attrs) {
const m = attrs.match(/\bclass="([^"]*)"/i);
return m ? m[1] : "";
}
/** @param {{tag:string,id:string,cls:string}} occ */
function score(occ) {
// plus petit = mieux (on garde)
if (occ.tag === "span" && /\bdetails-anchor\b/.test(occ.cls)) return 0;
if (/^h[1-6]$/.test(occ.tag)) return 1;
if (occ.tag === "p" && occ.id.startsWith("p-")) return 2;
return 10; // tout le reste (toc, nav, etc.)
}
async function main() {
let changedFiles = 0;
let removed = 0;
const files = await walkHtml(DIST_DIR);
for (const file of files) {
let html = await fs.readFile(file, "utf8");
// capture: <tag ... id="X" ...>
const re = /<([A-Za-z][\w:-]*)([^>]*?)\s+id="([^"]+)"([^>]*?)>/g;
/** @type {Array<{id:string,tag:string,pre:string,post:string,start:number,end:number,cls:string,idx:number}>} */
const occs = [];
let m;
let idx = 0;
while ((m = re.exec(html)) !== null) {
const tag = m[1].toLowerCase();
const pre = m[2] || "";
const id = m[3] || "";
const post = m[4] || "";
const fullAttrs = `${pre}${post}`;
const cls = getClass(fullAttrs);
occs.push({
id,
tag,
pre,
post,
start: m.index,
end: m.index + m[0].length,
cls,
idx: idx++,
});
}
if (occs.length === 0) continue;
/** @type {Map<string, Array<typeof occs[number]>>} */
const byId = new Map();
for (const o of occs) {
if (!o.id) continue;
const arr = byId.get(o.id) || [];
arr.push(o);
byId.set(o.id, arr);
}
/** @type {Array<{start:number,end:number,repl:string}>} */
const edits = [];
for (const [id, arr] of byId.entries()) {
if (arr.length <= 1) continue;
// choisir le “meilleur” porteur did : details-anchor > h2/h3... > p-... > reste
const sorted = [...arr].sort((a, b) => {
const sa = score(a);
const sb = score(b);
if (sa !== sb) return sa - sb;
return a.idx - b.idx; // stable: premier
});
const keep = sorted[0];
for (const o of sorted.slice(1)) {
// remplacer louverture de tag en supprimant lattribut id
// <tag{pre} id="X"{post}> ==> <tag{pre}{post}>
const repl = `<${o.tag}${o.pre}${o.post}>`;
edits.push({ start: o.start, end: o.end, repl });
removed++;
}
// sécurité: on “force” l'id sur le keep (au cas où il aurait été modifié plus haut)
// (on ne touche pas au keep ici, juste on ne le retire pas)
void keep;
void id;
}
if (edits.length === 0) continue;
// appliquer de la fin vers le début
edits.sort((a, b) => b.start - a.start);
for (const e of edits) {
html = html.slice(0, e.start) + e.repl + html.slice(e.end);
}
await fs.writeFile(file, html, "utf8");
changedFiles++;
}
if (changedFiles > 0) {
console.log(`✅ dedupe-ids-dist: files_changed=${changedFiles} ids_removed=${removed}`);
} else {
console.log(" dedupe-ids-dist: no duplicates found");
}
}
main().catch((err) => {
console.error("❌ dedupe-ids-dist failed:", err);
process.exit(1);
});

View File

@@ -0,0 +1,59 @@
schema: 1
# optionnel (si présent, doit matcher le chemin du fichier)
page: archicratie/archicrat-ia/prologue
paras:
p-0-d7974f88:
refs:
- label: "Happycratie — (Cabanas & Illouz) via Cairn"
url: "https://shs.cairn.info/revue-ethnologie-francaise-2019-4-page-813?lang=fr"
kind: "article"
- label: "Techno-féodalisme — Variations (OpenEdition)"
url: "https://journals.openedition.org/variations/2290"
kind: "article"
authors:
- "Eva Illouz"
- "Yanis Varoufakis"
quotes:
- text: "Dans Happycratie, Edgar Cabanas et Eva Illouz..."
source: "Happycratie, p.1"
- text: "En eux-mêmes, les actifs ne sont ni féodaux ni capitalistes..."
source: "Entretien Morozov/Varoufakis — techno-féodalisme"
media:
- type: "image"
src: "/public/media/archicratie/archicrat-ia/prologue/p-0-d7974f88/schema-1.svg"
caption: "Tableau explicatif"
credit: "ChatGPT"
- type: "image"
src: "/public/media/archicratie/archicrat-ia/prologue/p-0-d7974f88/schema-2.svg"
caption: "Diagramme dévolution"
credit: "Yanis Varoufakis"
comments_editorial:
- text: "TODO: nuancer / préciser — commentaire éditorial versionné (pas public)."
status: "draft"
p-1-2ef25f29:
refs:
- label: "Kafka et le pouvoir — Bernard Lahire (Cairn)"
url: "https://shs.cairn.info/franz-kafka--9782707159410-page-475?lang=fr"
kind: "book"
authors:
- "Bernard Lahire"
quotes:
- text: "Si lon voulait chercher quelque chose comme une vision du monde chez Kafka..."
source: "Bernard Lahire, Franz Kafka, p.475+"
media:
- type: "video"
src: "/media/prologue/p-1-2ef25f29/bien_commun.mp4"
caption: "Entretien avec Bernard Lahire"
credit: "Cairn.info"
comments_editorial: []

View File

@@ -1,35 +1,128 @@
---
// src/components/LevelToggle.astro
const { initialLevel = 1 } = Astro.props;
---
<div class="level-toggle" role="group" aria-label="Niveau de lecture">
<button type="button" class="lvl-btn" data-level="1" aria-pressed="true">Niveau 1</button>
<button type="button" class="lvl-btn" data-level="2" aria-pressed="false">Niveau 2</button>
<button type="button" class="lvl-btn" data-level="3" aria-pressed="false">Niveau 3</button>
<div class="level-toggle" role="group" aria-label="Mode dédition">
<button type="button" class="level-btn" data-level="1">Propos</button>
<button type="button" class="level-btn" data-level="2">Références</button>
<button type="button" class="level-btn" data-level="3">Illustrations</button>
<button type="button" class="level-btn" data-level="4">Commentaires</button>
</div>
<script is:inline>
<script is:inline define:vars={{ initialLevel }}>
(() => {
const KEY = "archicratie.readingLevel";
const buttons = Array.from(document.querySelectorAll(".lvl-btn"));
const BODY = document.body;
function apply(level) {
document.body.setAttribute("data-reading-level", String(level));
buttons.forEach((b) => b.setAttribute("aria-pressed", b.dataset.level === String(level) ? "true" : "false"));
const wrap = document.querySelector(".level-toggle");
if (!wrap) return;
const buttons = Array.from(wrap.querySelectorAll("button[data-level]"));
if (!buttons.length) return;
const KEY = "archicratie:readingLevel";
function clampLevel(n) {
const x = Number.parseInt(String(n), 10);
if (!Number.isFinite(x)) return 1;
return Math.min(4, Math.max(1, x));
}
// Valeur par défaut : si rien n'est stocké, on met 1 (citoyen).
// Si JS est absent/casse, le site reste lisible (tout s'affiche).
const stored = Number(localStorage.getItem(KEY));
const level = (stored === 1 || stored === 2 || stored === 3) ? stored : 1;
function setActiveUI(lvl) {
for (const b of buttons) {
const on = String(b.dataset.level) === String(lvl);
b.classList.toggle("is-active", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
}
}
apply(level);
function captureBeforeLevelSwitch() {
const paraId =
window.__archiCurrentParaId ||
window.__archiLastParaId ||
String(location.hash || "").replace(/^#/, "") ||
"";
buttons.forEach((b) => {
b.addEventListener("click", () => {
const lvl = Number(b.dataset.level);
localStorage.setItem(KEY, String(lvl));
apply(lvl);
});
window.__archiLevelSwitchCtx = {
paraId,
hash: location.hash || "",
scrollY: window.scrollY || 0,
t: Date.now(),
};
}
function applyLevel(lvl, { persist = true } = {}) {
const v = clampLevel(lvl);
if (BODY) BODY.dataset.readingLevel = String(v);
setActiveUI(v);
if (persist) {
try { localStorage.setItem(KEY, String(v)); } catch {}
}
try {
window.dispatchEvent(
new CustomEvent("archicratie:readingLevel", { detail: { level: v } })
);
} catch {}
}
// init : storage > initialLevel
let start = clampLevel(initialLevel);
try {
const stored = localStorage.getItem(KEY);
if (stored) start = clampLevel(stored);
} catch {}
applyLevel(start, { persist: false });
// clicks
wrap.addEventListener("click", (ev) => {
const btn = ev.target?.closest?.("button[data-level]");
if (!btn) return;
ev.preventDefault();
// ✅ crucial : on capture la position AVANT le reflow lié au changement de niveau
captureBeforeLevelSwitch();
applyLevel(btn.dataset.level);
});
})();
</script>
<style>
.level-toggle{
display: inline-flex;
gap: 8px;
align-items: center;
}
.level-btn{
border: 1px solid rgba(127,127,127,0.40);
background: rgba(127,127,127,0.08);
border-radius: 999px;
padding: 6px 10px;
font-size: 13px;
cursor: pointer;
user-select: none;
transition: filter .12s ease, transform .12s ease, background .12s ease, border-color .12s ease;
}
.level-btn:hover{
filter: brightness(1.08);
}
.level-btn.is-active{
border-color: rgba(160,160,255,0.95);
background: rgba(140,140,255,0.18);
font-weight: 900;
}
.level-btn.is-active:hover{
filter: brightness(1.12);
}
.level-btn:active{
transform: translateY(1px);
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -80,6 +80,12 @@ main { padding: 0; }
border-top: 1px dashed rgba(127,127,127,0.35);
font-size: 14px;
}
/* Edition-bar: cacher des badges (non destructif) */
.edition-bar [data-badge="edition"],
.edition-bar [data-badge="status"],
.edition-bar [data-badge="version"]{
display: none;
}
.badge {
padding: 2px 8px;
@@ -95,7 +101,34 @@ main { padding: 0; }
padding: 5px 12px;
}
/* Toggle niveaux */
/* Jump by paragraph id */
.jump-form{
display: inline-flex;
gap: 6px;
align-items: center;
}
.jump-input{
border: 1px solid rgba(127,127,127,0.55);
background: transparent;
padding: 4px 10px;
border-radius: 999px;
font-size: 13px;
width: 320px;
}
.jump-input.is-error{
outline: 2px solid rgba(127,127,127,0.55);
outline-offset: 2px;
}
.jump-btn{
border: 1px solid rgba(127,127,127,0.55);
background: transparent;
padding: 4px 10px;
border-radius: 999px;
cursor: pointer;
font-size: 13px;
}
/* Toggle niveaux (legacy, non bloquant) */
.level-toggle { display: inline-flex; gap: 6px; }
.lvl-btn {
border: 1px solid rgba(127,127,127,0.55);
@@ -112,14 +145,22 @@ main { padding: 0; }
/* Règles niveaux */
body[data-reading-level="1"] .level-2,
body[data-reading-level="1"] .level-3 { display: none; }
body[data-reading-level="2"] .level-3 { display: none; }
body[data-reading-level="1"] .level-3,
body[data-reading-level="1"] .level-4 { display: none; }
body[data-reading-level="2"] .level-3,
body[data-reading-level="2"] .level-4 { display: none; }
body[data-reading-level="3"] .level-2,
body[data-reading-level="3"] .level-4 { display: none; }
body[data-reading-level="4"] .level-2,
body[data-reading-level="4"] .level-3 { display: none; }
/* ==========================
Scroll offset (anchors / headings / paras)
========================== */
/* Paragraph tools + bookmark */
.reading p[id]{
position: relative;
padding-right: 14rem;
@@ -183,6 +224,14 @@ body[data-reading-level="2"] .level-3 { display: none; }
}
.para-bookmark:hover{ text-decoration: underline; }
/* Highlight (jump / resume / arrivée hash) */
.para-highlight{
background: rgba(127,127,127,0.10);
border-radius: 10px;
box-shadow: 0 0 0 2px rgba(127,127,127,0.35);
transition: box-shadow 160ms ease;
}
.build-stamp {
margin-top: 28px;
padding-top: 14px;
@@ -196,15 +245,51 @@ body[data-reading-level="2"] .level-3 { display: none; }
border-radius: 16px;
padding: 10px 12px;
margin: 14px 0;
position: relative;
}
.details-summary {
cursor: pointer;
font-weight: 650;
/* ✅ Handle minimal pour sections fermées : pas de titre visible, mais ouvrable */
.details-summary{
list-style: none;
cursor: pointer;
user-select: none;
border: 1px dashed rgba(127,127,127,.25);
border-radius: 999px;
padding: 6px 10px;
margin: 10px 0;
background: rgba(127,127,127,0.06);
position: relative;
/* cache le texte réel (souvent le titre), sans casser laccessibilité */
color: transparent;
}
.details-summary::-webkit-details-marker { display: none; }
.details-summary a { text-decoration: none; }
.details-summary a:hover { text-decoration: underline; }
.details-summary::before{
content: "▸ Ouvrir la section";
color: rgba(127,127,127,0.85);
font-size: 12px;
font-weight: 850;
}
@media (prefers-color-scheme: dark){
.details-summary::before{ color: rgba(220,220,220,0.82); }
}
details[open] > .details-summary{
/* une fois ouvert, on le rend “SR-only” pour éviter le doublon visuel */
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.details-body { margin-top: 10px; }
/* Smooth scroll */
@@ -224,7 +309,6 @@ html{ scroll-behavior: smooth; }
width: var(--reading-width);
right: auto;
/* colle au header */
top: var(--sticky-header-h);
z-index: 60;
@@ -247,7 +331,7 @@ html{ scroll-behavior: smooth; }
box-sizing: border-box;
padding: 8px 12px;
padding-right: 84px; /* réserve pour les boutons */
padding-right: 84px;
border: 1px solid rgba(127,127,127,.20);
border-top: 0;
@@ -259,7 +343,7 @@ html{ scroll-behavior: smooth; }
box-shadow: 0 10px 22px rgba(0,0,0,.06);
position: relative; /* pour rf-actions */
position: relative;
}
@media (prefers-color-scheme: dark){
@@ -278,7 +362,6 @@ html{ scroll-behavior: smooth; }
cursor: pointer;
margin: 0;
}
.rf-line[hidden]{ display: none !important; }
.rf-h1{
@@ -298,7 +381,6 @@ html{ scroll-behavior: smooth; }
font-weight: var(--rf-h3-fw);
opacity: .92;
}
.rf-line:hover{ text-decoration: underline; }
/* Actions */
@@ -327,3 +409,14 @@ html{ scroll-behavior: smooth; }
background: rgba(127,127,127,0.16);
transform: translateY(-1px);
}
/* ==========================
PATCH CRUCIAL : éviter les “rectangles vides”
(details fermés + summary handle minimal)
========================== */
.reading details.details-section:not([open]){
border: 0;
padding: 0;
background: transparent;
}