Compare commits

..

65 Commits

Author SHA1 Message Date
5e95dc9898 ci: fix deploy (install docker compose plugin)
All checks were successful
CI / build-and-anchors (push) Successful in 1m42s
SMOKE / smoke (push) Successful in 23s
2026-02-26 11:15:53 +01:00
2b612214bb ci: auto deploy staging+live (annotations-only)
All checks were successful
CI / build-and-anchors (push) Successful in 1m55s
SMOKE / smoke (push) Successful in 18s
2026-02-26 10:29:17 +01:00
29a6c349aa Merge pull request 'anno: apply ticket #121' (#122) from bot/anno-121-20260225-191130 into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m18s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #122
2026-02-26 09:01:52 +01:00
archicratie-bot
33a227c401 anno: apply ticket #121 (archicrat-ia/chapitre-4#p-7-1da4a458 type/media)
All checks were successful
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 21s
2026-02-25 19:11:36 +00:00
396ad4df7c Merge pull request 'anno: apply ticket #115' (#120) from bot/anno-115-20260225-185830 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #120
2026-02-25 20:00:04 +01:00
archicratie-bot
0b39427090 anno: apply ticket #115 (archicrat-ia/chapitre-4#p-2-31b12529 type/media)
All checks were successful
CI / build-and-anchors (push) Successful in 1m42s
SMOKE / smoke (push) Successful in 15s
2026-02-25 18:58:34 +00:00
8fcb18cb46 Merge pull request 'ci: anno apply workflow builds dist for strict verify' (#119) from chore/fix-anno-verify-build2 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m49s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #119
2026-02-25 19:51:24 +01:00
d03fc519de ci: anno apply workflow builds dist for strict verify
All checks were successful
CI / build-and-anchors (push) Successful in 1m50s
SMOKE / smoke (push) Successful in 19s
2026-02-25 19:50:47 +01:00
97dd3797d6 Merge pull request 'ci: anno apply workflow builds dist for strict verify' (#118) from chore/fix-anno-verify-build into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m35s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #118
2026-02-25 19:31:45 +01:00
6c7b7ab6a0 ci: anno apply workflow builds dist for strict verify
All checks were successful
CI / build-and-anchors (push) Successful in 1m59s
SMOKE / smoke (push) Successful in 23s
2026-02-25 19:29:36 +01:00
105dfe1b5b Merge pull request 'ci: add apply-annotation-ticket script for anno bot' (#117) from chore/add-apply-annotation-ticket into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m47s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #117
2026-02-25 19:02:33 +01:00
82f6453538 ci: add apply-annotation-ticket script for anno bot
All checks were successful
CI / build-and-anchors (push) Successful in 1m43s
SMOKE / smoke (push) Successful in 17s
2026-02-25 19:02:03 +01:00
fe862102d3 Merge pull request 'ci: fix anno apply/reject workflows (yaml valid)' (#116) from chore/fix-anno-workflows into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m49s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #116
2026-02-25 18:28:36 +01:00
6ef538a0c4 ci: fix anno apply/reject workflows (yaml valid)
All checks were successful
CI / build-and-anchors (push) Successful in 1m36s
SMOKE / smoke (push) Successful in 20s
2026-02-25 18:26:03 +01:00
689612ff7f Merge pull request 'anno-workflows' (#114) from chore/anno-workflows into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m52s
SMOKE / smoke (push) Successful in 18s
Reviewed-on: #114
2026-02-25 17:51:16 +01:00
7b135a4707 ci: add anno apply bot workflow
All checks were successful
CI / build-and-anchors (push) Successful in 2m0s
SMOKE / smoke (push) Successful in 16s
2026-02-25 17:50:45 +01:00
0cb8a54195 Merge pull request 'fix: remove specimen media from prologue annotations' (#108) from chore/Fix_whoami into main
Some checks failed
CI / build-and-anchors (push) Has started running
SMOKE / smoke (push) Has been cancelled
Reviewed-on: #108
2026-02-23 12:37:39 +01:00
a7a333397d fix: remove specimen media from prologue annotations
All checks were successful
CI / build-and-anchors (push) Successful in 1m36s
SMOKE / smoke (push) Successful in 15s
2026-02-23 12:33:55 +01:00
eb1d444776 Merge pull request 'fix: …' (#107) from chore/Fix_whoami into main
Some checks failed
CI / build-and-anchors (push) Failing after 1m45s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #107
2026-02-23 12:17:21 +01:00
68c3416594 fix: …
Some checks failed
CI / build-and-anchors (push) Failing after 2m6s
SMOKE / smoke (push) Successful in 13s
2026-02-23 12:07:01 +01:00
ae809e0152 Merge pull request 'docs: add pro runbooks (deploy/edge/public_site) + annotations spec + start-here v2' (#106) from fix/canonical-public-site into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m25s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #106
2026-02-21 15:36:07 +01:00
7444eeb532 docs: add pro runbooks (deploy/edge/public_site) + annotations spec + start-here v2
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 11s
2026-02-21 15:34:47 +01:00
9bbebf5886 Merge pull request 'fix(seo): enforce PUBLIC_SITE at docker build (canonical/sitemap) + set per blue/green' (#105) from fix/canonical-public-site into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m55s
SMOKE / smoke (push) Successful in 23s
Reviewed-on: #105
2026-02-21 12:32:08 +01:00
fe7810671d fix(seo): enforce PUBLIC_SITE at docker build (canonical/sitemap) + set per blue/green
All checks were successful
CI / build-and-anchors (push) Successful in 2m26s
SMOKE / smoke (push) Successful in 20s
2026-02-21 12:31:23 +01:00
53562025ac Merge pull request 'fix/anchors-baseline-archicrat-ia-20260220' (#104) from fix/anchors-baseline-archicrat-ia-20260220 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m32s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #104
2026-02-20 22:50:22 +01:00
2b35315466 Merge branch 'main' into fix/anchors-baseline-archicrat-ia-20260220
All checks were successful
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 15s
2026-02-20 22:50:06 +01:00
1b7f23d0a6 fix(home): Essai-thèse -> /archicrat-ia/ + rename ArchiCraT-IA
All checks were successful
CI / build-and-anchors (push) Successful in 1m49s
SMOKE / smoke (push) Successful in 17s
2026-02-20 22:42:49 +01:00
3d1d4d7952 fix(annotations): update archicrat-ia prologue specimen paths 2026-02-20 22:29:55 +01:00
3320563e1b chore: add ops scripts + diagrams PNG renders 2026-02-20 22:29:47 +01:00
798b2ddd0b chore: ignore .DS_Store 2026-02-20 22:22:35 +01:00
31d4896f5d Merge pull request 'test(anchors): update baseline after URL migration to /archicrat-ia' (#103) from fix/anchors-baseline-archicrat-ia-20260220 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m24s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #103
2026-02-20 21:29:19 +01:00
3fda37491d test(anchors): update baseline after URL migration to /archicrat-ia
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 13s
2026-02-20 21:28:45 +01:00
488c02b8b5 Merge pull request 'chore/url-migration-archicrat-ia-20260220' (#102) from chore/url-migration-archicrat-ia-20260220 into main
Some checks failed
CI / build-and-anchors (push) Failing after 1m31s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #102
2026-02-20 21:15:55 +01:00
672e6d03d0 fix(url): migrate Essai-thèse to /archicrat-ia (routes + index + annotations)
Some checks failed
CI / build-and-anchors (push) Failing after 1m37s
SMOKE / smoke (push) Successful in 17s
2026-02-20 21:14:45 +01:00
2881fdaf01 fix(toc): Essai-thèse links -> /archicrat-ia/* (no /archicratie prefix)
All checks were successful
CI / build-and-anchors (push) Successful in 1m42s
SMOKE / smoke (push) Successful in 13s
2026-02-20 20:48:31 +01:00
db98a3787b fix(nav): Essai-thèse -> /archicrat-ia/
All checks were successful
CI / build-and-anchors (push) Successful in 1m38s
SMOKE / smoke (push) Successful in 15s
2026-02-20 20:35:56 +01:00
f9ea3760e2 Merge pull request 'chore: add diagrams + scripts + archicrat-ia route' (#101) from chore/url-migration-archicrat-ia-20260220 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #101
2026-02-20 18:31:40 +01:00
78eb9cbb58 chore: add diagrams + scripts + archicrat-ia route
All checks were successful
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 14s
2026-02-20 18:27:25 +01:00
00e1a1d4b0 Merge pull request 'chore: add missing diagrams/scripts + archicrat-ia routes' (#100) from chore/url-migration-archicrat-ia-20260220 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m31s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #100
2026-02-20 18:17:31 +01:00
ab3758bbc2 chore: add missing diagrams/scripts + archicrat-ia routes
All checks were successful
CI / build-and-anchors (push) Successful in 2m13s
SMOKE / smoke (push) Successful in 17s
2026-02-20 18:15:27 +01:00
12d3d81518 Merge pull request 'fix(etape8): resync hotfix edition depuis NAS (2026-02-19)' (#99) from sync/etape8-hotfix-20260219 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m34s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #99
2026-02-19 23:01:47 +01:00
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
92 changed files with 15632 additions and 750 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

View File

@@ -3,7 +3,7 @@ name: "Correction paragraphe"
about: "Proposer une correction ciblée (un paragraphe) avec justification." about: "Proposer une correction ciblée (un paragraphe) avec justification."
--- ---
## Chemin (ex: /archicratie/prologue/) ## Chemin (ex: /archicrat-ia/prologue/)
<!-- obligatoire --> <!-- obligatoire -->
/... /...

View File

@@ -3,7 +3,7 @@ name: "Vérification factuelle / sources"
about: "Signaler une assertion à sourcer ou à corriger (preuves, références)." about: "Signaler une assertion à sourcer ou à corriger (preuves, références)."
--- ---
## Chemin (ex: /archicratie/prologue/) ## Chemin (ex: /archicrat-ia/prologue/)
<!-- obligatoire --> <!-- obligatoire -->
/... /...

View File

@@ -0,0 +1,343 @@
name: Anno Apply (PR)
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to apply"
required: true
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
jobs:
apply-approved:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
npm ping --registry=https://registry.npmjs.org
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
node --input-type=module - <<'NODE' > /tmp/anno.env
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
let owner =
repoObj?.owner?.login ||
repoObj?.owner?.username ||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
let repo =
repoObj?.name ||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
if (!owner || !repo) {
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
}
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
const defaultBranch = repoObj?.default_branch || "main";
const issueNumber =
ev?.issue?.number ||
ev?.issue?.index ||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
throw new Error("No issue number in event.json or workflow_dispatch input");
}
const labelName =
ev?.label?.name ||
ev?.label ||
"workflow_dispatch";
const u = new URL(cloneUrl);
const origin = u.origin;
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
: origin;
function sh(s){ return JSON.stringify(String(s)); }
process.stdout.write([
`CLONE_URL=${sh(cloneUrl)}`,
`OWNER=${sh(owner)}`,
`REPO=${sh(repo)}`,
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
echo "✅ context:"
sed -n '1,120p' /tmp/anno.env
- name: Gate on label state/approved
run: |
set -euo pipefail
source /tmp/anno.env
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
echo " label=$LABEL_NAME => skip"
echo "SKIP=1" >> /tmp/anno.env
exit 0
fi
echo "✅ proceed (issue=$ISSUE_NUMBER)"
- name: Checkout default branch
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
rm -rf .git
git init -q
git remote add origin "$CLONE_URL"
git fetch --depth 1 origin "$DEFAULT_BRANCH"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
git log -1 --oneline
- name: Install deps
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
npm ci
- name: Check apply script exists
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -f scripts/apply-annotation-ticket.mjs || {
echo "❌ missing scripts/apply-annotation-ticket.mjs on $DEFAULT_BRANCH"
ls -la scripts | sed -n '1,200p' || true
exit 1
}
- name: Build dist (needed for --verify)
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
npm run build:clean
test -f dist/para-index.json || {
echo "❌ missing dist/para-index.json after build"
ls -la dist | sed -n '1,200p' || true
exit 1
}
echo "✅ dist/para-index.json present"
- name: Apply ticket on bot branch (strict+verify, commit)
continue-on-error: true
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
START_SHA="$(git rev-parse HEAD)"
TS="$(date -u +%Y%m%d-%H%M%S)"
BR="bot/anno-${ISSUE_NUMBER}-${TS}"
echo "BRANCH=$BR" >> /tmp/anno.env
git checkout -b "$BR"
export FORGE_API="$API_BASE"
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
LOG="/tmp/apply.log"
set +e
node scripts/apply-annotation-ticket.mjs "$ISSUE_NUMBER" --strict --verify --commit >"$LOG" 2>&1
RC=$?
set -e
echo "APPLY_RC=$RC" >> /tmp/anno.env
echo "== apply log (tail) =="
tail -n 180 "$LOG" || true
END_SHA="$(git rev-parse HEAD)"
if [[ "$RC" -ne 0 ]]; then
echo "NOOP=0" >> /tmp/anno.env
exit 0
fi
if [[ "$START_SHA" == "$END_SHA" ]]; then
echo "NOOP=1" >> /tmp/anno.env
else
echo "NOOP=0" >> /tmp/anno.env
echo "END_SHA=$END_SHA" >> /tmp/anno.env
fi
- name: Comment issue on failure (strict/verify/etc)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
RC="${APPLY_RC:-0}"
if [[ "$RC" == "0" ]]; then
echo " no failure detected"
exit 0
fi
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
MSG="❌ apply-annotation-ticket a échoué (rc=${RC}).\n\n\`\`\`\n${BODY}\n\`\`\`\n"
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
- name: Comment issue if no-op (already applied)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "1" ]] || exit 0
MSG=" Ticket #${ISSUE_NUMBER} : rien à appliquer (déjà présent / dédupliqué)."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
- name: Push bot branch
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo " apply failed -> skip push"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo " no-op -> skip push"; exit 0; }
AUTH_URL="$(node --input-type=module -e '
const [clone, tok] = process.argv.slice(1);
const u = new URL(clone);
u.username = "oauth2";
u.password = tok;
console.log(u.toString());
' "$CLONE_URL" "$FORGE_TOKEN")"
git remote set-url origin "$AUTH_URL"
git push -u origin "$BRANCH"
- name: Create PR + comment issue
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo " apply failed -> skip PR"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo " no-op -> skip PR"; exit 0; }
PR_TITLE="anno: apply ticket #${ISSUE_NUMBER}"
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA}\n\nMerge si CI OK."
PR_PAYLOAD="$(node --input-type=module -e '
const [title, body, base, head] = process.argv.slice(1);
console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"
PR_JSON="$(curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
--data-binary "$PR_PAYLOAD")"
PR_URL="$(node --input-type=module -e '
const pr = JSON.parse(process.argv[1] || "{}");
console.log(pr.html_url || pr.url || "");
' "$PR_JSON")"
test -n "$PR_URL" || { echo "❌ PR URL missing. Raw: $PR_JSON"; exit 1; }
MSG="✅ PR créée pour ticket #${ISSUE_NUMBER} : ${PR_URL}"
C_PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$C_PAYLOAD"
echo "✅ PR: $PR_URL"
- name: Finalize (fail job if apply failed)
if: ${{ always() }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
RC="${APPLY_RC:-0}"
if [[ "$RC" != "0" ]]; then
echo "❌ apply failed (rc=$RC)"
exit "$RC"
fi
echo "✅ apply ok"

View File

@@ -0,0 +1,98 @@
name: Anno Reject
on:
issues:
types: [labeled]
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
jobs:
reject:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Derive context
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
node --input-type=module - <<'NODE' > /tmp/reject.env
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
if (!cloneUrl) throw new Error("No repository url");
let owner =
repoObj?.owner?.login ||
repoObj?.owner?.username ||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
let repo =
repoObj?.name ||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
if (!owner || !repo) {
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
}
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
const issueNumber = ev?.issue?.number || ev?.issue?.index;
if (!issueNumber) throw new Error("No issue number");
const labelName = ev?.label?.name || ev?.label || "";
const u = new URL(cloneUrl);
function sh(s){ return JSON.stringify(String(s)); }
process.stdout.write([
`OWNER=${sh(owner)}`,
`REPO=${sh(repo)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`API_BASE=${sh(u.origin)}`
].join("\n") + "\n");
NODE
- name: Gate on label state/rejected
run: |
set -euo pipefail
source /tmp/reject.env
if [[ "$LABEL_NAME" != "state/rejected" ]]; then
echo " label=$LABEL_NAME => skip"
exit 0
fi
echo "✅ reject issue=$ISSUE_NUMBER"
- name: Comment + close issue
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/reject.env
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
curl -fsS -X PATCH \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
--data-binary '{"state":"closed"}'

View File

@@ -79,22 +79,7 @@ jobs:
set -euo pipefail set -euo pipefail
npm ci npm ci
- name: Inline scripts syntax check - name: Full test suite (CI=1)
run: | run: |
set -euo pipefail set -euo pipefail
node scripts/check-inline-js.mjs npm run ci
- name: Build (includes postbuild injection + pagefind)
run: |
set -euo pipefail
npm run build
- name: Anchors contract
run: |
set -euo pipefail
npm run test:anchors
- name: Verify anchor aliases injected in dist
run: |
set -euo pipefail
node scripts/verify-anchor-aliases-in-dist.mjs

View File

@@ -1,103 +0,0 @@
name: CI
on:
push: {}
pull_request:
branches: ["master"]
workflow_dispatch: {}
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
jobs:
build-and-anchors:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
npm ping --registry=https://registry.npmjs.org
- name: Checkout (from event.json, no external actions)
run: |
set -euo pipefail
EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || (echo "❌ Missing $EVENT_JSON" && exit 1)
eval "$(node - <<'NODE'
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync("/var/run/act/workflow/event.json","utf8"));
const repo =
ev?.repository?.clone_url ||
(ev?.repository?.html_url ? (ev.repository.html_url.replace(/\/$/,'') + ".git") : "");
const sha =
ev?.after ||
ev?.pull_request?.head?.sha ||
ev?.head_commit?.id ||
ev?.sha ||
"";
if (!repo) { console.error("No repository.clone_url/html_url in event.json"); process.exit(1); }
if (!sha) { console.error("No sha/after/pull_request.head.sha in event.json"); process.exit(1); }
console.log(`REPO_URL=${JSON.stringify(repo)}`);
console.log(`SHA=${JSON.stringify(sha)}`);
NODE
)"
echo "Repo URL: $REPO_URL"
echo "SHA: $SHA"
rm -rf .git
git init
git remote add origin "$REPO_URL"
git fetch --depth 1 origin "$SHA"
git checkout -q FETCH_HEAD
git log -1 --oneline
- name: Anchor aliases schema
run: |
set -euo pipefail
node scripts/check-anchor-aliases.mjs
- name: NPM harden
run: |
set -euo pipefail
npm config set fetch-retries 5
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set registry https://registry.npmjs.org
npm config get registry
- name: Install deps
run: |
set -euo pipefail
npm ci
- name: Inline scripts syntax check
run: |
set -euo pipefail
node scripts/check-inline-js.mjs
- name: Build (includes postbuild injection + pagefind)
run: |
set -euo pipefail
npm run build
- name: Anchors contract
run: |
set -euo pipefail
npm run test:anchors
- name: Verify anchor aliases injected in dist
run: |
set -euo pipefail
node scripts/verify-anchor-aliases-in-dist.mjs

View File

@@ -0,0 +1,191 @@
name: Deploy staging+live (annotations)
on:
push:
branches: [main]
workflow_dispatch:
env:
NODE_OPTIONS: --dns-result-order=ipv4first
DOCKER_API_VERSION: "1.43"
COMPOSE_VERSION: "2.29.7"
defaults:
run:
shell: bash
concurrency:
group: deploy-staging-live-main
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
- name: Checkout (from event.json, no external actions)
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
eval "$(node --input-type=module -e 'import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON,"utf8"));
const repo =
ev?.repository?.clone_url ||
(ev?.repository?.html_url ? (ev.repository.html_url.replace(/\/$/,"") + ".git") : "");
const sha =
ev?.after ||
ev?.pull_request?.head?.sha ||
ev?.head_commit?.id ||
ev?.sha ||
"";
if (!repo) throw new Error("No repository url in event.json");
if (!sha) throw new Error("No sha in event.json");
process.stdout.write(`REPO_URL=${JSON.stringify(repo)}\nSHA=${JSON.stringify(sha)}\n`);
')"
echo "Repo URL: $REPO_URL"
echo "SHA: $SHA"
rm -rf .git
git init -q
git remote add origin "$REPO_URL"
git fetch --depth 1 origin "$SHA"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
git log -1 --oneline
echo "SHA=$SHA" >> /tmp/deploy.env
echo "REPO_URL=$REPO_URL" >> /tmp/deploy.env
- name: Gate — auto deploy only on annotations/media changes
run: |
set -euo pipefail
source /tmp/deploy.env
# fichiers touchés par CE commit (merge commit inclus)
CHANGED="$(git diff-tree --no-commit-id --name-only -r "$SHA" || true)"
echo "== changed files =="
echo "$CHANGED" | sed -n '1,200p'
# Gate strict : uniquement annotations + media + le workflow lui-même (si tu veux autoriser)
if echo "$CHANGED" | grep -qE '^(src/annotations/|public/media/)'; then
echo "GO=1" >> /tmp/deploy.env
echo "✅ deploy allowed (annotations/media change detected)"
exit 0
fi
echo "GO=0" >> /tmp/deploy.env
echo " no annotations/media change -> skip deploy"
exit 0
- name: Install docker client + docker compose plugin (v2)
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update
apt-get install -y --no-install-recommends ca-certificates curl docker.io
rm -rf /var/lib/apt/lists/*
mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL \
"https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64" \
-o /usr/local/lib/docker/cli-plugins/docker-compose
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
docker version
docker compose version
- name: Assert required vars (PUBLIC_GITEA_*)
env:
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
test -n "${PUBLIC_GITEA_BASE:-}" || { echo "❌ missing repo var PUBLIC_GITEA_BASE"; exit 2; }
test -n "${PUBLIC_GITEA_OWNER:-}" || { echo "❌ missing repo var PUBLIC_GITEA_OWNER"; exit 2; }
test -n "${PUBLIC_GITEA_REPO:-}" || { echo "❌ missing repo var PUBLIC_GITEA_REPO"; exit 2; }
echo "✅ vars OK"
- name: Build + deploy staging (blue) then smoke
env:
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
# backup tags (best effort)
TS="$(date -u +%Y%m%d-%H%M%S)"
echo "TS=$TS" >> /tmp/deploy.env
docker image tag archicratie-web:blue "archicratie-web:blue.BAK.${TS}" || true
docker image tag archicratie-web:green "archicratie-web:green.BAK.${TS}" || true
# build + restart staging (blue=8081)
docker compose build --no-cache web_blue
docker compose up -d --force-recreate web_blue
# smoke staging (local port)
curl -fsS "http://127.0.0.1:8081/para-index.json" >/dev/null
curl -fsS "http://127.0.0.1:8081/annotations-index.json" >/dev/null
curl -fsS "http://127.0.0.1:8081/pagefind/pagefind.js" >/dev/null
CANON="$(curl -fsS "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
echo "canonical(blue)=$CANON"
echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || {
echo "❌ staging canonical mismatch"; exit 3;
}
echo "✅ staging OK"
- name: Build + deploy live (green) then smoke + rollback if needed
env:
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}"
rollback() {
echo "⚠️ rollback green -> previous image tag (best effort)"
docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true
docker compose up -d --force-recreate web_green || true
}
set +e
docker compose build --no-cache web_green
docker compose up -d --force-recreate web_green
curl -fsS "http://127.0.0.1:8082/para-index.json" >/dev/null
curl -fsS "http://127.0.0.1:8082/annotations-index.json" >/dev/null
curl -fsS "http://127.0.0.1:8082/pagefind/pagefind.js" >/dev/null
CANON="$(curl -fsS "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
echo "canonical(green)=$CANON"
echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || {
echo "❌ live canonical mismatch"; rollback; exit 4;
}
echo "✅ live OK"
set -e

7
.gitignore vendored
View File

@@ -3,6 +3,10 @@
.env.* .env.*
!.env.example !.env.example
# dev-only
public/_auth/whoami
public/_auth/whoami/*
# --- local backups --- # --- local backups ---
*.bak *.bak
*.bak.* *.bak.*
@@ -21,3 +25,6 @@ dist/
# local backups # local backups
Dockerfile.bak.* Dockerfile.bak.*
public/favicon_io.zip public/favicon_io.zip
# macOS
.DS_Store

View File

@@ -12,7 +12,7 @@ ENV npm_config_update_notifier=false \
# (Optionnel mais propre) git + certificats # (Optionnel mais propre) git + certificats
RUN apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update \ RUN apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update \
&& apt-get install -y --no-install-recommends ca-certificates git \ && apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Déps dabord (cache Docker) # Déps dabord (cache Docker)
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
@@ -25,9 +25,21 @@ COPY . .
ARG PUBLIC_GITEA_BASE ARG PUBLIC_GITEA_BASE
ARG PUBLIC_GITEA_OWNER ARG PUBLIC_GITEA_OWNER
ARG PUBLIC_GITEA_REPO ARG PUBLIC_GITEA_REPO
# ✅ Canonical + sitemap base (astro.config.mjs lit process.env.PUBLIC_SITE)
ARG PUBLIC_SITE
# ✅ Garde-fou : si 1 → build fail si PUBLIC_SITE absent
ARG REQUIRE_PUBLIC_SITE=0
ENV PUBLIC_GITEA_BASE=$PUBLIC_GITEA_BASE \ ENV PUBLIC_GITEA_BASE=$PUBLIC_GITEA_BASE \
PUBLIC_GITEA_OWNER=$PUBLIC_GITEA_OWNER \ PUBLIC_GITEA_OWNER=$PUBLIC_GITEA_OWNER \
PUBLIC_GITEA_REPO=$PUBLIC_GITEA_REPO PUBLIC_GITEA_REPO=$PUBLIC_GITEA_REPO \
PUBLIC_SITE=$PUBLIC_SITE \
REQUIRE_PUBLIC_SITE=$REQUIRE_PUBLIC_SITE
# ✅ antifragile : refuse de builder sans PUBLIC_SITE quand on lexige
RUN node -e "if (process.env.REQUIRE_PUBLIC_SITE==='1' && !process.env.PUBLIC_SITE) { console.error('FATAL: PUBLIC_SITE is required (canonical/sitemap).'); process.exit(1) }"
# Build Astro (postbuild tourne via npm scripts) # Build Astro (postbuild tourne via npm scripts)
RUN npm run build RUN npm run build
@@ -38,4 +50,4 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/ /usr/share/nginx/html/ COPY --from=build /app/dist/ /usr/share/nginx/html/
RUN find /usr/share/nginx/html -type d -exec chmod 755 {} \; \ RUN find /usr/share/nginx/html -type d -exec chmod 755 {} \; \
&& find /usr/share/nginx/html -type f -exec chmod 644 {} \; && find /usr/share/nginx/html -type f -exec chmod 644 {} \;
EXPOSE 80 EXPOSE 80

View File

@@ -10,41 +10,101 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs"; import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs";
import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js"; import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
const must = (name, fn) => { /**
if (typeof fn !== "function") { * Cast minimal pour satisfaire @ts-check sans dépendre de types internes Astro/Unified.
throw new Error(`[astro.config] rehype plugin "${name}" is not a function (export default vs named?)`); * @param {unknown} x
} * @returns {any}
return fn; */
}; 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({ export default defineConfig({
output: "static", output: "static",
trailingSlash: "always", trailingSlash: "always",
site: process.env.PUBLIC_SITE ?? "http://localhost:4321", site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
integrations: [ integrations: [
mdx(), // Important: MDX hérite du pipeline markdown (ids p-… + autres plugins)
mdx({ extendMarkdownConfig: true }),
sitemap({ sitemap({
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"), 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: { markdown: {
rehypePlugins: [ rehypePlugins: [
must("rehype-slug", rehypeSlug), asAny(rehypeSlug),
[must("rehype-autolink-headings", rehypeAutolinkHeadings), { behavior: "append" }], [asAny(rehypeAutolinkHeadings), { behavior: "append" }],
must("rehype-paragraph-ids", rehypeParagraphIds), asAny(rehypeDetailsSections),
asAny(rehypeParagraphIds),
asAny(rehypeDedupeIds),
], ],
}, },
}); });

7
bridge/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --omit=dev
COPY server.mjs ./
EXPOSE 8787
CMD ["node","server.mjs"]

View File

@@ -0,0 +1,15 @@
services:
issue_bridge:
build: ./bridge
environment:
GITEA_API_BASE: "http://gitea:3000"
GITEA_TOKEN: "${GITEA_TOKEN}"
GITEA_OWNER: "Archicratia"
GITEA_REPO: "archicratie-edition"
restart: unless-stopped
networks:
- internal
networks:
internal:
external: true

10
bridge/package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "issue-bridge",
"private": true,
"type": "module",
"dependencies": {
"express": "^4.19.2",
"multer": "^1.4.5-lts.1"
}
}

89
bridge/server.mjs Normal file
View File

@@ -0,0 +1,89 @@
import express from "express";
import multer from "multer";
const app = express();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } }); // 25 MB
const {
GITEA_API_BASE, // ex: http://gitea:3000 (ou https://forge.tld)
GITEA_TOKEN, // PAT du bot
GITEA_OWNER, // owner/org
GITEA_REPO // repo
} = process.env;
function mustEnv(name) {
if (!process.env[name]) throw new Error(`Missing env ${name}`);
}
["GITEA_API_BASE","GITEA_TOKEN","GITEA_OWNER","GITEA_REPO"].forEach(mustEnv);
function isEditor(req) {
// Adapte selon tes headers Authelia. Souvent Remote-Groups / Remote-User.
const groups = String(req.header("Remote-Groups") || req.header("X-Remote-Groups") || "");
return groups.split(/[,\s]+/).includes("editors");
}
async function giteaFetch(path, init = {}) {
const url = String(GITEA_API_BASE).replace(/\/+$/, "") + path;
const headers = new Headers(init.headers || {});
headers.set("Authorization", `token ${GITEA_TOKEN}`);
return fetch(url, { ...init, headers });
}
app.get("/health", (_req, res) => res.json({ ok: true }));
app.post("/media", upload.single("file"), async (req, res) => {
try {
if (!isEditor(req)) return res.status(403).json({ ok: false, error: "forbidden" });
const file = req.file;
const title = String(req.body.title || "").trim();
const body = String(req.body.body || "").trim();
const suggestedName = String(req.body.suggestedName || "").trim();
if (!file) return res.status(400).json({ ok: false, error: "missing_file" });
if (!title) return res.status(400).json({ ok: false, error: "missing_title" });
if (!body) return res.status(400).json({ ok: false, error: "missing_body" });
// 1) Create issue
const r1 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body })
});
if (!r1.ok) {
const t = await r1.text().catch(() => "");
return res.status(502).json({ ok: false, step: "create_issue", status: r1.status, detail: t.slice(0, 2000) });
}
const issue = await r1.json();
const index = issue?.number ?? issue?.index;
const issueUrl = issue?.html_url;
if (!index) return res.status(502).json({ ok: false, step: "create_issue", error: "missing_issue_index" });
// 2) Upload attachment (multipart field name = "attachment") :contentReference[oaicite:1]{index=1}
const fd = new FormData();
fd.append("attachment", new Blob([file.buffer], { type: file.mimetype || "application/octet-stream" }), file.originalname);
const q = suggestedName ? `?name=${encodeURIComponent(suggestedName)}` : "";
const r2 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues/${encodeURIComponent(String(index))}/assets${q}`, {
method: "POST",
body: fd
});
if (!r2.ok) {
const t = await r2.text().catch(() => "");
return res.status(502).json({ ok: false, step: "upload_asset", status: r2.status, detail: t.slice(0, 2000), issueUrl });
}
const asset = await r2.json().catch(() => ({}));
return res.json({ ok: true, issueUrl, issueIndex: index, asset });
} catch (e) {
return res.status(500).json({ ok: false, error: String(e?.message || e) });
}
});
app.listen(8787, "0.0.0.0", () => {
console.log("issue-bridge listening on :8787");
});

View File

@@ -5,6 +5,8 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
network: host network: host
args: args:
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: "https://staging.archicratie.trans-hands.synology.me"
PUBLIC_GITEA_BASE: ${PUBLIC_GITEA_BASE} PUBLIC_GITEA_BASE: ${PUBLIC_GITEA_BASE}
PUBLIC_GITEA_OWNER: ${PUBLIC_GITEA_OWNER} PUBLIC_GITEA_OWNER: ${PUBLIC_GITEA_OWNER}
PUBLIC_GITEA_REPO: ${PUBLIC_GITEA_REPO} PUBLIC_GITEA_REPO: ${PUBLIC_GITEA_REPO}
@@ -20,6 +22,8 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
network: host network: host
args: args:
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: "https://archicratie.trans-hands.synology.me"
PUBLIC_GITEA_BASE: ${PUBLIC_GITEA_BASE} PUBLIC_GITEA_BASE: ${PUBLIC_GITEA_BASE}
PUBLIC_GITEA_OWNER: ${PUBLIC_GITEA_OWNER} PUBLIC_GITEA_OWNER: ${PUBLIC_GITEA_OWNER}
PUBLIC_GITEA_REPO: ${PUBLIC_GITEA_REPO} PUBLIC_GITEA_REPO: ${PUBLIC_GITEA_REPO}
@@ -27,4 +31,4 @@ services:
container_name: archicratie-web-green container_name: archicratie-web-green
ports: ports:
- "127.0.0.1:8082:80" - "127.0.0.1:8082:80"
restart: unless-stopped restart: unless-stopped

View File

@@ -10,6 +10,15 @@ Si un seul de ces 3 paramètres est faux → on obtient :
- 404 / redirect login inattendu - 404 / redirect login inattendu
- ou un repo/owner incorrect - 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) ## 1) Variables utilisées (publique, côté build Astro)
- `PUBLIC_GITEA_BASE` - `PUBLIC_GITEA_BASE`

View File

@@ -1,5 +1,15 @@
# Déploiement production (Synology DS220+ / DSM 7.3) — Astro → Nginx statique # Déploiement production (Synology DS220+ / DSM 7.3) — Astro → Nginx statique
> ✅ **CANONIQUE** — Procédure de référence “prod DS220+ / DSM 7.3”.
> 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 Dernière mise à jour : 2026-02-01
Ce document décrit une mise en place stable sur NAS : Ce document décrit une mise en place stable sur NAS :
@@ -73,6 +83,17 @@ 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 [ -f "$f" ] && echo "---- $f" && grep -nE '^PUBLIC_GITEA_(BASE|OWNER|REPO)=' "$f" || true
done 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é) ## 5) Reverse Proxy DSM (le point clé)
### DSM 7.3 : ### DSM 7.3 :

View File

@@ -0,0 +1,327 @@
# SPEC — Annotations éditoriales (YAML v1) + merge + anti-doublon
> Objectif : permettre aux tickets (Gitea) de déposer “Références / Médias / Commentaires” dans `src/annotations/**`,
> de façon univoque, stable, et sans régression.
## 0) Contexte et intention
Le site est statique. Lédition collaborative se fait via :
- un mode “proposition” (UI / modal)
- un ticket Gitea (issue) standardisé
- un script dapplication côté éditeur (`apply-ticket.mjs` ou équivalent)
- génération dun YAML dannotations versionné dans Git
La donnée dannotation doit être :
- **audit-able** (Git)
- **merge-able** (sans tout casser)
- **stable** (IDs paragraphes / liens / médias)
- **scalable** (éviter YAML monstrueux à long terme)
## 1) Arborescence canonique
### 1.1 Un workKey par “ouvrage / section du site”
On veut une univocité entre :
- SiteNav (Méthode, Essai-thèse, Traité, Cas IA, Glossaire, Atlas)
et
- larborescence annotations
Proposition canonique (workKey = route racine) :
- `methode`
- `archicrat-ia` (Essai-thèse ArchiCraT-IA)
- `traite`
- `ia`
- `glossaire`
- `atlas`
### 1.2 Règle de stockage “v1”
**Par page**, un YAML unique :
src/annotations/<workKey>/<slugSansWorkKey>.yml
Exemples :
- Page : `/archicrat-ia/prologue/`
- slug content = `archicrat-ia/prologue`
- fichier : `src/annotations/archicrat-ia/prologue.yml`
- Page : `/traite/00-demarrage/`
- fichier : `src/annotations/traite/00-demarrage.yml`
> Note : “slugSansWorkKey” = la partie après `<workKey>/`.
> Sil y a des sous-dossiers (chapitres), le chemin reflète la structure : `chapitre-1/section-a.yml` si on choisit du sharding.
## 2) Question “gros YAML” : page unique vs sharding par paragraphe
### 2.1 Option A (v1 recommandée) : 1 YAML par page
Avantages :
- simple
- peu de fichiers
- diff lisible si volume modéré
- cohérent avec un modèle “annotations par page”
Inconvénients :
- YAML peut grossir si milliers dannotations
### 2.2 Option B (v2 future) : sharding par paragraphe
src/annotations/<workKey>/<slugSansWorkKey>/<paraId>.yml
Avantages :
- fichiers petits
- merges moins conflictuels
Inconvénients :
- plus de fichiers
- tooling plus complexe (indexation + merge multi-fichiers)
### 2.3 Recommandation de mission (sans casser lexistant)
- On démarre en **Option A**.
- On se garde une migration future (v2) quand le volume réel le justifie.
- On impose dès v1 : **clé unique + merge déterministe + anti-doublon**, ce qui rend la migration future possible.
## 3) Format YAML v1 (schéma complet)
### 3.1 Top-level
en yaml :
schema: 1
# Optionnel mais recommandé (doit matcher la page)
page: "<workKey>/<slugSansWorkKey>"
meta:
title: "Titre de la page (optionnel)"
updatedAt: "2026-02-21T12:34:56Z" # ISO8601
updatedBy: "username" # compte editor
source:
kind: "ticket"
id: 123
url: "https://gitea.../issues/123"
paras:
"<paraId>":
references: []
media: []
comments: []
### 3.2 paras : clé = paraId (ex: p-0-d7974f88)
Chaque paragraphe peut porter 3 types déléments :
references
media
comments
Règle : si une section est vide, elle peut être [] ou absente.
Mais pour simplifier les merges, on recommande de garder la forme canonique avec [].
## 4) Formats des items + clés uniques
### 4.1 References
#### 4.1.1 Format
references:
- id: "ref:doi:10.1234/abcd.efgh" # clé stable (voir 4.1.2)
kind: "doi" # doi | url | isbn | arxiv | hal | other
label: "Titre court"
target: "https://doi.org/10.1234/abcd.efgh"
note: "Pourquoi cest pertinent (optionnel)"
addedAt: "2026-02-21T12:34:56Z"
addedBy: "username"
#### 4.1.2 Règle de clé unique (anti-doublon)
id doit être stable et déterministe :
doi → ref:doi:<doi>
isbn → ref:isbn:<isbn>
url → ref:url:<normalizedUrl>
Normalisation URL (v1) : au minimum
trim
lowercase scheme/host
retirer trailing slash si non significatif
conserver query si importante
#### 4.1.3 Merge / précédence
Quand on merge deux listes references :
union par id (clé unique)
si même id existe des deux côtés :
conserver kind/target de litem le plus “riche” (target non vide gagne)
concat/merge note :
si notes différentes : garder les deux en les séparant (ex: noteA + "\n---\n" + noteB)
addedAt : conserver le plus ancien
addedBy : conserver le premier (ou liste si on veut, mais v1 simple : first)
### 4.2 Media
#### 4.2.1 Format
media:
- id: "media:image:sha256:abcd..." # clé stable (voir 4.2.2)
type: "image" # image | video | audio | file
src: "/public/media/<workKey>/<slugSansWorkKey>/<paraId>/<filename>"
caption: "Légende (optionnel)"
credit: "Auteur/source (optionnel)"
license: "CC-BY (optionnel)"
addedAt: "2026-02-21T12:34:56Z"
addedBy: "username"
#### 4.2.2 Règle de clé unique
id déterministe :
idéal : hash du fichier (sha256)
sinon : hash de type + src
v1 (si on ne calcule pas de hash fichier) :
media:<type>:<src>
#### 4.2.3 Merge / précédence
union par id
si collision :
garder src identique (sinon cest un bug)
fusionner caption/credit/license selon “non vide gagne”
addedAt : plus ancien
### 4.3 Comments
#### 4.3.1 Format
comments:
- id: "cmt:20260221T123456Z:username:0001"
kind: "comment" # comment | question | objection | todo | validation
text: "Texte du commentaire"
status: "open" # open | resolved
addedAt: "2026-02-21T12:34:56Z"
addedBy: "username"
source:
kind: "ticket"
id: 123
#### 4.3.2 Clé unique
Les commentaires sont “append-only” → id peut être générée (timestamp + user + compteur)
Anti-doublon : si on ré-applique un ticket, on refuse de dupliquer un id existant.
#### 4.3.3 Merge / précédence
union par id
collisions rares, mais si elles arrivent :
si textes différents → garder les deux (on renomme lid du second)
## 5) Règles globales de merge (résumé)
Quand on applique un ticket sur un YAML existant :
vérifier schema == 1
vérifier page si présent :
doit matcher <workKey>/<slugSansWorkKey>
paras :
créer paras[paraId] si absent
pour chaque liste (references/media/comments) :
merge par id (anti-doublon)
appliquer règles de précédence (non vide gagne / concat note / append-only comments)
## 6) Table de correspondance “UI ticket → YAML”
Cette table permet à un successeur IA dimplémenter apply-ticket.mjs sans ambiguïté.
### 6.1 Champs UI minimaux
workKey (sélection implicite via page)
pagePath (ex: /archicrat-ia/prologue/)
pageSlug (ex: archicrat-ia/prologue)
paraId (ex: p-0-d7974f88)
kind :
reference
media
comment
### 6.2 Mapping exact
| UI kind | UI champs | YAML cible |
| --------- | ----------------------------------------------------------- | ---------------------------- |
| reference | kind(doi/url/isbn), target, label, note | `paras[paraId].references[]` |
| media | type(image/video/audio/file), src, caption, credit, license | `paras[paraId].media[]` |
| comment | kind(comment/question/objection/todo/validation), text | `paras[paraId].comments[]` |
### 6.3 Règles de génération dID (implémentation)
reference.id :
doi : ref:doi:${doi}
isbn : ref:isbn:${isbn}
url : ref:url:${normalize(url)}
media.id :
media:${type}:${src}
comment.id :
cmt:${timestamp}:${user}:${counter}
## 7) Validation YAML (sanity)
Avant commit (et en CI) :
YAML parse OK
schema OK
page si présent cohérent
paras est un mapping
paraId match pattern : ^p-\d+-[a-f0-9]{8}$ (existant)
src media pointe dans /public/media/... (ou /media/... si on choisit un alias, mais v1 canon : /public/media/...)
## 8) Notes de compatibilité
Les routes “Essai-thèse” ont été migrées vers /archicrat-ia/*.
Les anciennes routes /archicratie/archicrat-ia/* peuvent exister en legacy, mais la donnée canonique dannotation doit suivre le workKey final (archicrat-ia).
## 9) Ce que létape 9 devra implémenter
pipeline : ticket → YAML (apply-ticket)
index : build-annotations-index + check-annotations
tooling : détection médias orphelins / liens cassés
éventuellement : migration vers sharding par paragraphe (v2) si volume réel le justifie

View File

@@ -43,61 +43,15 @@ Le flow ne doit jamais ouvrir deux onglets.
- un seul `a.target="_blank"` (ou équivalent) déclenché - un seul `a.target="_blank"` (ou équivalent) déclenché
- sur click : handler doit neutraliser les propagations parasites - sur click : handler doit neutraliser les propagations parasites
### Vérification (NAS) ## Diagnostic (canonique)
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
Doit retourner 0 ligne. Le diagnostic détaillé est centralisé dans `docs/TROUBLESHOOTING.md` pour éviter les doublons.
## 3) Diagnostic “trace ouverture onglet” (navigateur) - 404 / non autorisé / redirect login :
- voir : `TROUBLESHOOTING.md#proposer-404`
- cause la plus fréquente : `PUBLIC_GITEA_OWNER/REPO` faux (souvent casse)
Dans la console, tu peux surcharger temporairement les mécanismes douverture pour tracer : - Double onglet :
- voir : `TROUBLESHOOTING.md#proposer-double-onglet`
- cause la plus fréquente : double handler (bubbling) ou `window.open` + `a.click()`
si un window.open survient,
ou si un a.click target _blank est appelé.
But : prouver quil ny a quun seul événement douverture.
## 4) URL attendue (forme)
Longlet doit ressembler à :
{PUBLIC_GITEA_BASE}/{OWNER}/{REPO}/issues/new?title=...&body=...
Important : owner et repo doivent être exactement ceux du repo canonique.
## 5) Pré-requis daccès
Lutilisateur doit être loggé sur Gitea pour accéder à /issues/new
Si non loggé : redirect vers /user/login (comportement normal)
## 6) Tests fonctionnels (checklist)
Ouvrir une page chapitre (ex chapitre 4)
Clic Proposer (sur un paragraphe)
Choix 1 puis choix 2
Vérifier :
1 seul onglet
URL du repo correct
formulaire new issue visible
title/body pré-remplis (chemin + ancre + texte actuel)
Créer lissue → vérifier le traitement CI/runner
## 7) Pannes typiques + causes
404 sur issue/new : PUBLIC_GITEA_OWNER/REPO faux (souvent casse)
2 onglets : double handler (bubbling + ouverture multiple)
pas de favicon : cache ou absence dans public/ → rebuild

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. 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 ## 0) Repères essentiels
Noms & domaines Noms & domaines
• Site public (prod) : https://archicratie.trans-hands.synology.me • 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 rm -rf node_modules .astro dist
npm ci npm ci
npm run dev npm run dev
</details>

View File

@@ -4,7 +4,7 @@ Document “pivot” : liens, invariants, conventions, commandes réflexes.
## 0) Invariants (à ne pas casser) ## 0) Invariants (à ne pas casser)
- **Source de vérité Git** : `origin/main` sur :contentReference[oaicite:0]{index=0}. - **Source de vérité Git** : origin/main (repo Archicratia/archicratie-edition sur Gitea).
- **Prod** : conteneur `archicratie-web-*` (nginx) derrière reverse proxy DSM. - **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. - **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). - **Branches** : `main` = travail ; `master` = legacy/compat (alignée mais protégée).

View File

@@ -1,5 +1,15 @@
# OPS Runbook — Archicratie Web (NAS Synology DS220 + Gitea) # 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 ## 0. Objectif
Ce document décrit la procédure **exacte** pour : Ce document décrit la procédure **exacte** pour :
- maintenir un état cohérent entre **Local (Mac Studio)**, **Gitea**, **NAS (prod)** ; - maintenir un état cohérent entre **Local (Mac Studio)**, **Gitea**, **NAS (prod)** ;

View File

@@ -71,8 +71,7 @@ Ce document décrit la synchronisation **sans ambiguïté** entre :
### Étape B — NAS : aligner `current` sur `origin/main` ### Étape B — NAS : aligner `current` sur `origin/main`
Sur NAS, git nest pas forcément installé : on utilise un conteneur git. Sur NAS, git nest pas forcément installé : on utilise un conteneur git.
en sh :
en sh:
APP="/volume2/docker/archicratie-web/current" APP="/volume2/docker/archicratie-web/current"
U_ID="$(id -u)"; G_ID="$(id -g)" U_ID="$(id -u)"; G_ID="$(id -g)"

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.

176
docs/START-HERE.md Normal file
View File

@@ -0,0 +1,176 @@
# START-HERE — Archicratie / Édition Web (v2)
> Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO)
## 0) TL;DR (la règle dor)
- **Gitea = source canonique**.
- **main est protégé** : toute modification passe par **branche → PR → CI → merge**.
- **Le NAS nest pas la source** : si un hotfix est fait sur NAS, on **backporte** via PR immédiatement.
- **Le site est statique Astro** : la prod sert du HTML (nginx), laccès est contrôlé au niveau reverse-proxy (Traefik + Authelia).
## 1) Architecture mentale (ultra simple)
- **DEV (Mac Studio)** : édition + tests + commit + push
- **Gitea** : dépôt canon + PR + CI (CI.yaml)
- **NAS (DS220+)** : déploiement “blue/green”
- `web_blue` (staging upstream) → `127.0.0.1:8081`
- `web_green` (live upstream) → `127.0.0.1:8082`
- **Edge (Traefik)** : route les hosts
- `staging.archicratie...` → 8081
- `archicratie...` → 8082
- **Authelia** devant, via middleware `chain-auth@file`
## 2) Répertoires & conventions (repo)
### 2.1 Contenu canon (édition)
- `src/content/**` : contenu MD / MDX canon (Astro content collections)
- `src/pages/**` : routes Astro (index, [...slug], etc.)
- `src/components/**` : composants UI (SiteNav, TOC, SidePanel, etc.)
- `src/layouts/**` : layouts (EditionLayout, SiteLayout)
- `src/styles/**` : CSS global
### 2.2 Annotations (pré-Édition “tickets”)
- `src/annotations/<workKey>/<slug>.yml`
- Exemple : `src/annotations/archicrat-ia/prologue.yml`
- Objectif : stocker “Références / Médias / Commentaires” par page et par paragraphe (`p-...`).
### 2.3 Scripts (tooling / build)
- `scripts/inject-anchor-aliases.mjs` : injection aliases dans dist
- `scripts/dedupe-ids-dist.mjs` : retire IDs dupliqués dans dist
- `scripts/build-para-index.mjs` : index paragraphes (postbuild / predev)
- `scripts/build-annotations-index.mjs` : index annotations (postbuild / predev)
- `scripts/check-anchors.mjs` : contrat stabilité dancres (CI)
- `scripts/check-annotations*.mjs` : sanity YAML + médias
> Important : les scripts sont **partie intégrante** de la stabilité (IDs/ancres/indexation).
> On évite “la magie” : tout est scripté + vérifié.
## 3) Workflow Git “pro” (main protégé)
### 3.1 Cycle standard (toute modif)
en bash :
git checkout main
git pull --ff-only
BR="chore/xxx-$(date +%Y%m%d)"
git checkout -b "$BR"
# dev…
npm i
npm run build
npm run test:anchors
git add -A
git commit -m "xxx: description claire"
git push -u origin "$BR"
### 3.2 PR vers main
Ouvrir PR dans Gitea
CI doit être verte
Merge PR → main
### 3.3 Cas spécial : hotfix prod (NAS)
On peut faire un hotfix “urgence” en prod/staging si nécessaire…
MAIS : létat final doit revenir dans Gitea : branche → PR → CI → merge.
## 4) Déploiement (NAS) — principe
### 4.1 Release pack
On génère un pack “reproductible” (source + config + scripts) puis on déploie.
### 4.2 Blue/Green
web_blue = staging upstream (8081)
web_green = live upstream (8082)
Edge Traefik sélectionne quel host pointe vers quel upstream.
## 5) Check-list “≤ 10 commandes” (happy path complet)
### 5.1 DEV (Mac)
git checkout main && git pull --ff-only
git checkout -b chore/my-change-$(date +%Y%m%d)
npm i
rm -rf .astro node_modules/.vite dist
npm run build
npm run test:anchors
npm run dev
### 5.2 Push + PR
git add -A
git commit -m "chore: my change"
git push -u origin chore/my-change-YYYYMMDD
# ouvrir PR dans Gitea
### 5.3 Déploiement NAS (résumé)
Voir docs/runbooks/DEPLOY-BLUE-GREEN.md.
## 6) Problèmes “classiques” + diagnostic rapide
### 6.1 “Le staging ne ressemble pas au local”
# Comparer upstream direct 8081 vs 8082 :
curl -sS http://127.0.0.1:8081/ | head -n 2
curl -sS http://127.0.0.1:8082/ | head -n 2
# Vérifier quel routeur edge répond (header diag) :
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router'
# Lire docs/runbooks/EDGE-TRAEFIK.md.
### 6.2 Canonical incorrect (localhost en prod)
Cause racine : site dans Astro = PUBLIC_SITE non injecté au build.
Fix canonique : voir docs/runbooks/ENV-PUBLIC_SITE.md.
Test :
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1
### 6.3 Contrat “anchors” en échec après migration dURL
Quand on déplace des routes (ex: /archicratie/archicrat-ia/* → /archicrat-ia/*), le test dancres peut échouer même si les IDs nont pas changé, car les pages ont changé de chemin.
# Procédure safe :
Backup baseline :
cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S)
Mettre à jour les clés (chemins) sans toucher aux IDs :
node - <<'NODE'
import fs from 'fs';
const p='tests/anchors-baseline.json';
const j=JSON.parse(fs.readFileSync(p,'utf8'));
const out={};
for (const [k,v] of Object.entries(j)) {
const nk = k.replace(/^archicratie\/archicrat-ia\//, 'archicrat-ia/');
out[nk]=v;
}
fs.writeFileSync(p, JSON.stringify(out,null,2)+'\n');
console.log('updated keys:', Object.keys(j).length, '->', Object.keys(out).length);
NODE
Re-run :
npm run test:anchors
## 7) Ce que létape 9 doit faire (orientation)
Stabiliser le pipeline “tickets → YAML annotations”
Formaliser la spec YAML + merge + anti-doublon (voir docs/EDITORIAL-ANNOTATIONS-SPEC.md)
Durcir lonboarding (ce START-HERE + runbooks)
Éviter les régressions par tests (anchors / annotations / smoke)

View File

@@ -15,6 +15,7 @@ Toujours isoler : **Local**, **Gitea**, **NAS**, **Navigateur**.
--- ---
<a id="proposer-404"></a>
## 1) “Proposer” ouvre Gitea mais retourne 404 / non autorisé ## 1) “Proposer” ouvre Gitea mais retourne 404 / non autorisé
### Symptôme ### Symptôme
@@ -38,6 +39,7 @@ Puis rebuild + restart du container + smoke.
--- ---
<a id="proposer-double-onglet"></a>
## 2) Double onglet à la validation du flow “Proposer” ## 2) Double onglet à la validation du flow “Proposer”
### Symptôme ### Symptôme
@@ -58,13 +60,15 @@ en sh :
curl -fsS http://127.0.0.1:8082/archicratie/archicrat-ia/chapitre-4/ > /tmp/page.html curl -fsS http://127.0.0.1:8082/archicratie/archicrat-ia/chapitre-4/ > /tmp/page.html
grep -n "window.open" /tmp/page.html | head grep -n "window.open" /tmp/page.html | head
# Fix Fix
garder un seul mécanisme douverture garder un seul mécanisme douverture
sur click : preventDefault() + stopImmediatePropagation() sur click : preventDefault() + stopImmediatePropagation()
<a id="Favicon-504-erreurs"></a>
## 3) Favicon 504 / erreurs console sur favicon ## 3) Favicon 504 / erreurs console sur favicon
# Symptôme # Symptôme
Console navigateur : GET /favicon.ico 504 Console navigateur : GET /favicon.ico 504
@@ -101,6 +105,7 @@ git fetch impossible sur le NAS.
Git non installé sur DSM shell. Git non installé sur DSM shell.
# Fix standard (recommandé) # Fix standard (recommandé)
Utiliser un conteneur git : Utiliser un conteneur git :
APP="/volume2/docker/archicratie-web/current" APP="/volume2/docker/archicratie-web/current"
@@ -176,9 +181,9 @@ BuildKit is enabled but the buildx component is missing
client version ... too new. Maximum supported API version ... client version ... too new. Maximum supported API version ...
Fix “robuste” (principe) # Fix “robuste” (principe)
# installer buildx si nécessaire installer buildx si nécessaire
si DSM/docker API ancienne : définir DOCKER_API_VERSION=<compatible> (selon ton environnement) si DSM/docker API ancienne : définir DOCKER_API_VERSION=<compatible> (selon ton environnement)

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. 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). 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 ## 2) Le mapping dalias
- Fichier versionné (ex) : `docs/anchor-aliases.json` - Fichier versionné (ex) : `src/anchors/anchor-aliases.json`
- Format : `oldId -> newId` par page - Format : `oldId -> newId` par page
Ex en json : 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,427 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg49"
sodipodi:docname="archicratie-web-edition-blue-green-runbook-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview49"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="726.17247"
inkscape:cy="401.21029"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg49" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Runbook Blue/Green (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Mise à jour 2026-02-20 — release-pack → releases/&lt;ts&gt;/app → current → docker compose web_blue/web_green</text>
<rect
x="40"
y="130"
width="500"
height="240"
class="box"
id="rect5" />
<text
x="58"
y="160"
class="h"
id="text6">0) Pré-requis</text>
<text
x="58"
y="184"
class="t"
id="text7">main protégé → travail via branches + PR</text>
<text
x="58"
y="202"
class="t"
id="text8">CI doit rester source de vérité</text>
<text
x="58"
y="220"
class="t"
id="text9">Éviter d'éditer une release en prod (hotfix = exception)</text>
<text
x="58"
y="238"
class="s"
id="text10">Si hotfix: on le re-synchronise ensuite dans Git (cf. étape 5)</text>
<rect
x="40"
y="400"
width="500"
height="260"
class="box2"
id="rect10" />
<text
x="58"
y="430"
class="h"
id="text11">1) Préparer une release (atelier DEV)</text>
<text
x="58"
y="454"
class="mono"
id="text12">npm ci &amp;&amp; npm run build</text>
<text
x="58"
y="472"
class="mono"
id="text13">release-pack.sh → tarball/artefact</text>
<text
x="58"
y="490"
class="mono"
id="text14">inclut dist/ + pagefind + indexes + build stamp</text>
<text
x="58"
y="508"
class="t"
id="text15">ouvrir PR → merge → CI</text>
<rect
x="40"
y="690"
width="500"
height="170"
class="box"
id="rect15" />
<text
x="58"
y="720"
class="h"
id="text16">2) Déposer sur NAS</text>
<text
x="58"
y="744"
class="mono"
id="text17">/volume2/docker/archicratie-web/releases/&lt;ts&gt;/app</text>
<text
x="58"
y="762"
class="mono"
id="text18">current → pointe vers la release active</text>
<text
x="58"
y="780"
class="t"
id="text19">build context docker = current OU release/app (selon compose)</text>
<rect
x="600"
y="130"
width="520"
height="210"
class="box"
id="rect19" />
<text
x="618"
y="160"
class="h"
id="text20">3) Build images</text>
<text
x="618"
y="184"
class="mono"
id="text21">sudo env DOCKER_API_VERSION=1.43 \</text>
<text
x="618"
y="202"
class="mono"
id="text22">docker compose -f docker-compose.yml build --no-cache \</text>
<text
x="618"
y="220"
class="mono"
id="text23"> web_blue web_green</text>
<text
x="618"
y="238"
class="s"
id="text24">les 2 images doivent builder OK</text>
<rect
x="639.93951"
y="378.7897"
width="480.06052"
height="241.21028"
class="box2"
id="rect24" />
<text
x="658"
y="410"
class="h"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">4) Switch trafic (blue ↔ green)</text>
<text
x="658"
y="434"
class="t"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">déterminer couleur active (proxy / conf)</text>
<text
x="658"
y="452"
class="t"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">mettre à jour routing vers l'autre couleur</text>
<text
x="658"
y="470"
class="t"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">reload proxy, vérifier 200/302</text>
<text
x="658"
y="488"
class="mono"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">curl -sSI -H 'Host: staging.*' http://127.0.0.1:18080/</text>
<text
x="658"
y="506"
class="s"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">rollback = revenir à l'ancienne couleur</text>
<rect
x="600"
y="660"
width="520"
height="210"
class="box"
id="rect30" />
<text
x="618"
y="690"
class="h"
id="text31">5) Hotfix de release → re-synchroniser Git (step8)</text>
<text
x="618"
y="714"
class="t"
id="text32">A) NAS: find src -mtime -3 → liste fichiers</text>
<text
x="618"
y="732"
class="t"
id="text33">B) NAS: tar -czf /tmp/hotfix.tgz -T liste</text>
<text
x="618"
y="750"
class="t"
id="text34">C) sha256 + manifest, puis scp vers Mac</text>
<text
x="618"
y="768"
class="t"
id="text35">D) Mac: tar -xzf → rsync --checksum vers repo</text>
<text
x="618"
y="786"
class="t"
id="text36">E) commit sur branche dédiée → push → PR vers main</text>
<rect
x="1180"
y="160"
width="380"
height="520"
class="box2"
id="rect36" />
<text
x="1198"
y="190"
class="h"
id="text37">Arborescence NAS (rappel)</text>
<text
x="1198"
y="214"
class="mono"
id="text38">/volume2/docker/archicratie-web/</text>
<text
x="1198"
y="232"
class="mono"
id="text39"> releases/</text>
<text
x="1198"
y="250"
class="mono"
id="text40"> 20260219-103222/</text>
<text
x="1198"
y="268"
class="mono"
id="text41"> app/ (ctx build)</text>
<text
x="1198"
y="286"
class="mono"
id="text42"> current -&gt; releases/…/app</text>
<text
x="1198"
y="304"
class="mono"
id="text43"> compose/ docker-compose.yml</text>
<text
x="1198"
y="322"
class="s"
id="text44">compose.expanded.yml t'indique le build.context effectif</text>
<path
d="M 540,520 H 642.36006"
class="arrowA"
id="path44"
sodipodi:nodetypes="cc" />
<text
x="545"
y="500"
class="s"
id="text45">→ NAS + build</text>
<path
d="M860 340 C860 360 860 360 860 380"
class="arrowA"
id="path45" />
<text
x="875"
y="365"
class="s"
id="text46">images OK</text>
<path
d="M860 620 C860 640 860 640 860 660"
class="arrowG"
id="path46" />
<text
x="875"
y="645"
class="s"
id="text47">si hotfix</text>
<rect
x="40"
y="20"
width="1520"
height="80"
class="box2"
id="rect47" />
<text
x="58"
y="50"
class="h"
id="text48">Règle d'or</text>
<text
x="58"
y="74"
class="t"
id="text49">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.</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,551 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg1"
sodipodi:docname="archicratie-web-edition-blue-green-runbook-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-blue-green-runbook-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="1.0606602"
inkscape:cx="675.05126"
inkscape:cy="300.28467"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="222"
inkscape:window-y="74"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<style
id="style1"><![CDATA[
.bg { fill: #fff; }
.outer { fill: #fff; stroke: #111; stroke-width: 3; rx: 18; }
.box { fill: #fff; stroke: #111; stroke-width: 2; rx: 14; }
.ok { fill: #eafff1; stroke: #1a7f37; stroke-width: 2; rx: 14; }
.warn { fill: #fff0f0; stroke: #b42318; stroke-width: 2; rx: 14; }
.title { font: 800 40px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.subtitle { font: 500 16px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #333; }
.h2 { font: 800 24px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.h3 { font: 800 20px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.txt { font: 500 16px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #111; }
.small { font: 500 12px system-ui, -apple-system, Segoe UI, Roboto, Arial; fill: #333; }
.mono { font: 500 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #111; }
.arrow { stroke: #111; stroke-width: 2; fill: none; marker-end: url(#arrow); }
.arrowThin { stroke: #111; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
.cap { stroke-linecap: round; stroke-linejoin: round; }
]]></style>
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#111"
id="path1" />
</marker>
</defs>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1">
<!-- Title -->
<text
x="40"
y="55"
class="title"
id="text1">Archicratie Web Edition : Blue/Green Runbook visuel (VERBATIM)</text>
<text
x="40"
y="88"
class="subtitle"
id="text2">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.</text>
<!-- Outer frame -->
<rect
x="30"
y="110"
width="1540"
height="870"
class="outer"
id="rect2" />
<!-- Invariants box -->
<rect
x="60"
y="140"
width="1480"
height="130"
class="box"
id="rect3" />
<text
x="90"
y="175"
class="h2"
id="text3">Invariants (ce qui évite de casser la prod)</text>
<text
x="90"
y="198"
class="txt"
id="text4">• Les 2 slots existent en parallèle : archicratie-web-blue = 127.0.0.1:8081 et archicratie-web-green = 127.0.0.1:8082.</text>
<text
x="90"
y="220"
class="txt"
id="text5">• Traefik edge écoute :18080 et choisit le slot LIVE via /volume2/docker/edge/config/dynamic/20-archicratie-backend.yml.</text>
<text
x="90"
y="242"
class="txt"
id="text7">• Une seule cible active dans Traefik (pas de load-balance non déterministe). Rollback = remettre lURL précédente dans le même fichier.</text>
<!-- RIGUEUR ABSOLUE (ajout non destructif) -->
<rect
x="64"
y="269"
width="1480"
height="30"
rx="12"
class="warn"
id="rect-rigueur" />
<text
x="84"
y="289"
class="txt"
id="text-rigueur"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">• RIGUEUR ABSOLUE : STAGING = slot INACTIF (opposé au LIVE). LIVE = 20-archicratie-backend.yml ; STAGING = 21-archicratie-staging.yml.</text>
<!-- Step columns -->
<!-- Column 1 -->
<rect
x="60"
y="300"
width="470"
height="610"
class="box"
id="rect4" />
<text
x="90"
y="335"
class="h2"
id="text8">Étape 1 — Build &amp; déployer</text>
<text
x="90"
y="365"
class="h3"
id="text9">But</text>
<text
x="90"
y="387"
class="txt"
id="text10">Mettre la nouvelle version sur le slot inactif</text>
<text
x="90"
y="409"
class="txt"
id="text11">(sans toucher au slot LIVE actuel).</text>
<!-- Où box -->
<rect
x="90"
y="435"
width="410"
height="210"
class="box"
id="rect5" />
<text
x="110"
y="465"
class="h3"
id="text12"></text>
<text
x="110"
y="490"
class="mono"
id="text13">/volume2/docker/archicratie-web/current/</text>
<text
x="110"
y="515"
class="txt"
id="text14">Compose : docker-compose.yml</text>
<text
x="110"
y="545"
class="txt"
id="text15">Slots :</text>
<text
x="140"
y="568"
class="mono"
id="text16">web_blue → 127.0.0.1:8081</text>
<text
x="140"
y="590"
class="mono"
id="text17">web_green → 127.0.0.1:8082</text>
<text
x="110"
y="622"
class="small"
id="text18">Le build injecte aussi PUBLIC_GITEA_* via build args</text>
<text
x="110"
y="640"
class="small"
id="text19">(déjà dans ton compose).</text>
<!-- Commands box -->
<rect
x="90"
y="665"
width="410"
height="225"
class="box"
id="rect6" />
<text
x="110"
y="695"
class="h3"
id="text20">Commandes (safe)</text>
<text
x="110"
y="720"
class="txt"
id="text21">1) Choisir le slot cible (inactif)</text>
<text
x="100"
y="742"
class="small"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Astuce : le LIVE = ce que pointe Traefik dans 20-archicratie-backend.yml</text>
<text
x="110"
y="770"
class="txt"
id="text23">2) Build + redémarrer uniquement ce slot</text>
<text
x="130"
y="794"
class="mono"
id="text24">cd /volume2/docker/archicratie-web/current</text>
<text
x="130"
y="814"
class="mono"
id="text25">sudo docker compose build web_green</text>
<text
x="130"
y="834"
class="mono"
id="text26">sudo docker compose up -d --no-deps web_green</text>
<text
x="110"
y="862"
class="small"
id="text27">Remplace web_green par web_blue selon la cible.</text>
<text
x="110"
y="880"
class="small"
id="text28">Ne pas toucher lautre service.</text>
<!-- Arrow to column 2 -->
<path
d="M 530 605 L 560 605"
class="arrow cap"
id="path6" />
<!-- Column 2 -->
<rect
x="560"
y="300"
width="470"
height="610"
class="box"
id="rect7" />
<text
x="590"
y="335"
class="h2"
id="text29">Étape 2 — Switch Traefik (LIVE)</text>
<text
x="590"
y="365"
class="h3"
id="text30">But</text>
<text
x="590"
y="387"
class="txt"
id="text31">Basculer le LIVE en modifiant 1 fichier</text>
<text
x="590"
y="409"
class="txt"
id="text32">et laisser Traefik recharger automatiquement.</text>
<!-- Canon file box -->
<rect
x="565.48694"
y="433.11438"
width="458.08331"
height="176.31372"
class="box"
id="rect8"
ry="0" />
<text
x="610"
y="465"
class="h3"
id="text33">Fichier canonique (LIVE switch)</text>
<text
x="572"
y="490"
class="mono"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/volume2/docker/edge/config/dynamic/20-archicratie-backend.yml</text>
<text
x="610"
y="520"
class="txt"
id="text35">Contient :</text>
<text
x="588"
y="545"
class="mono"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http.services.archicratie_web.loadBalancer.servers[0].url</text>
<text
x="610"
y="575"
class="txt"
id="text37">Ex : http://127.0.0.1:8082 (green)</text>
<text
x="610"
y="597"
class="txt"
id="text38">ou http://127.0.0.1:8081 (blue)</text>
<!-- Procedure warn box -->
<rect
x="567.37256"
y="621.88562"
width="454.31201"
height="279.4281"
class="warn"
id="rect9" />
<text
x="610"
y="644"
class="h3"
id="text39"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:20px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Procédure (anti-casse)</text>
<text
x="576"
y="668"
class="txt"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">1) Backup horodaté du fichier</text>
<text
x="591"
y="690"
class="mono"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">cd /volume2/docker/edge/config/dynamic</text>
<text
x="577"
y="712"
class="mono"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
sodipodi:role="line"
id="tspan2"
x="577"
y="712">sudo cp 20-archicratie-backend.yml</tspan><tspan
sodipodi:role="line"
id="tspan3"
x="577"
y="727">20-archicratie-backend.yml.bak.$(date +%F-%H%M%S)</tspan></text>
<text
x="576"
y="752"
class="txt"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">2) Éditer lURL (un seul backend)</text>
<text
x="591"
y="774"
class="mono"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">sudo vi 20-archicratie-backend.yml</text>
<text
x="591"
y="788"
class="small"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Changer uniquement la valeur url : 8081 ↔ 8082</text>
<text
x="571"
y="810"
class="txt"
id="text45bis"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
sodipodi:role="line"
id="tspan4"
x="571"
y="810">2bis) Mettre à jour 21-archicratie-staging.yml sur lautre port</tspan><tspan
sodipodi:role="line"
id="tspan5"
x="571"
y="830">(opposé au LIVE)</tspan></text>
<text
x="571"
y="856"
class="txt"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">3) Traefik recharge (watch=true)</text>
<text
x="591"
y="878"
class="small"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan71"
x="591"
y="878">Pas de restart requis si provider file watch=true</tspan><tspan
sodipodi:role="line"
id="tspan72"
x="591"
y="893">(ton traefik.yml).</tspan></text>
<!-- Arrow to column 3 -->
<path
d="M 1030 605 L 1060 605"
class="arrow cap"
id="path7" />
<!-- Column 3 -->
<rect
x="1060"
y="300"
width="480"
height="610"
class="box"
id="rect10" />
<text
x="1090"
y="335"
class="h2"
id="text48">Étape 3 — Smoke tests</text>
<text
x="1090"
y="365"
class="h3"
id="text49">But</text>
<text
x="1090"
y="387"
class="txt"
id="text50">Prouver que le nouveau LIVE répond,</text>
<text
x="1090"
y="409"
class="txt"
id="text51">et que lauth (Authelia/whoami) est OK.</text>
<!-- Slot direct ok box -->
<rect
x="1090"
y="435"
width="420"
height="185"
class="ok"
id="rect11" />
<text
x="1110"
y="465"
class="h3"
id="text52">Tests “slot direct” (preuve build)</text>
<text
x="1110"
y="490"
class="txt"
id="text53">Le slot construit doit répondre en 200 :</text>
<text
x="1130"
y="515"
class="mono"
id="text54">curl -sS -I http://127.0.0.1:8081/ | head -n 12</text>
<text
x="1130"
y="540"
class="mono"
id="text55">curl -sS -I http://127.0.0.1:8082/ | head -n 12</text>
<text
x="1110"
y="570"
class="small"
id="text56">Lun des deux peut rester lancien LIVE.</text>
<text
x="1110"
y="590"
class="small"
id="text57">Lobjectif est que le slot cible soit OK.</text>
<!-- Edge tests box -->
<rect
x="1090"
y="638.4342"
width="420"
height="251.56581"
class="box"
id="rect54" />
<text
x="1110"
y="675"
class="h3"
id="text57b"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:20px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Tests “edge” (preuve routage + auth)</text>
<text
x="1110"
y="700"
class="txt"
id="text57c"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16px;line-height:normal;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Host rules : tester AVEC Host header :</text>
<text
x="1130"
y="724"
class="mono"
id="text57d"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
sodipodi:role="line"
id="tspan57d1"
x="1130"
y="724">curl -sS -I -H 'Host:</tspan><tspan
sodipodi:role="line"
id="tspan57d2"
x="1130"
y="739">archicratie.trans-hands.synology.me'</tspan><tspan
sodipodi:role="line"
x="1130"
y="754"
id="tspan57d3">http://127.0.0.1:18080/ | head -n 20</tspan></text>
<text
x="1130"
y="780"
class="mono"
id="text58"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"><tspan
id="tspan1" /></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,437 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg43"
sodipodi:docname="archicratie-web-edition-edge-routing-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview43"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="424.81089"
inkscape:cy="464.14523"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg43" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Edge routing (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Mise à jour 2026-02-20 — Host routing + Authelia + Blue/Green web_*</text>
<rect
x="40"
y="140"
width="420"
height="180"
class="box"
id="rect5" />
<text
x="58"
y="170"
class="h"
id="text6">Client (navigateur)</text>
<text
x="58"
y="194"
class="t"
id="text7">https://archicratie.* / https://staging.archicratie.*</text>
<text
x="58"
y="212"
class="t"
id="text8">cookies authelia_session</text>
<text
x="58"
y="230"
class="s"
id="text9">HEAD/GET → 302 si non auth</text>
<rect
x="60"
y="320"
width="110"
height="28"
class="chip2"
id="rect9" />
<text
x="74"
y="339"
class="mono"
id="text10">HTTPS 443</text>
<rect
x="520"
y="120"
width="520"
height="260"
class="box"
id="rect10" />
<text
x="538"
y="150"
class="h"
id="text11">Reverse-proxy (Nginx / DSM)</text>
<text
x="538"
y="174"
class="t"
id="text12">Routage par Host</text>
<text
x="538"
y="192"
class="t"
id="text13">auth_request → Authelia</text>
<text
x="538"
y="210"
class="t"
id="text14">proxy_pass → service web_blue ou web_green</text>
<text
x="538"
y="228"
class="t"
id="text15">headers: X-Forwarded-* + Host</text>
<text
x="538"
y="246"
class="s"
id="text16">en local: curl -H 'Host: ...' http://127.0.0.1:18080/</text>
<rect
x="540"
y="320"
width="142"
height="28"
class="chip"
id="rect16" />
<text
x="554"
y="339"
class="mono"
id="text17">auth_request</text>
<rect
x="520"
y="420"
width="520"
height="210"
class="box2"
id="rect17" />
<text
x="538"
y="450"
class="h"
id="text18">Auth stack</text>
<text
x="538"
y="474"
class="t"
id="text19">Authelia (portal login)</text>
<text
x="538"
y="492"
class="t"
id="text20">LLDAP (backend LDAP)</text>
<text
x="538"
y="510"
class="t"
id="text21">Redis (sessions / storage)</text>
<text
x="538"
y="528"
class="s"
id="text22">auth.* domain</text>
<rect
x="540"
y="600"
width="250"
height="28"
class="chipW"
id="rect22" />
<text
x="554"
y="619"
class="mono"
id="text23">302 → auth.*?rd=...</text>
<rect
x="1175.0378"
y="237.57942"
width="384.96219"
height="162.42058"
class="box"
id="rect23" />
<text
x="1198"
y="270"
class="h"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Service web_blue (container)</text>
<text
x="1198"
y="294"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">nginx static (dist/)</text>
<text
x="1198"
y="312"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">/pagefind/*, /assets/*</text>
<rect
x="1172.6172"
y="417.57944"
width="387.38275"
height="162.42058"
class="box"
id="rect26" />
<text
x="1198"
y="450"
class="h"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Service web_green (container)</text>
<text
x="1198"
y="474"
class="mono"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">nginx static (dist/)</text>
<text
x="1198"
y="492"
class="mono"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">build identique, couleur swap</text>
<rect
x="1120"
y="600"
width="230"
height="28"
class="chip2"
id="rect29" />
<text
x="1134"
y="619"
class="mono"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">une seule couleur active</text>
<rect
x="40"
y="410"
width="420"
height="220"
class="box2"
id="rect30" />
<text
x="58"
y="440"
class="h"
id="text31">Atelier DEV (local)</text>
<text
x="58"
y="464"
class="mono"
id="text32">astro dev : http://localhost:4321</text>
<text
x="58"
y="482"
class="mono"
id="text33">pas d'authelia</text>
<text
x="58"
y="500"
class="mono"
id="text34">predev génère:</text>
<text
x="58"
y="518"
class="mono"
id="text35"> /annotations-index.json</text>
<text
x="58"
y="536"
class="mono"
id="text36"> /para-index.json</text>
<text
x="58"
y="554"
class="s"
id="text37">404 = index manquant (relancer predev/dev)</text>
<path
d="M460 230 C500 230 500 230 520 230"
class="arrow"
id="path37" />
<text
x="465"
y="210"
class="s"
id="text38"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan45">requête</tspan></text>
<path
d="M780 380 C780 410 780 410 780 420"
class="arrow"
id="path38" />
<text
x="795"
y="410"
class="s"
id="text39">auth_request</text>
<path
d="m 1040,332 h 132.6172"
class="arrowA"
id="path39"
sodipodi:nodetypes="cc" />
<path
d="m 1040,464 131.407,2.42057"
class="arrowA"
id="path40"
sodipodi:nodetypes="cc" />
<text
x="1045"
y="317"
class="s"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan44">proxy_pass (active)</tspan></text>
<path
d="M780 630 C780 700 340 700 250 630"
class="arrow"
id="path41" />
<text
x="462.36005"
y="707.89716"
class="s"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
id="tspan43"
style="font-size:14.6667px">callback + cookie</tspan></text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect41" />
<text
x="58"
y="850"
class="h"
id="text42">Note importante (debug)</text>
<text
x="58"
y="874"
class="s"
id="text43">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.</text>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,324 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="820"
viewBox="0 0 1600 820"
version="1.1"
id="svg36"
sodipodi:docname="archicratie-web-edition-edge-routing-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-edge-routing-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview36"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="665.65809"
inkscape:cy="354.00908"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg36" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<text
x="40"
y="45"
class="title"
id="text1">Edge Traefik (verbatim) — routers Host(...) + middlewares + services</text>
<text
x="40"
y="75"
class="small"
id="text2">Source : /volume2/docker/edge/config/dynamic/10-core.yml + 20-archicratie-backend.yml + 21-archicratie-staging.yml + 30-lldap-ui.yml</text>
<rect
x="35"
y="110"
width="1530"
height="670"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">Traefik : entryPoint web = :18080 — provider file (dynamic/) watch=true</text>
<!-- Middlewares -->
<rect
x="60"
y="185"
width="601.60364"
height="196.36914"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="215"
class="h2"
id="text4">Middlewares (10-core.yml)</text>
<text
x="80"
y="242"
class="txt"
id="text5">sanitize-remote : purge Remote-* + force X-Forwarded-Proto/Port</text>
<text
x="80"
y="264"
class="txt"
id="text6">authelia : forwardAuth → <tspan
class="mono"
id="tspan5">http://127.0.0.1:9091/api/authz/forward-auth</tspan></text>
<text
x="80"
y="286"
class="txt"
id="text7">chain-auth : [sanitize-remote, authelia]</text>
<!-- Routers -->
<rect
x="60"
y="415"
width="823.08624"
height="205.02269"
rx="12"
class="box"
id="rect7" />
<text
x="80"
y="445"
class="h2"
id="text8">Routers</text>
<text
x="80"
y="472"
class="mono"
id="text9">archicratie</text>
<text
x="200"
y="472"
class="txt"
id="text10">Host(archicratie.trans-hands.synology.me) + chain-auth → service archicratie_web</text>
<text
x="80"
y="498"
class="mono"
id="text11">archicratie-authinfo</text>
<text
x="290"
y="498"
class="txt"
id="text12">Host(archicratie…) PathPrefix(/_auth/whoami) + chain-auth → whoami</text>
<text
x="80"
y="524"
class="mono"
id="text13">gitea</text>
<text
x="200"
y="524"
class="txt"
id="text14">Host(gitea.archicratie.trans-hands.synology.me) + sanitize-remote → gitea_web</text>
<text
x="80"
y="550"
class="mono"
id="text15">archicratie-staging</text>
<text
x="290"
y="550"
class="txt"
id="text16">Host(staging.archicratie.trans-hands.synology.me) + chain-auth → archicratie_blue</text>
<text
x="80"
y="576"
class="mono"
id="text17">lldap-ui</text>
<text
x="200"
y="576"
class="txt"
id="text18">Host(lldap.archicratie.trans-hands.synology.me) + chain-auth → lldap_ui</text>
<rect
x="925.50684"
y="181.36914"
width="614.49316"
height="553.63086"
rx="12"
class="box"
id="rect18" />
<text
x="985"
y="215"
class="h2"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Services (loadBalancer → url)</text>
<text
x="985"
y="250"
class="mono"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">whoami</text>
<text
x="1120"
y="250"
class="txt"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:18081</tspan> (edge-whoami)</text>
<text
x="985"
y="285"
class="mono"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea_web</text>
<text
x="1120"
y="285"
class="txt"
id="text23"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan22"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:3000</tspan> (Gitea)</text>
<text
x="985"
y="320"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">archicratie_web</text>
<text
x="1120"
y="320"
class="txt"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">→ défini par <tspan
class="mono"
id="tspan24"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">20-archicratie-backend.yml</tspan></text>
<text
x="1140"
y="345"
class="txt"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• actuel : <tspan
class="mono"
id="tspan25"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:8082</tspan> (green)</text>
<text
x="985"
y="390"
class="mono"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">archicratie_blue</text>
<text
x="1170"
y="390"
class="txt"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan27"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:8081</tspan> (staging)</text>
<text
x="985"
y="435"
class="mono"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">lldap_ui</text>
<text
x="1120"
y="435"
class="txt"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan29"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">http://127.0.0.1:17170</tspan> (LLDAP UI)</text>
<rect
x="954.1377"
y="493.94855"
width="560.8623"
height="216.05144"
rx="12"
class="note"
id="rect30" />
<text
x="975"
y="530"
class="h2"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Interprétation debug (safe)</text>
<text
x="975"
y="555"
class="txt"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si tu testes sans Host header sur :18080 → 404 (normal)</text>
<text
x="975"
y="577"
class="txt"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si archicratie → 302 auth.* : Authelia forward-auth OK</text>
<text
x="975"
y="599"
class="txt"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Si /_auth/whoami → 302 auth.* : gate OK (non-auth)</text>
<text
x="975"
y="621"
class="txt"
id="text35"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Pour basculer blue/green : modifier 20-archicratie-backend.yml (8081 ↔ 8082)</text>
<text
x="975"
y="643"
class="small"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">But : une seule cible active (évite load-balance non déterministe).</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,870 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1500"
height="940"
viewBox="0 0 1500 940"
role="img"
aria-label="Workflow Git CI - main protégé, PR, CI, release-pack, déploiement blue/green"
version="1.1"
id="svg93"
sodipodi:docname="archicratie-web-edition-git-ci-workflow-v1.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview93"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.88133333"
inkscape:cx="253.02572"
inkscape:cy="536.11952"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg93" />
<defs
id="defs2">
<style
id="style1">
/* ✅ Version “Inkscape-safe” : pas de var(), pas de rgba() */
.canvasBg { fill:#f8fafc; stroke:#e2e8f0; stroke-width:1; }
.title { font:800 26px/1.2 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; }
.subtitle { font:600 14px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
.laneTitle { font:800 14px/1 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; letter-spacing:.2px; }
.laneNote { font:600 12px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
.boxTitle { font:800 14px/1.2 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#0f172a; }
.boxText { font:600 12px/1.35 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
.mono { font:700 11px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace; fill:#334155; }
.lane { fill:#f1f5f9; fill-opacity:.70; stroke:#cbd5e1; stroke-width:1; }
.laneAlt { fill:#e2e8f0; fill-opacity:.55; stroke:#cbd5e1; stroke-width:1; }
.box { fill:#ffffff; fill-opacity:.92; stroke:#94a3b8; stroke-width:1.4; }
.boxAlt { fill:#f8fafc; fill-opacity:.92; stroke:#94a3b8; stroke-width:1.4; }
.tag { font:800 10px/1 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; }
.tagOk { fill:#16a34a; }
.tagWarn { fill:#f59e0b; }
.tagInfo { fill:#2563eb; }
.tagDanger { fill:#dc2626; }
.arrow { stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#arrowHead); }
.arrowSoft { stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#arrowHeadSoft); }
.dashed { stroke-dasharray:7 6; }
.callout { fill:#ffffff; fill-opacity:.70; stroke:#cbd5e1; stroke-width:1.1; }
.small { font:600 11px/1.35 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; fill:#475569; }
</style>
<marker
id="arrowHead"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#64748b"
id="path1" />
</marker>
<marker
id="arrowHeadSoft"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#94a3b8"
id="path2" />
</marker>
</defs>
<!-- ✅ Fond explicite + bordure douce -->
<rect
x="0.5"
y="0.5"
width="1499"
height="939"
class="canvasBg"
rx="26"
id="rect2" />
<!-- Header -->
<text
x="44"
y="54"
class="title"
id="text2">Archicratie — Workflow Git “pro” (main protégé) + CI + Release + Blue/Green</text>
<text
x="44"
y="82"
class="subtitle"
id="text3">
Objectif : partir dun 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.
</text>
<!-- Lanes -->
<rect
x="40"
y="115"
width="440"
height="770"
class="lane"
rx="18"
id="rect3" />
<rect
x="520"
y="115"
width="430"
height="770"
class="laneAlt"
rx="18"
id="rect4" />
<rect
x="980"
y="115"
width="250"
height="770"
class="lane"
rx="18"
id="rect5" />
<rect
x="1250"
y="115"
width="210"
height="770"
class="laneAlt"
rx="18"
id="rect6" />
<text
x="60"
y="142"
class="laneTitle"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Atelier DEV (Mac Studio)</text>
<text
x="60"
y="164"
class="laneNote"
id="text7"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.4;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Travail local, build, commit, push de branche</text>
<text
x="540"
y="148"
class="laneTitle"
id="text8">Gitea (remote)</text>
<text
x="540"
y="170"
class="laneNote"
id="text9">main verrouillé · PR obligatoire · historique canon</text>
<text
x="1000"
y="148"
class="laneTitle"
id="text10">CI (CI.yaml)</text>
<text
x="1000"
y="170"
class="laneNote"
id="text11">build checks · gate de merge</text>
<text
x="1270"
y="148"
class="laneTitle"
id="text12">NAS (Prod)</text>
<text
x="1270"
y="170"
class="laneNote"
id="text13">release-pack + blue/green</text>
<!-- Boxes: Mac -->
<rect
x="70"
y="170"
width="380"
height="105"
class="box"
rx="16"
id="rect13" />
<text
x="92"
y="198"
class="boxTitle"
id="text14"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">1) Se baser sur main (canon)</text>
<text
x="92"
y="220"
class="boxText"
id="text15"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Synchroniser le dépôt local sur le dernier état validé.</text>
<text
x="92"
y="242"
class="mono"
id="text16"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git checkout main git pull --ff-only</text>
<text
x="92"
y="264"
class="small"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan16"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">INFO</tspan> main est protégé : pas de commit direct.</text>
<rect
x="70"
y="300"
width="380"
height="125"
class="boxAlt"
rx="16"
id="rect17" />
<text
x="92"
y="328"
class="boxTitle"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">2) Créer une branche dédiée “hotfix sync”</text>
<text
x="92"
y="350"
class="boxText"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Nom explicite + date. Toute la synchro se fait ici.</text>
<text
x="92"
y="372"
class="mono"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git checkout -b chore/step8-sync-hotfix-YYYYMMDD</text>
<text
x="92"
y="394"
class="small"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan94"
x="92"
y="394"><tspan
class="tag tagWarn"
id="tspan20"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Optionnel : appliquer un pack hotfix (tar/sha/rsync)</tspan><tspan
sodipodi:role="line"
id="tspan95"
x="92"
y="408.85001">si prod a bougé.</tspan></text>
<rect
x="70"
y="455"
width="380"
height="160"
class="box"
rx="16"
id="rect21" />
<text
x="92"
y="483"
class="boxTitle"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">3) Appliquer les changements vérifier</text>
<text
x="72"
y="505"
class="boxText"
id="text23"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Copier/merge les fichiers (rsync/checksum), puis tester build/dev.</text>
<text
x="92"
y="527"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">rm -rf .astro node_modules/.vite</text>
<text
x="92"
y="548"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm i npm run build</text>
<text
x="92"
y="569"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm run dev</text>
<text
x="92"
y="593"
class="small"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagOk"
id="tspan26"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">OK</tspan> On ne push que si build + postbuild passent.</text>
<rect
x="70"
y="640"
width="380"
height="145"
class="boxAlt"
rx="16"
id="rect27" />
<text
x="92"
y="668"
class="boxTitle"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">4) Commit propre + diff lisible</text>
<text
x="92"
y="690"
class="boxText"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Inspecter, puis commiter en message clair (hotfix étape X).</text>
<text
x="92"
y="712"
class="mono"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git status git diff</text>
<text
x="92"
y="733"
class="mono"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"><tspan
sodipodi:role="line"
id="tspan96"
x="92"
y="733">git add -A git commit -m &quot;step8: sync hotfix</tspan><tspan
sodipodi:role="line"
id="tspan97"
x="92"
y="747.84998">(SidePanel/reading)&quot;</tspan></text>
<text
x="92"
y="770"
class="small"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan31"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">TIP</tspan> Garder le commit “gros” mais unique si cest un backport prod.</text>
<rect
x="70"
y="805"
width="380"
height="65"
class="box"
rx="16"
id="rect32" />
<text
x="92"
y="833"
class="boxTitle"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">5) Push de branche vers Gitea</text>
<text
x="92"
y="855"
class="mono"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">git push -u origin chore/step8-sync-hotfix-YYYYMMDD</text>
<!-- Boxes: Gitea -->
<rect
x="536.38428"
y="241.13464"
width="396.0968"
height="123.86536"
class="box"
rx="16"
id="rect34" />
<text
x="572"
y="268"
class="boxTitle"
id="text35">6) Ouvrir une PR vers main</text>
<text
x="554"
y="290"
class="boxText"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">main protégé → PR obligatoire. Décrire : “backport hotfix prod”.</text>
<text
x="554"
y="312"
class="small"
id="text37"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagWarn"
id="tspan36"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Ajouter contexte : fichiers touchés, risque, checks attendus.</text>
<text
x="572"
y="334"
class="small"
id="text38"><tspan
class="tag tagInfo"
id="tspan37">INFO</tspan> La PR déclenche CI.yaml (pipeline de validation).</text>
<rect
x="538.65356"
y="396.13464"
width="389.28894"
height="135"
class="boxAlt"
rx="16"
id="rect38" />
<text
x="572"
y="423"
class="boxTitle"
id="text39">7) Review + décisions</text>
<text
x="552"
y="445"
class="boxText"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Lecture diff, vérif logique, pas de secrets, pas de régressions UI.</text>
<text
x="572"
y="468"
class="small"
id="text41"><tspan
class="tag tagDanger"
id="tspan40">STOP</tspan> Si CI rouge : corriger sur la branche, push → CI relancé.</text>
<text
x="572"
y="490"
class="small"
id="text42"><tspan
class="tag tagOk"
id="tspan41">OK</tspan> Si CI vert + review OK : merge autorisé.</text>
<rect
x="537.51892"
y="548.65356"
width="390.42358"
height="138.82753"
class="box"
rx="16"
id="rect42" />
<text
x="572"
y="588"
class="boxTitle"
id="text43">8) Merge PR → main (canon)</text>
<text
x="572"
y="610"
class="boxText"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan107"
x="572"
y="610">main devient lunique source officielle.</tspan><tspan
sodipodi:role="line"
id="tspan108"
x="572"
y="626.20001">La prod se recale dessus.</tspan></text>
<text
x="572"
y="646"
class="mono"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">Merge (UI Gitea) → origin/main updated</text>
<text
x="572"
y="668"
class="small"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan45"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">INFO</tspan> Optionnel : tagger une release (vX.Y / date).</text>
<rect
x="550"
y="705"
width="370"
height="145"
class="boxAlt"
rx="16"
id="rect46" />
<text
x="572"
y="733"
class="boxTitle"
id="text47">9) Préparer une release packagée</text>
<text
x="572"
y="755"
class="boxText"
id="text48"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan105"
x="572"
y="755">Générer un paquet de release reproductible</tspan><tspan
sodipodi:role="line"
id="tspan106"
x="572"
y="771.20001">(sources + scripts + config).</tspan></text>
<text
x="572"
y="791"
class="mono"
id="text49"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">./release-pack.sh</text>
<text
x="572"
y="813"
class="small"
id="text50"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagWarn"
id="tspan49"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">MANUEL</tspan> Le pack sert au déploiement sur NAS (blue/green).</text>
<text
x="572"
y="835"
class="small"
id="text51"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
class="tag tagInfo"
id="tspan50"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">TIP</tspan> Conserver checksum + manifest (traçabilité).</text>
<!-- Boxes: CI -->
<rect
x="1000"
y="260"
width="210"
height="160"
class="box"
rx="16"
id="rect51" />
<text
x="1018"
y="288"
class="boxTitle"
id="text52">CI : checks</text>
<text
x="1018"
y="310"
class="boxText"
id="text53">npm ci</text>
<text
x="1018"
y="330"
class="boxText"
id="text54">astro build</text>
<text
x="1018"
y="350"
class="boxText"
id="text55">postbuild scripts</text>
<text
x="1018"
y="370"
class="boxText"
id="text56">pagefind</text>
<text
x="1018"
y="394"
class="small"
id="text57"><tspan
class="tag tagOk"
id="tspan56">PASS</tspan> → merge autorisé</text>
<text
x="1018"
y="414"
class="small"
id="text58"><tspan
class="tag tagDanger"
id="tspan57">FAIL</tspan> → corriger branche</text>
<rect
x="1000"
y="450"
width="210"
height="105"
class="boxAlt"
rx="16"
id="rect58" />
<text
x="1018"
y="478"
class="boxTitle"
id="text59">Artefacts</text>
<text
x="1018"
y="500"
class="boxText"
id="text60">Logs + traces</text>
<text
x="1018"
y="520"
class="boxText"
id="text61">Optionnel : build artefact</text>
<text
x="1018"
y="540"
class="small"
id="text62"><tspan
class="tag tagInfo"
id="tspan61">INFO</tspan> Sert au diagnostic rapide.</text>
<!-- Boxes: NAS -->
<rect
x="1270"
y="260"
width="170"
height="155"
class="box"
rx="16"
id="rect62" />
<text
x="1288"
y="288"
class="boxTitle"
id="text63">Déploiement</text>
<text
x="1288"
y="312"
class="boxText"
id="text64">Importer release</text>
<text
x="1288"
y="332"
class="boxText"
id="text65">docker build</text>
<text
x="1288"
y="352"
class="boxText"
id="text66">web_blue / web_green</text>
<text
x="1288"
y="376"
class="boxText"
id="text67">switch proxy</text>
<text
x="1288"
y="398"
class="small"
id="text68"><tspan
class="tag tagOk"
id="tspan67">OK</tspan> rollback possible</text>
<rect
x="1270"
y="450"
width="170"
height="140"
class="boxAlt"
rx="16"
id="rect68" />
<text
x="1288"
y="478"
class="boxTitle"
id="text69">Runbook</text>
<text
x="1288"
y="500"
class="boxText"
id="text70">healthchecks</text>
<text
x="1288"
y="520"
class="boxText"
id="text71">logs</text>
<text
x="1288"
y="540"
class="boxText"
id="text72">validation UI</text>
<text
x="1288"
y="566"
class="small"
id="text73"><tspan
class="tag tagWarn"
id="tspan72">MANUEL</tspan> staging dabord</text>
<rect
x="1270"
y="640"
width="170"
height="210"
class="box"
rx="16"
id="rect73" />
<text
x="1288"
y="668"
class="boxTitle"
id="text74">Hotfix prod</text>
<text
x="1288"
y="690"
class="boxText"
id="text75">À éviter si possible</text>
<text
x="1288"
y="710"
class="boxText"
id="text76">Si nécessaire :</text>
<text
x="1288"
y="732"
class="boxText"
id="text77">pack (tar+sha)</text>
<text
x="1288"
y="754"
class="boxText"
id="text78">→ rapatrier DEV</text>
<text
x="1288"
y="778"
class="small"
id="text79"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan109"
x="1288"
y="778"><tspan
style="fill:#ff0000"
id="tspan111">RISK</tspan> Toujours backporter</tspan><tspan
sodipodi:role="line"
id="tspan110"
x="1288"
y="792.84998">via PR.</tspan></text>
<!-- Callout -->
<rect
x="988.19214"
y="665.28741"
width="231.68678"
height="196.73224"
class="callout"
rx="14"
id="rect79" />
<text
x="999"
y="701"
class="boxTitle"
id="text80"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:14px;line-height:1.2;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">Règle dor</text>
<text
x="999"
y="725"
class="boxText"
id="text81"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan98"
x="999"
y="725">Le NAS nest pas le dépôt source.</tspan><tspan
sodipodi:role="line"
id="tspan99"
x="999"
y="741.20001">Même si un hotfix a été fait en prod,</tspan></text>
<text
x="999"
y="760"
class="boxText"
id="text82"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan100"
x="999"
y="760">létat final “vrai” doit être : branche</tspan><tspan
sodipodi:role="line"
id="tspan101"
x="999"
y="776.20001">→ PR → CI → merge main → release</tspan><tspan
sodipodi:role="line"
x="999"
y="792.87439"
id="tspan102">→ deploy.</tspan></text>
<text
x="999"
y="812"
class="small"
id="text83"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:11px;line-height:1.35;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial"><tspan
sodipodi:role="line"
id="tspan103"
x="999"
y="812"><tspan
class="tag tagOk"
id="tspan82"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:10px;line-height:1;font-family:ui-sans-serif, system-ui, '-apple-system', 'Segoe UI', Roboto, Helvetica, Arial">BUT</tspan> Passation étape 9 = base Git propre</tspan><tspan
sodipodi:role="line"
id="tspan104"
x="999"
y="826.84998">+ reproductible.</tspan></text>
<!-- Arrows -->
<path
class="arrow"
d="m 450,222.34493 c 50,0 46.38427,58.78971 86.38427,78.78971"
id="path83"
sodipodi:nodetypes="cc" />
<path
class="arrow"
d="m 450,835 c 50,0 60,-15 100,-55"
id="path84" />
<path
class="arrow"
d="M 931.75492,299.19062 C 995.29501,300.15129 924.67474,339.65204 1000,340"
id="path85"
sodipodi:nodetypes="cc" />
<path
class="arrowSoft dashed"
d="M 888.63843,365 C 909.06203,422.26929 925.80938,435.71104 1000,500"
id="path86"
sodipodi:nodetypes="cc" />
<path
class="arrow"
d="M1210 340 C1240 340, 1245 340, 1270 340"
id="path87" />
<path
class="arrow"
d="M920 620 C980 620, 1000 620, 1080 620"
id="path88" />
<path
class="arrow"
d="m 920,620 c 60,0 311.3313,0.2118 347.7307,-236.67171"
id="path89"
sodipodi:nodetypes="cc" />
<path
class="arrowSoft dashed"
d="m 1269.652,672.90469 c -83.9636,0.6354 -155.1134,-27.51891 -348.51736,-27.76853"
id="path90"
sodipodi:nodetypes="cc" />
<!-- Footnote -->
<text
x="44"
y="916"
class="subtitle"
id="text93">
Légende : <tspan
class="tag tagInfo"
id="tspan90">INFO</tspan> invariant / contexte · <tspan
class="tag tagWarn"
id="tspan91">MANUEL</tspan> action humaine ·
<tspan
class="tag tagOk"
id="tspan92">OK</tspan> attendu · <tspan
class="tag tagDanger"
id="tspan93">STOP</tspan> bloquant.
</text>
</svg>

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,537 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg57"
sodipodi:docname="archicratie-web-edition-global-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview57"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="540.99849"
inkscape:cy="369.74281"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg57" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Vue globale (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Étape 8 (hotfix UI + sync Git) — mise à jour 2026-02-20 — Astro static + Pagefind + Authelia + Blue/Green</text>
<rect
x="40"
y="120"
width="470"
height="290"
class="box"
id="rect5" />
<text
x="58"
y="150"
class="h"
id="text6">Atelier DEV (Mac Studio)</text>
<text
x="58"
y="174"
class="mono"
id="text7">repo git (branches, PR vers main)</text>
<text
x="58"
y="192"
class="mono"
id="text8">npm run dev → http://localhost:4321</text>
<text
x="58"
y="210"
class="mono"
id="text9">npm run build → dist/ (static)</text>
<text
x="58"
y="228"
class="mono"
id="text10">postbuild:</text>
<text
x="58"
y="246"
class="mono"
id="text11"> inject-anchor-aliases.mjs</text>
<text
x="58"
y="264"
class="mono"
id="text12"> dedupe-ids-dist.mjs</text>
<text
x="58"
y="282"
class="mono"
id="text13"> build-para-index.mjs → dist/para-index.json</text>
<text
x="58"
y="300"
class="mono"
id="text14"> build-annotations-index.mjs → dist/annotations-index.json</text>
<text
x="58"
y="318"
class="mono"
id="text15"> pagefind → dist/pagefind/</text>
<text
x="58"
y="336"
class="s"
id="text16">predev: build public/para-index.json + public/annotations-index.json</text>
<rect
x="60"
y="420"
width="206"
height="28"
class="chip2"
id="rect16" />
<text
x="74"
y="439"
class="mono"
id="text17">dist/ + pagefind + indexes</text>
<rect
x="300"
y="420"
width="198"
height="28"
class="chip"
id="rect17" />
<text
x="314"
y="439"
class="mono"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">public/*-index.json</text>
<rect
x="40"
y="470"
width="470"
height="330"
class="box2"
id="rect18" />
<text
x="58"
y="500"
class="h"
id="text19">UI Lecture / Édition (dans le site)</text>
<text
x="58"
y="524"
class="t"
id="text20">EditionLayout.astro (globals + meta)</text>
<text
x="58"
y="542"
class="t"
id="text21">SidePanel.astro (reading-follow + annotations + propose)</text>
<text
x="58"
y="560"
class="t"
id="text22">LevelToggle.astro (Niveaux)</text>
<text
x="58"
y="578"
class="t"
id="text23">global.css (UX lecture + TOC-local sync)</text>
<text
x="58"
y="596"
class="s"
id="text24">SidePanel consomme para-index + annotations-index</text>
<text
x="58"
y="614"
class="s"
id="text25">ProposeModal ouvre une issue Gitea (direct ou via bridge)</text>
<text
x="58"
y="632"
class="mono"
id="text26">env publics: PUBLIC_GITEA_* + PUBLIC_ISSUE_BRIDGE_PATH</text>
<rect
x="673.92584"
y="119.57942"
width="344.13016"
height="250"
class="box"
id="rect26" />
<text
x="723"
y="180"
class="h"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Gitea (sur NAS) — source of truth</text>
<text
x="723"
y="204"
class="t"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">main protégé (push direct interdit)</text>
<text
x="723"
y="222"
class="t"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">branches de travail → PR → merge</text>
<text
x="723"
y="240"
class="t"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">CI (workflow) : build + checks + artefacts</text>
<text
x="723"
y="258"
class="t"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">issues + labels (tickets)</text>
<rect
x="745"
y="320"
width="126"
height="28"
class="chip2"
id="rect31" />
<text
x="759"
y="339"
class="mono"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PR → CI.yaml</text>
<rect
x="565"
y="430"
width="470"
height="160"
class="box2"
id="rect32" />
<text
x="583"
y="460"
class="h"
id="text33">Hotfix de release → re-sync Git (méthode step8)</text>
<text
x="583"
y="484"
class="t"
id="text34">1) lister fichiers modifiés sur NAS</text>
<text
x="583"
y="502"
class="t"
id="text35">2) tar + sha256 → transfert</text>
<text
x="583"
y="520"
class="t"
id="text36">3) rsync --checksum vers repo local</text>
<text
x="583"
y="538"
class="t"
id="text37">4) commit sur branche dédiée + push + PR</text>
<rect
x="1183.1921"
y="122.42058"
width="376.80786"
height="247.57942"
class="box"
id="rect37" />
<text
x="1228"
y="150"
class="h"
id="text38"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">NAS DS220+ — runtime Blue/Green</text>
<text
x="1228"
y="174"
class="mono"
id="text39"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">/volume2/docker/archicratie-web/</text>
<text
x="1228"
y="192"
class="mono"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">releases/&lt;timestamp&gt;/app (build context)</text>
<text
x="1228"
y="210"
class="mono"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">current → release active</text>
<text
x="1228"
y="228"
class="mono"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">docker compose: web_blue / web_green</text>
<text
x="1228"
y="246"
class="s"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">une seule couleur sert le trafic (reverse-proxy)</text>
<rect
x="1210"
y="310"
width="322"
height="28"
class="chipW"
id="rect43" />
<text
x="1224"
y="329"
class="mono"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">docker compose build --no-cache web_*</text>
<rect
x="1184.4025"
y="393.94858"
width="375.59756"
height="246.05144"
class="box2"
id="rect44" />
<text
x="1208"
y="430"
class="h"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Edge &amp; Auth</text>
<text
x="1208"
y="454"
class="t"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Reverse-proxy (Nginx) protège le site</text>
<text
x="1208"
y="472"
class="t"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Authelia (SSO) + LLDAP (LDAP) + Redis</text>
<text
x="1208"
y="490"
class="t"
id="text48"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">302 vers auth.* si non authentifié</text>
<text
x="1208"
y="508"
class="t"
id="text49"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Host header: staging.* / archicratie.*</text>
<text
x="1208"
y="526"
class="s"
id="text50"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">/_auth/whoami utilisé en check côté client (peut 404 en local)</text>
<path
d="m 510,260 163.92587,-1.21029"
class="arrowA"
id="path50"
sodipodi:nodetypes="cc" />
<text
x="514"
y="245"
class="s"
id="text51"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan57">git push (branche) + PR</tspan></text>
<path
d="m 1016.8457,240 h 166.3464"
class="arrowA"
id="path51"
sodipodi:nodetypes="cc" />
<text
x="1025.3177"
y="206.9062"
class="s"
id="text52"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
sodipodi:role="line"
id="tspan58"
x="1025.3177"
y="206.9062"
style="font-size:14.6667px">CI/artefact</tspan><tspan
sodipodi:role="line"
id="tspan59"
x="1025.3177"
y="226.70625"
style="font-size:14.6667px">→ déploiement release</tspan></text>
<path
d="M1420 650 C1480 650 1520 650 1560 650"
class="arrow"
id="path52" />
<text
x="1420"
y="632"
class="s"
id="text53">HTTPS → navigateur</text>
<path
d="M 510,640.25719 C 540,640.25719 540,520 565,520"
class="arrow"
id="path53"
sodipodi:nodetypes="cc" />
<text
x="534.84113"
y="620.20575"
class="s"
id="text54"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan60">Propose → issue</tspan></text>
<path
d="m 1181.9818,520 -145.7715,-1.21029"
class="arrowG"
id="path54"
sodipodi:nodetypes="cc" />
<path
d="M565 520 C520 520 520 520 510 520"
class="arrowG"
id="path55" />
<text
x="800"
y="505"
class="s"
id="text55">tar+sha256 → scp → rsync --checksum</text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect55" />
<text
x="58"
y="850"
class="h"
id="text56">Légende</text>
<text
x="58"
y="874"
class="s"
id="text57"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial"><tspan
style="font-size:14.6667px"
id="tspan61">Bleu = flux Git/CI · Vert = flux de re-sync hotfix · Orange = build runtime · Le reste = navigation/HTTP</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,828 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg80"
sodipodi:docname="archicratie-web-edition-global-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-global-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview80"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="594.25113"
inkscape:cy="481.08926"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg80" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.h3 { font: 700 14px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie Web Edition : schéma global VERBATIM (Mac Studio ↔ NAS Synology DS220+)</text>
<text
x="40"
y="75"
class="small"
id="text2">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.</text>
<!-- LOCAL -->
<rect
x="35"
y="110"
width="520"
height="880"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">LOCAL — Mac Studio (atelier)</text>
<rect
x="60"
y="175"
width="470"
height="110"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Repo site (Astro)</text>
<text
x="80"
y="232"
class="txt"
id="text5">• build statique → dist/</text>
<text
x="80"
y="254"
class="txt"
id="text6">• postbuild : inject aliases + dedupe IDs + indexes + pagefind</text>
<rect
x="60"
y="305"
width="470"
height="115"
rx="12"
class="box"
id="rect6" />
<text
x="80"
y="335"
class="h2"
id="text7">Tooling (scripts/)</text>
<text
x="66"
y="360"
class="txt"
id="text8"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/inject-anchor-aliases.mjs</text>
<text
x="66"
y="382"
class="txt"
id="text9"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/apply-ticket.mjs --alias</text>
<text
x="66"
y="404"
class="txt"
id="text10"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• scripts/check-anchor-aliases.mjs + verify-anchor-aliases-in-dist.mjs</text>
<rect
x="60"
y="445"
width="470"
height="105"
rx="12"
class="box"
id="rect10" />
<text
x="80"
y="475"
class="h2"
id="text11">Déploiement (release pack)</text>
<text
x="80"
y="500"
class="txt"
id="text12">• build Docker avec ARG/ENV : PUBLIC_GITEA_BASE/OWNER/REPO</text>
<text
x="80"
y="522"
class="txt"
id="text13">• pousse/maj sur NAS (containers web_blue/web_green)</text>
<rect
x="60"
y="569"
width="470"
height="165"
rx="12"
class="note"
id="rect13" />
<text
x="80"
y="605"
class="h2"
id="text14">Repères “vrais” côté site</text>
<text
x="80"
y="632"
class="txt"
id="text15">• whoami runtime : <tspan
class="mono"
id="tspan14">/_auth/whoami</tspan></text>
<text
x="80"
y="654"
class="txt"
id="text16">• variables injectées : <tspan
class="mono"
id="tspan15">PUBLIC_GITEA_BASE/OWNER/REPO</tspan></text>
<text
x="80"
y="676"
class="txt"
id="text17">• anchors canon : <tspan
class="mono"
id="tspan16">src/anchors/anchor-aliases.json</tspan></text>
<text
x="80"
y="698"
class="txt"
id="text18">• injection build-time : <tspan
class="mono"
id="tspan17">scripts/inject-anchor-aliases.mjs</tspan></text>
<!-- NAS -->
<rect
x="590"
y="110"
width="975"
height="880"
rx="18"
class="zone"
id="rect18" />
<text
x="615"
y="145"
class="h2"
id="text19">DISTANT — NAS Synology DS220+ (DSM + Container Manager)</text>
<!-- Users -->
<rect
x="615"
y="175"
width="270"
height="90"
rx="12"
class="box"
id="rect19" />
<text
x="635"
y="205"
class="h2"
id="text20">Utilisateurs</text>
<text
x="635"
y="230"
class="txt"
id="text21">• Web (public)</text>
<text
x="635"
y="252"
class="txt"
id="text22">• Éditeurs (groupe LDAP)</text>
<!-- DSM RP -->
<rect
x="905"
y="175"
width="630"
height="120"
rx="12"
class="box"
id="rect22" />
<text
x="930"
y="205"
class="h2"
id="text23">DSM Reverse Proxy (TLS terminé ici)</text>
<text
x="930"
y="230"
class="txt"
id="text24">• Host archicratie.trans-hands.synology.me → 127.0.0.1:18080</text>
<text
x="930"
y="252"
class="txt"
id="text25">• Host gitea.archicratie.trans-hands.synology.me → 127.0.0.1:18080</text>
<text
x="930"
y="274"
class="txt"
id="text26">• (idem staging.*, lldap.* si routés via Traefik)</text>
<!-- Edge Traefik -->
<rect
x="1012.7156"
y="321.2103"
width="522.28442"
height="148.78972"
rx="12"
class="box"
id="rect26" />
<text
x="1050"
y="350"
class="h2"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">edge-traefik (traefik:v2.11) — network_mode: host</text>
<text
x="1050"
y="375"
class="txt"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• entryPoint web : <tspan
class="mono"
id="tspan27"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">:18080</tspan></text>
<text
x="1050"
y="397"
class="txt"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• provider file : <tspan
class="mono"
id="tspan28"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/etc/traefik/dynamic</tspan> (watch: true)</text>
<text
x="1050"
y="419"
class="txt"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Host rules (routers) + middlewares (chain-auth / sanitize-remote)</text>
<text
x="1050"
y="441"
class="small"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">Tes 404 initiaux venaient dun test sans Host: les routers utilisent Host(...)</text>
<!-- Dynamic files -->
<rect
x="615"
y="290"
width="270"
height="180"
rx="12"
class="note"
id="rect31" />
<text
x="623"
y="320"
class="h2"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Fichiers dynamiques (edge)</text>
<text
x="623"
y="345"
class="mono"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/volume2/docker/edge/config/dynamic/</text>
<text
x="623"
y="368"
class="txt"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 10-core.yml (routers + chain-auth)</text>
<text
x="623"
y="390"
class="txt"
id="text35"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 20-archicratie-backend.yml (slot actif)</text>
<text
x="623"
y="412"
class="txt"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan88"
x="623"
y="412">• 21-archicratie-staging.yml</tspan><tspan
sodipodi:role="line"
id="tspan89"
x="623"
y="428.25">(staging→8081)</tspan></text>
<text
x="623"
y="448"
class="txt"
id="text37"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 30-lldap-ui.yml (lldap UI)</text>
<!-- Auth stack -->
<rect
x="615"
y="495"
width="376.80786"
height="258.41147"
rx="12"
class="box"
id="rect37" />
<text
x="635"
y="525"
class="h2"
id="text38">Auth stack (auth)</text>
<text
x="635"
y="550"
class="txt"
id="text39">auth-authelia (authelia:4.39.13) — host</text>
<text
x="635"
y="572"
class="txt"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan92"
x="635"
y="572">• forward-auth :</tspan><tspan
sodipodi:role="line"
id="tspan93"
x="635"
y="588.25">http://127.0.0.1:9091/api/authz/forward-auth</tspan></text>
<text
x="635"
y="614"
class="txt"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">auth-lldap (lldap:stable)</text>
<text
x="635"
y="640"
class="txt"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• LDAP : <tspan
class="mono"
id="tspan41"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:3890</tspan> • UI : <tspan
class="mono"
id="tspan42"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:17170</tspan></text>
<text
x="635"
y="662"
class="txt"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">auth-redis (redis:7-alpine)</text>
<text
x="635"
y="684"
class="txt"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• exposé : <tspan
class="mono"
id="tspan43"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">127.0.0.1:6380</tspan></text>
<text
x="635"
y="708"
class="small"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan94"
x="635"
y="708">Traefik injecte Remote-* via forward-auth,</tspan><tspan
sodipodi:role="line"
id="tspan95"
x="635"
y="723">et purge lentrée (sanitize-remote).</tspan></text>
<!-- Whoami service -->
<rect
x="1110"
y="495"
width="365.69592"
height="117.57942"
rx="12"
class="box"
id="rect45" />
<text
x="1135"
y="525"
class="h2"
id="text46">edge-whoami (traefik/whoami)</text>
<text
x="1135"
y="550"
class="txt"
id="text47">• exposé : <tspan
class="mono"
id="tspan46">127.0.0.1:18081 → 80</tspan></text>
<text
x="1135"
y="572"
class="txt"
id="text48">• router Traefik : <tspan
class="mono"
id="tspan47">PathPrefix('/_auth/whoami')</tspan></text>
<text
x="1135"
y="594"
class="txt"
id="text49">• protégé par <tspan
class="mono"
id="tspan48">chain-auth</tspan> (302 login si non auth)</text>
<!-- Web blue/green -->
<rect
x="1047.9349"
y="639.94855"
width="224.96217"
height="116.11195"
rx="12"
class="box"
id="rect49" />
<text
x="1070"
y="670"
class="h2"
id="text50"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">archicratie-web-blue</text>
<text
x="1070"
y="695"
class="txt"
id="text51"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 127.0.0.1:8081 → 80</text>
<text
x="1070"
y="717"
class="txt"
id="text52"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Nginx sert dist/</text>
<text
x="1070"
y="739"
class="small"
id="text53"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">slot blue (staging cible 8081)</text>
<rect
x="1295"
y="640"
width="240.69592"
height="116.11195"
rx="12"
class="box"
id="rect53" />
<text
x="1320"
y="670"
class="h2"
id="text54"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">archicratie-web-green</text>
<text
x="1320"
y="695"
class="txt"
id="text55"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• 127.0.0.1:8082 → 80</text>
<text
x="1320"
y="717"
class="txt"
id="text56"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Nginx sert dist/</text>
<text
x="1320"
y="739"
class="small"
id="text57"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">slot green (backend actuel)</text>
<rect
x="1156.7399"
y="778.21478"
width="374.62918"
height="105.4161"
rx="12"
class="note"
id="rect57" />
<text
x="1190.2118"
y="797.89716"
class="txt"
id="text58"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
id="tspan96"
x="1190.2118"
y="797.89716"
sodipodi:role="line">Bascule blue/green (Traefik) :</tspan><tspan
x="1190.2118"
y="814.14716"
id="tspan104"
sodipodi:role="line">modifier <tspan
class="mono"
id="tspan57"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">dynamic/20-archicratie-backend.yml</tspan></tspan><tspan
id="tspan97"
x="1190.2118"
y="830.39716"
sodipodi:role="line">→ url 8081/8082 (un seul backend actif)</tspan></text>
<text
x="1202.3751"
y="858.05145"
class="small"
id="text59"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan101"
x="1202.3751"
y="858.05145">Actuellement (daprès ton dump) :<tspan
class="mono"
id="tspan58"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"></tspan></tspan><tspan
sodipodi:role="line"
id="tspan102"
x="1202.3751"
y="873.05145"><tspan
class="mono"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace"
id="tspan103">archicratie_web → http://127.0.0.1:8082</tspan></tspan></text>
<!-- Gitea + Runner -->
<rect
x="615"
y="790"
width="440.12103"
height="180.57489"
rx="12"
class="box"
id="rect59" />
<text
x="635"
y="820"
class="h2"
id="text60"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Gitea (actuel)</text>
<text
x="635"
y="845"
class="txt"
id="text61"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• conteneur : <tspan
class="mono"
id="tspan60"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea-old-2026-02-09-105211</tspan></text>
<text
x="635"
y="867"
class="txt"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• port : <tspan
class="mono"
id="tspan61"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">0.0.0.0:3000</tspan> (Traefik route aussi vers 127.0.0.1:3000)</text>
<text
x="635"
y="889"
class="txt"
id="text63"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan90"
x="635"
y="889">• router Traefik : Host(gitea.archicratie...)</tspan><tspan
sodipodi:role="line"
id="tspan91"
x="635"
y="905.25">+ middleware sanitize-remote (pas chain-auth)</tspan></text>
<text
x="635"
y="929"
class="small"
id="text64"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan99"
x="635"
y="929">“Proposer” dépend de PUBLIC_GITEA_* corrects</tspan><tspan
sodipodi:role="line"
id="tspan100"
x="635"
y="944">(owner casse sensible).</tspan></text>
<rect
x="1160"
y="895"
width="375"
height="85"
rx="12"
class="box"
id="rect64" />
<text
x="1185"
y="925"
class="h2"
id="text65"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">gitea-act-runner</text>
<text
x="1185"
y="950"
class="txt"
id="text66"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• image : <tspan
class="mono"
id="tspan65"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">gitea/act_runner:0.2.11</tspan></text>
<text
x="1185"
y="972"
class="small"
id="text67"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">CI : labels / checks (anchors, aliases, etc.).</text>
<!-- Connections -->
<path
d="M 885 220 L 905 220"
class="line"
id="path67" />
<!-- Users -> DSM -->
<path
d="M 1220 295 L 1220 320"
class="line"
id="path68" />
<!-- DSM -> Traefik -->
<!-- Traefik -> auth (forward auth) -->
<path
d="m 1005,420 -45,75"
class="dash"
id="path69" />
<text
x="120.96539"
y="1057.8674"
class="small"
id="text69"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"
transform="rotate(-57.013356)">forward-auth</text>
<!-- Traefik -> whoami -->
<path
d="M 1220 470 L 1220 495"
class="line"
id="path70" />
<!-- Traefik -> web service -->
<path
d="m 1082.9955,470 -0.3782,168.78971"
class="dash"
id="path71"
sodipodi:nodetypes="cc" />
<path
d="m 1500,470 -0.416,170"
class="dash"
id="path72"
sodipodi:nodetypes="cc" />
<!-- Traefik -> gitea -->
<path
d="m 1034.826,471.21029 1.3616,315.67322"
class="dash"
id="path73"
sodipodi:nodetypes="cc" />
<!-- Gitea -> runner -->
<path
d="m 1060,890 98.7897,59.52345"
class="line"
id="path74"
sodipodi:nodetypes="cc" />
<!-- Local -> NAS (release/deploy) -->
<path
d="M 530 505 L 615 505"
class="line"
id="path75" />
<!-- Legend -->
<rect
x="61.210289"
y="750.47656"
width="467.57938"
height="215.31776"
rx="12"
class="note"
id="rect75" />
<text
x="80"
y="777"
class="h2"
id="text75"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Lecture (opérationnelle)</text>
<text
x="80"
y="798"
class="txt"
id="text76"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan86"
x="80"
y="798">1) Web public : DSM → Traefik :18080 → Host(archicratie...)</tspan><tspan
sodipodi:role="line"
id="tspan87"
x="80"
y="814.40051">→ chain-auth → backend (8081/8082)</tspan></text>
<text
x="80"
y="836"
class="txt"
id="text77"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan84"
x="80"
y="836">2) Gate éditeurs : site appelle /_auth/whoami</tspan><tspan
sodipodi:role="line"
id="tspan85"
x="80"
y="852.25">→ Traefik route vers edge-whoami (protégé)</tspan></text>
<text
x="80"
y="874"
class="txt"
id="text78"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan82"
x="80"
y="874">3) Gitea : Host(gitea...) → Traefik → 127.0.0.1:3000</tspan><tspan
sodipodi:role="line"
id="tspan83"
x="80"
y="890.40051">(sanitize-remote)</tspan></text>
<text
x="80"
y="912"
class="txt"
id="text79"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan80"
x="80"
y="912">4) Blue/green : changer <tspan
class="mono"
id="tspan78"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">dynamic/20-archicratie-backend.yml</tspan></tspan><tspan
sodipodi:role="line"
id="tspan81"
x="80"
y="928.25">(un seul backend actif)</tspan></text>
<text
x="80"
y="950"
class="small"
id="text80"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">NB : un test sans Host sur :18080 renvoie 404 (normal, Host rules).</text>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,409 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg43"
sodipodi:docname="archicratie-web-edition-machine-editoriale-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview43"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="1108.6233"
inkscape:cy="435.09834"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg43" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Machine éditoriale (v2)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">De la source au site (lecture + annotations + propositions) — 2026-02-20</text>
<rect
x="40"
y="140"
width="460"
height="256.62631"
class="box"
id="rect5" />
<text
x="118"
y="230"
class="h"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Sources (repo)</text>
<text
x="118"
y="254"
class="t"
id="text7"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Contenu : src/content/** (MD/MDX)</text>
<text
x="118"
y="272"
class="t"
id="text8"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Annotations : src/annotations/** (YAML)</text>
<text
x="118"
y="290"
class="t"
id="text9"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI : src/layouts + src/components + global.css</text>
<text
x="118"
y="308"
class="s"
id="text10"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Plugin paragraph-ids ajoute des ids stables sur paragraphes</text>
<rect
x="560"
y="140"
width="500"
height="260"
class="box"
id="rect10" />
<text
x="698"
y="230"
class="h"
id="text11"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Build (Astro static)</text>
<text
x="698"
y="254"
class="t"
id="text12"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">astro build → dist/**/index.html</text>
<text
x="698"
y="272"
class="t"
id="text13"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">meta Pagefind: edition/level/status/version</text>
<text
x="698"
y="290"
class="t"
id="text14"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Layout : EditionLayout + SiteLayout</text>
<text
x="698"
y="308"
class="s"
id="text15"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">data-pagefind-body = zone indexée</text>
<rect
x="560"
y="430"
width="497.57944"
height="249.74281"
class="box2"
id="rect15" />
<text
x="658"
y="520"
class="h"
id="text16"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Postbuild (qualité + recherche + indexes)</text>
<text
x="658"
y="544"
class="t"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Aliases d'ancres (backward compat)</text>
<text
x="658"
y="562"
class="t"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Dédoublonnage d'IDs (anti-régression)</text>
<text
x="658"
y="580"
class="t"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Index des paragraphes (para-index)</text>
<text
x="658"
y="598"
class="t"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Index des annotations (annotations-index)</text>
<text
x="658"
y="616"
class="t"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Pagefind (recherche full-text)</text>
<rect
x="1120"
y="140"
width="436.36914"
height="259.36459"
class="box"
id="rect21" />
<text
x="1238"
y="210"
class="h"
id="text22"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Artefacts (dist/)</text>
<text
x="1238"
y="234"
class="mono"
id="text23"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">HTML statique + assets</text>
<text
x="1238"
y="252"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/pagefind/**</text>
<text
x="1238"
y="270"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/para-index.json</text>
<text
x="1238"
y="288"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dist/annotations-index.json</text>
<text
x="1238"
y="306"
class="s"
id="text27"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">(en dev) recopiés dans public/*-index.json</text>
<rect
x="1120"
y="430"
width="436.36917"
height="249.48566"
class="box2"
id="rect27" />
<text
x="1178"
y="520"
class="h"
id="text28"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Runtime navigateur (lecture)</text>
<text
x="1178"
y="544"
class="t"
id="text29"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">LocalToc sync (H2/H3)</text>
<text
x="1178"
y="562"
class="t"
id="text30"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">banner-follow + reading-follow__inner</text>
<text
x="1178"
y="580"
class="t"
id="text31"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">SidePanel: niveaux + annotations + propose</text>
<text
x="1178"
y="598"
class="s"
id="text32"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Comportement lecture: H2/H3 unifiés (plus daccordéon gênant)</text>
<rect
x="40"
y="430"
width="460"
height="250"
class="box2"
id="rect32" />
<text
x="138"
y="524"
class="h"
id="text33"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Flux “Proposer” (tickets)</text>
<text
x="138"
y="548"
class="t"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI collecte: page + paragraphe + type + message</text>
<text
x="138"
y="566"
class="t"
id="text35"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Création d'issue Gitea (labels)</text>
<text
x="138"
y="584"
class="t"
id="text36"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Lien retour: issue → page + id</text>
<text
x="138"
y="602"
class="s"
id="text37"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Option: bridge same-origin pour éviter CORS/auth</text>
<path
d="M500 250 C530 250 530 250 560 250"
class="arrowA"
id="path37" />
<path
d="M810 400 C810 420 810 420 810 430"
class="arrowA"
id="path38" />
<path
d="M1060 250 C1090 250 1090 250 1120 250"
class="arrowA"
id="path39" />
<path
d="M 1338.7897,398.15432 1340,430"
class="arrow"
id="path40"
sodipodi:nodetypes="cc" />
<path
d="M500 540 C620 540 620 520 560 520"
class="arrow"
id="path41" />
<text
x="520"
y="525"
class="s"
id="text41">issues</text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect41" />
<text
x="58"
y="850"
class="h"
id="text42">Conseil de maintenance</text>
<text
x="58"
y="874"
class="s"
id="text43">Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR.</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,596 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1500"
height="940"
viewBox="0 0 1500 940"
role="img"
aria-label="Archicratie — Machine éditoriale (synthèse) : DEV/PROD, indices, postbuild, proposer/bridge"
version="1.1"
id="svg71"
sodipodi:docname="archicratie-web-edition-machine-editoriale-v3.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview71"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.88133333"
inkscape:cx="794.25113"
inkscape:cy="350.03782"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg71" />
<defs
id="defs2">
<style
id="style1">
/* Inkscape-safe: pas de CSS variables, fond explicite */
.title{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:26px; font-weight:800; fill:#0f172a;}
.sub{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:600; fill:#475569;}
.laneT{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:800; fill:#0f172a; letter-spacing:.2px;}
.laneN{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:600; fill:#475569;}
.h{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:800; fill:#0f172a;}
.p{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:600; fill:#475569;}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace; font-size:11px; font-weight:700; fill:#334155;}
.small{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:11px; font-weight:600; fill:#475569;}
.canvas{fill:#f8fafc; stroke:#e2e8f0; stroke-width:1;}
.lane{fill:#f1f5f9; stroke:#cbd5e1; stroke-width:1;}
.laneAlt{fill:#eef2f7; stroke:#cbd5e1; stroke-width:1;}
.box{fill:#ffffff; stroke:#94a3b8; stroke-width:1.4;}
.boxAlt{fill:#f8fafc; stroke:#94a3b8; stroke-width:1.4;}
.call{fill:#ffffff; fill-opacity:.72; stroke:#cbd5e1; stroke-width:1.1;}
.arrow{stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#ah);}
.arrowSoft{stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#ahSoft);}
.dash{stroke-dasharray:7 6;}
</style>
<marker
id="ah"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#64748b"
id="path1" />
</marker>
<marker
id="ahSoft"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#94a3b8"
id="path2" />
</marker>
</defs>
<!-- Fond explicite -->
<rect
x="0.5"
y="0.5"
width="1499"
height="939"
rx="26"
class="canvas"
id="rect2" />
<!-- Titre -->
<text
x="44"
y="54"
class="title"
id="text2">Archicratie — Machine éditoriale (synthèse “exploitation + onboarding”)</text>
<text
x="44"
y="82"
class="sub"
id="text3">Inclut : DEV vs PROD (indices), ordre exact du postbuild, et fork “Proposer” (direct vs bridge same-origin).</text>
<!-- Lanes -->
<rect
x="40"
y="115"
width="390"
height="770"
rx="18"
class="lane"
id="rect3" />
<rect
x="450"
y="115"
width="390"
height="770"
rx="18"
class="laneAlt"
id="rect4" />
<rect
x="860"
y="115"
width="330"
height="770"
rx="18"
class="lane"
id="rect5" />
<rect
x="1210"
y="115"
width="250"
height="770"
rx="18"
class="laneAlt"
id="rect6" />
<text
x="60"
y="148"
class="laneT"
id="text6">1) Sources &amp; vérité éditoriale</text>
<text
x="60"
y="170"
class="laneN"
id="text7">Ce qui est versionné (canon) et transforme lédition</text>
<text
x="470"
y="148"
class="laneT"
id="text8">2) Build Astro (static)</text>
<text
x="470"
y="170"
class="laneN"
id="text9">Rendu HTML + UI dédition (EditionLayout)</text>
<text
x="880"
y="148"
class="laneT"
id="text10">3) Postbuild (ordre exact)</text>
<text
x="880"
y="170"
class="laneN"
id="text11">Anti-régressions + index + recherche</text>
<text
x="1230"
y="148"
class="laneT"
id="text12">4) Runtime &amp; feedback</text>
<text
x="1230"
y="170"
class="laneN"
id="text13">DEV (public) / PROD (dist) + Proposer</text>
<!-- Lane 1 -->
<rect
x="70"
y="210"
width="330"
height="135"
rx="16"
class="box"
id="rect13" />
<text
x="92"
y="238"
class="h"
id="text14">Sources amont (traçabilité)</text>
<text
x="92"
y="260"
class="p"
id="text15">Fichiers “sources/” (docx/pdf) + historiques.</text>
<text
x="92"
y="282"
class="mono"
id="text16">sources/** (non servi tel quel)</text>
<text
x="92"
y="304"
class="small"
id="text17">→ import/pipeline vers le contenu canon.</text>
<rect
x="70"
y="365"
width="330"
height="190"
rx="16"
class="boxAlt"
id="rect17" />
<text
x="92"
y="393"
class="h"
id="text18">Contenu canon (site)</text>
<text
x="92"
y="415"
class="p"
id="text19">Pages : MD/MDX (Astro content)</text>
<text
x="92"
y="437"
class="mono"
id="text20">src/content/**</text>
<text
x="92"
y="461"
class="p"
id="text21">Annotations : YAML</text>
<text
x="92"
y="483"
class="mono"
id="text22">src/annotations/**</text>
<text
x="78"
y="507"
class="small"
id="text23">Ces deux entrées alimentent lUI (SidePanel, highlights, etc.).</text>
<rect
x="70"
y="575"
width="330"
height="140"
rx="16"
class="box"
id="rect23" />
<text
x="92"
y="603"
class="h"
id="text24">Scripts dimport / qualité</text>
<text
x="92"
y="625"
class="p"
id="text25">Import DOCX, contrôle dIDs, aliases, etc.</text>
<text
x="92"
y="647"
class="mono"
id="text26">scripts/*.mjs</text>
<text
x="86"
y="671"
class="small"
id="text27">Objectif : build reproductible + pas de régression dancres.</text>
<!-- Lane 2 -->
<rect
x="480"
y="210"
width="330"
height="170"
rx="16"
class="box"
id="rect27" />
<text
x="502"
y="238"
class="h"
id="text28">EditionLayout (UI dédition)</text>
<text
x="502"
y="260"
class="p"
id="text29"><tspan
sodipodi:role="line"
id="tspan71"
x="502"
y="260">SiteNav + TOC global + TOC local + reading-follow</tspan><tspan
sodipodi:role="line"
id="tspan72"
x="502"
y="275">+ SidePanel.</tspan></text>
<text
x="508"
y="300"
class="mono"
id="text30">src/layouts/EditionLayout.astro</text>
<text
x="508"
y="322"
class="mono"
id="text31">src/components/SidePanel.astro</text>
<text
x="508"
y="344"
class="small"
id="text32"><tspan
sodipodi:role="line"
id="tspan73"
x="508"
y="344">Globals boot (flags/env)</tspan><tspan
sodipodi:role="line"
id="tspan74"
x="508"
y="357.75">+ interactions “Propos / Réfs / Illus / Com”.</tspan></text>
<rect
x="480"
y="400"
width="330"
height="160"
rx="16"
class="boxAlt"
id="rect32" />
<text
x="502"
y="428"
class="h"
id="text33">Build statique</text>
<text
x="502"
y="450"
class="p"
id="text34">Astro génère HTML &amp; assets.</text>
<text
x="502"
y="472"
class="mono"
id="text35">npm run build → astro build</text>
<text
x="502"
y="496"
class="p"
id="text36">Sortie :</text>
<text
x="502"
y="518"
class="mono"
id="text37">dist/**</text>
<!-- Lane 3 -->
<rect
x="890"
y="210"
width="270"
height="265"
rx="16"
class="box"
id="rect37" />
<text
x="912"
y="238"
class="h"
id="text38">Postbuild : ordre exact (fixe)</text>
<text
x="912"
y="264"
class="mono"
id="text39">1) inject-anchor-aliases.mjs</text>
<text
x="912"
y="286"
class="mono"
id="text40">2) dedupe-ids-dist.mjs</text>
<text
x="912"
y="308"
class="mono"
id="text41">3) build-para-index.mjs</text>
<text
x="912"
y="330"
class="mono"
id="text42">4) build-annotations-index.mjs</text>
<text
x="912"
y="352"
class="mono"
id="text43">5) pagefind</text>
<text
x="912"
y="380"
class="p"
id="text44">Sorties PROD :</text>
<text
x="912"
y="402"
class="mono"
id="text45">dist/para-index.json</text>
<text
x="912"
y="424"
class="mono"
id="text46">dist/annotations-index.json</text>
<text
x="912"
y="446"
class="mono"
id="text47">dist/pagefind/**</text>
<!-- Lane 4 -->
<rect
x="1230"
y="210"
width="210"
height="200"
rx="16"
class="box"
id="rect47" />
<text
x="1252"
y="238"
class="h"
id="text48">DEV vs PROD : indices</text>
<text
x="1252"
y="262"
class="p"
id="text49">PROD (statique) lit dans :</text>
<text
x="1252"
y="284"
class="mono"
id="text50">dist/*.json</text>
<text
x="1252"
y="308"
class="p"
id="text51">DEV (astro dev) sert depuis :</text>
<text
x="1252"
y="330"
class="mono"
id="text52">public/*.json</text>
<text
x="1252"
y="356"
class="small"
id="text53"><tspan
sodipodi:role="line"
id="tspan75"
x="1252"
y="356">DEV : predev copie/génère</tspan><tspan
sodipodi:role="line"
id="tspan76"
x="1252"
y="369.75">les index pour éviter les 404.</tspan></text>
<rect
x="1230"
y="430"
width="210"
height="215"
rx="16"
class="boxAlt"
id="rect53" />
<text
x="1252"
y="458"
class="h"
id="text54">“Proposer” : 2 modes</text>
<text
x="1252"
y="482"
class="p"
id="text55">A) Direct client → Gitea API</text>
<text
x="1252"
y="504"
class="small"
id="text56">⚠️ CORS/auth/token (déconseillé)</text>
<text
x="1252"
y="536"
class="p"
id="text57">B) Bridge same-origin (reco)</text>
<text
x="1252"
y="558"
class="mono"
id="text58">PUBLIC_ISSUE_BRIDGE_PATH</text>
<text
x="1252"
y="582"
class="small"
id="text59"><tspan
sodipodi:role="line"
id="tspan77"
x="1252"
y="582">UI → bridge → Gitea</tspan><tspan
sodipodi:role="line"
id="tspan78"
x="1252"
y="596.18488">(secrets côté serveur)</tspan></text>
<rect
x="1230"
y="665"
width="210"
height="120"
rx="16"
class="box"
id="rect59" />
<text
x="1252"
y="693"
class="h"
id="text60">Gitea (Issues)</text>
<text
x="1252"
y="717"
class="p"
id="text61">Création + suivi</text>
<text
x="1252"
y="739"
class="mono"
id="text62">/issues/new</text>
<text
x="1232"
y="763"
class="small"
id="text63">Labels/assignee selon règles déquipe.</text>
<!-- Callout (rappel pro) -->
<rect
x="470"
y="590"
width="720"
height="120"
rx="14"
class="call"
id="rect63" />
<text
x="490"
y="618"
class="h"
id="text64">Rappel “pro” (anti régression)</text>
<text
x="490"
y="642"
class="p"
id="text65">• Lordre du postbuild ne doit pas changer sans raison : il garantit ancres stables + index cohérents.</text>
<text
x="490"
y="664"
class="p"
id="text66">• DEV sert des index dans <tspan
class="mono"
id="tspan65">public/</tspan> ; PROD lit dans <tspan
class="mono"
id="tspan66">dist/</tspan>.</text>
<text
x="490"
y="686"
class="p"
id="text67">• Pour “Proposer”, préférer le bridge same-origin : pas de token côté navigateur.</text>
<!-- Arrows -->
<path
class="arrow"
d="M400 460 C430 460, 450 450, 480 480"
id="path67" />
<path
class="arrow"
d="M810 480 C840 480, 860 470, 890 430"
id="path68" />
<path
class="arrowSoft dash"
d="M1030 460 C1120 500, 1180 520, 1230 330"
id="path69" />
<path
class="arrow"
d="M1180 590 C1210 590, 1220 590, 1230 540"
id="path70" />
<path
class="arrow"
d="M1340 645 C1340 660, 1340 660, 1340 665"
id="path71" />
<!-- Foot -->
<text
x="44"
y="916"
class="sub"
id="text71">Astuce : si Inkscape affichait “noir”, cétait très souvent des CSS variables. Ici : couleurs explicites + fond explicite.</text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,510 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg53"
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim-v2.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview53"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="655.97579"
inkscape:cy="478.66868"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg53" />
<defs
id="defs4">
<!-- Fond clair lisible partout -->
<linearGradient
id="bg"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0"
stop-color="#ffffff"
id="stop1" />
<stop
offset="1"
stop-color="#f1f5f9"
id="stop2" />
</linearGradient>
<!-- Flèches -->
<marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#334155"
id="path2" />
</marker>
<marker
id="arrowA"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#2563eb"
id="path3" />
</marker>
<marker
id="arrowG"
markerWidth="10"
markerHeight="10"
refX="9"
refY="3"
orient="auto">
<path
d="M0,0 L9,3 L0,6 Z"
fill="#059669"
id="path4" />
</marker>
<!-- Styles SANS variables CSS (compat max) -->
<style
id="style4"><![CDATA[
.title{font:800 28px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.subtitle{font:500 14px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.h{font:800 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#0f172a}
.t{font:500 13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#111827}
.s{font:500 12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;fill:#475569}
.mono{font:600 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;fill:#0f172a}
/* Cadres lisibles */
.box{fill:#ffffff;stroke:#0ea5e9;stroke-width:1.4}
.box2{fill:#f8fafc;stroke:#94a3b8;stroke-width:1.2}
/* Pills */
.chip{fill:#dbeafe;stroke:#2563eb;stroke-width:1}
.chip2{fill:#d1fae5;stroke:#059669;stroke-width:1}
.chipW{fill:#fef3c7;stroke:#d97706;stroke-width:1}
.chipD{fill:#fee2e2;stroke:#dc2626;stroke-width:1}
/* Traits / flèches */
.line{stroke:#334155;stroke-width:1.4;fill:none}
.dash{stroke-dasharray:6 6}
.arrow{stroke:#334155;stroke-width:1.8;fill:none;marker-end:url(#arrow)}
.arrowA{stroke:#2563eb;stroke-width:2.0;fill:none;marker-end:url(#arrowA)}
.arrowG{stroke:#059669;stroke-width:2.0;fill:none;marker-end:url(#arrowG)}
]]></style>
</defs>
<rect
x="0"
y="0"
width="1600"
height="900"
fill="url(#bg)"
id="rect4" />
<text
x="40"
y="56"
class="title"
id="text4">Archicratie — Machine éditoriale (v2, verbatim)</text>
<text
x="40"
y="84"
class="subtitle"
id="text5">Détails scripts/fichiers — 2026-02-20</text>
<rect
x="40"
y="140"
width="460"
height="236.05144"
class="box"
id="rect5" />
<text
x="58"
y="170"
class="h"
id="text6">Sources (repo)</text>
<text
x="58"
y="194"
class="t"
id="text7">Contenu : src/content/** (MD/MDX)</text>
<text
x="58"
y="212"
class="t"
id="text8">Annotations : src/annotations/** (YAML)</text>
<text
x="58"
y="230"
class="t"
id="text9">UI : src/layouts + src/components + global.css</text>
<text
x="58"
y="248"
class="s"
id="text10">Plugin paragraph-ids ajoute des ids stables sur paragraphes</text>
<rect
x="166"
y="296"
width="190"
height="28"
class="chip"
id="rect10" />
<text
x="180"
y="315"
class="mono"
id="text11"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">rehype-paragraph-ids.js</text>
<rect
x="560"
y="140"
width="500"
height="239.42511"
class="box"
id="rect11" />
<text
x="578"
y="170"
class="h"
id="text12">Build (Astro static)</text>
<text
x="578"
y="194"
class="t"
id="text13">astro build → dist/**/index.html</text>
<text
x="578"
y="212"
class="t"
id="text14">meta Pagefind: edition/level/status/version</text>
<text
x="578"
y="230"
class="t"
id="text15">Layout : EditionLayout + SiteLayout</text>
<text
x="578"
y="248"
class="s"
id="text16">data-pagefind-body = zone indexée</text>
<rect
x="750"
y="300"
width="128"
height="28"
class="chip2"
id="rect16" />
<text
x="764"
y="319"
class="mono"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">npm run build</text>
<rect
x="560"
y="430"
width="500"
height="280"
class="box2"
id="rect17" />
<text
x="578"
y="460"
class="h"
id="text18">Postbuild (qualité + recherche + indexes)</text>
<text
x="578"
y="484"
class="t"
id="text19">Aliases d'ancres (backward compat)</text>
<text
x="578"
y="502"
class="t"
id="text20">Dédoublonnage d'IDs (anti-régression)</text>
<text
x="578"
y="520"
class="t"
id="text21">Index des paragraphes (para-index)</text>
<text
x="578"
y="538"
class="t"
id="text22">Index des annotations (annotations-index)</text>
<text
x="578"
y="556"
class="t"
id="text23">Pagefind (recherche full-text)</text>
<rect
x="690.71106"
y="578.59302"
width="246"
height="28"
class="chip"
id="rect23" />
<text
x="704.71106"
y="597.59302"
class="mono"
id="text24"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">inject-anchor-aliases.mjs</text>
<rect
x="705"
y="622"
width="214"
height="28"
class="chip"
id="rect24" />
<text
x="719"
y="641"
class="mono"
id="text25"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">dedupe-ids-dist.mjs</text>
<rect
x="710.3858"
y="664.52344"
width="206"
height="28"
class="chip2"
id="rect25" />
<text
x="724.3858"
y="683.52344"
class="mono"
id="text26"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">build-para-index.mjs</text>
<rect
x="1265"
y="660"
width="254"
height="28"
class="chip2"
id="rect26" />
<text
x="1279"
y="679"
class="mono"
id="text27">build-annotations-index.mjs</text>
<rect
x="1120"
y="140"
width="440"
height="240"
class="box"
id="rect27" />
<text
x="1138"
y="170"
class="h"
id="text28">Artefacts (dist/)</text>
<text
x="1138"
y="194"
class="mono"
id="text29">HTML statique + assets</text>
<text
x="1138"
y="212"
class="mono"
id="text30">dist/pagefind/**</text>
<text
x="1138"
y="230"
class="mono"
id="text31">dist/para-index.json</text>
<text
x="1138"
y="248"
class="mono"
id="text32">dist/annotations-index.json</text>
<text
x="1138"
y="266"
class="s"
id="text33">(en dev) recopiés dans public/*-index.json</text>
<rect
x="1120"
y="430"
width="441.2103"
height="280.95309"
class="box2"
id="rect33" />
<text
x="1138"
y="460"
class="h"
id="text34">Runtime navigateur (lecture)</text>
<text
x="1138"
y="484"
class="t"
id="text35">LocalToc sync (H2/H3)</text>
<text
x="1138"
y="502"
class="t"
id="text36">banner-follow + reading-follow__inner</text>
<text
x="1138"
y="520"
class="t"
id="text37">SidePanel: niveaux + annotations + propose</text>
<text
x="1138"
y="538"
class="s"
id="text38">Comportement lecture: H2/H3 unifiés (plus daccordéon gênant)</text>
<rect
x="1263.4493"
y="588.65356"
width="150"
height="28"
class="chip2"
id="rect38" />
<text
x="1277.4493"
y="607.65356"
class="mono"
id="text39"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">SidePanel.astro</text>
<rect
x="1260.8547"
y="633.4342"
width="160"
height="28"
class="chip"
id="rect39" />
<text
x="1274.8547"
y="652.4342"
class="mono"
id="text40"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">LevelToggle.astro</text>
<rect
x="41.210289"
y="431.52798"
width="462.42056"
height="275.41605"
class="box2"
id="rect40" />
<text
x="58"
y="470"
class="h"
id="text41"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16px;line-height:1.2;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Flux “Proposer” (tickets)</text>
<text
x="58"
y="494"
class="t"
id="text42"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">UI collecte: page + paragraphe + type + message</text>
<text
x="58"
y="512"
class="t"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Création d'issue Gitea (labels)</text>
<text
x="58"
y="530"
class="t"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Lien retour: issue → page + id</text>
<text
x="58"
y="548"
class="s"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:12px;line-height:1.35;font-family:system-ui, '-apple-system', 'Segoe UI', Roboto, Arial">Option: bridge same-origin pour éviter CORS/auth</text>
<rect
x="60"
y="610"
width="150"
height="28"
class="chip"
id="rect45" />
<text
x="74"
y="629"
class="mono"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PUBLIC_GITEA_*</text>
<rect
x="230"
y="610"
width="262"
height="28"
class="chip2"
id="rect46" />
<text
x="244"
y="629"
class="mono"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:12px;line-height:1.35;font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">PUBLIC_ISSUE_BRIDGE_PATH</text>
<path
d="M500 250 C530 250 530 250 560 250"
class="arrowA"
id="path47" />
<path
d="M810 400 C810 420 810 420 810 430"
class="arrowA"
id="path48" />
<path
d="M1060 250 C1090 250 1090 250 1120 250"
class="arrowA"
id="path49" />
<path
d="M1340 380 C1340 410 1340 410 1340 430"
class="arrow"
id="path50" />
<path
d="M500 540 C620 540 620 520 560 520"
class="arrow"
id="path51" />
<text
x="520"
y="525"
class="s"
id="text51">issues</text>
<rect
x="40"
y="820"
width="1520"
height="60"
class="box2"
id="rect51" />
<text
x="58"
y="850"
class="h"
id="text52">Conseil de maintenance</text>
<text
x="58"
y="874"
class="s"
id="text53">Toute évolution UI/indices doit rester déterministe : build identique sur Mac, CI, et NAS. En cas de hotfix, re-sync via PR.</text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,613 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1500"
height="1050"
viewBox="0 0 1500 1050"
role="img"
aria-label="Archicratie — Machine éditoriale (verbatim) : scripts, indices DEV/PROD, postbuild exact, proposer direct vs bridge"
version="1.1"
id="svg77"
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim-v3.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview77"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.83714286"
inkscape:cx="756.14334"
inkscape:cy="367.32082"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg77" />
<defs
id="defs2">
<style
id="style1">
/* Inkscape-safe: pas de CSS variables */
.title{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:26px; font-weight:900; fill:#0f172a;}
.sub{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:650; fill:#475569;}
.laneT{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:900; fill:#0f172a;}
.laneN{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:650; fill:#475569;}
.h{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:14px; font-weight:900; fill:#0f172a;}
.p{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:12px; font-weight:650; fill:#475569;}
.small{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial; font-size:11px; font-weight:650; fill:#475569;}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace; font-size:11px; font-weight:800; fill:#334155;}
.canvas{fill:#f8fafc; stroke:#e2e8f0; stroke-width:1;}
.lane{fill:#f1f5f9; stroke:#cbd5e1; stroke-width:1;}
.laneAlt{fill:#eef2f7; stroke:#cbd5e1; stroke-width:1;}
.box{fill:#ffffff; stroke:#94a3b8; stroke-width:1.4;}
.boxAlt{fill:#f8fafc; stroke:#94a3b8; stroke-width:1.4;}
.call{fill:#ffffff; fill-opacity:.72; stroke:#cbd5e1; stroke-width:1.1;}
.arrow{stroke:#64748b; stroke-width:2.2; fill:none; marker-end:url(#ah);}
.arrowSoft{stroke:#94a3b8; stroke-width:2; fill:none; marker-end:url(#ahSoft);}
.dash{stroke-dasharray:7 6;}
</style>
<marker
id="ah"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#64748b"
id="path1" />
</marker>
<marker
id="ahSoft"
markerWidth="10"
markerHeight="10"
refX="9"
refY="5"
orient="auto">
<path
d="M0,0 L10,5 L0,10 Z"
fill="#94a3b8"
id="path2" />
</marker>
</defs>
<rect
x="0.5"
y="0.5"
width="1499"
height="1049"
rx="26"
class="canvas"
id="rect2" />
<text
x="44"
y="54"
class="title"
id="text2">Archicratie — Machine éditoriale (verbatim technique)</text>
<text
x="44"
y="82"
class="sub"
id="text3">3 ajouts “pro” inclus : (1) indices DEV vs PROD, (2) fork Proposer direct vs bridge, (3) ordre postbuild exact.</text>
<!-- Lanes -->
<rect
x="40"
y="115"
width="420"
height="880"
rx="18"
class="lane"
id="rect3" />
<rect
x="480"
y="115"
width="520"
height="880"
rx="18"
class="laneAlt"
id="rect4" />
<rect
x="1020"
y="115"
width="440"
height="880"
rx="18"
class="lane"
id="rect5" />
<text
x="60"
y="148"
class="laneT"
id="text5">A) Entrées &amp; canon</text>
<text
x="60"
y="170"
class="laneN"
id="text6">Ce qui est versionné et alimente la build</text>
<text
x="500"
y="148"
class="laneT"
id="text7">B) Build + postbuild</text>
<text
x="500"
y="170"
class="laneN"
id="text8">Astro (static) + scripts (ordre fixe)</text>
<text
x="1040"
y="148"
class="laneT"
id="text9">C) Runtime (DEV/PROD) + “Proposer”</text>
<text
x="1040"
y="170"
class="laneN"
id="text10">Indices servis, UI, et création dissues</text>
<!-- A) Entrées -->
<rect
x="70"
y="210"
width="360"
height="165"
rx="16"
class="box"
id="rect10" />
<text
x="92"
y="238"
class="h"
id="text11">Contenu canon (pages)</text>
<text
x="92"
y="260"
class="p"
id="text12">Astro Content : MD / MDX (pages, chapitres, etc.)</text>
<text
x="92"
y="284"
class="mono"
id="text13">src/content/**</text>
<text
x="92"
y="310"
class="small"
id="text14">Layouts/TOC/reading-follow consomment ces pages.</text>
<text
x="92"
y="334"
class="small"
id="text15">Les IDs de paragraphes doivent rester stables (anti-régression).</text>
<rect
x="70"
y="395"
width="360"
height="190"
rx="16"
class="boxAlt"
id="rect15" />
<text
x="92"
y="423"
class="h"
id="text16">Annotations (surcouche)</text>
<text
x="92"
y="445"
class="p"
id="text17">YAML : notes, refs, illus, commentaires par paragraphe.</text>
<text
x="92"
y="468"
class="mono"
id="text18">src/annotations/**</text>
<text
x="92"
y="494"
class="p"
id="text19">Indexé en JSON pour le SidePanel :</text>
<text
x="92"
y="516"
class="mono"
id="text20">dist/annotations-index.json (PROD)</text>
<text
x="92"
y="538"
class="mono"
id="text21">public/annotations-index.json (DEV)</text>
<rect
x="70"
y="605"
width="360"
height="170"
rx="16"
class="box"
id="rect21" />
<text
x="92"
y="633"
class="h"
id="text22">Scripts (import &amp; qualité)</text>
<text
x="92"
y="655"
class="p"
id="text23">Import DOCX, checks dIDs, aliases dancres, etc.</text>
<text
x="92"
y="678"
class="mono"
id="text24">scripts/import-docx.mjs</text>
<text
x="92"
y="700"
class="mono"
id="text25">scripts/check-anchors.mjs</text>
<text
x="92"
y="724"
class="small"
id="text26">Objectif : build reproductible + compat backward (ancres).</text>
<!-- B) Build + postbuild -->
<rect
x="510"
y="210"
width="460"
height="190"
rx="16"
class="box"
id="rect26" />
<text
x="532"
y="238"
class="h"
id="text27">Build Astro (static)</text>
<text
x="532"
y="260"
class="p"
id="text28">Génère le HTML + assets + routes (output: static).</text>
<text
x="532"
y="284"
class="mono"
id="text29">npm run build</text>
<text
x="532"
y="306"
class="mono"
id="text30">→ astro build → dist/**</text>
<text
x="532"
y="330"
class="p"
id="text31">UI dédition (côté pages) :</text>
<text
x="532"
y="352"
class="mono"
id="text32">src/layouts/EditionLayout.astro</text>
<text
x="532"
y="374"
class="mono"
id="text33">src/components/SidePanel.astro</text>
<rect
x="510"
y="420"
width="460"
height="330"
rx="16"
class="boxAlt"
id="rect33" />
<text
x="532"
y="448"
class="h"
id="text34">Postbuild (ordre exact = contrat)</text>
<text
x="532"
y="472"
class="p"
id="text35">À conserver tel quel pour éviter les régressions.</text>
<text
x="532"
y="500"
class="mono"
id="text36">1) node scripts/inject-anchor-aliases.mjs</text>
<text
x="532"
y="522"
class="mono"
id="text37">2) node scripts/dedupe-ids-dist.mjs</text>
<text
x="532"
y="544"
class="mono"
id="text38"><tspan
sodipodi:role="line"
id="tspan79"
x="532"
y="544">3) node scripts/build-para-index.mjs</tspan><tspan
sodipodi:role="line"
id="tspan80"
x="532"
y="557.75">--in dist --out dist/para-index.json</tspan></text>
<text
x="532"
y="578"
class="mono"
id="text39"><tspan
sodipodi:role="line"
id="tspan81"
x="532"
y="578">4) node scripts/build-annotations-index.mjs</tspan><tspan
sodipodi:role="line"
id="tspan82"
x="532"
y="591.75">--in src/annotations --out dist/annotations-index.json</tspan></text>
<text
x="532"
y="612"
class="mono"
id="text40">5) npx pagefind --site dist (→ dist/pagefind/**)</text>
<text
x="532"
y="638"
class="p"
id="text41">Sorties PROD (statique) :</text>
<text
x="532"
y="660"
class="mono"
id="text42">dist/para-index.json</text>
<text
x="532"
y="682"
class="mono"
id="text43">dist/annotations-index.json</text>
<text
x="532"
y="704"
class="mono"
id="text44">dist/pagefind/**</text>
<text
x="532"
y="736"
class="small"
id="text45">Mini-règle : inject (aliases) AVANT dedupe, et indices AVANT pagefind.</text>
<rect
x="510"
y="770"
width="460"
height="195"
rx="16"
class="box"
id="rect45" />
<text
x="532"
y="798"
class="h"
id="text46">DEV server : indices “public/” (mini-ajout #1)</text>
<text
x="532"
y="822"
class="p"
id="text47">En DEV, lUI lit via HTTP depuis <tspan
class="mono"
id="tspan46">public/</tspan> (pas <tspan
class="mono"
id="tspan47">dist/</tspan>).</text>
<text
x="532"
y="846"
class="mono"
id="text48">predev: build-annotations-index → public/annotations-index.json</text>
<text
x="532"
y="868"
class="mono"
id="text49">predev: build-para-index (depuis dist) → public/para-index.json</text>
<text
x="532"
y="892"
class="small"
id="text50">Si ces fichiers manquent : 404 (normal) → relancer <tspan
class="mono"
id="tspan49">npm run dev</tspan> (predev).</text>
<!-- C) Runtime + proposer -->
<rect
x="1050"
y="210"
width="380"
height="200"
rx="16"
class="box"
id="rect50" />
<text
x="1072"
y="238"
class="h"
id="text51">Runtime PROD</text>
<text
x="1072"
y="262"
class="p"
id="text52">Site statique servi depuis dist/**</text>
<text
x="1072"
y="286"
class="mono"
id="text53">dist/*.html</text>
<text
x="1072"
y="308"
class="mono"
id="text54">dist/para-index.json</text>
<text
x="1072"
y="330"
class="mono"
id="text55">dist/annotations-index.json</text>
<text
x="1072"
y="352"
class="mono"
id="text56">dist/pagefind/**</text>
<text
x="1072"
y="378"
class="small"
id="text57">Ces artefacts sont reproductibles via CI.</text>
<rect
x="1050"
y="430"
width="380"
height="250"
rx="16"
class="boxAlt"
id="rect57" />
<text
x="1072"
y="458"
class="h"
id="text58">“Proposer” (mini-ajout #2)</text>
<text
x="1072"
y="482"
class="p"
id="text59">Depuis SidePanel / ProposeModal (UI).</text>
<text
x="1072"
y="512"
class="p"
id="text60">Mode A — Direct navigateur → Gitea API</text>
<text
x="1072"
y="534"
class="small"
id="text61">⚠️ CORS + auth + token : fragile / déconseillé.</text>
<text
x="1072"
y="566"
class="p"
id="text62">Mode B — Bridge same-origin (recommandé)</text>
<text
x="1072"
y="588"
class="mono"
id="text63">PUBLIC_ISSUE_BRIDGE_PATH (ex: /bridge/issues)</text>
<text
x="1072"
y="612"
class="small"
id="text64">Le serveur/proxy ajoute lauth (secrets) et appelle Gitea côté backend.</text>
<text
x="1072"
y="638"
class="small"
id="text65">Résultat : pas de secret exposé au client + moins de soucis CORS.</text>
<rect
x="1050"
y="700"
width="380"
height="165"
rx="16"
class="box"
id="rect65" />
<text
x="1072"
y="728"
class="h"
id="text66">Gitea : Issues</text>
<text
x="1072"
y="752"
class="p"
id="text67">Création de tickets (PR/CI séparés du flux éditorial)</text>
<text
x="1072"
y="776"
class="mono"
id="text68">/issues/new</text>
<text
x="1072"
y="798"
class="mono"
id="text69">labels / assignee / templates</text>
<text
x="1072"
y="828"
class="small"
id="text70">Le workflow Git/PR/CI reste la source canon (pas la prod).</text>
<!-- Callout -->
<rect
x="1050"
y="885"
width="380"
height="95.972694"
rx="14"
class="call"
id="rect70" />
<text
x="1072"
y="912"
class="h"
id="text71">Mini-ajout #3 — ordre postbuild</text>
<text
x="1072"
y="936"
class="p"
id="text72"><tspan
sodipodi:role="line"
id="tspan77"
x="1072"
y="936">inject-anchor-aliases → dedupe-ids → para-index</tspan><tspan
sodipodi:role="line"
id="tspan78"
x="1072"
y="951.47437">→ annotations-index → pagefind</tspan></text>
<text
x="1072"
y="968"
class="small"
id="text73">Cest le “contrat” anti-régression : si tu changes lordre, tu re-tests tout.</text>
<!-- Arrows (liaisons principales) -->
<path
class="arrow"
d="M430 300 C455 300, 475 300, 510 300"
id="path73" />
<path
class="arrow"
d="M430 500 C455 500, 480 490, 510 520"
id="path74" />
<path
class="arrow"
d="M970 330 C1000 330, 1010 330, 1050 330"
id="path75" />
<path
class="arrowSoft dash"
d="M 967.84983,823.85666 C 1006.4505,737.84983 1012.5597,719.6587 1050,610"
id="path76"
sodipodi:nodetypes="cc" />
<path
class="arrow"
d="M1240 680 C1240 695, 1240 695, 1240 700"
id="path77" />
<text
x="44"
y="1028"
class="sub"
id="text77">Inkscape-safe : couleurs &amp; fond explicites (zéro var()).</text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,634 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg77"
sodipodi:docname="archicratie-web-edition-machine-editoriale-verbatim.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-machine-editoriale-verbatim.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview77"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="524.05446"
inkscape:cy="684.41755"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="235"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg77" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.h3 { font: 700 14px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket)</text>
<text
x="40"
y="75"
class="small"
id="text2">Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH=&quot;/_auth/whoami&quot;, GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias.</text>
<!-- ZONE A: Runtime -->
<rect
x="35"
y="110"
width="1530"
height="420"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea</text>
<!-- Reader -->
<rect
x="60"
y="175"
width="380"
height="160"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Utilisateur (lecteur/éditeur)</text>
<text
x="80"
y="230"
class="txt"
id="text5">• lit une page</text>
<text
x="80"
y="252"
class="txt"
id="text6"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• sur un paragraphe : Citer / Proposer / Marque-page</text>
<text
x="80"
y="274"
class="txt"
id="text7">• Proposer visible uniquement si “editors”</text>
<text
x="80"
y="300"
class="small"
id="text8">(le gate est runtime via whoami)</text>
<!-- Astro page + EditionLayout -->
<rect
x="470"
y="175"
width="560"
height="330"
rx="12"
class="box"
id="rect8" />
<text
x="490"
y="205"
class="h2"
id="text9">Site Astro statique — EditionLayout</text>
<text
x="490"
y="230"
class="mono"
id="text10">src/layouts/EditionLayout.astro</text>
<text
x="490"
y="260"
class="h3"
id="text11">Variables publiques injectées</text>
<text
x="490"
y="282"
class="txt"
id="text13"><tspan
class="mono"
id="tspan11">PUBLIC_GITEA_BASE</tspan>, <tspan
class="mono"
id="tspan12">PUBLIC_GITEA_OWNER</tspan>, <tspan
class="mono"
id="tspan13">PUBLIC_GITEA_REPO</tspan></text>
<text
x="490"
y="304"
class="txt"
id="text14">• si une manque : giteaReady=false → Proposer désactivé</text>
<text
x="490"
y="338"
class="h3"
id="text15">Outils paragraphe</text>
<text
x="490"
y="362"
class="txt"
id="text17"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Citer : copie une citation structurée (titre + URL#ancre)</text>
<text
x="490"
y="384"
class="txt"
id="text18"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• Proposer : modal 2 étapes → ouvre <tspan
class="mono"
id="tspan17"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">/issues/new?...</tspan></text>
<text
x="490"
y="418"
class="h3"
id="text19"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:14px;line-height:normal;font-family:sans-serif">Gate “editors” (whoami)</text>
<text
x="490"
y="440"
class="txt"
id="text20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan19"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">WHOAMI_PATH=&quot;/_auth/whoami&quot;</tspan> + fetch same-origin</text>
<text
x="490"
y="462"
class="txt"
id="text21"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• lit header <tspan
class="mono"
id="tspan20"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">Remote-Groups</tspan> → affiche/retire Proposer du DOM</text>
<!-- Edge/Auth -->
<rect
x="1060"
y="175"
width="250"
height="160"
rx="12"
class="box"
id="rect21" />
<text
x="1080"
y="205"
class="h2"
id="text22">Traefik edge</text>
<text
x="1080"
y="230"
class="txt"
id="text23">• Host(archicratie…)</text>
<text
x="1080"
y="252"
class="txt"
id="text24">• middleware : chain-auth</text>
<text
x="1080"
y="274"
class="txt"
id="text25">• route <tspan
class="mono"
id="tspan24">/_auth/whoami</tspan></text>
<text
x="1080"
y="300"
class="small"
id="text26">forward-auth vers Authelia</text>
<rect
x="1060"
y="350"
width="250"
height="155"
rx="12"
class="box"
id="rect26" />
<text
x="1080"
y="380"
class="h2"
id="text27">Authelia + LLDAP</text>
<text
x="1080"
y="405"
class="txt"
id="text28">• forward-auth : <tspan
class="mono"
id="tspan27">:9091</tspan></text>
<text
x="1080"
y="427"
class="txt"
id="text29">• groupes via LDAP</text>
<text
x="1080"
y="449"
class="txt"
id="text30">• injecte headers Remote-*</text>
<text
x="1080"
y="471"
class="small"
id="text31">non auth ⇒ 302 vers auth.*</text>
<!-- Gitea issue -->
<rect
x="1330"
y="175"
width="235"
height="330"
rx="12"
class="box"
id="rect31" />
<text
x="1350"
y="205"
class="h2"
id="text32">Gitea (UI)</text>
<text
x="1350"
y="230"
class="txt"
id="text33">Issue préremplie :</text>
<text
x="1350"
y="255"
class="mono"
id="text34">BASE/OWNER/REPO/issues/new</text>
<text
x="1350"
y="287"
class="txt"
id="text35">Contenu typique :</text>
<text
x="1350"
y="309"
class="txt"
id="text36">• URL page + <tspan
class="mono"
id="tspan35">#p-…</tspan></text>
<text
x="1350"
y="331"
class="txt"
id="text37">• Type / State / Category</text>
<text
x="1350"
y="353"
class="txt"
id="text38">• proposition / commentaire</text>
<text
x="1350"
y="385"
class="txt"
id="text39">Résultat :</text>
<text
x="1350"
y="407"
class="txt"
id="text40">• issue = backlog éditorial</text>
<text
x="1350"
y="429"
class="txt"
id="text41">• labels (CI/bot) pour tri</text>
<!-- Runtime connections -->
<path
d="M 440 260 L 470 260"
class="line"
id="path41" />
<path
d="M 1030 470 L 1060 470"
class="dash"
id="path42" />
<path
d="M 1310 320 L 1330 320"
class="line"
id="path43" />
<path
d="M 1030 270 L 1060 270"
class="dash"
id="path44" />
<rect
x="60"
y="355"
width="380"
height="150"
rx="12"
class="note"
id="rect44" />
<text
x="80"
y="385"
class="h2"
id="text44">Contrat runtime (robuste)</text>
<text
x="80"
y="410"
class="txt"
id="text45">• Citer marche sans droits (copie + lien)</text>
<text
x="80"
y="432"
class="txt"
id="text46">• Proposer nexiste pas si non “editors”</text>
<text
x="80"
y="454"
class="txt"
id="text47">• whoami renvoie 302 login si non auth</text>
<text
x="80"
y="476"
class="txt"
id="text48">• si PUBLIC_GITEA_* faux → 404/login loop</text>
<!-- ZONE B: CI / labels -->
<rect
x="35"
y="545"
width="1530"
height="210"
rx="18"
class="zone"
id="rect48" />
<text
x="60"
y="580"
class="h2"
id="text49">B — Automatisation : issue → labels + checks qualité</text>
<rect
x="60"
y="610"
width="520"
height="120"
rx="12"
class="box"
id="rect49" />
<text
x="80"
y="640"
class="h2"
id="text50">Gitea Actions (workflows)</text>
<text
x="80"
y="665"
class="txt"
id="text51">• triggers : issues opened / edited (labels)</text>
<text
x="80"
y="687"
class="txt"
id="text52">• checks build : anchors / aliases / inline-js / dist audit</text>
<text
x="80"
y="709"
class="small"
id="text53">token API requis côté job (ex : FORGE_TOKEN) pour écrire labels</text>
<rect
x="610"
y="610"
width="430"
height="120"
rx="12"
class="box"
id="rect53" />
<text
x="630"
y="640"
class="h2"
id="text54">Runner</text>
<text
x="630"
y="665"
class="txt"
id="text55">• conteneur : <tspan
class="mono"
id="tspan54">gitea-act-runner (gitea/act_runner)</tspan></text>
<text
x="630"
y="687"
class="txt"
id="text56">• exécute jobs (souvent en conteneur)</text>
<text
x="630"
y="709"
class="txt"
id="text57">• appelle API Gitea pour labels</text>
<rect
x="1070"
y="610"
width="465"
height="120"
rx="12"
class="box"
id="rect57" />
<text
x="1090"
y="640"
class="h2"
id="text58">Gitea (API) + labels</text>
<text
x="1090"
y="665"
class="txt"
id="text59">• labels = tri natif (type/state/cat)</text>
<text
x="1090"
y="687"
class="txt"
id="text60">• backlog propre, opérable</text>
<text
x="1090"
y="709"
class="small"
id="text61">si 401 : token manquant/mauvais droits</text>
<path
d="M 580 670 L 610 670"
class="line"
id="path61" />
<path
d="M 1040 670 L 1070 670"
class="line"
id="path62" />
<!-- ZONE C: Re-integration + anchors -->
<rect
x="35"
y="780"
width="1530"
height="210"
rx="18"
class="zone"
id="rect62" />
<text
x="400"
y="815"
class="h2"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time)</text>
<rect
x="60"
y="850"
width="520"
height="115"
rx="12"
class="box"
id="rect63" />
<text
x="80"
y="880"
class="h2"
id="text63">Apply-ticket</text>
<text
x="80"
y="905"
class="mono"
id="text64">scripts/apply-ticket.mjs &lt;issue_number&gt; --alias</text>
<text
x="80"
y="929"
class="txt"
id="text65">• applique patch dans src/content/…</text>
<text
x="80"
y="951"
class="txt"
id="text66">• écrit alias old→new dans <tspan
class="mono"
id="tspan65">src/anchors/anchor-aliases.json</tspan></text>
<rect
x="610.74738"
y="848.78973"
width="591.40692"
height="117.42059"
rx="12"
class="box"
id="rect66" />
<text
x="630"
y="880"
class="h2"
id="text67">Aliases canon + injection</text>
<text
x="630"
y="905"
class="mono"
id="text68">src/anchors/anchor-aliases.json</text>
<text
x="630"
y="929"
class="txt"
id="text69">postbuild : <tspan
class="mono"
id="tspan68">node scripts/inject-anchor-aliases.mjs</tspan></text>
<text
x="630"
y="951"
class="txt"
id="text70">• injecte <tspan
class="mono"
id="tspan69">&lt;span id=&quot;oldId&quot; class=&quot;para-alias&quot;&gt;</tspan> avant newId dans dist/**/index.html</text>
<rect
x="1227.5703"
y="850"
width="331.42966"
height="115"
rx="12"
class="box"
id="rect70" />
<text
x="1240"
y="880"
class="h2"
id="text71"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Preuves (tests)</text>
<text
x="1240"
y="905"
class="txt"
id="text72"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan71"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/check-anchor-aliases.mjs</tspan></text>
<text
x="1240"
y="929"
class="txt"
id="text73"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan72"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/verify-anchor-aliases-in-dist.mjs</tspan></text>
<text
x="1240"
y="951"
class="txt"
id="text74"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
class="mono"
id="tspan73"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:ui-monospace, SFMono-Regular, Menlo, monospace">scripts/check-anchors.mjs</tspan></text>
<path
d="M 1495 545 L 1495 505"
class="dash"
id="path74" />
<path
d="M 320 730 L 320 850"
class="line"
id="path75" />
<path
d="M 580 908 L 610 908"
class="line"
id="path76" />
<path
d="M 1202.0514,908 H 1226"
class="line"
id="path77"
sodipodi:nodetypes="cc" />
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,631 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="1020"
viewBox="0 0 1600 1020"
version="1.1"
id="svg77"
sodipodi:docname="archicratie-web-edition-machine-editoriale.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
inkscape:export-filename="out/archicratie-web-edition-machine-editoriale.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview77"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="409.07716"
inkscape:cy="573.0711"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg77" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.h3 { font: 700 14px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.mono { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; fill:#111; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie Web Edition : Machine éditoriale VERBATIM (Proposer / Citer / /_auth/whoami / aliases / apply-ticket)</text>
<text
x="40"
y="75"
class="small"
id="text2">Sources “vraies” : src/layouts/EditionLayout.astro (WHOAMI_PATH=&quot;/_auth/whoami&quot;, GITEA_* via import.meta.env.PUBLIC_*), src/anchors/anchor-aliases.json, scripts/inject-anchor-aliases.mjs, scripts/apply-ticket.mjs --alias.</text>
<!-- ZONE A: Runtime -->
<rect
x="35"
y="110"
width="1530"
height="420"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">A — Runtime (navigateur) : paragraphe → outils (¶ / Citer / Proposer) → issue Gitea</text>
<!-- Reader -->
<rect
x="60"
y="175"
width="380"
height="160"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Utilisateur (lecteur/éditeur)</text>
<text
x="80"
y="230"
class="txt"
id="text5">• lit une page</text>
<text
x="80"
y="252"
class="txt"
id="text6">• sur un paragraphe : ¶ / Citer / Proposer</text>
<text
x="80"
y="274"
class="txt"
id="text7">• Proposer visible uniquement si “editors”</text>
<text
x="80"
y="300"
class="small"
id="text8">(le gate est runtime via whoami)</text>
<!-- Astro page + EditionLayout -->
<rect
x="470"
y="175"
width="560"
height="330"
rx="12"
class="box"
id="rect8" />
<text
x="490"
y="205"
class="h2"
id="text9">Site Astro statique — EditionLayout</text>
<text
x="490"
y="230"
class="mono"
id="text10">src/layouts/EditionLayout.astro</text>
<text
x="490"
y="260"
class="h3"
id="text11">Variables publiques injectées</text>
<text
x="490"
y="282"
class="txt"
id="text13"><tspan
class="mono"
id="tspan11">PUBLIC_GITEA_BASE</tspan>, <tspan
class="mono"
id="tspan12">PUBLIC_GITEA_OWNER</tspan>, <tspan
class="mono"
id="tspan13">PUBLIC_GITEA_REPO</tspan></text>
<text
x="490"
y="304"
class="txt"
id="text14">• si une manque : giteaReady=false → Proposer désactivé</text>
<text
x="490"
y="338"
class="h3"
id="text15">Outils paragraphe</text>
<text
x="490"
y="360"
class="txt"
id="text16">• ¶ : lien dancre vers <tspan
class="mono"
id="tspan15">#p-…</tspan></text>
<text
x="490"
y="382"
class="txt"
id="text17">• Citer : copie une citation structurée (titre + URL#ancre)</text>
<text
x="490"
y="404"
class="txt"
id="text18">• Proposer : modal 2 étapes → ouvre <tspan
class="mono"
id="tspan17">/issues/new?...</tspan></text>
<text
x="490"
y="438"
class="h3"
id="text19">Gate “editors” (whoami)</text>
<text
x="490"
y="460"
class="txt"
id="text20"><tspan
class="mono"
id="tspan19">WHOAMI_PATH=&quot;/_auth/whoami&quot;</tspan> + fetch same-origin</text>
<text
x="490"
y="482"
class="txt"
id="text21">• lit header <tspan
class="mono"
id="tspan20">Remote-Groups</tspan> → affiche/retire Proposer du DOM</text>
<!-- Edge/Auth -->
<rect
x="1060"
y="175"
width="250"
height="160"
rx="12"
class="box"
id="rect21" />
<text
x="1080"
y="205"
class="h2"
id="text22">Traefik edge</text>
<text
x="1080"
y="230"
class="txt"
id="text23">• Host(archicratie…)</text>
<text
x="1080"
y="252"
class="txt"
id="text24">• middleware : chain-auth</text>
<text
x="1080"
y="274"
class="txt"
id="text25">• route <tspan
class="mono"
id="tspan24">/_auth/whoami</tspan></text>
<text
x="1080"
y="300"
class="small"
id="text26">forward-auth vers Authelia</text>
<rect
x="1060"
y="350"
width="250"
height="155"
rx="12"
class="box"
id="rect26" />
<text
x="1080"
y="380"
class="h2"
id="text27">Authelia + LLDAP</text>
<text
x="1080"
y="405"
class="txt"
id="text28">• forward-auth : <tspan
class="mono"
id="tspan27">:9091</tspan></text>
<text
x="1080"
y="427"
class="txt"
id="text29">• groupes via LDAP</text>
<text
x="1080"
y="449"
class="txt"
id="text30">• injecte headers Remote-*</text>
<text
x="1080"
y="471"
class="small"
id="text31">non auth ⇒ 302 vers auth.*</text>
<!-- Gitea issue -->
<rect
x="1330"
y="175"
width="220.47655"
height="328.7897"
rx="12"
class="box"
id="rect31" />
<text
x="1350"
y="205"
class="h2"
id="text32">Gitea (UI)</text>
<text
x="1350"
y="230"
class="txt"
id="text33">Issue préremplie :</text>
<text
x="1350"
y="255"
class="mono"
id="text34">BASE/OWNER/REPO/issues/new</text>
<text
x="1350"
y="287"
class="txt"
id="text35">Contenu typique :</text>
<text
x="1350"
y="309"
class="txt"
id="text36">• URL page + <tspan
class="mono"
id="tspan35">#p-…</tspan></text>
<text
x="1350"
y="331"
class="txt"
id="text37">• Type / State / Category</text>
<text
x="1350"
y="353"
class="txt"
id="text38">• proposition / commentaire</text>
<text
x="1350"
y="385"
class="txt"
id="text39">Résultat :</text>
<text
x="1350"
y="407"
class="txt"
id="text40">• issue = backlog éditorial</text>
<text
x="1350"
y="429"
class="txt"
id="text41">• labels (CI/bot) pour tri</text>
<!-- Runtime connections -->
<path
d="M 440 260 L 470 260"
class="line"
id="path41" />
<path
d="M 1030 470 L 1060 470"
class="dash"
id="path42" />
<path
d="M 1310 320 L 1330 320"
class="line"
id="path43" />
<path
d="M 1030 270 L 1060 270"
class="dash"
id="path44" />
<rect
x="60"
y="355"
width="380"
height="150"
rx="12"
class="note"
id="rect44" />
<text
x="80"
y="385"
class="h2"
id="text44">Contrat runtime (robuste)</text>
<text
x="80"
y="410"
class="txt"
id="text45">• Citer marche sans droits (copie + lien)</text>
<text
x="80"
y="432"
class="txt"
id="text46">• Proposer nexiste pas si non “editors”</text>
<text
x="80"
y="454"
class="txt"
id="text47">• whoami renvoie 302 login si non auth</text>
<text
x="80"
y="476"
class="txt"
id="text48">• si PUBLIC_GITEA_* faux → 404/login loop</text>
<!-- ZONE B: CI / labels -->
<rect
x="35"
y="545"
width="1530"
height="210"
rx="18"
class="zone"
id="rect48" />
<text
x="60"
y="580"
class="h2"
id="text49">B — Automatisation : issue → labels + checks qualité</text>
<rect
x="60"
y="610"
width="520"
height="120"
rx="12"
class="box"
id="rect49" />
<text
x="80"
y="640"
class="h2"
id="text50">Gitea Actions (workflows)</text>
<text
x="80"
y="665"
class="txt"
id="text51">• triggers : issues opened / edited (labels)</text>
<text
x="80"
y="687"
class="txt"
id="text52">• checks build : anchors / aliases / inline-js / dist audit</text>
<text
x="80"
y="709"
class="small"
id="text53">token API requis côté job (ex : FORGE_TOKEN) pour écrire labels</text>
<rect
x="610"
y="610"
width="430"
height="120"
rx="12"
class="box"
id="rect53" />
<text
x="630"
y="640"
class="h2"
id="text54">Runner</text>
<text
x="630"
y="665"
class="txt"
id="text55">• conteneur : <tspan
class="mono"
id="tspan54">gitea-act-runner (gitea/act_runner)</tspan></text>
<text
x="630"
y="687"
class="txt"
id="text56">• exécute jobs (souvent en conteneur)</text>
<text
x="630"
y="709"
class="txt"
id="text57">• appelle API Gitea pour labels</text>
<rect
x="1070"
y="610"
width="465"
height="120"
rx="12"
class="box"
id="rect57" />
<text
x="1090"
y="640"
class="h2"
id="text58">Gitea (API) + labels</text>
<text
x="1090"
y="665"
class="txt"
id="text59">• labels = tri natif (type/state/cat)</text>
<text
x="1090"
y="687"
class="txt"
id="text60">• backlog propre, opérable</text>
<text
x="1090"
y="709"
class="small"
id="text61">si 401 : token manquant/mauvais droits</text>
<path
d="M 580 670 L 610 670"
class="line"
id="path61" />
<path
d="M 1040 670 L 1070 670"
class="line"
id="path62" />
<!-- ZONE C: Re-integration + anchors -->
<rect
x="35"
y="780"
width="1530"
height="210"
rx="18"
class="zone"
id="rect62" />
<text
x="300"
y="815"
class="h2"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">C — Réintégration : correction → contenu web + stabilité des ancres (aliases build-time)</text>
<rect
x="60"
y="850"
width="520"
height="115"
rx="12"
class="box"
id="rect63" />
<text
x="80"
y="880"
class="h2"
id="text63">Apply-ticket</text>
<text
x="80"
y="905"
class="mono"
id="text64">scripts/apply-ticket.mjs &lt;issue_number&gt; --alias</text>
<text
x="80"
y="929"
class="txt"
id="text65">• applique patch dans src/content/…</text>
<text
x="80"
y="951"
class="txt"
id="text66">• écrit alias old→new dans <tspan
class="mono"
id="tspan65">src/anchors/anchor-aliases.json</tspan></text>
<rect
x="610"
y="850"
width="518.78973"
height="130.73373"
rx="12"
class="box"
id="rect66" />
<text
x="630"
y="880"
class="h2"
id="text67">Aliases canon + injection</text>
<text
x="630"
y="905"
class="mono"
id="text68">src/anchors/anchor-aliases.json</text>
<text
x="630"
y="929"
class="txt"
id="text69">postbuild : <tspan
class="mono"
id="tspan68">node scripts/inject-anchor-aliases.mjs</tspan></text>
<text
x="630"
y="951"
class="txt"
id="text70"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan77"
x="630"
y="951">• injecte</tspan><tspan
sodipodi:role="line"
id="tspan78"
x="630"
y="967.25">&lt;span id=&quot;oldId&quot; class=&quot;para-alias&quot;&gt; avant newId dans dist/**/index.html</tspan></text>
<rect
x="1160"
y="850"
width="375"
height="115"
rx="12"
class="box"
id="rect70" />
<text
x="1180"
y="880"
class="h2"
id="text71">Preuves (tests)</text>
<text
x="1180"
y="905"
class="txt"
id="text72"><tspan
class="mono"
id="tspan71">scripts/check-anchor-aliases.mjs</tspan></text>
<text
x="1180"
y="929"
class="txt"
id="text73"><tspan
class="mono"
id="tspan72">scripts/verify-anchor-aliases-in-dist.mjs</tspan></text>
<text
x="1180"
y="951"
class="txt"
id="text74"><tspan
class="mono"
id="tspan73">scripts/check-anchors.mjs</tspan></text>
<path
d="M 1495 545 L 1495 505"
class="dash"
id="path74" />
<path
d="M 220,730 V 850"
class="line"
id="path75" />
<path
d="M 580 908 L 610 908"
class="line"
id="path76" />
<path
d="M 1130 908 L 1160 908"
class="line"
id="path77" />
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

618
docs/diagrams/diagram.svg Normal file
View File

@@ -0,0 +1,618 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="1600"
height="980"
viewBox="0 0 1600 980"
version="1.1"
id="svg66"
sodipodi:docname="diagram.svg"
inkscape:version="1.3-alpha (95f74fb, 2023-03-31)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview66"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.82625"
inkscape:cx="1002.118"
inkscape:cy="617.85174"
inkscape:window-width="1472"
inkscape:window-height="1022"
inkscape:window-x="234"
inkscape:window-y="30"
inkscape:window-maximized="0"
inkscape:current-layer="svg66" />
<defs
id="defs1">
<marker
id="arrow"
viewBox="0 0 10 10"
refX="9.5"
refY="5"
markerWidth="8"
markerHeight="8"
orient="auto-start-reverse">
<path
d="M 0 0 L 10 5 L 0 10 z"
fill="#222"
id="path1" />
</marker>
<style
id="style1">
.title { font: 700 22px sans-serif; fill:#111; }
.h2 { font: 700 16px sans-serif; fill:#111; }
.txt { font: 13px sans-serif; fill:#111; }
.small { font: 12px sans-serif; fill:#111; }
.box { fill:#fafafa; stroke:#222; stroke-width:1.5; }
.zone { fill:#f3f3f3; stroke:#111; stroke-width:2; }
.note { fill:#fff; stroke:#666; stroke-width:1.2; }
.line { stroke:#222; stroke-width:2; fill:none; marker-end:url(#arrow); }
.dash { stroke:#222; stroke-width:2; fill:none; stroke-dasharray:7 6; marker-end:url(#arrow); }
</style>
</defs>
<!-- Header -->
<text
x="40"
y="45"
class="title"
id="text1">Archicratie Web Edition : schéma global (Local Mac Studio vs NAS Synology DS220+)</text>
<text
x="40"
y="75"
class="small"
id="text2">Lecture : (1) utilisateur → DSM → Traefik/Authelia → site ; (2) dev → package → slot green/blue → switch Traefik (provider file) ; (3) proposer → issues → runner → labels.</text>
<!-- LOCAL ZONE -->
<rect
x="35"
y="110"
width="520"
height="820"
rx="18"
class="zone"
id="rect2" />
<text
x="60"
y="145"
class="h2"
id="text3">LOCAL — Mac Studio (atelier de dev)</text>
<rect
x="60"
y="175"
width="470"
height="95"
rx="12"
class="box"
id="rect3" />
<text
x="80"
y="205"
class="h2"
id="text4">Repo Astro (édition)</text>
<text
x="80"
y="230"
class="txt"
id="text5">• src/content/ (contenu web)</text>
<text
x="80"
y="250"
class="txt"
id="text6">• scripts tooling (anchors, import docx, apply-ticket)</text>
<rect
x="60"
y="295"
width="470"
height="80"
rx="12"
class="box"
id="rect6" />
<text
x="80"
y="325"
class="h2"
id="text7">Build statique</text>
<text
x="80"
y="350"
class="txt"
id="text8">npm run build → dist/ (site statique)</text>
<rect
x="60"
y="400"
width="470"
height="95"
rx="12"
class="box"
id="rect8" />
<text
x="80"
y="430"
class="h2"
id="text9">Release pack</text>
<text
x="80"
y="455"
class="txt"
id="text10">archive .tar.gz + .sha256</text>
<text
x="80"
y="475"
class="txt"
id="text11">destination NAS : /volume2/docker/archicratie-web/incoming/</text>
<rect
x="60"
y="520"
width="470"
height="120"
rx="12"
class="note"
id="rect11" />
<text
x="80"
y="550"
class="h2"
id="text12">Centre de vérité</text>
<text
x="80"
y="575"
class="txt"
id="text13">Tu commits/push vers Gitea (NAS) :</text>
<text
x="80"
y="598"
class="txt"
id="text14">• code + docs + diagrammes (SVG)</text>
<text
x="80"
y="621"
class="txt"
id="text15">• issues = backlog éditorial</text>
<!-- NAS ZONE -->
<rect
x="590"
y="110"
width="975"
height="820"
rx="18"
class="zone"
id="rect15" />
<text
x="615"
y="145"
class="h2"
id="text16">DISTANT — NAS Synology DS220+ (DSM + Container Manager)</text>
<!-- USERS -->
<rect
x="615"
y="175"
width="275"
height="90"
rx="12"
class="box"
id="rect16" />
<text
x="635"
y="205"
class="h2"
id="text17">Utilisateurs (web)</text>
<text
x="635"
y="230"
class="txt"
id="text18">• visiteurs</text>
<text
x="635"
y="250"
class="txt"
id="text19">• éditeurs (accès protégé)</text>
<!-- DSM Reverse Proxy -->
<rect
x="920"
y="175"
width="615"
height="90"
rx="12"
class="box"
id="rect19" />
<text
x="945"
y="205"
class="h2"
id="text20">DSM Reverse Proxy (HTTPS public → Traefik)</text>
<text
x="945"
y="230"
class="txt"
id="text21">• pointe vers 127.0.0.1:18080 (Traefik edge)</text>
<text
x="945"
y="252"
class="txt"
id="text22">• bascule/rollback = switch Traefik (reload) ; DSM reste stable</text>
<!-- Edge Traefik -->
<rect
x="920"
y="295"
width="615"
height="110"
rx="12"
class="box"
id="rect22" />
<text
x="945"
y="325"
class="h2"
id="text23">Traefik (edge) — écoute 127.0.0.1:18080</text>
<text
x="945"
y="350"
class="txt"
id="text24">• entrée unique derrière DSM</text>
<text
x="945"
y="372"
class="txt"
id="text25">• middlewares : sanitize-remote + forward-auth Authelia</text>
<text
x="945"
y="394"
class="txt"
id="text26">• un seul backend site actif (blue OU green) via 20-archicratie-backend.yml</text>
<!-- Auth Stack -->
<rect
x="615"
y="310"
width="275"
height="250"
rx="12"
class="box"
id="rect26" />
<text
x="635"
y="340"
class="h2"
id="text27">Auth stack</text>
<text
x="635"
y="365"
class="txt"
id="text28">Authelia</text>
<text
x="635"
y="387"
class="small"
id="text29">• forward-auth : /api/authz/forward-auth</text>
<text
x="635"
y="415"
class="txt"
id="text30">LLDAP</text>
<text
x="635"
y="438"
class="small"
id="text31">• annuaire LDAP “source of truth”</text>
<text
x="635"
y="466"
class="txt"
id="text32">Redis</text>
<text
x="635"
y="489"
class="small"
id="text33">• sessions / cache (selon config)</text>
<text
x="635"
y="525"
class="small"
id="text34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan68"
x="635"
y="525">Objectif : SSO/MFA + anti lock-out</tspan><tspan
sodipodi:role="line"
id="tspan69"
x="635"
y="540">déploiement progressif)</tspan></text>
<!-- Web Blue/Green -->
<rect
x="920"
y="435"
width="300"
height="135"
rx="12"
class="box"
id="rect34" />
<text
x="945"
y="465"
class="h2"
id="text35">web_blue (slot A)</text>
<text
x="945"
y="490"
class="txt"
id="text36">127.0.0.1:8081 → container:80</text>
<text
x="945"
y="515"
class="txt"
id="text37">sert dist/ (Nginx/HTTP)</text>
<text
x="945"
y="540"
class="small"
id="text38">ne jamais modifier si LIVE</text>
<rect
x="1235"
y="435"
width="300"
height="135"
rx="12"
class="box"
id="rect38" />
<text
x="1260"
y="465"
class="h2"
id="text39">web_green (slot B)</text>
<text
x="1260"
y="490"
class="txt"
id="text40">127.0.0.1:8082 → container:80</text>
<text
x="1260"
y="515"
class="txt"
id="text41">sert dist/ (Nginx/HTTP)</text>
<text
x="1260"
y="540"
class="small"
id="text42">slot “next” (staging)</text>
<!-- Switch script -->
<rect
x="847.41852"
y="587.45636"
width="691.17664"
height="69.928497"
rx="13.486373"
class="note"
id="rect42" />
<text
x="932.38275"
y="617.42059"
class="txt"
id="text43"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan67"
x="932.38275"
y="617.42059">switch-archicratie.sh : bascule blue/green en réécrivant 20-archicratie-backend.yml</tspan><tspan
sodipodi:role="line"
x="932.38275"
y="633.67059"
id="tspan3">puis reload Traefik (provider file)</tspan></text>
<!-- Gitea -->
<rect
x="615"
y="690"
width="520"
height="160"
rx="12"
class="box"
id="rect43" />
<text
x="635"
y="720"
class="h2"
id="text44"
style="font-style:normal;font-variant:normal;font-weight:700;font-stretch:normal;font-size:16px;line-height:normal;font-family:sans-serif">Gitea (forge web + API)</text>
<text
x="635"
y="745"
class="txt"
id="text45"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• repo = centre de vérité</text>
<text
x="635"
y="768"
class="txt"
id="text46"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• issues = backlog</text>
<text
x="635"
y="791"
class="txt"
id="text47"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif">• labels = tri natif (type/state/cat)</text>
<text
x="635"
y="814"
class="small"
id="text48"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif">Note : parfois laissé sans forward-auth (runner/accès API) selon réglage</text>
<!-- Runner -->
<rect
x="1160"
y="690"
width="375"
height="170"
rx="12"
class="box"
id="rect48" />
<text
x="1185"
y="720"
class="h2"
id="text49">Gitea Actions Runner (act_runner)</text>
<text
x="1185"
y="745"
class="txt"
id="text50">• exécute les workflows</text>
<text
x="1185"
y="768"
class="txt"
id="text51">• doit monter /var/run/docker.sock</text>
<text
x="1185"
y="791"
class="txt"
id="text52">• jobs en conteneur (ex : python:3.12-slim)</text>
<text
x="1185"
y="814"
class="small"
id="text53"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan78"
x="1185"
y="814">• applique labels via API avec PAT (FORGE_TOKEN)</tspan><tspan
sodipodi:role="line"
id="tspan79"
x="1185"
y="829">— sinon 401</tspan></text>
<!-- Connections -->
<!-- User -> DSM -->
<path
d="M 890 220 L 920 220"
class="line"
id="path53" />
<!-- DSM -> Traefik -->
<path
d="M 1225 265 L 1225 295"
class="line"
id="path54" />
<!-- Traefik -> Web (one active) -->
<path
d="M 1100 405 L 1070 435"
class="dash"
id="path55" />
<path
d="M 1355 405 L 1385 435"
class="dash"
id="path56" />
<!-- Traefik -> Auth -->
<path
d="M 920 350 L 890 350"
class="line"
id="path57" />
<!-- DSM -> Gitea (via router / host rules) -->
<path
d="M 917.44325,255.3177 883.05597,688.03328"
class="dash"
id="path58"
sodipodi:nodetypes="cc" />
<!-- Gitea -> Runner -->
<path
d="M 1135 710 L 1160 750"
class="line"
id="path59" />
<!-- Runner -> Gitea API -->
<path
d="M 1160 805 L 1135 740"
class="dash"
id="path60" />
<!-- Local -> NAS incoming -->
<path
d="M 530 448 L 615 448"
class="line"
id="path61" />
<!-- Legend -->
<rect
x="60"
y="670"
width="470"
height="245"
rx="12"
class="note"
id="rect61" />
<text
x="80"
y="700"
class="h2"
id="text61">Légende / invariants (ce qui casse “pour de vrai”)</text>
<text
x="80"
y="725"
class="txt"
id="text62"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan70"
x="80"
y="725">• Prod safe : on ne touche jamais au slot LIVE ;</tspan><tspan
sodipodi:role="line"
id="tspan71"
x="80"
y="741.25">build/test sur lautre ; switch Traefik ; DSM ne change pas.</tspan></text>
<text
x="80"
y="768"
class="txt"
id="text63"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan76"
x="80"
y="768">• Runner : sans docker.sock → aucun job ;</tspan><tspan
sodipodi:role="line"
id="tspan77"
x="80"
y="784.40051">sans FORGE_TOKEN → 401 (labels).</tspan></text>
<text
x="80"
y="811"
class="txt"
id="text64"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan74"
x="80"
y="811">• Edge : Traefik :18080 derrière DSM ; sanitize-remote</tspan><tspan
sodipodi:role="line"
id="tspan75"
x="80"
y="827.25">+ forward-auth Authelia.</tspan></text>
<text
x="80"
y="854"
class="txt"
id="text65"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan1"
x="80"
y="854">• Blue/green : 8081/8082 ; Traefik décide le LIVE</tspan><tspan
sodipodi:role="line"
id="tspan2"
x="80"
y="870.25">(DSM pointe toujours sur :18080).</tspan></text>
<text
x="80"
y="891"
class="small"
id="text66"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;line-height:normal;font-family:sans-serif"><tspan
sodipodi:role="line"
id="tspan72"
x="80"
y="891">Astuce : exporte en PNG/PDF pour lecture “grand public”,</tspan><tspan
sodipodi:role="line"
id="tspan73"
x="80"
y="906">garde SVG comme source éditable.</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

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

@@ -0,0 +1,202 @@
# RUNBOOK — Déploiement Blue/Green (NAS DS220+)
> Objectif : déployer une release **sans casser**, avec rollback immédiat.
## 0) Portée
Ce runbook décrit le déploiement de lédition web Archicratie sur NAS (Synology), en mode blue/green :
- `web_blue` : upstream staging → `127.0.0.1:8081`
- `web_green` : upstream live → `127.0.0.1:8082`
- Edge Traefik publie :
- `staging.archicratie.trans-hands.synology.me` → 8081
- `archicratie.trans-hands.synology.me` → 8082
## 1) Pré-requis
- Accès shell NAS (user `archicratia`) + `sudo`
- Docker Compose Synology nécessite souvent :
- `sudo env DOCKER_API_VERSION=1.43 docker compose ...`
- Les fichiers edge Traefik sont dans :
- `/volume2/docker/edge/config/dynamic/`
## 2) Répertoires canon (NAS)
On considère ces chemins (adapter si besoin, mais rester cohérent) :
- Base : `/volume2/docker/archicratie-web`
- Releases : `/volume2/docker/archicratie-web/releases/YYYYMMDD-HHMMSS/app`
- Symlink actif : `/volume2/docker/archicratie-web/current` → pointe vers le `.../app` actif
## 3) Garde-fous (AVANT toute action)
### 3.1 Snapshot de létat actuel
en bash :
cd /volume2/docker/archicratie-web
ls -la current || true
readlink current || true
### 3.2 Vérifier létat live/staging upstream direct
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
### 3.3 Vérifier létat edge (host routing)
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
Si tu nes pas authentifié, tu verras un 302 vers auth... : cest normal.
## 4) Procédure de déploiement (release pack → nouvelle release)
### 4.1 Déposer le pack
Hypothèse : tu as un .tgz “release pack” (issu de release-pack.sh) dans incoming/ :
cd /volume2/docker/archicratie-web
ls -la incoming | tail -n 20
### 4.2 Créer un répertoire release
TS="$(date +%Y%m%d-%H%M%S)"
REL="/volume2/docker/archicratie-web/releases/$TS"
APP="$REL/app"
sudo mkdir -p "$APP"
### 4.3 Extraire le pack
PKG="/volume2/docker/archicratie-web/incoming/archicratie-web.tar.gz" # adapter au nom réel
sudo tar -xzf "$PKG" -C "$APP"
### 4.4 Sanity check (fichiers attendus)
sudo test -f "$APP/Dockerfile" && echo "OK Dockerfile"
sudo test -f "$APP/docker-compose.yml" && echo "OK compose"
sudo test -f "$APP/astro.config.mjs" && echo "OK astro config"
sudo test -f "$APP/src/layouts/EditionLayout.astro" && echo "OK layout"
sudo test -f "$APP/src/pages/archicrat-ia/index.astro" && echo "OK archicrat-ia index"
sudo test -f "$APP/docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg" && echo "OK diagrams"
### 4.5 Permissions (crucial sur Synology)
But : archicratia:users doit pouvoir traverser le parent + lire le contenu.
sudo chown -R archicratia:users "$REL"
sudo chmod -R u+rwX,g+rX,o-rwx "$REL"
sudo chmod 750 "$REL" "$APP"
Vérifier :
ls -ld "$REL" "$APP"
ls -la "$APP" | head
## 5) Activation : basculer current vers la nouvelle release
### 5.1 Backup du current existant
cd /volume2/docker/archicratie-web
TS2="$(date +%F-%H%M%S)"
# on backup "current" (symlink ou dossier)
if [ -e current ] || [ -L current ]; then
sudo mv -f current "current.BAK.$TS2"
echo "✅ backup: current.BAK.$TS2"
fi
### 5.2 Recréer current (symlink propre)
sudo ln -s "$APP" current
ls -la current
readlink current
sudo test -f current/docker-compose.yml && echo "✅ OK: current/docker-compose.yml"
Si cd current échoue, cest que current nest pas un symlink correct OU que le parent nest pas traversable (permissions).
## 6) Build & run : (re)construire web_blue/web_green
### 6.1 Vérifier la config compose
cd /volume2/docker/archicratie-web/current
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml config \
| grep -nE 'services:|web_blue:|web_green:|context:|dockerfile:|PUBLIC_SITE|REQUIRE_PUBLIC_SITE' \
| sed -n '1,220p'
### 6.2 Build propre (recommandé si changement de code/config)
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
### 6.3 Up (force recreate)
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
### 6.4 Vérifier upstream direct (8081/8082)
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
## 7) Tests de non-régression (MINIMAL CHECKLIST)
À exécuter systématiquement après up.
### 7.1 Upstreams directs
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
### 7.2 Canonical (anti “localhost en prod”)
curl -sS http://127.0.0.1:8081/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
Attendu :
blue (8081) → https://staging.archicratie.../
green (8082) → https://archicratie.../
### 7.3 Edge routing (Host header + diag)
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/_auth/whoami \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
### 7.4 Smoke UI (manuel)
Home : lien “Essai-thèse — ArchiCraT-IA” → /archicrat-ia/
TOC global : liens /archicrat-ia/* (pas de préfixe /archicratie/archicrat-ia/*)
Reading-follow/TOC local : scroll ok
## 8) Rollback (si un seul test est mauvais)
Objectif : revenir immédiatement à létat précédent.
### 8.1 Repointer current sur lancien backup
cd /volume2/docker/archicratie-web
ls -la current.BAK.* | tail -n 5
# choisir le plus récent
OLD="current.BAK.YYYY-MM-DD-HHMMSS"
sudo rm -f current
sudo ln -s "$(readlink -f "$OLD")" current 2>/dev/null || sudo ln -s "$(readlink "$OLD")" current
ls -la current
readlink current
### 8.2 Rebuild + recreate
cd /volume2/docker/archicratie-web/current
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
### 8.3 Re-tester la checklist (section 7)
Si rollback OK : investiguer en environnement isolé (staging upstream uniquement, ou release dans un autre current).
## 9) Notes opérationnelles
Ne jamais modifier dist/ “à la main” sur NAS.
Si un hotfix prod est indispensable : documenter et backporter via PR Gitea.
Le canonical dépend du build : PUBLIC_SITE doit être injecté (voir runbook ENV-PUBLIC_SITE).

View File

@@ -0,0 +1,147 @@
# RUNBOOK — Edge Traefik (routing + SSO Authelia)
> Objectif : comprendre et diagnostiquer rapidement qui route quoi, et pourquoi staging/live peuvent diverger.
## 0) Portée
Edge Traefik route plusieurs hosts vers des backends locaux (127.0.0.1:*), avec Auth via Authelia.
Répertoire :
- `/volume2/docker/edge/config/dynamic/`
Port dentrée edge :
- `http://127.0.0.1:18080/` (entryPoint `web`)
- Les hosts publics pointent vers cet edge.
## 1) Fichiers dynamiques (canon)
### 00-smoke.yml
- route `/__smoke` vers le service `smoke_svc``127.0.0.1:18081`
### 10-core.yml
- définit les middlewares :
- `sanitize-remote`
- `authelia` (forwardAuth vers 9091)
- `chain-auth` (chain sanitize-remote + authelia)
### 20-archicratie-backend.yml
- définit service `archicratie_web``127.0.0.1:8082` (live upstream)
### 21-archicratie-staging.yml
- route staging host vers `127.0.0.1:8081` (staging upstream)
- applique middlewares `diag-staging@file` et `chain-auth@file`
- IMPORTANT : `diag-staging@file` doit exister
### 22-archicratie-authinfo-staging.yml
- route `/ _auth /` sur staging vers `whoami@file`
- applique `diag-staging-authinfo@file` + `chain-auth@file`
- IMPORTANT : `diag-staging-authinfo@file` doit exister
### 90-overlay-staging-fix.yml (overlay de diagnostic + fallback)
Rôle :
- **fournir** les middlewares manquants (`diag-staging`, `diag-staging-authinfo`)
- optionnel : fallback route si 21/22 sont cassés
- injecter un header `X-Archi-Router` pour identifier le routeur utilisé
### 92-overlay-live-fix.yml
- route live host `archicratie.trans-hands.synology.me``archicratie_web@file` (8082)
- route `/ _auth/whoami``whoami@file` (18081)
## 2) Diagnostiquer rapidement : quel routeur répond ?
### 2.1 Test “host header” (sans UI)
# en bash :
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/_auth/whoami \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
# Interprétation :
X-Archi-Router: staging@21 → routeur 21-archicratie-staging.yml OK
X-Archi-Router: staging-authinfo@22 → routeur authinfo OK
Si tu vois staging-fallback@90 → tu es tombé sur le fallback 90 (donc 21/22 potentiellement invalides)
### 2.2 Vérifier lupstream direct derrière edge
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
Si 8081 et 8082 servent des versions différentes : cest “normal” en blue/green, mais il faut savoir laquelle est censée être staging/live.
## 3) Diagnostiquer les erreurs Traefik (fichier invalide / middleware manquant)
### 3.1 Grep “level=error”
sudo docker logs edge-traefik --since 5m | grep -Ei 'level=error|middleware|router|service|yaml' | tail -n 80
# Cas typique :
middleware "diag-staging@file" does not exist
→ 21-archicratie-staging.yml référence un middleware absent. Solution : le définir (souvent dans 90-overlay-staging-fix.yml).
## 4) Procédure safe de modification (jamais en aveugle)
### 4.1 Backup
cd /volume2/docker/edge/config/dynamic
TS="$(date +%F-%H%M%S)"
sudo cp -a 90-overlay-staging-fix.yml "90-overlay-staging-fix.yml.bak.$TS"
### 4.2 Édition (ex : ajouter middlewares diag)
Faire une modif minimale
Ne pas casser les règles existantes (Host + PathPrefix)
Respecter les priorités (voir section 5)
### 4.3 Reload Traefik
sudo docker restart edge-traefik
### 4.4 Tests immédiats
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router'
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/_auth/whoami \
| grep -iE 'HTTP/|location:|x-archi-router'
## 5) Priorités Traefik (le point subtil)
Traefik choisit le routeur selon :
la correspondance de règle
la priority (plus grand gagne)
en cas dégalité, lordre interne (à éviter)
### 5.1 Canon pour staging
21-archicratie-staging.yml : priority 10
22-archicratie-authinfo-staging.yml : priority 10000
90-overlay-staging-fix.yml :
fallback host : priority faible (ex: 5) pour ne PAS écraser 21
fallback whoami : priority < 10000 (ex: 9000) pour ne PAS écraser 22
=> On garde 90 comme filet de sécurité / diag, pas comme “source”.
## 6) Rollback (si un changement edge casse staging/live)
cd /volume2/docker/edge/config/dynamic
# choisir le bon backup
sudo mv -f 90-overlay-staging-fix.yml "90-overlay-staging-fix.yml.BAD.$(date +%F-%H%M%S)"
sudo cp -a 90-overlay-staging-fix.yml.bak.YYYY-MM-DD-HHMMSS 90-overlay-staging-fix.yml
sudo docker restart edge-traefik
Puis re-tests section 2.
## 7) Remarques
Les 302 Authelia sont normaux si non authentifié.
Un 404 “Not Found” depuis edge alors que 8081 répond : souvent routeur manquant / invalidé / middleware absent.

View File

@@ -0,0 +1,114 @@
# RUNBOOK — PUBLIC_SITE (canonical + sitemap) “anti localhost en prod”
> Objectif : ne plus jamais voir `rel="canonical" href="http://localhost:4321/"` en staging/live.
## 0) Pourquoi cest critique
Astro génère :
- `<link rel="canonical" href="...">`
- `sitemap-index.xml`
Ces valeurs dépendent de `site` dans `astro.config.mjs`.
Si `site` vaut `http://localhost:4321` au moment du build Docker, **la prod sortira des canonical faux** :
- SEO / partage / cohérence de navigation impactés
- confusion staging/live
## 1) Règle canonique
- `astro.config.mjs` :
# en js :
site: process.env.PUBLIC_SITE ?? "http://localhost:4321"
# Donc :
En DEV local : pas besoin de PUBLIC_SITE (fallback ok)
En build “déploiement” : on DOIT fournir PUBLIC_SITE
## 2) Exigence “antifragile”
### 2.1 Dockerfile (build stage)
On injecte PUBLIC_SITE au build et on peut le rendre obligatoire :
ARG PUBLIC_SITE
ARG REQUIRE_PUBLIC_SITE=0
ENV PUBLIC_SITE=$PUBLIC_SITE
# garde-fou :
RUN if [ "$REQUIRE_PUBLIC_SITE" = "1" ] && [ -z "$PUBLIC_SITE" ]; then \
echo "ERROR: PUBLIC_SITE is required (REQUIRE_PUBLIC_SITE=1)"; exit 1; \
fi
=> Si quelquun oublie lURL en prod, le build casse au lieu de produire une release mauvaise.
## 3) docker-compose : blue/staging vs green/live
Objectif : injecter deux valeurs différentes, sans bricolage.
### 3.1 .env (NAS)
Exemple canonique :
PUBLIC_SITE_BLUE=https://staging.archicratie.trans-hands.synology.me
PUBLIC_SITE_GREEN=https://archicratie.trans-hands.synology.me
### 3.2 docker-compose.yml
web_blue :
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: ${PUBLIC_SITE_BLUE}
web_green :
REQUIRE_PUBLIC_SITE: "1"
PUBLIC_SITE: ${PUBLIC_SITE_GREEN}
## 4) Tests (obligatoires après build)
### 4.1 Vérifier linjection dans compose
sudo env DOCKER_API_VERSION=1.43 docker compose config \
| grep -nE 'PUBLIC_SITE|REQUIRE_PUBLIC_SITE|web_blue:|web_green:' | sed -n '1,200p'
### 4.2 Vérifier canonical (upstream direct)
curl -sS http://127.0.0.1:8081/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
# Attendu :
blue : https://staging.../
green : https://archicratie.../
## 5) Procédure de correction (si canonical est faux)
### 5.1 Vérifier astro.config.mjs dans la release courante
cd /volume2/docker/archicratie-web/current
grep -nE 'site:\s*process\.env\.PUBLIC_SITE' astro.config.mjs
### 5.2 Vérifier que Dockerfile exporte PUBLIC_SITE
grep -nE 'ARG PUBLIC_SITE|ENV PUBLIC_SITE|REQUIRE_PUBLIC_SITE' Dockerfile
### 5.3 Vérifier .env et compose
grep -nE 'PUBLIC_SITE_BLUE|PUBLIC_SITE_GREEN' .env
grep -nE 'PUBLIC_SITE|REQUIRE_PUBLIC_SITE' docker-compose.yml
### 5.4 Rebuild + recreate
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
Puis tests section 4.
## 6) Notes
Cette mécanique doit être backportée dans Gitea (source canonique), sinon ça re-cassera au prochain pack.
En DEV local, conserver le fallback http://localhost:4321 est utile et normal.

473
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.3.13", "@astrojs/mdx": "^4.3.13",
"astro": "^5.16.11" "astro": "^5.17.3"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/sitemap": "^3.7.0", "@astrojs/sitemap": "^3.7.0",
@@ -1905,9 +1905,9 @@
} }
}, },
"node_modules/astro": { "node_modules/astro": {
"version": "5.16.11", "version": "5.17.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz", "resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
"integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==", "integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@astrojs/compiler": "^2.13.0", "@astrojs/compiler": "^2.13.0",
@@ -1933,7 +1933,7 @@
"dlv": "^1.1.3", "dlv": "^1.1.3",
"dset": "^3.1.4", "dset": "^3.1.4",
"es-module-lexer": "^1.7.0", "es-module-lexer": "^1.7.0",
"esbuild": "^0.25.0", "esbuild": "^0.27.3",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"flattie": "^1.1.1", "flattie": "^1.1.1",
"fontace": "~0.4.0", "fontace": "~0.4.0",
@@ -1954,16 +1954,16 @@
"prompts": "^2.4.2", "prompts": "^2.4.2",
"rehype": "^13.0.2", "rehype": "^13.0.2",
"semver": "^7.7.3", "semver": "^7.7.3",
"shiki": "^3.20.0", "shiki": "^3.21.0",
"smol-toml": "^1.6.0", "smol-toml": "^1.6.0",
"svgo": "^4.0.0", "svgo": "^4.0.0",
"tinyexec": "^1.0.2", "tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15", "tinyglobby": "^0.2.15",
"tsconfck": "^3.1.6", "tsconfck": "^3.1.6",
"ultrahtml": "^1.6.0", "ultrahtml": "^1.6.0",
"unifont": "~0.7.1", "unifont": "~0.7.3",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"unstorage": "^1.17.3", "unstorage": "^1.17.4",
"vfile": "^6.0.3", "vfile": "^6.0.3",
"vite": "^6.4.1", "vite": "^6.4.1",
"vitefu": "^1.1.1", "vitefu": "^1.1.1",
@@ -1990,6 +1990,463 @@
"sharp": "^0.34.0" "sharp": "^0.34.0"
} }
}, },
"node_modules/astro/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/android-arm": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/android-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/android-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/darwin-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/darwin-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/freebsd-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-arm": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-ia32": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-loong64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-mips64el": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-ppc64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-riscv64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-s390x": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/linux-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/netbsd-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/openbsd-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/sunos-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/win32-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/win32-ia32": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/@esbuild/win32-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/astro/node_modules/esbuild": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.3",
"@esbuild/android-arm": "0.27.3",
"@esbuild/android-arm64": "0.27.3",
"@esbuild/android-x64": "0.27.3",
"@esbuild/darwin-arm64": "0.27.3",
"@esbuild/darwin-x64": "0.27.3",
"@esbuild/freebsd-arm64": "0.27.3",
"@esbuild/freebsd-x64": "0.27.3",
"@esbuild/linux-arm": "0.27.3",
"@esbuild/linux-arm64": "0.27.3",
"@esbuild/linux-ia32": "0.27.3",
"@esbuild/linux-loong64": "0.27.3",
"@esbuild/linux-mips64el": "0.27.3",
"@esbuild/linux-ppc64": "0.27.3",
"@esbuild/linux-riscv64": "0.27.3",
"@esbuild/linux-s390x": "0.27.3",
"@esbuild/linux-x64": "0.27.3",
"@esbuild/netbsd-arm64": "0.27.3",
"@esbuild/netbsd-x64": "0.27.3",
"@esbuild/openbsd-arm64": "0.27.3",
"@esbuild/openbsd-x64": "0.27.3",
"@esbuild/openharmony-arm64": "0.27.3",
"@esbuild/sunos-x64": "0.27.3",
"@esbuild/win32-arm64": "0.27.3",
"@esbuild/win32-ia32": "0.27.3",
"@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",

View File

@@ -4,32 +4,29 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev", "dev": "node scripts/write-dev-whoami.mjs && astro dev",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"build": "astro build", "build": "astro build",
"build:clean": "npm run clean && npm run build", "build:clean": "npm run clean && npm run build",
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && node scripts/build-para-index.mjs && node scripts/build-annotations-index.mjs && node scripts/purge-dist-dev-whoami.mjs && npx pagefind --site dist",
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
"import": "node scripts/import-docx.mjs", "import": "node scripts/import-docx.mjs",
"apply:ticket": "node scripts/apply-ticket.mjs", "apply:ticket": "node scripts/apply-ticket.mjs",
"audit:dist": "node scripts/audit-dist.mjs", "audit:dist": "node scripts/audit-dist.mjs",
"build:para-index": "node scripts/build-para-index.mjs",
"build:annotations-index": "node scripts/build-annotations-index.mjs",
"test:aliases": "node scripts/check-anchor-aliases.mjs", "test:aliases": "node scripts/check-anchor-aliases.mjs",
"test:anchors": "node scripts/check-anchors.mjs", "test:anchors": "node scripts/check-anchors.mjs",
"test:anchors:update": "node scripts/check-anchors.mjs --update", "test:anchors:update": "node scripts/check-anchors.mjs --update",
"test:annotations": "node scripts/check-annotations.mjs",
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && node scripts/check-inline-js.mjs", "test:annotations:media": "node scripts/check-annotations-media.mjs",
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && npm run test:annotations && npm run test:annotations:media && node scripts/check-inline-js.mjs",
"ci": "CI=1 npm test" "ci": "CI=1 npm test"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.3.13", "@astrojs/mdx": "^4.3.13",
"astro": "^5.16.11" "astro": "^5.17.3"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/sitemap": "^3.7.0", "@astrojs/sitemap": "^3.7.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

View File

@@ -0,0 +1,688 @@
#!/usr/bin/env node
// scripts/apply-annotation-ticket.mjs
// Applique un ticket Gitea "type/media | type/reference | type/comment" vers src/annotations + public/media
// Robuste, idempotent, non destructif
//
// DRY RUN par défaut si --dry-run
// Options: --dry-run --no-download --verify --strict --commit --close
//
// Env requis:
// FORGE_API = base API Gitea (LAN) ex: http://192.168.1.20:3000
// FORGE_TOKEN = PAT Gitea (repo + issues)
//
// Env optionnel:
// GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote)
// ANNO_DIR (défaut: src/annotations)
// PUBLIC_DIR (défaut: public)
// MEDIA_ROOT (défaut URL: /media)
//
// Ticket attendu (body):
// Chemin: /archicrat-ia/chapitre-4/
// Ancre: #p-0-xxxxxxxx
// Type: type/media | type/reference | type/comment
//
// Exit codes:
// 0 ok
// 1 erreur fatale
// 2 refus (strict/verify/usage)
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
import YAML from "yaml";
/* ---------------------------------- usage --------------------------------- */
function usage(exitCode = 0) {
console.log(`
apply-annotation-ticket — applique un ticket SidePanel (media/ref/comment) vers src/annotations/
Usage:
node scripts/apply-annotation-ticket.mjs <issue_number> [--dry-run] [--no-download] [--verify] [--strict] [--commit] [--close]
Flags:
--dry-run : n'écrit rien (affiche un aperçu)
--no-download : n'essaie pas de télécharger les pièces jointes (media)
--verify : tente de vérifier que (page, ancre) existent (baseline/dist si dispo)
--strict : refuse si URL ref invalide (http/https) OU caption media vide
--commit : git add + git commit (le script commit dans la branche courante)
--close : ferme le ticket (nécessite --commit)
Env requis:
FORGE_API = base API Gitea (LAN) ex: http://192.168.1.20:3000
FORGE_TOKEN = PAT Gitea (repo + issues)
Env optionnel:
GITEA_OWNER / GITEA_REPO (sinon auto-détecté via git remote)
ANNO_DIR (défaut: src/annotations)
PUBLIC_DIR (défaut: public)
MEDIA_ROOT (défaut URL: /media) -> écrit dans public/media/...
Exit codes:
0 ok
1 erreur fatale
2 refus (strict/verify/close sans commit / incohérence)
`);
process.exit(exitCode);
}
/* ---------------------------------- args ---------------------------------- */
const argv = process.argv.slice(2);
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) usage(0);
const issueNum = Number(argv[0]);
if (!Number.isFinite(issueNum) || issueNum <= 0) {
console.error("❌ Numéro de ticket invalide.");
usage(2);
}
const DRY_RUN = argv.includes("--dry-run");
const NO_DOWNLOAD = argv.includes("--no-download");
const DO_VERIFY = argv.includes("--verify");
const STRICT = argv.includes("--strict");
const DO_COMMIT = argv.includes("--commit");
const DO_CLOSE = argv.includes("--close");
if (DO_CLOSE && !DO_COMMIT) {
console.error("❌ --close nécessite --commit.");
process.exit(2);
}
if (typeof fetch !== "function") {
console.error("❌ fetch() indisponible. Utilise Node 18+.");
process.exit(1);
}
/* --------------------------------- config --------------------------------- */
const CWD = process.cwd();
const ANNO_DIR = path.join(CWD, process.env.ANNO_DIR || "src", "annotations");
const PUBLIC_DIR = path.join(CWD, process.env.PUBLIC_DIR || "public");
const MEDIA_URL_ROOT = String(process.env.MEDIA_ROOT || "/media").replace(/\/+$/, "");
function getEnv(name, fallback = "") {
return (process.env[name] ?? fallback).trim();
}
function run(cmd, args, opts = {}) {
const r = spawnSync(cmd, args, { stdio: "inherit", ...opts });
if (r.error) throw r.error;
if (r.status !== 0) throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
}
function runQuiet(cmd, args, opts = {}) {
const r = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts });
if (r.error) throw r.error;
if (r.status !== 0) {
const out = (r.stdout || "") + (r.stderr || "");
throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${out}`);
}
return r.stdout || "";
}
async function exists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
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(/[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/);
if (!m?.groups) return null;
return { owner: m.groups.owner, repo: m.groups.repo };
}
function gitHasStagedChanges() {
const r = spawnSync("git", ["diff", "--cached", "--quiet"]);
return r.status === 1;
}
/* ------------------------------ gitea helpers ------------------------------ */
function apiBaseNorm(forgeApiBase) {
return forgeApiBase.replace(/\/+$/, "");
}
async function giteaGET(url, token) {
const res = await fetch(url, {
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"User-Agent": "archicratie-apply-annotation/1.0",
},
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} GET ${url}\n${t}`);
}
return await res.json();
}
async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
return await giteaGET(url, token);
}
async function fetchIssueAssets({ forgeApiBase, owner, repo, token, issueNum }) {
// ✅ Gitea: /issues/{index}/assets
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}/assets`;
try {
const json = await giteaGET(url, token);
return Array.isArray(json) ? json : [];
} catch {
return [];
}
}
async function postIssueComment({ forgeApiBase, owner, repo, token, issueNum, comment }) {
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}/comments`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-apply-annotation/1.0",
},
body: JSON.stringify({ body: comment }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} POST comment ${url}\n${t}`);
}
}
async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment }) {
if (comment) await postIssueComment({ forgeApiBase, owner, repo, token, issueNum, comment });
const url = `${apiBaseNorm(forgeApiBase)}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
const res = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-apply-annotation/1.0",
},
body: JSON.stringify({ state: "closed" }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} closing issue: ${url}\n${t}`);
}
}
/* ------------------------------ parsing helpers ---------------------------- */
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function pickLine(body, key) {
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = String(body || "").match(re);
return m ? m[1].trim() : "";
}
function pickSection(body, markers) {
const text = String(body || "").replace(/\r\n/g, "\n");
const idx = markers
.map((m) => ({ m, i: text.toLowerCase().indexOf(m.toLowerCase()) }))
.filter((x) => x.i >= 0)
.sort((a, b) => a.i - b.i)[0];
if (!idx) return "";
const start = idx.i + idx.m.length;
const tail = text.slice(start);
const stops = [
"\n## ",
"\n---",
"\nJustification",
"\nProposition",
"\nSources",
];
let end = tail.length;
for (const s of stops) {
const j = tail.toLowerCase().indexOf(s.toLowerCase());
if (j >= 0 && j < end) end = j;
}
return tail.slice(0, end).trim();
}
function normalizeChemin(chemin) {
let c = String(chemin || "").trim();
if (!c) return "";
if (!c.startsWith("/")) c = "/" + c;
if (!c.endsWith("/")) c = c + "/";
c = c.replace(/\/{2,}/g, "/");
return c;
}
function normalizePageKeyFromChemin(chemin) {
return normalizeChemin(chemin).replace(/^\/+|\/+$/g, "");
}
function normalizeAnchorId(s) {
let a = String(s || "").trim();
if (a.startsWith("#")) a = a.slice(1);
return a;
}
function assert(cond, msg, code = 1) {
if (!cond) {
const e = new Error(msg);
e.__exitCode = code;
throw e;
}
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
/* ----------------------------- verify helpers ------------------------------ */
function paraIndexFromId(id) {
const m = String(id).match(/^p-(\d+)-/i);
return m ? Number(m[1]) : Number.NaN;
}
async function tryVerifyAnchor(pageKey, anchorId) {
// 1) dist/para-index.json (si build déjà faite)
const distIdx = path.join(CWD, "dist", "para-index.json");
if (await exists(distIdx)) {
const raw = await fs.readFile(distIdx, "utf8");
const idx = JSON.parse(raw);
const byId = idx?.byId;
if (byId && typeof byId === "object" && byId[anchorId] != null) return true;
}
// 2) tests/anchors-baseline.json (si dispo)
const base = path.join(CWD, "tests", "anchors-baseline.json");
if (await exists(base)) {
const raw = await fs.readFile(base, "utf8");
const j = JSON.parse(raw);
// tolérant: cherche un array d'ids associé à la page
const candidates = [];
// cas 1: j.pages[...]
if (j?.pages && typeof j.pages === "object") {
for (const [k, v] of Object.entries(j.pages)) {
if (!Array.isArray(v)) continue;
// on matche large: pageKey inclus dans le path
if (String(k).includes(pageKey)) candidates.push(...v);
}
}
// cas 2: j.entries = [{page, ids}]
if (Array.isArray(j?.entries)) {
for (const it of j.entries) {
const p = String(it?.page || "");
const ids = it?.ids;
if (Array.isArray(ids) && p.includes(pageKey)) candidates.push(...ids);
}
}
if (candidates.length) {
return candidates.some((x) => String(x) === anchorId);
}
}
// impossible à vérifier
return null;
}
/* ----------------------------- annotations I/O ----------------------------- */
async function loadAnnoDoc(fileAbs, pageKey) {
if (!(await exists(fileAbs))) {
return { schema: 1, page: pageKey, paras: {} };
}
const raw = await fs.readFile(fileAbs, "utf8");
let doc;
try {
doc = YAML.parse(raw);
} catch (e) {
throw new Error(`${path.relative(CWD, fileAbs)}: parse failed: ${String(e?.message ?? e)}`);
}
assert(isPlainObject(doc), `${path.relative(CWD, fileAbs)}: doc must be an object`);
assert(doc.schema === 1, `${path.relative(CWD, fileAbs)}: schema must be 1`);
assert(isPlainObject(doc.paras), `${path.relative(CWD, fileAbs)}: missing object key "paras"`);
if (doc.page != null) {
const got = String(doc.page).replace(/^\/+/, "").replace(/\/+$/, "");
assert(got === pageKey, `${path.relative(CWD, fileAbs)}: page mismatch (page="${doc.page}" vs path="${pageKey}")`);
} else {
doc.page = pageKey;
}
return doc;
}
function sortParasObject(paras) {
const keys = Object.keys(paras || {});
keys.sort((a, b) => {
const ia = paraIndexFromId(a);
const ib = paraIndexFromId(b);
if (Number.isFinite(ia) && Number.isFinite(ib) && ia !== ib) return ia - ib;
return String(a).localeCompare(String(b));
});
const out = {};
for (const k of keys) out[k] = paras[k];
return out;
}
async function saveAnnoDocYaml(fileAbs, doc) {
await fs.mkdir(path.dirname(fileAbs), { recursive: true });
doc.paras = sortParasObject(doc.paras);
const out = YAML.stringify(doc);
await fs.writeFile(fileAbs, out, "utf8");
}
/* ------------------------------ apply per type ----------------------------- */
function ensureEntry(doc, paraId) {
if (!doc.paras[paraId] || !isPlainObject(doc.paras[paraId])) doc.paras[paraId] = {};
return doc.paras[paraId];
}
function uniqPush(arr, item, keyFn) {
const k = keyFn(item);
const exists = arr.some((x) => keyFn(x) === k);
if (!exists) arr.push(item);
return !exists;
}
function stableSortByTs(arr) {
if (!Array.isArray(arr)) return;
arr.sort((a, b) => {
const ta = Date.parse(a?.ts || "") || 0;
const tb = Date.parse(b?.ts || "") || 0;
if (ta !== tb) return ta - tb;
return JSON.stringify(a).localeCompare(JSON.stringify(b));
});
}
function parseReferenceBlock(body) {
const block =
pickSection(body, ["Référence (à compléter):", "Reference (à compléter):"]) ||
pickSection(body, ["Référence:", "Reference:"]);
const lines = String(block || "").split(/\r?\n/).map((l) => l.trim());
const get = (k) => {
const re = new RegExp(`^[-*]\\s*${escapeRegExp(k)}\\s*:\\s*(.*)$`, "i");
const m = lines.map((l) => l.match(re)).find(Boolean);
return (m?.[1] ?? "").trim();
};
return {
url: get("URL") || "",
label: get("Label") || "",
kind: get("Kind") || "",
citation: get("Citation") || get("Passage") || get("Extrait") || "",
rawBlock: block || "",
};
}
function inferMediaTypeFromFilename(name) {
const n = String(name || "").toLowerCase();
if (/\.(png|jpe?g|webp|gif|svg)$/.test(n)) return "image";
if (/\.(mp4|webm|mov|m4v)$/.test(n)) return "video";
if (/\.(mp3|wav|ogg|m4a)$/.test(n)) return "audio";
return "link";
}
function sanitizeFilename(name) {
return String(name || "file")
.replace(/[\/\\]/g, "_")
.replace(/[^\w.\-]+/g, "_")
.replace(/_+/g, "_")
.slice(0, 180);
}
function isHttpUrl(u) {
try {
const x = new URL(String(u));
return x.protocol === "http:" || x.protocol === "https:";
} catch {
return false;
}
}
async function downloadToFile(url, token, destAbs) {
const res = await fetch(url, {
headers: {
// la plupart des /attachments sont publics, mais on garde le token “au cas où”
Authorization: `token ${token}`,
"User-Agent": "archicratie-apply-annotation/1.0",
},
redirect: "follow",
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`download failed HTTP ${res.status}: ${url}\n${t}`);
}
const buf = Buffer.from(await res.arrayBuffer());
await fs.mkdir(path.dirname(destAbs), { recursive: true });
await fs.writeFile(destAbs, buf);
return buf.length;
}
/* ----------------------------------- main ---------------------------------- */
async function main() {
const token = getEnv("FORGE_TOKEN");
assert(token, "❌ FORGE_TOKEN manquant.", 2);
const forgeApiBase = getEnv("FORGE_API") || getEnv("FORGE_BASE");
assert(forgeApiBase, "❌ FORGE_API (ou FORGE_BASE) manquant.", 2);
const inferred = inferOwnerRepoFromGit() || {};
const owner = getEnv("GITEA_OWNER", inferred.owner || "");
const repo = getEnv("GITEA_REPO", inferred.repo || "");
assert(owner && repo, "❌ Impossible de déterminer owner/repo. Fix: export GITEA_OWNER=... GITEA_REPO=...", 2);
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo}`);
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
if (issue?.pull_request) {
console.error(`❌ #${issueNum} est une Pull Request, pas un ticket annotations.`);
process.exit(2);
}
const body = String(issue.body || "").replace(/\r\n/g, "\n");
const title = String(issue.title || "");
const type = pickLine(body, "Type").toLowerCase();
const chemin = normalizeChemin(pickLine(body, "Chemin"));
const ancre = normalizeAnchorId(pickLine(body, "Ancre"));
assert(chemin, "Ticket: Chemin manquant.", 2);
assert(ancre && /^p-\d+-/i.test(ancre), `Ticket: Ancre invalide ("${ancre}")`, 2);
assert(type, "Ticket: Type manquant.", 2);
const pageKey = normalizePageKeyFromChemin(chemin);
assert(pageKey, "Ticket: impossible de dériver pageKey.", 2);
if (DO_VERIFY) {
const ok = await tryVerifyAnchor(pageKey, ancre);
if (ok === false) {
throw Object.assign(new Error(`Ticket verify: ancre introuvable pour page "${pageKey}" => ${ancre}`), { __exitCode: 2 });
}
if (ok === null) {
// pas de source de vérité dispo
if (STRICT) throw Object.assign(new Error(`Ticket verify (strict): impossible de vérifier (pas de baseline/dist)`), { __exitCode: 2 });
console.warn("⚠️ verify: impossible de vérifier (pas de baseline/dist) — on continue.");
}
}
const annoFileAbs = path.join(ANNO_DIR, `${pageKey}.yml`);
const annoFileRel = path.relative(CWD, annoFileAbs).replace(/\\/g, "/");
console.log("✅ Parsed:", { type, chemin, ancre: `#${ancre}`, pageKey, annoFile: annoFileRel });
const doc = await loadAnnoDoc(annoFileAbs, pageKey);
const entry = ensureEntry(doc, ancre);
const touchedFiles = [];
const notes = [];
let changed = false;
const nowIso = new Date().toISOString();
if (type === "type/comment") {
const comment = pickSection(body, ["Commentaire:", "Comment:", "Commentaires:"]) || "";
const text = comment.trim();
assert(text.length >= 3, "Ticket comment: bloc 'Commentaire:' introuvable ou trop court.", 2);
if (!Array.isArray(entry.comments_editorial)) entry.comments_editorial = [];
const item = { text, status: "new", ts: nowIso, fromIssue: issueNum };
const added = uniqPush(entry.comments_editorial, item, (x) => `${(x?.text || "").trim()}`);
if (added) { changed = true; notes.push(`+ comment added (len=${text.length})`); }
else notes.push(`~ comment already present (dedup)`);
stableSortByTs(entry.comments_editorial);
}
else if (type === "type/reference") {
const ref = parseReferenceBlock(body);
assert(ref.url || ref.label, "Ticket reference: renseigne au moins - URL: ou - Label: dans le ticket.", 2);
if (STRICT && ref.url && !isHttpUrl(ref.url)) {
throw Object.assign(new Error(`Ticket reference (strict): URL invalide (http/https requis): "${ref.url}"`), { __exitCode: 2 });
}
if (!Array.isArray(entry.refs)) entry.refs = [];
const item = {
url: ref.url || "",
label: ref.label || (ref.url ? ref.url : "Référence"),
kind: ref.kind || "",
ts: nowIso,
fromIssue: issueNum,
};
if (ref.citation) item.citation = ref.citation;
const added = uniqPush(entry.refs, item, (x) => `${x?.url || ""}||${x?.label || ""}||${x?.kind || ""}||${x?.citation || ""}`);
if (added) { changed = true; notes.push(`+ reference added (${item.url ? "url" : "label"})`); }
else notes.push(`~ reference already present (dedup)`);
stableSortByTs(entry.refs);
}
else if (type === "type/media") {
if (!Array.isArray(entry.media)) entry.media = [];
const atts = NO_DOWNLOAD ? [] : await fetchIssueAssets({ forgeApiBase, owner, repo, token, issueNum });
if (!atts.length) {
notes.push("! no assets found (nothing to download).");
}
for (const a of atts) {
const name = sanitizeFilename(a?.name || `asset-${a?.id || "x"}`);
const dl = a?.browser_download_url || a?.download_url || "";
if (!dl) { notes.push(`! asset missing download url: ${name}`); continue; }
// caption = title du ticket (fallback ".")
const caption = (title || "").trim() || ".";
if (STRICT && !caption.trim()) {
throw Object.assign(new Error("Ticket media (strict): caption vide."), { __exitCode: 2 });
}
const mediaDirAbs = path.join(PUBLIC_DIR, "media", pageKey, ancre);
const destAbs = path.join(mediaDirAbs, name);
const urlPath = `${MEDIA_URL_ROOT}/${pageKey}/${ancre}/${name}`.replace(/\/{2,}/g, "/");
if (await exists(destAbs)) {
notes.push(`~ media already exists: ${urlPath}`);
} else if (!DRY_RUN) {
const bytes = await downloadToFile(dl, token, destAbs);
notes.push(`+ downloaded ${name} (${bytes} bytes) -> ${urlPath}`);
touchedFiles.push(path.relative(CWD, destAbs).replace(/\\/g, "/"));
} else {
notes.push(`(dry) would download ${name} -> ${urlPath}`);
}
const item = {
type: inferMediaTypeFromFilename(name),
src: urlPath,
caption,
credit: "",
ts: nowIso,
fromIssue: issueNum,
};
const added = uniqPush(entry.media, item, (x) => String(x?.src || ""));
if (added) changed = true;
}
stableSortByTs(entry.media);
}
else {
throw Object.assign(new Error(`Type non supporté: "${type}"`), { __exitCode: 2 });
}
if (!changed) {
console.log(" No changes to apply.");
for (const n of notes) console.log(" ", n);
return;
}
if (DRY_RUN) {
console.log("\n--- DRY RUN (no write) ---");
console.log(`Would update: ${annoFileRel}`);
for (const n of notes) console.log(" ", n);
console.log("\nExcerpt (resulting entry):");
console.log(YAML.stringify({ [ancre]: doc.paras[ancre] }).trimEnd());
console.log("\n✅ Dry-run terminé.");
return;
}
await saveAnnoDocYaml(annoFileAbs, doc);
touchedFiles.unshift(annoFileRel);
console.log(`✅ Updated: ${annoFileRel}`);
for (const n of notes) console.log(" ", n);
if (DO_COMMIT) {
run("git", ["add", ...touchedFiles], { cwd: CWD });
if (!gitHasStagedChanges()) {
console.log(" Nothing to commit (aucun changement staged).");
return;
}
const msg = `anno: apply ticket #${issueNum} (${pageKey}#${ancre} ${type})`;
run("git", ["commit", "-m", msg], { cwd: CWD });
const sha = runQuiet("git", ["rev-parse", "--short", "HEAD"], { cwd: CWD }).trim();
console.log(`✅ Committed: ${msg} (${sha})`);
if (DO_CLOSE) {
const comment = `✅ Appliqué par apply-annotation-ticket.\nCommit: ${sha}`;
await closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment });
console.log(`✅ Ticket #${issueNum} fermé.`);
}
} else {
console.log("\nNext (manuel) :");
console.log(` git diff -- ${touchedFiles[0]}`);
console.log(` git add ${touchedFiles.join(" ")}`);
console.log(` git commit -m "anno: apply ticket #${issueNum} (${pageKey}#${ancre} ${type})"`);
}
}
main().catch((e) => {
const code = e?.__exitCode || 1;
console.error("💥", e?.message || e);
process.exit(code);
});

View File

@@ -0,0 +1,159 @@
// scripts/build-annotations-index.mjs
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
function parseArgs(argv) {
const out = {
inDir: "src/annotations",
outFile: "dist/annotations-index.json",
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--in" && argv[i + 1]) out.inDir = argv[++i];
else if (a.startsWith("--in=")) out.inDir = a.slice("--in=".length);
if (a === "--out" && argv[i + 1]) out.outFile = argv[++i];
else if (a.startsWith("--out=")) out.outFile = a.slice("--out=".length);
}
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 inferPageKeyFromFile(inDirAbs, fileAbs) {
// src/annotations/<page>.yml -> "<page>"
const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/");
return rel.replace(/\.(ya?ml|json)$/i, "");
}
function assert(cond, msg) {
if (!cond) throw new Error(msg);
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function normalizePageKey(s) {
// pas de / en tête/fin
return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
}
function validateAndNormalizeDoc(doc, pageKey, fileRel) {
assert(isPlainObject(doc), `${fileRel}: document must be an object`);
assert(doc.schema === 1, `${fileRel}: schema must be 1`);
if (doc.page != null) {
assert(
normalizePageKey(doc.page) === pageKey,
`${fileRel}: page mismatch (page="${doc.page}" vs path="${pageKey}")`
);
}
assert(isPlainObject(doc.paras), `${fileRel}: missing object key "paras"`);
const parasOut = Object.create(null);
for (const [paraId, entry] of Object.entries(doc.paras)) {
assert(/^p-\d+-/i.test(paraId), `${fileRel}: invalid para id "${paraId}"`);
// entry peut être vide, mais doit être un objet si présent
assert(entry == null || isPlainObject(entry), `${fileRel}: paras.${paraId} must be an object`);
const e = entry ? { ...entry } : {};
// Sanity checks (non destructifs : on nécrase pas, on vérifie juste les types)
if (e.refs != null) assert(Array.isArray(e.refs), `${fileRel}: paras.${paraId}.refs must be an array`);
if (e.authors != null) assert(Array.isArray(e.authors), `${fileRel}: paras.${paraId}.authors must be an array`);
if (e.quotes != null) assert(Array.isArray(e.quotes), `${fileRel}: paras.${paraId}.quotes must be an array`);
if (e.media != null) assert(Array.isArray(e.media), `${fileRel}: paras.${paraId}.media must be an array`);
if (e.comments_editorial != null) assert(Array.isArray(e.comments_editorial), `${fileRel}: paras.${paraId}.comments_editorial must be an array`);
parasOut[paraId] = e;
}
return parasOut;
}
async function readDoc(fileAbs) {
const raw = await fs.readFile(fileAbs, "utf8");
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
return YAML.parse(raw);
}
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
if (!(await exists(inDirAbs))) {
console.log(` annotations-index: skip (input missing): ${inDir}`);
process.exit(0);
}
const files = (await walk(inDirAbs)).filter((p) => /\.(ya?ml|json)$/i.test(p));
if (!files.length) {
console.log(` annotations-index: skip (no .yml/.yaml/.json found in): ${inDir}`);
process.exit(0);
}
const pages = Object.create(null);
let paraCount = 0;
for (const f of files) {
const fileRel = path.relative(CWD, f).replace(/\\/g, "/");
const pageKey = normalizePageKey(inferPageKeyFromFile(inDirAbs, f));
assert(pageKey, `${fileRel}: cannot infer page key`);
let doc;
try {
doc = await readDoc(f);
} catch (e) {
throw new Error(`${fileRel}: parse failed: ${String(e?.message ?? e)}`);
}
const paras = validateAndNormalizeDoc(doc, pageKey, fileRel);
// 1 fichier = 1 page (canon)
assert(!pages[pageKey], `${fileRel}: duplicate page "${pageKey}" (only one file per page)`);
pages[pageKey] = { paras };
paraCount += Object.keys(paras).length;
}
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
pages,
stats: {
pages: Object.keys(pages).length,
paras: paraCount,
},
};
await fs.mkdir(path.dirname(outAbs), { recursive: true });
await fs.writeFile(outAbs, JSON.stringify(out), "utf8");
console.log(`✅ annotations-index: pages=${out.stats.pages} paras=${out.stats.paras} -> ${path.relative(CWD, outAbs)}`);
}
main().catch((e) => {
console.error("FAIL: build-annotations-index crashed:", e);
process.exit(1);
});

View File

@@ -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(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " ");
}
function decodeEntities(s) {
// minimal, volontairement (évite dépendances)
return String(s || "")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/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/<page>/index.html -> "/<page>/"
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);
// <p ... id="p-...">...</p>
// (regex volontairement stricte sur l'id pour éviter faux positifs)
const reP = /<p\b([^>]*\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);
});

View File

@@ -0,0 +1,97 @@
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 PUBLIC_DIR = path.join(CWD, "public");
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 parseDoc(raw, fileAbs) {
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
return YAML.parse(raw);
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function toPublicPathFromUrl(urlPath) {
// "/media/..." -> "public/media/..."
const clean = String(urlPath || "").split("?")[0].split("#")[0];
if (!clean.startsWith("/media/")) return null;
return path.join(PUBLIC_DIR, clean.replace(/^\/+/, ""));
}
async function main() {
if (!(await exists(ANNO_DIR))) {
console.log("✅ annotations-media: aucun src/annotations — rien à vérifier.");
process.exit(0);
}
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
let checked = 0;
let missing = 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) {
missing++;
notes.push(`- PARSE FAIL: ${rel} (${String(e?.message ?? e)})`);
continue;
}
if (!isPlainObject(doc) || doc.schema !== 1 || !isPlainObject(doc.paras)) continue;
for (const [paraId, entry] of Object.entries(doc.paras)) {
const media = entry?.media;
if (!Array.isArray(media)) continue;
for (const m of media) {
const src = String(m?.src || "");
if (!src.startsWith("/media/")) continue; // externes ok, ou autres conventions futures
checked++;
const p = toPublicPathFromUrl(src);
if (!p) continue;
if (!(await exists(p))) {
missing++;
notes.push(`- MISSING MEDIA: ${src} (from ${rel} para ${paraId})`);
}
}
}
}
if (missing > 0) {
console.error(`FAIL: annotations media missing (checked=${checked} missing=${missing})`);
for (const n of notes) console.error(n);
process.exit(1);
}
console.log(`✅ annotations-media OK: checked=${checked}`);
}
main().catch((e) => {
console.error("FAIL: check-annotations-media crashed:", e);
process.exit(1);
});

View File

@@ -0,0 +1,175 @@
// 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) { "<pageKey>": { "<old>": "<new>" } }
// 2) { "<old>": "<new>" }
const k1 = String(pageKey || "");
const k2 = k1 ? ("/" + k1.replace(/^\/+|\/+$/g, "") + "/") : "";
const a1 = (aliases?.[k1]?.[oldId]) || (k2 ? aliases?.[k2]?.[oldId] : "");
if (a1) return String(a1);
const a2 = aliases?.[oldId];
if (a2) return String(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 dabord `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);
});

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

@@ -14,6 +14,24 @@ const STRICT = argv.includes("--strict") || process.env.CI === "1" || process.en
function escRe(s) { function escRe(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
function normalizeRoute(route) {
let r = String(route || "").trim();
if (!r.startsWith("/")) r = "/" + r;
if (!r.endsWith("/")) r = r + "/";
r = r.replace(/\/{2,}/g, "/");
return r;
}
function countIdAttr(html, id) { function countIdAttr(html, id) {
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi"); const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi");
let c = 0; let c = 0;
@@ -22,7 +40,6 @@ function countIdAttr(html, id) {
} }
function findStartTagWithId(html, id) { function findStartTagWithId(html, id) {
// 1er élément qui porte id="..."
const re = new RegExp( const re = new RegExp(
`<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`, `<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`,
"i" "i"
@@ -36,34 +53,10 @@ function isInjectedAliasSpan(html, id) {
const found = findStartTagWithId(html, id); const found = findStartTagWithId(html, id);
if (!found) return false; if (!found) return false;
if (found.tagName !== "span") return false; if (found.tagName !== "span") return false;
// class="... para-alias ..."
return /\bclass=(["'])(?:(?!\1).)*\bpara-alias\b(?:(?!\1).)*\1/i.test(found.tag); return /\bclass=(["'])(?:(?!\1).)*\bpara-alias\b(?:(?!\1).)*\1/i.test(found.tag);
} }
function normalizeRoute(route) {
let r = String(route || "").trim();
if (!r.startsWith("/")) r = "/" + r;
if (!r.endsWith("/")) r = r + "/";
r = r.replace(/\/{2,}/g, "/");
return r;
}
async function exists(p) {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
function hasId(html, id) {
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "i");
return re.test(html);
}
function injectBeforeId(html, newId, injectHtml) { function injectBeforeId(html, newId, injectHtml) {
// insère juste avant la balise qui porte id="newId"
const re = new RegExp( const re = new RegExp(
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`, `(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
"i" "i"
@@ -82,6 +75,7 @@ async function main() {
} }
const raw = await fs.readFile(ALIASES_PATH, "utf-8"); const raw = await fs.readFile(ALIASES_PATH, "utf-8");
/** @type {Record<string, Record<string,string>>} */ /** @type {Record<string, Record<string,string>>} */
let aliases; let aliases;
try { try {
@@ -89,6 +83,7 @@ async function main() {
} catch (e) { } catch (e) {
throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`); throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`);
} }
if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) { if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) {
throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`); throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`);
} }
@@ -114,10 +109,10 @@ async function main() {
console.log(msg); console.log(msg);
warnCount++; warnCount++;
} }
if (entries.length === 0) continue; if (entries.length === 0) continue;
const rel = route.replace(/^\/+|\/+$/g, ""); // sans slash const rel = route.replace(/^\/+|\/+$/g, "");
const htmlPath = path.join(DIST_ROOT, rel, "index.html"); const htmlPath = path.join(DIST_ROOT, rel, "index.html");
if (!(await exists(htmlPath))) { if (!(await exists(htmlPath))) {
@@ -135,24 +130,8 @@ async function main() {
if (!oldId || !newId) continue; if (!oldId || !newId) continue;
const oldCount = countIdAttr(html, oldId); const oldCount = countIdAttr(html, oldId);
if (oldCount > 0) {
// ✅ déjà injecté (idempotent)
if (isInjectedAliasSpan(html, oldId)) continue;
// ⛔️ oldId existe déjà "en vrai" (ex: <p id="oldId">) // ✅ déjà injecté => idempotent
// => alias inutile / inversé / obsolète
const found = findStartTagWithId(html, oldId);
const where = found ? `<${found.tagName} … id="${oldId}" …>` : `id="${oldId}"`;
const msg =
`⚠️ alias inutile/inversé: oldId déjà présent dans la page (${where}). ` +
`Supprime l'alias ${oldId} -> ${newId} (ou corrige le sens) pour route=${route}`;
if (STRICT) throw new Error(msg);
console.log(msg);
warnCount++;
continue;
}
// juste après avoir calculé oldCount
if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) { if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) {
if (STRICT && oldCount !== 1) { if (STRICT && oldCount !== 1) {
throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`); throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`);
@@ -160,18 +139,23 @@ async function main() {
continue; continue;
} }
// avant l'injection, après hasId(newId) // ⛔️ oldId existe déjà "en vrai" => alias inutile/inversé
const newCount = countIdAttr(html, newId); if (oldCount > 0) {
if (newCount !== 1) { const found = findStartTagWithId(html, oldId);
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`; const where = found ? `<${found.tagName} … id="${oldId}" …>` : `id="${oldId}"`;
const msg =
`⚠️ alias inutile/inversé: oldId déjà présent (${where}). ` +
`Supprime ${oldId} -> ${newId} (ou corrige le sens) pour route=${route}`;
if (STRICT) throw new Error(msg); if (STRICT) throw new Error(msg);
console.log(msg); console.log(msg);
warnCount++; warnCount++;
continue; continue;
} }
if (!hasId(html, newId)) { // newId doit exister UNE fois (sinon injection ambiguë)
const msg = `⚠️ newId introuvable: ${route} old=${oldId} -> new=${newId}`; const newCount = countIdAttr(html, newId);
if (newCount !== 1) {
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`;
if (STRICT) throw new Error(msg); if (STRICT) throw new Error(msg);
console.log(msg); console.log(msg);
warnCount++; warnCount++;

View File

@@ -0,0 +1,31 @@
// scripts/purge-dist-dev-whoami.mjs
import fs from "node:fs/promises";
import path from "node:path";
const CWD = process.cwd();
const targetDir = path.join(CWD, "dist", "_auth", "whoami");
const targetIndex = path.join(CWD, "dist", "_auth", "whoami", "index.html");
// Purge idempotente (force=true => pas d'erreur si absent)
async function rmSafe(p) {
try {
await fs.rm(p, { recursive: true, force: true });
return true;
} catch {
return false;
}
}
async function main() {
const removedIndex = await rmSafe(targetIndex);
const removedDir = await rmSafe(targetDir);
// Optionnel: si dist/_auth devient vide, on laisse tel quel (pas besoin de toucher)
const any = removedIndex || removedDir;
console.log(`✅ purge-dist-dev-whoami: ${any ? "purged" : "nothing to purge"}`);
}
main().catch((e) => {
console.error("❌ purge-dist-dev-whoami failed:", e);
process.exit(1);
});

View File

@@ -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(/[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.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);
});

131
scripts/switch-archicratie.sh Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
# switch-archicratie.sh — SAFE switch LIVE + STAGING (avec backups horodatés)
#
# Usage (NAS recommandé) :
# sudo bash -c 'LIVE_PORT=8081 /volume2/docker/archicratie-web/current/scripts/switch-archicratie.sh'
# sudo bash -c 'LIVE_PORT=8082 /volume2/docker/archicratie-web/current/scripts/switch-archicratie.sh'
#
# Usage (test local R&D, sans NAS) :
# D=/tmp/dynamic-test LIVE_PORT=8081 bash scripts/switch-archicratie.sh --dry-run
# D=/tmp/dynamic-test LIVE_PORT=8081 bash scripts/switch-archicratie.sh
usage() {
cat <<'EOF'
SAFE switch LIVE + STAGING (avec backups horodatés).
Variables / options :
LIVE_PORT=8081|8082 (obligatoire) port LIVE cible
D=/volume2/docker/edge/config/dynamic (optionnel) dossier des yml Traefik dynamiques
--dry-run n'écrit rien, affiche seulement ce qui serait fait
-h, --help aide
Exemples :
sudo bash -c 'LIVE_PORT=8082 /volume2/docker/archicratie-web/current/scripts/switch-archicratie.sh'
D=/tmp/dynamic-test LIVE_PORT=8081 bash scripts/switch-archicratie.sh --dry-run
EOF
}
DRY_RUN=0
for arg in "${@:-}"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) ;;
esac
done
D="${D:-/volume2/docker/edge/config/dynamic}"
F_LIVE="$D/20-archicratie-backend.yml"
F_STAG="$D/21-archicratie-staging.yml"
LIVE_PORT="${LIVE_PORT:-}"
if [[ "$LIVE_PORT" != "8081" && "$LIVE_PORT" != "8082" ]]; then
echo "❌ LIVE_PORT doit valoir 8081 ou 8082."
usage
exit 1
fi
if [[ ! -f "$F_LIVE" || ! -f "$F_STAG" ]]; then
echo "❌ Fichiers manquants :"
echo " $F_LIVE"
echo " $F_STAG"
echo " (Astuce R&D locale : mets D=/tmp/dynamic-test et crée 20/21 dedans.)"
exit 1
fi
OTHER_PORT="8081"
[[ "$LIVE_PORT" == "8081" ]] && OTHER_PORT="8082"
show_urls() {
local f="$1"
echo "$f"
grep -nE '^\s*-\s*url:\s*".*"' "$f" || true
}
# Garde-fou : on attend au moins un "url:" dans chaque fichier
grep -qE '^\s*-\s*url:\s*"' "$F_LIVE" || { echo "❌ Format inattendu dans $F_LIVE (pas de - url: \")"; exit 1; }
grep -qE '^\s*-\s*url:\s*"' "$F_STAG" || { echo "❌ Format inattendu dans $F_STAG (pas de - url: \")"; exit 1; }
echo "Avant :"
show_urls "$F_LIVE"
show_urls "$F_STAG"
echo
echo "Plan : LIVE -> $LIVE_PORT ; STAGING -> $OTHER_PORT"
echo
if [[ "$DRY_RUN" == "1" ]]; then
echo "DRY-RUN : aucune écriture."
exit 0
fi
TS="$(date +%F-%H%M%S)"
cp -a "$F_LIVE" "$F_LIVE.bak.$TS"
cp -a "$F_STAG" "$F_STAG.bak.$TS"
# sed inplace portable (macOS vs Linux/DSM)
sed_inplace() {
local expr="$1" file="$2"
if [[ "$(uname -s)" == "Darwin" ]]; then
sed -i '' -e "$expr" "$file"
else
sed -i -e "$expr" "$file"
fi
}
# Remplacement ciblé UNIQUEMENT sur la ligne - url: "http://127.0.0.1:808X"
sed_inplace \
"s#^\([[:space:]]*-[[:space:]]*url:[[:space:]]*\"http://127\\.0\\.0\\.1:\\)808[12]\\(\"[[:space:]]*\)#\\1${LIVE_PORT}\\2#g" \
"$F_LIVE"
sed_inplace \
"s#^\([[:space:]]*-[[:space:]]*url:[[:space:]]*\"http://127\\.0\\.0\\.1:\\)808[12]\\(\"[[:space:]]*\)#\\1${OTHER_PORT}\\2#g" \
"$F_STAG"
# Post-check : on confirme que les fichiers contiennent bien les ports attendus
grep -qE "http://127\.0\.0\.1:${LIVE_PORT}\"" "$F_LIVE" || {
echo "❌ Post-check FAIL : $F_LIVE ne contient pas http://127.0.0.1:${LIVE_PORT}"
echo "➡️ rollback backups : $F_LIVE.bak.$TS / $F_STAG.bak.$TS"
exit 1
}
grep -qE "http://127\.0\.0\.1:${OTHER_PORT}\"" "$F_STAG" || {
echo "❌ Post-check FAIL : $F_STAG ne contient pas http://127.0.0.1:${OTHER_PORT}"
echo "➡️ rollback backups : $F_LIVE.bak.$TS / $F_STAG.bak.$TS"
exit 1
}
echo "✅ OK. Backups :"
echo " - $F_LIVE.bak.$TS"
echo " - $F_STAG.bak.$TS"
echo
echo "Après :"
show_urls "$F_LIVE"
show_urls "$F_STAG"
echo
echo "Smoke tests :"
echo " curl -sS -I http://127.0.0.1:${LIVE_PORT}/ | head -n 12"
echo " curl -sS -I http://127.0.0.1:${OTHER_PORT}/ | head -n 12"
echo " curl -sS -I -H 'Host: archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ | head -n 20"
echo " curl -sS -I -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ | head -n 20"

View File

@@ -205,7 +205,7 @@ for (const [route, mapping] of Object.entries(data)) {
newId, newId,
htmlPath, htmlPath,
msg: msg:
`oldId present but is NOT an injected alias span (<span class="para-alias">).</n` + `oldId present but is NOT an injected alias span (<span class="para-alias">).\n` +
`Saw: ${seen}`, `Saw: ${seen}`,
}); });
continue; continue;

View File

@@ -0,0 +1,26 @@
import fs from "node:fs/promises";
import path from "node:path";
const OUT = path.join(process.cwd(), "public", "_auth", "whoami");
const groupsRaw = process.env.PUBLIC_WHOAMI_GROUPS ?? "editors";
const user = process.env.PUBLIC_WHOAMI_USER ?? "dev";
const name = process.env.PUBLIC_WHOAMI_NAME ?? "Dev Local";
const email = process.env.PUBLIC_WHOAMI_EMAIL ?? "area.technik@proton.me";
const groups = groupsRaw
.split(/[;,]/)
.map((s) => s.trim())
.filter(Boolean)
.join(",");
const body =
`Remote-User: ${user}\n` +
`Remote-Name: ${name}\n` +
`Remote-Email: ${email}\n` +
`Remote-Groups: ${groups}\n`;
await fs.mkdir(path.dirname(OUT), { recursive: true });
await fs.writeFile(OUT, body, "utf8");
console.log(`✅ dev whoami written: ${path.relative(process.cwd(), OUT)} (${groups})`);

View File

@@ -0,0 +1,21 @@
schema: 1
page: archicrat-ia/chapitre-4
paras:
p-2-31b12529:
media:
- type: image
src: /media/archicrat-ia/chapitre-4/p-2-31b12529/Capture_d_e_cran_2026-02-16_a_13.05.58.png
caption: "[Media] p-2-31b12529 — Chapitre 4 — Histoire archicratique des
révolutions industrielles"
credit: ""
ts: 2026-02-25T18:58:32.359Z
fromIssue: 115
p-7-1da4a458:
media:
- type: image
src: /media/archicrat-ia/chapitre-4/p-7-1da4a458/Capture_d_e_cran_2026-02-16_a_13.05.58.png
caption: "[Media] p-7-1da4a458 — Chapitre 4 — Histoire archicratique des
révolutions industrielles"
credit: ""
ts: 2026-02-25T19:11:32.634Z
fromIssue: 121

View File

@@ -0,0 +1,50 @@
schema: 1
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+"
comments_editorial: []

View File

@@ -7,7 +7,10 @@ const entries = (await getCollection("archicratie"))
.filter((e) => e.slug.startsWith("archicrat-ia/")) .filter((e) => e.slug.startsWith("archicrat-ia/"))
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0)); .sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
const href = (slug) => `/archicratie/${slug}/`; // ✅ On route lEssai-thèse sur /archicrat-ia/<slug-sans-prefix>/
// (Astro trailingSlash = always → on garde le "/" final)
const strip = (s) => String(s || "").replace(/^archicrat-ia\//, "");
const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
--- ---
<nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA"> <nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA">
@@ -66,7 +69,6 @@ const href = (slug) => `/archicratie/${slug}/`;
opacity: .88; opacity: .88;
} }
/* On garde <ol> mais on neutralise tout marker/numéro */
.toc-global__list{ .toc-global__list{
list-style: none; list-style: none;
margin: 0; margin: 0;
@@ -148,7 +150,6 @@ const href = (slug) => `/archicratie/${slug}/`;
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
@media (prefers-color-scheme: dark){ @media (prefers-color-scheme: dark){
.toc-global{ background: rgba(255,255,255,0.04); } .toc-global{ background: rgba(255,255,255,0.04); }
.toc-link:hover{ background: rgba(255,255,255,0.06); } .toc-link:hover{ background: rgba(255,255,255,0.06); }

View File

@@ -1,35 +1,128 @@
--- ---
// src/components/LevelToggle.astro
const { initialLevel = 1 } = Astro.props; 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> <div class="level-toggle" role="group" aria-label="Mode dédition">
<button type="button" class="lvl-btn" data-level="2" aria-pressed="false">Niveau 2</button> <button type="button" class="level-btn" data-level="1">Propos</button>
<button type="button" class="lvl-btn" data-level="3" aria-pressed="false">Niveau 3</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> </div>
<script is:inline> <script is:inline define:vars={{ initialLevel }}>
(() => { (() => {
const KEY = "archicratie.readingLevel"; const BODY = document.body;
const buttons = Array.from(document.querySelectorAll(".lvl-btn"));
function apply(level) { const wrap = document.querySelector(".level-toggle");
document.body.setAttribute("data-reading-level", String(level)); if (!wrap) return;
buttons.forEach((b) => b.setAttribute("aria-pressed", b.dataset.level === String(level) ? "true" : "false"));
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). function setActiveUI(lvl) {
// Si JS est absent/casse, le site reste lisible (tout s'affiche). for (const b of buttons) {
const stored = Number(localStorage.getItem(KEY)); const on = String(b.dataset.level) === String(lvl);
const level = (stored === 1 || stored === 2 || stored === 3) ? stored : 1; 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) => { window.__archiLevelSwitchCtx = {
b.addEventListener("click", () => { paraId,
const lvl = Number(b.dataset.level); hash: location.hash || "",
localStorage.setItem(KEY, String(lvl)); scrollY: window.scrollY || 0,
apply(lvl); 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> </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

View File

@@ -3,7 +3,7 @@
<a href="/editions/">Carte des œuvres</a><span aria-hidden="true"> · </span> <a href="/editions/">Carte des œuvres</a><span aria-hidden="true"> · </span>
<a href="/methode/">Méthode</a><span aria-hidden="true"> · </span> <a href="/methode/">Méthode</a><span aria-hidden="true"> · </span>
<a href="/recherche/">Recherche</a><span aria-hidden="true"> · </span> <a href="/recherche/">Recherche</a><span aria-hidden="true"> · </span>
<a href="/archicratie/">Essai-thèse</a><span aria-hidden="true"> · </span> <a href="/archicrat-ia/">Essai-thèse</a><span aria-hidden="true"> · </span>
<a href="/traite/">Traité</a><span aria-hidden="true"> · </span> <a href="/traite/">Traité</a><span aria-hidden="true"> · </span>
<a href="/ia/">Cas IA</a><span aria-hidden="true"> · </span> <a href="/ia/">Cas IA</a><span aria-hidden="true"> · </span>
<a href="/glossaire/">Glossaire</a><span aria-hidden="true"> · </span> <a href="/glossaire/">Glossaire</a><span aria-hidden="true"> · </span>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,197 @@
import type { APIRoute } from "astro";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { parse as parseYAML } from "yaml";
const CWD = process.cwd();
const ANNO_DIR = path.join(CWD, "src", "annotations");
// Strict en CI (ou override explicite)
const STRICT =
process.env.ANNOTATIONS_STRICT === "1" ||
process.env.CI === "1" ||
process.env.CI === "true";
async function exists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function walk(dir: string): Promise<string[]> {
const out: string[] = [];
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 isPlainObject(x: unknown): x is Record<string, unknown> {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function normalizePageKey(s: unknown): string {
return String(s ?? "")
.replace(/^\/+/, "")
.replace(/\/+$/, "")
.trim();
}
function inferPageKeyFromFile(inDirAbs: string, fileAbs: string): string {
const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/");
return rel.replace(/\.(ya?ml|json)$/i, "");
}
function parseDoc(raw: string, fileAbs: string): unknown {
if (/\.json$/i.test(fileAbs)) return JSON.parse(raw);
return parseYAML(raw);
}
function hardFailOrCollect(errors: string[], msg: string): void {
if (STRICT) throw new Error(msg);
errors.push(msg);
}
function sanitizeEntry(
fileRel: string,
paraId: string,
entry: unknown,
errors: string[]
): Record<string, unknown> {
if (entry == null) return {};
if (!isPlainObject(entry)) {
hardFailOrCollect(errors, `${fileRel}: paras.${paraId} must be an object`);
return {};
}
const e: Record<string, unknown> = { ...entry };
const arrayFields = [
"refs",
"authors",
"quotes",
"media",
"comments_editorial",
] as const;
for (const k of arrayFields) {
if (e[k] == null) continue;
if (!Array.isArray(e[k])) {
errors.push(`${fileRel}: paras.${paraId}.${k} must be an array (coerced to [])`);
e[k] = [];
}
}
return e;
}
export const GET: APIRoute = async () => {
if (!(await exists(ANNO_DIR))) {
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
pages: {},
stats: { pages: 0, paras: 0, errors: 0 },
errors: [] as string[],
};
return new Response(JSON.stringify(out), {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
});
}
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
const pages: Record<string, { paras: Record<string, Record<string, unknown>> }> =
Object.create(null);
const errors: string[] = [];
let paraCount = 0;
for (const f of files) {
const fileRel = path.relative(CWD, f).replace(/\\/g, "/");
const pageKey = normalizePageKey(inferPageKeyFromFile(ANNO_DIR, f));
if (!pageKey) {
hardFailOrCollect(errors, `${fileRel}: cannot infer page key`);
continue;
}
let doc: unknown;
try {
const raw = await fs.readFile(f, "utf8");
doc = parseDoc(raw, f);
} catch (e) {
hardFailOrCollect(errors, `${fileRel}: parse failed: ${String((e as any)?.message ?? e)}`);
continue;
}
if (!isPlainObject(doc) || (doc as any).schema !== 1) {
hardFailOrCollect(errors, `${fileRel}: schema must be 1`);
continue;
}
if ((doc as any).page != null) {
const declared = normalizePageKey((doc as any).page);
if (declared !== pageKey) {
hardFailOrCollect(
errors,
`${fileRel}: page mismatch (page="${declared}" vs path="${pageKey}")`
);
}
}
const parasAny = (doc as any).paras;
if (!isPlainObject(parasAny)) {
hardFailOrCollect(errors, `${fileRel}: missing object key "paras"`);
continue;
}
if (pages[pageKey]) {
hardFailOrCollect(errors, `${fileRel}: duplicate page "${pageKey}" (only one file per page)`);
continue;
}
const parasOut: Record<string, Record<string, unknown>> = Object.create(null);
for (const [paraId, entry] of Object.entries(parasAny)) {
if (!/^p-\d+-/i.test(paraId)) {
hardFailOrCollect(errors, `${fileRel}: invalid para id "${paraId}"`);
continue;
}
parasOut[paraId] = sanitizeEntry(fileRel, paraId, entry, errors);
}
pages[pageKey] = { paras: parasOut };
paraCount += Object.keys(parasOut).length;
}
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
pages,
stats: {
pages: Object.keys(pages).length,
paras: paraCount,
errors: errors.length,
},
errors,
};
return new Response(JSON.stringify(out), {
headers: {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
});
};

View File

@@ -0,0 +1,38 @@
---
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"))
.filter((e) => e.slug.startsWith("archicrat-ia/"));
return entries.map((entry) => ({
// ✅ inline : jamais de helper externe (évite "stripPrefix is not defined")
params: { slug: entry.slug.replace(/^archicrat-ia\//, "") },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content, headings } = await entry.render();
---
<EditionLayout
title={entry.data.title}
editionLabel="Essai-thèse"
editionKey="archicrat-ia"
statusLabel="essai-thèse"
statusKey="essai_these"
level={entry.data.level}
version={entry.data.version}
>
<Fragment slot="aside">
<EditionToc currentSlug={entry.slug} />
<LocalToc headings={headings} />
</Fragment>
<h1>{entry.data.title}</h1>
<Content />
</EditionLayout>

View File

@@ -0,0 +1,22 @@
---
import SiteLayout from "../../layouts/SiteLayout.astro";
import { getCollection } from "astro:content";
const entries = (await getCollection("archicratie"))
.filter((e) => e.slug.startsWith("archicrat-ia/"));
entries.sort((a, b) => (a.data.order ?? 9999) - (b.data.order ?? 9999));
const strip = (slug) => slug.replace(/^archicrat-ia\//, "");
const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
---
<SiteLayout title="Essai-thèse — ArchiCraT-IA">
<h1>Essai-thèse — ArchiCraT-IA</h1>
<ul>
{entries.map((e) => (
<li><a href={href(e.slug)}>{e.data.title}</a></li>
))}
</ul>
</SiteLayout>

View File

@@ -5,7 +5,8 @@ import EditionToc from "../../components/EditionToc.astro";
import LocalToc from "../../components/LocalToc.astro"; import LocalToc from "../../components/LocalToc.astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const entries = await getCollection("archicratie"); const entries = (await getCollection("archicratie"))
.filter((e) => !e.slug.startsWith("archicrat-ia/"));
return entries.map((entry) => ({ return entries.map((entry) => ({
params: { slug: entry.slug }, params: { slug: entry.slug },
props: { entry }, props: { entry },

View File

@@ -4,13 +4,13 @@ import SiteLayout from "../layouts/SiteLayout.astro";
<SiteLayout title="Accueil"> <SiteLayout title="Accueil">
<h1>Archicratie — Édition web</h1> <h1>Archicratie — Édition web</h1>
<p> <p>
Portail daccès aux éditions : Traité (Ontodynamique générative), Essai-thèse (Archicratie), Portail daccès aux éditions : Traité (Ontodynamique générative), Essai-thèse (ArchiCraT-IA),
Cas pratique (IA), Glossaire, Atlas. Cas pratique (IA), Glossaire, Atlas.
</p> </p>
<ul> <ul>
<li><a href="/editions/">Carte des œuvres</a></li> <li><a href="/editions/">Carte des œuvres</a></li>
<li><a href="/archicratie/">Essai-thèse — Archicratie</a></li> <li><a href="/archicrat-ia/">Essai-thèse — ArchiCraT-IA</a></li>
<li><a href="/traite/">Traité — Ontodynamique générative</a></li> <li><a href="/traite/">Traité — Ontodynamique générative</a></li>
<li><a href="/ia/">Cas pratique — Gouvernance des systèmes IA</a></li> <li><a href="/ia/">Cas pratique — Gouvernance des systèmes IA</a></li>
<li><a href="/glossaire/">Glossaire archicratique</a></li> <li><a href="/glossaire/">Glossaire archicratique</a></li>

View File

@@ -0,0 +1,42 @@
import type { APIRoute } from "astro";
import * as fs from "node:fs/promises";
import * as path from "node:path";
export const prerender = true;
async function exists(p: string) {
try { await fs.access(p); return true; } catch { return false; }
}
export const GET: APIRoute = async () => {
const distFile = path.join(process.cwd(), "dist", "para-index.json");
// Si dist existe (ex: après un build), on renvoie le vrai fichier.
if (await exists(distFile)) {
const raw = await fs.readFile(distFile, "utf8");
return new Response(raw, {
status: 200,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
},
});
}
// Sinon stub (dev sans build) : pas derreur, pas de crash, pas de 404.
const stub = {
schema: 1,
generatedAt: new Date().toISOString(),
items: [],
byId: {},
note: "para-index not built yet (run: npm run build to generate dist/para-index.json)",
};
return new Response(JSON.stringify(stub), {
status: 200,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
},
});
};

View File

@@ -80,6 +80,12 @@ main { padding: 0; }
border-top: 1px dashed rgba(127,127,127,0.35); border-top: 1px dashed rgba(127,127,127,0.35);
font-size: 14px; 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 { .badge {
padding: 2px 8px; padding: 2px 8px;
@@ -95,7 +101,34 @@ main { padding: 0; }
padding: 5px 12px; 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; } .level-toggle { display: inline-flex; gap: 6px; }
.lvl-btn { .lvl-btn {
border: 1px solid rgba(127,127,127,0.55); border: 1px solid rgba(127,127,127,0.55);
@@ -112,14 +145,22 @@ main { padding: 0; }
/* Règles niveaux */ /* Règles niveaux */
body[data-reading-level="1"] .level-2, body[data-reading-level="1"] .level-2,
body[data-reading-level="1"] .level-3 { display: none; } body[data-reading-level="1"] .level-3,
body[data-reading-level="2"] .level-3 { display: none; } 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) Scroll offset (anchors / headings / paras)
========================== */ ========================== */
/* Paragraph tools + bookmark */
.reading p[id]{ .reading p[id]{
position: relative; position: relative;
padding-right: 14rem; padding-right: 14rem;
@@ -183,6 +224,14 @@ body[data-reading-level="2"] .level-3 { display: none; }
} }
.para-bookmark:hover{ text-decoration: underline; } .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 { .build-stamp {
margin-top: 28px; margin-top: 28px;
padding-top: 14px; padding-top: 14px;
@@ -196,15 +245,51 @@ body[data-reading-level="2"] .level-3 { display: none; }
border-radius: 16px; border-radius: 16px;
padding: 10px 12px; padding: 10px 12px;
margin: 14px 0; margin: 14px 0;
position: relative;
} }
.details-summary {
cursor: pointer; /* ✅ Handle minimal pour sections fermées : pas de titre visible, mais ouvrable */
font-weight: 650; .details-summary{
list-style: none; 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::-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; } .details-body { margin-top: 10px; }
/* Smooth scroll */ /* Smooth scroll */
@@ -224,7 +309,6 @@ html{ scroll-behavior: smooth; }
width: var(--reading-width); width: var(--reading-width);
right: auto; right: auto;
/* colle au header */
top: var(--sticky-header-h); top: var(--sticky-header-h);
z-index: 60; z-index: 60;
@@ -247,7 +331,7 @@ html{ scroll-behavior: smooth; }
box-sizing: border-box; box-sizing: border-box;
padding: 8px 12px; padding: 8px 12px;
padding-right: 84px; /* réserve pour les boutons */ padding-right: 84px;
border: 1px solid rgba(127,127,127,.20); border: 1px solid rgba(127,127,127,.20);
border-top: 0; border-top: 0;
@@ -259,7 +343,7 @@ html{ scroll-behavior: smooth; }
box-shadow: 0 10px 22px rgba(0,0,0,.06); box-shadow: 0 10px 22px rgba(0,0,0,.06);
position: relative; /* pour rf-actions */ position: relative;
} }
@media (prefers-color-scheme: dark){ @media (prefers-color-scheme: dark){
@@ -278,7 +362,6 @@ html{ scroll-behavior: smooth; }
cursor: pointer; cursor: pointer;
margin: 0; margin: 0;
} }
.rf-line[hidden]{ display: none !important; } .rf-line[hidden]{ display: none !important; }
.rf-h1{ .rf-h1{
@@ -298,7 +381,6 @@ html{ scroll-behavior: smooth; }
font-weight: var(--rf-h3-fw); font-weight: var(--rf-h3-fw);
opacity: .92; opacity: .92;
} }
.rf-line:hover{ text-decoration: underline; } .rf-line:hover{ text-decoration: underline; }
/* Actions */ /* Actions */
@@ -327,3 +409,14 @@ html{ scroll-behavior: smooth; }
background: rgba(127,127,127,0.16); background: rgba(127,127,127,0.16);
transform: translateY(-1px); 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;
}

View File

@@ -1,9 +1,5 @@
{ {
"archicratie/00-demarrage/index.html": [ "archicrat-ia/chapitre-1/index.html": [
"p-0-d64c1c39",
"p-1-3f750540"
],
"archicratie/archicrat-ia/chapitre-1/index.html": [
"p-0-8d27a7f5", "p-0-8d27a7f5",
"p-1-8a6c18bf", "p-1-8a6c18bf",
"p-2-39c6e4f4", "p-2-39c6e4f4",
@@ -664,7 +660,7 @@
"p-657-7c465f0f", "p-657-7c465f0f",
"p-658-3fc26620" "p-658-3fc26620"
], ],
"archicratie/archicrat-ia/chapitre-2/index.html": [ "archicrat-ia/chapitre-2/index.html": [
"p-0-32820f76", "p-0-32820f76",
"p-1-63506bae", "p-1-63506bae",
"p-2-206c653a", "p-2-206c653a",
@@ -1981,7 +1977,7 @@
"p-1313-c83e97d4", "p-1313-c83e97d4",
"p-1314-4c2ed9ba" "p-1314-4c2ed9ba"
], ],
"archicratie/archicrat-ia/chapitre-3/index.html": [ "archicrat-ia/chapitre-3/index.html": [
"p-0-ace27175", "p-0-ace27175",
"p-1-60c7ea48", "p-1-60c7ea48",
"p-2-1167ed0e", "p-2-1167ed0e",
@@ -2973,7 +2969,7 @@
"p-988-d8a0cce7", "p-988-d8a0cce7",
"p-989-fdbb8595" "p-989-fdbb8595"
], ],
"archicratie/archicrat-ia/chapitre-4/index.html": [ "archicrat-ia/chapitre-4/index.html": [
"p-0-ba984c9d", "p-0-ba984c9d",
"p-1-ea84724d", "p-1-ea84724d",
"p-2-31b12529", "p-2-31b12529",
@@ -3730,7 +3726,7 @@
"p-753-f778aef4", "p-753-f778aef4",
"p-754-d99a24c1" "p-754-d99a24c1"
], ],
"archicratie/archicrat-ia/chapitre-5/index.html": [ "archicrat-ia/chapitre-5/index.html": [
"p-0-96edff22", "p-0-96edff22",
"p-1-a51a4ee1", "p-1-a51a4ee1",
"p-2-dff32cbc", "p-2-dff32cbc",
@@ -4771,7 +4767,7 @@
"p-1037-4825033b", "p-1037-4825033b",
"p-1038-54aa72be" "p-1038-54aa72be"
], ],
"archicratie/archicrat-ia/conclusion/index.html": [ "archicrat-ia/conclusion/index.html": [
"p-0-5ec4522a", "p-0-5ec4522a",
"p-1-e481f7e6", "p-1-e481f7e6",
"p-2-7a56c59b", "p-2-7a56c59b",
@@ -4892,7 +4888,7 @@
"p-117-3a086369", "p-117-3a086369",
"p-118-67afae83" "p-118-67afae83"
], ],
"archicratie/archicrat-ia/prologue/index.html": [ "archicrat-ia/prologue/index.html": [
"p-0-d7974f88", "p-0-d7974f88",
"p-1-2ef25f29", "p-1-2ef25f29",
"p-2-edb49e0a", "p-2-edb49e0a",