Compare commits

...

150 Commits

Author SHA1 Message Date
1c2b9ddbb6 test: A full-auto gate (touch src/content)
All checks were successful
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-03 20:54:46 +01:00
9e1b704aa6 ci(deploy): fix gate bash + robust merge-proof diff + full/hotpatch auto
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 35s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-03 20:46:03 +01:00
0b4a31a432 ci(deploy): make gate merge-proof (diff before..after)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 37s
2026-03-03 19:58:27 +01:00
c617dc3979 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#183) from testB-hotpatch-auto-20260303-183745 into main
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 7m50s
Reviewed-on: #183
2026-03-03 18:40:39 +01:00
1b95161de0 test: B hotpatch-auto gate (touch src/annotations)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-03 18:38:52 +01:00
ebd976bd46 Merge pull request 'chore: cleanup testA/testB markers' (#182) from chore/cleanup-testA-testB-20260303-175846 into main
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 8m55s
Reviewed-on: #182
2026-03-03 18:01:40 +01:00
f8d57d8fe0 chore: cleanup testA/testB markers
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 38s
2026-03-03 18:00:01 +01:00
09a4d2c472 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#181) from testB-hotpatch-auto-20260303-174037 into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 8m0s
Reviewed-on: #181
2026-03-03 17:43:31 +01:00
1f6dc874d0 test: B hotpatch-auto gate (touch src/annotations)
All checks were successful
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (push) Successful in 36s
CI / build-and-anchors (pull_request) Successful in 36s
2026-03-03 17:42:04 +01:00
4dd63945ee Merge pull request 'test: A full-auto gate (touch src/content)' (#180) from testA-full-auto-20260303-173032 into main
Some checks failed
SMOKE / smoke (push) Successful in 15s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
Reviewed-on: #180
2026-03-03 17:36:14 +01:00
ba64b0694b test: A full-auto gate (touch src/content)
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-03 17:34:32 +01:00
58e5ceda59 Merge pull request 'ci(deploy): auto FULL when content/anchors/pages/scripts change' (#179) from chore/deploy-gate-full-on-content-anchors-pages-scripts-20260303-171645 into main
All checks were successful
SMOKE / smoke (push) Successful in 15s
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Successful in 7m35s
Reviewed-on: #179
2026-03-03 17:20:36 +01:00
08f826ee01 ci(deploy): auto FULL when content/anchors/pages/scripts change
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 46s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-03 17:16:45 +01:00
3358d280ec Merge pull request 'edit: apply ticket #174 (/archicrat-ia/chapitre-3/#p-1-60c7ea48)' (#178) from chore/migrate-content-archicrat-ia-root-20260303-132407 into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #178
2026-03-03 15:04:07 +01:00
9cb0d5e416 content: wire archicrat-ia as first-class collection (routes + toc + schema)
All checks were successful
CI / build-and-anchors (push) Successful in 38s
CI / build-and-anchors (pull_request) Successful in 37s
SMOKE / smoke (push) Successful in 5s
2026-03-03 15:02:50 +01:00
a46f058917 edit: apply ticket #174 (/archicrat-ia/chapitre-3/#p-1-60c7ea48)
Some checks failed
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Failing after 39s
CI / build-and-anchors (pull_request) Failing after 36s
2026-03-03 14:27:35 +01:00
604b2199da Merge pull request 'ci: fix proposer apply workflow (checkout before APP_DIR detect)' (#177) from chore/fix-proposer-apply-checkout-order-20260303-122611 into main
All checks were successful
SMOKE / smoke (push) Successful in 14s
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 1m6s
Reviewed-on: #177
2026-03-03 12:32:34 +01:00
d153f71be6 ci: fix proposer apply workflow (checkout before APP_DIR detect)
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-03 12:26:11 +01:00
8f64e4b098 Merge pull request 'ci: fix proposer workflow (auto APP_DIR + guards)' (#176) from chore/fix-proposer-workflow-appdir-20260303-115843 into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 1m10s
Reviewed-on: #176
2026-03-03 12:01:18 +01:00
459bf195d8 ci: fix proposer workflow (auto APP_DIR + guards)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-03 11:58:43 +01:00
0c46b0d19b Merge pull request 'ci: add Proposer Apply workflow (apply-ticket -> PR bot)' (#175) from chore/proposer-apply-workflow-20260302-234255 into main
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #175
2026-03-02 23:49:57 +01:00
bfbdc7b688 ci: add Proposer Apply workflow (apply-ticket -> PR bot)
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-02 23:42:55 +01:00
8fd53dd4d2 Merge pull request 'anno: apply ticket #172' (#173) from bot/anno-172-20260302-200155 into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #173
2026-03-02 21:03:36 +01:00
archicratie-bot
c8bbee4f74 anno: apply ticket #172 (archicrat-ia/chapitre-3#p-1-60c7ea48 type/reference)
All checks were successful
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 5s
2026-03-02 20:01:55 +00:00
04cdf54eb7 Merge pull request 'anno: apply ticket #169' (#171) from bot/anno-169-20260302-195320 into main
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 56s
Reviewed-on: #171
2026-03-02 20:59:08 +01:00
archicratie-bot
d6bf645ae9 anno: apply ticket #169 (archicrat-ia/chapitre-3#p-0-ace27175 type/reference)
All checks were successful
CI / build-and-anchors (push) Successful in 47s
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-02 19:53:21 +00:00
1ca6bcbd81 Merge pull request 'ci: make anno apply/reject gates API-hard (approved/rejected label present)' (#170) from chore/fix-anno-apply-approved-gate-v1 into main
All checks were successful
SMOKE / smoke (push) Successful in 13s
Deploy staging+live (annotations) / deploy (push) Successful in 46s
CI / build-and-anchors (push) Successful in 42s
Reviewed-on: #170
2026-03-02 20:17:58 +01:00
dec5f8eba7 ci: make anno apply/reject gates API-hard (approved/rejected label present)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-02 20:12:29 +01:00
716c887045 Merge pull request 'ci: fix auto-label (no array fallback, retries, post-verify)' (#167) from chore/fix-auto-label-422-v1 into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 45s
Reviewed-on: #167
2026-03-02 19:37:43 +01:00
9b1789a164 ci: fix auto-label (no array fallback, retries, post-verify)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 50s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-02 19:36:09 +01:00
17fa39c7ff Merge pull request 'ci: hard-gate anno apply/reject + fix JSON parsing' (#164) from chore/fix-anno-workflows-jsonparse-v3 into main
All checks were successful
SMOKE / smoke (push) Successful in 15s
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 55s
Reviewed-on: #164
2026-03-02 18:53:19 +01:00
8132e315f4 ci: hard-gate anno apply/reject + fix JSON parsing
All checks were successful
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-02 18:48:39 +01:00
8d993915d7 Merge pull request 'ci: stabilize anno apply/reject (event parsing + strict gating)' (#161) from chore/fix-anno-workflows-jsonparse-v2 into main
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 1m10s
Reviewed-on: #161
2026-03-02 12:49:18 +01:00
497bddd05d ci: fix anno apply/reject JSON parsing + hard gates
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 40s
2026-03-02 12:47:37 +01:00
7c8e49c1a9 ci: stabilize anno apply/reject (event parsing + strict gating)
Some checks failed
CI / build-and-anchors (push) Successful in 52s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Failing after 12m55s
2026-03-02 11:10:53 +01:00
901d28b89b Merge pull request 'deploy: pin nas-deploy image by digest' (#159) from chore/pin-nas-deploy-image-digest into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 36s
Deploy staging+live (annotations) / deploy (push) Successful in 45s
Reviewed-on: #159
2026-02-28 20:24:46 +01:00
43e2862c89 deploy: pin nas-deploy image by digest
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 37s
2026-02-28 20:22:17 +01:00
73fb38c4d1 Merge pull request 'deploy: use prebaked nas-deploy image; remove apt-get step' (#158) from chore/deploy-use-prebaked-image into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 36s
Deploy staging+live (annotations) / deploy (push) Successful in 39s
Reviewed-on: #158
2026-02-28 19:51:27 +01:00
a81d206aba deploy: use prebaked nas-deploy image; remove apt-get step
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 36s
2026-02-28 19:49:25 +01:00
9801ea3cea Merge pull request 'ci: lock deploy workflow to nas-deploy runner' (#157) from chore/lock-nas-deploy into main
All checks were successful
SMOKE / smoke (push) Successful in 16s
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 3m40s
Reviewed-on: #157
2026-02-28 17:45:35 +01:00
c11189fe11 ci: lock deploy workflow to nas-deploy runner
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 40s
2026-02-28 17:43:19 +01:00
b47edb24cf Merge pull request 'ci: fix YAML newlines after runs-on (mac-ci)' (#156) from chore/fix-yaml-runs-on-newlines into main
Some checks failed
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 3s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
Reviewed-on: #156
2026-02-28 15:54:48 +01:00
be191b09a0 ci: fix YAML newlines after runs-on (mac-ci)
All checks were successful
SMOKE / smoke (push) Successful in 25s
CI / build-and-anchors (push) Successful in 1m6s
CI / build-and-anchors (pull_request) Successful in 37s
2026-02-28 15:50:48 +01:00
e06587478d Merge pull request 'ci: route CI/bots to mac runner; keep deploy on NAS' (#155) from chore/route-ci-to-mac-runner into main
All checks were successful
Deploy staging+live (annotations) / deploy (push) Successful in 1m58s
Reviewed-on: #155
2026-02-28 15:29:35 +01:00
402ffb04cd ci: route CI/bots to mac runner; keep deploy on NAS 2026-02-28 15:28:49 +01:00
1cbfc02670 Merge pull request 'ci: harden anno-reject (dispatch + conflict guard) and keep deploy concurrency safe' (#153) from chore/fix-anno-reject-close-guard into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m49s
Deploy staging+live (annotations) / deploy (push) Successful in 3m7s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #153
2026-02-28 10:05:26 +01:00
28d2fbbd2f ci: harden anno-reject (dispatch + conflict guard) and keep deploy concurrency safe
All checks were successful
CI / build-and-anchors (push) Successful in 2m21s
SMOKE / smoke (push) Successful in 19s
2026-02-28 09:55:37 +01:00
225368a952 Merge pull request 'ci: anno-apply gate on supported types (skip proposer)' (#149) from chore/fix-anno-apply-gate-types into main
All checks were successful
Deploy staging+live (annotations) / deploy (push) Successful in 2m12s
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #149
2026-02-27 20:06:57 +01:00
3574695041 ci: anno-apply gate on supported types (skip proposer)
All checks were successful
CI / build-and-anchors (push) Successful in 1m52s
SMOKE / smoke (push) Successful in 15s
2026-02-27 20:03:16 +01:00
ea68025a1d Merge pull request 'anno: apply ticket #144' (#148) from bot/anno-144-20260227-124313 into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m5s
Deploy staging+live (annotations) / deploy (push) Successful in 2m8s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #148
2026-02-27 15:49:11 +01:00
3a08698003 Merge pull request 'anno: apply ticket #143' (#147) from bot/anno-143-20260227-124037 into main
Some checks failed
CI / build-and-anchors (push) Has been cancelled
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Has been cancelled
Reviewed-on: #147
2026-02-27 15:48:28 +01:00
3d583608c2 Merge pull request 'anno: apply ticket #142' (#146) from bot/anno-142-20260227-123430 into main
Some checks failed
CI / build-and-anchors (push) Has been cancelled
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Successful in 21s
Reviewed-on: #146
2026-02-27 15:47:44 +01:00
archicratie-bot
01ae95ab43 anno: apply ticket #144 (archicrat-ia/chapitre-3#p-0-ace27175 type/media)
All checks were successful
CI / build-and-anchors (push) Successful in 2m0s
SMOKE / smoke (push) Successful in 18s
2026-02-27 12:43:16 +00:00
archicratie-bot
0d5821c640 anno: apply ticket #143 (archicrat-ia/chapitre-1#p-1-8a6c18bf type/comment)
All checks were successful
CI / build-and-anchors (push) Successful in 1m54s
SMOKE / smoke (push) Successful in 16s
2026-02-27 12:40:39 +00:00
archicratie-bot
2bcea39558 anno: apply ticket #142 (archicrat-ia/chapitre-1#p-0-8d27a7f5 type/reference)
All checks were successful
CI / build-and-anchors (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 14s
2026-02-27 12:34:32 +00:00
af85970d4a Merge pull request 'chore/fix-build-annotations-index-shards' (#141) from chore/fix-build-annotations-index-shards into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
Deploy staging+live (annotations) / deploy (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #141
2026-02-27 13:21:43 +01:00
210f621487 ci: support shard annotations in checks + endpoint (pageKey inference)
All checks were successful
CI / build-and-anchors (push) Successful in 1m58s
SMOKE / smoke (push) Successful in 13s
2026-02-27 13:13:31 +01:00
8ad960dc69 anno: build-annotations-index supports shard annotations
Some checks failed
SMOKE / smoke (push) Successful in 16s
CI / build-and-anchors (push) Failing after 1m48s
2026-02-27 12:27:35 +01:00
d45a8b285f anno: support shard annotations in annotations-index endpoint
Some checks failed
CI / build-and-anchors (push) Failing after 1m45s
SMOKE / smoke (push) Successful in 15s
2026-02-27 12:09:40 +01:00
b6e04a9138 anno: robust verify for para-index (normalize page keys)
Some checks failed
SMOKE / smoke (push) Successful in 33s
CI / build-and-anchors (push) Failing after 1m53s
2026-02-27 10:20:49 +01:00
dcf1fc2d0b anno: apply ticket #127 (archicrat-ia/chapitre-4#p-11-67c14c09 type/media) 2026-02-27 10:17:06 +01:00
41b0517c6c Merge pull request 'ci: deploy hotpatch-only + full rebuild warmup' (#139) from chore/fix-deploy-hotpatch-stable into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m50s
Deploy staging+live (annotations) / deploy (push) Successful in 2m12s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #139
2026-02-26 22:00:20 +01:00
6b43eb199d ci: deploy hotpatch-only + full rebuild warmup
All checks were successful
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 20s
2026-02-26 21:56:42 +01:00
d40f24e92d Merge pull request 'ci: fix hotpatch (yaml datetime -> json safe)' (#138) from chore/fix-hotpatch-json into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m54s
Deploy staging+live (annotations) / deploy (push) Failing after 5m35s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #138
2026-02-26 21:32:56 +01:00
480a61b071 ci: fix hotpatch (yaml datetime -> json safe)
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 22s
2026-02-26 21:29:52 +01:00
a5d68d6a7e Merge pull request 'ci: fix deploy workflow (warmup + hotpatch)' (#137) from chore/fix-deploy-warmup into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m38s
Deploy staging+live (annotations) / deploy (push) Failing after 8m16s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #137
2026-02-26 21:09:21 +01:00
390f2c33e5 ci: fix deploy workflow (warmup + hotpatch)
All checks were successful
CI / build-and-anchors (push) Successful in 2m2s
SMOKE / smoke (push) Successful in 17s
2026-02-26 21:06:01 +01:00
16485dc4a9 Merge pull request 'ci: shard annotations by para + deploy deep-merge hotpatch' (#135) from chore/anno-shard-deepmerge into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m54s
Deploy staging+live (annotations) / deploy (push) Failing after 7m20s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #135
2026-02-26 19:58:22 +01:00
a43ce5f188 ci: shard annotations by para + deploy deep-merge hotpatch
All checks were successful
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 29s
2026-02-26 19:54:46 +01:00
0519ae2dd0 Merge pull request 'ci: fix deploy checkout (no eval) + workflow_dispatch fallback' (#134) from chore/fix-deploy-container-conflict into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m52s
Deploy staging+live (annotations) / deploy (push) Successful in 8m48s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #134
2026-02-26 18:51:53 +01:00
0d5b790e52 ci: fix deploy checkout (no eval) + workflow_dispatch fallback
All checks were successful
CI / build-and-anchors (push) Successful in 1m50s
SMOKE / smoke (push) Successful in 17s
2026-02-26 18:48:12 +01:00
342e21b9ea Merge pull request 'ci: fix deploy checkout (no eval) + workflow_dispatch fallback' (#133) from chore/fix-deploy-checkout-quote into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m40s
Deploy staging+live (annotations) / deploy (push) Failing after 10m11s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #133
2026-02-26 18:01:24 +01:00
4dec9e182b ci: fix deploy checkout (no eval) + workflow_dispatch fallback
All checks were successful
CI / build-and-anchors (push) Successful in 1m38s
SMOKE / smoke (push) Successful in 18s
2026-02-26 17:58:58 +01:00
c7ae883c6a Merge pull request 'ci: fix deploy workflow (workflow_dispatch checkout + gate)' (#132) from chore/fix-deploy-workflow into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m37s
Deploy staging+live (annotations) / deploy (push) Failing after 19s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #132
2026-02-26 17:44:28 +01:00
9b4584f70a ci: fix deploy workflow (workflow_dispatch checkout + gate)
All checks were successful
CI / build-and-anchors (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 19s
2026-02-26 17:39:51 +01:00
7b64fb7401 Merge pull request 'anno: apply ticket #129' (#131) from bot/anno-129-20260226-131740 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m46s
Deploy staging+live (annotations) / deploy (push) Successful in 22s
SMOKE / smoke (push) Successful in 18s
Reviewed-on: #131
2026-02-26 14:23:46 +01:00
archicratie-bot
57cb23ce8b anno: apply ticket #129 (archicrat-ia/chapitre-4#p-11-67c14c09 type/media)
All checks were successful
CI / build-and-anchors (push) Successful in 1m39s
SMOKE / smoke (push) Successful in 18s
2026-02-26 13:17:43 +00:00
708b87ff35 Merge pull request 'ci: deploy staging+live (annotations) with manual force' (#126) from chore/deploy-staging-live into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m47s
Deploy staging+live (annotations) / deploy (push) Successful in 20s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #126
2026-02-26 12:41:21 +01:00
577cfd08e8 ci: deploy staging+live (annotations) with manual force
All checks were successful
CI / build-and-anchors (push) Successful in 1m51s
SMOKE / smoke (push) Successful in 15s
2026-02-26 12:40:47 +01:00
de9edbe532 Merge pull request 'ci: fix deploy (install docker compose plugin)' (#125) from chore/fix-deploy-compose into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m39s
Deploy staging+live (annotations) / deploy (push) Successful in 31s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #125
2026-02-26 11:16:19 +01:00
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
006fec7efd Merge pull request 'ci: auto deploy staging+live (annotations-only)' (#124) from chore/deploy-auto into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m48s
Deploy (staging + live) — annotations / deploy (push) Failing after 20s
SMOKE / smoke (push) Successful in 24s
Reviewed-on: #124
2026-02-26 10:29:50 +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
8b7cfdfd48 docs(ops): add triple-source sync + troubleshooting + proposer spec
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 17s
2026-02-01 14:30:18 +01:00
c12b6015ab Merge pull request 'docs: unwrap markdown blocks (render as real docs)' (#65) from docs/unwrap-20260201 into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m26s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #65
2026-02-01 13:46:49 +01:00
f7f6b8f770 docs: unwrap markdown blocks (render as real docs)
All checks were successful
CI / build-and-anchors (push) Successful in 1m46s
SMOKE / smoke (push) Successful in 12s
2026-02-01 13:45:43 +01:00
ae8ec42349 Merge pull request 'docs/ops-reference-20260201' (#64) from docs/ops-reference-20260201 into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m25s
SMOKE / smoke (push) Successful in 12s
Reviewed-on: #64
2026-02-01 13:36:44 +01:00
113 changed files with 17517 additions and 753 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."
---
## Chemin (ex: /archicratie/prologue/)
## Chemin (ex: /archicrat-ia/prologue/)
<!-- obligatoire -->
/...

View File

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

View File

@@ -0,0 +1,449 @@
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
concurrency:
group: anno-apply-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
apply-approved:
runs-on: mac-ci
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: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
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");
}
// label name: best-effort (non-bloquant)
let labelName = "workflow_dispatch";
const lab = ev?.label;
if (typeof lab === "string") labelName = lab;
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
else if (ev?.label?.name) labelName = ev.label.name;
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: Early gate (label event fast-skip, but tolerant)
run: |
set -euo pipefail
source /tmp/anno.env
echo " event label = $LABEL_NAME"
# Fast skip on obvious non-approved label events (avoid noise),
# BUT do NOT skip if label payload is weird/unknown.
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
echo " label=$LABEL_NAME => skip early"
echo "SKIP=1" >> /tmp/anno.env
echo "SKIP_REASON=\"label_not_approved_event\"" >> /tmp/anno.env
exit 0
fi
echo "✅ continue to API gating (issue=$ISSUE_NUMBER)"
- name: Fetch issue + hard gate on labels + Type
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
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; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/issue.json
node --input-type=module - <<'NODE' >> /tmp/anno.env
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
const title = String(issue.title || "");
const body = String(issue.body || "").replace(/\r\n/g, "\n");
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
const hasApproved = labels.includes("state/approved");
function pickLine(key) {
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = body.match(re);
return m ? m[1].trim() : "";
}
const typeRaw = pickLine("Type");
const type = String(typeRaw || "").trim().toLowerCase();
const allowed = new Set(["type/media","type/reference","type/comment"]);
const proposer = new Set(["type/correction","type/fact-check"]);
const out = [];
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
// HARD gate: must currently have state/approved (avoids depending on event payload)
if (!hasApproved) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("not_approved_label_present")}`);
process.stdout.write(out.join("\n") + "\n");
process.exit(0);
}
if (!type) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
} else if (allowed.has(type)) {
// proceed
} else if (proposer.has(type)) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("proposer_type:"+type)}`);
} else {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("unsupported_type:"+type)}`);
}
process.stdout.write(out.join("\n") + "\n");
NODE
echo "✅ gating result:"
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
- name: Comment issue if skipped (Proposer / unsupported / missing Type)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
# IMPORTANT: do NOT comment for "not_approved_label_present" (avoid spam on other label events)
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
echo " skip reason=${SKIP_REASON} -> no comment"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
REASON="${SKIP_REASON:-}"
TYPE="${ISSUE_TYPE:-}"
if [[ "$REASON" == proposer_type:* ]]; then
MSG=" Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors**.\n✅ Aucun traitement automatique."
elif [[ "$REASON" == unsupported_type:* ]]; then
MSG=" Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment."
else
MSG=" Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\nAjoute : Type: type/media|type/reference|type/comment"
fi
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: 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 --no-audit --no-fund
- 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
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 -d .git || { echo "❌ not a git repo (checkout failed)"; echo "APPLY_RC=90" >> /tmp/anno.env; 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 || true
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
RC="${APPLY_RC:-0}"
if [[ "$RC" == "0" ]]; then
echo " no failure detected"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
if [[ -f /tmp/apply.log ]]; then
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
else
BODY="(no apply log found)"
fi
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: Push bot branch
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${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; }
test -d .git || { echo " no git repo -> 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 || true
[[ "${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,181 @@
name: Anno Reject (close issue)
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to reject/close"
required: true
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
concurrency:
group: anno-reject-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
reject:
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
node --version
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
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") : "");
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) && cloneUrl) {
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 ||
(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");
}
// label name: best-effort (non-bloquant)
let labelName = "workflow_dispatch";
const lab = ev?.label;
if (typeof lab === "string") labelName = lab;
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
let apiBase = "";
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
apiBase = String(process.env.FORGE_API).trim().replace(/\/+$/,"");
} else if (cloneUrl) {
apiBase = new URL(cloneUrl).origin;
} else {
apiBase = "";
}
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(apiBase)}`
].join("\n") + "\n");
NODE
echo "✅ context:"
sed -n '1,120p' /tmp/reject.env
- name: Early gate (fast-skip, tolerant)
run: |
set -euo pipefail
source /tmp/reject.env
echo " event label = $LABEL_NAME"
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
echo " label=$LABEL_NAME => skip early"
echo "SKIP=1" >> /tmp/reject.env
echo "SKIP_REASON=\"label_not_rejected_event\"" >> /tmp/reject.env
exit 0
fi
- name: Comment + close (only if label state/rejected is PRESENT now, and no conflict)
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/reject.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/reject.issue.json
node --input-type=module - <<'NODE' > /tmp/reject.flags
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/reject.issue.json","utf8"));
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
const hasApproved = labels.includes("state/approved");
const hasRejected = labels.includes("state/rejected");
process.stdout.write(`HAS_APPROVED=${hasApproved ? "1":"0"}\nHAS_REJECTED=${hasRejected ? "1":"0"}\n`);
NODE
source /tmp/reject.flags
# Do nothing unless state/rejected is truly present now (anti payload weird)
if [[ "${HAS_REJECTED:-0}" != "1" ]]; then
echo " state/rejected not present -> skip"
exit 0
fi
if [[ "${HAS_APPROVED:-0}" == "1" && "${HAS_REJECTED:-0}" == "1" ]]; then
MSG="⚠️ Conflit d'état sur le ticket #${ISSUE_NUMBER} : labels **state/approved** et **state/rejected** présents.\n\n➡ Action manuelle requise : retirer l'un des deux labels avant relance."
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"
echo " conflict => stop"
exit 0
fi
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"}'
echo "✅ rejected+closed"

View File

@@ -4,22 +4,37 @@ on:
issues:
types: [opened, edited]
concurrency:
group: auto-label-${{ github.event.issue.number || github.event.issue.index || 'manual' }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Apply labels from Type/State/Category
env:
FORGE_BASE: ${{ vars.FORGE_API || vars.FORGE_BASE }}
# IMPORTANT: préfère FORGE_BASE (LAN) si défini, sinon FORGE_API
FORGE_BASE: ${{ vars.FORGE_BASE || vars.FORGE_API || vars.FORGE_API_BASE }}
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
REPO_FULL: ${{ gitea.repository }}
EVENT_PATH: ${{ github.event_path }}
NODE_OPTIONS: --dns-result-order=ipv4first
run: |
python3 - <<'PY'
import json, os, re, urllib.request, urllib.error
import json, os, re, time, urllib.request, urllib.error, socket
forge = (os.environ.get("FORGE_BASE") or "").rstrip("/")
if not forge:
raise SystemExit("Missing FORGE_BASE/FORGE_API repo variable (e.g. http://192.168.1.20:3000)")
token = os.environ.get("FORGE_TOKEN") or ""
if not token:
raise SystemExit("Missing secret FORGE_TOKEN")
forge = os.environ["FORGE_BASE"].rstrip("/")
token = os.environ["FORGE_TOKEN"]
owner, repo = os.environ["REPO_FULL"].split("/", 1)
event_path = os.environ["EVENT_PATH"]
@@ -46,12 +61,9 @@ jobs:
print("PARSED:", {"Type": t, "State": s, "Category": c})
# 1) explicite depuis le body
if t:
desired.add(t)
if s:
desired.add(s)
if c:
desired.add(c)
if t: desired.add(t)
if s: desired.add(s)
if c: desired.add(c)
# 2) fallback depuis le titre si Type absent
if not t:
@@ -76,42 +88,56 @@ jobs:
"Authorization": f"token {token}",
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-auto-label/1.0",
"User-Agent": "archicratie-auto-label/1.1",
}
def jreq(method, url, payload=None):
def jreq(method, url, payload=None, timeout=60, retries=4, backoff=2.0):
data = None if payload is None else json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=20) as r:
b = r.read()
return json.loads(b.decode("utf-8")) if b else None
except urllib.error.HTTPError as e:
b = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
last_err = None
for i in range(retries):
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
b = r.read()
return json.loads(b.decode("utf-8")) if b else None
except urllib.error.HTTPError as e:
b = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
last_err = e
# retry only on network/timeout
time.sleep(backoff * (i + 1))
raise RuntimeError(f"Network/timeout after retries: {method} {url}\n{last_err}")
# labels repo
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000") or []
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000", timeout=60) or []
name_to_id = {x.get("name"): x.get("id") for x in labels}
missing = [x for x in desired if x not in name_to_id]
if missing:
raise SystemExit("Missing labels in repo: " + ", ".join(sorted(missing)))
wanted_ids = [name_to_id[x] for x in desired]
wanted_ids = sorted({int(name_to_id[x]) for x in desired})
# labels actuels de l'issue
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels") or []
current_ids = {x.get("id") for x in current if x.get("id") is not None}
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
current_ids = {int(x.get("id")) for x in current if x.get("id") is not None}
final_ids = sorted(current_ids.union(wanted_ids))
# set labels = union (n'enlève rien)
# Replace labels = union (n'enlève rien)
url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels"
try:
jreq("PUT", url, {"labels": final_ids})
except Exception:
jreq("PUT", url, final_ids)
# IMPORTANT: on n'envoie JAMAIS une liste brute ici (ça a causé le 422)
jreq("PUT", url, {"labels": final_ids}, timeout=90, retries=4)
# vérif post-apply (anti "timeout mais appliqué")
post = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
post_ids = {int(x.get("id")) for x in post if x.get("id") is not None}
missing_ids = [i for i in wanted_ids if i not in post_ids]
if missing_ids:
raise RuntimeError(f"Labels not applied after PUT (missing ids): {missing_ids}")
print(f"OK labels #{number}: {sorted(desired)}")
PY

View File

@@ -3,7 +3,7 @@ name: CI
on:
push:
pull_request:
branches: [master]
branches: [main]
workflow_dispatch:
env:
@@ -15,7 +15,7 @@ defaults:
jobs:
build-and-anchors:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
@@ -79,22 +79,7 @@ jobs:
set -euo pipefail
npm ci
- name: Inline scripts syntax check
- name: Full test suite (CI=1)
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
npm run ci

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,561 @@
name: Deploy staging+live (annotations)
on:
push:
branches: [main]
workflow_dispatch:
inputs:
force:
description: "Force FULL deploy (rebuild+restart) even if gate would hotpatch-only (1=yes, 0=no)"
required: false
default: "0"
env:
NODE_OPTIONS: --dns-result-order=ipv4first
DOCKER_API_VERSION: "1.43"
COMPOSE_VERSION: "2.29.7"
ASTRO_TELEMETRY_DISABLED: "1"
defaults:
run:
shell: bash
concurrency:
group: deploy-staging-live-main
cancel-in-progress: false
jobs:
deploy:
runs-on: nas-deploy
container:
image: localhost:5000/archicratie/nas-deploy-node22@sha256:fefa8bb307005cebec07796661ab25528dc319c33a8f1e480e1d66f90cd5cff6
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
- name: Checkout (push or workflow_dispatch, no external actions)
env:
EVENT_JSON: /var/run/act/workflow/event.json
run: |
set -euo pipefail
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
node --input-type=module <<'NODE'
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");
const defaultBranch = repoObj?.default_branch || "main";
// Push-range (most reliable for change detection)
const before = String(ev?.before || "").trim();
const after =
(process.env.GITHUB_SHA && String(process.env.GITHUB_SHA).trim()) ||
String(ev?.after || ev?.sha || ev?.head_commit?.id || ev?.pull_request?.head?.sha || "").trim();
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
fs.writeFileSync("/tmp/deploy.env", [
`REPO_URL=${shq(cloneUrl)}`,
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
`BEFORE=${shq(before)}`,
`AFTER=${shq(after)}`
].join("\n") + "\n");
NODE
source /tmp/deploy.env
echo "Repo URL: $REPO_URL"
echo "Default branch: $DEFAULT_BRANCH"
echo "BEFORE: ${BEFORE:-<empty>}"
echo "AFTER: ${AFTER:-<empty>}"
rm -rf .git
git init -q
git remote add origin "$REPO_URL"
# Checkout AFTER (or default branch if missing)
if [[ -n "${AFTER:-}" ]]; then
git fetch --depth 50 origin "$AFTER"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
else
git fetch --depth 50 origin "$DEFAULT_BRANCH"
git -c advice.detachedHead=false checkout -q "origin/$DEFAULT_BRANCH"
AFTER="$(git rev-parse HEAD)"
echo "AFTER='$AFTER'" >> /tmp/deploy.env
echo "Resolved AFTER: $AFTER"
fi
git log -1 --oneline
- name: Gate — decide SKIP vs HOTPATCH vs FULL rebuild
env:
INPUT_FORCE: ${{ inputs.force }}
EVENT_JSON: /var/run/act/workflow/event.json
run: |
set -euo pipefail
source /tmp/deploy.env
FORCE="${INPUT_FORCE:-0}"
# Lire before/after du push depuis event.json (merge-proof)
node --input-type=module <<'NODE'
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const before = ev?.before || "";
const after = ev?.after || ev?.sha || "";
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
fs.writeFileSync("/tmp/gate.env", [
`EV_BEFORE=${shq(before)}`,
`EV_AFTER=${shq(after)}`
].join("\n") + "\n");
NODE
source /tmp/gate.env
BEFORE="${EV_BEFORE:-}"
AFTER="${EV_AFTER:-}"
if [[ -z "${AFTER:-}" ]]; then
AFTER="${SHA:-}"
fi
echo "Gate ctx: BEFORE=${BEFORE:-<empty>} AFTER=${AFTER:-<empty>} FORCE=${FORCE}"
# Produire une liste CHANGED fiable :
# - si BEFORE/AFTER valides -> git diff before..after
# - sinon fallback -> diff parent1..after ou show after
CHANGED=""
Z40="0000000000000000000000000000000000000000"
if [[ -n "${BEFORE:-}" && "${BEFORE}" != "${Z40}" ]] \
&& git cat-file -e "${BEFORE}^{commit}" 2>/dev/null \
&& git cat-file -e "${AFTER}^{commit}" 2>/dev/null; then
CHANGED="$(git diff --name-only "${BEFORE}" "${AFTER}" || true)"
else
P1="$(git rev-parse "${AFTER}^" 2>/dev/null || true)"
if [[ -n "${P1:-}" ]] && git cat-file -e "${P1}^{commit}" 2>/dev/null; then
CHANGED="$(git diff --name-only "${P1}" "${AFTER}" || true)"
else
CHANGED="$(git show --name-only --pretty="" "${AFTER}" | sed '/^$/d' || true)"
fi
fi
printf "%s\n" "${CHANGED}" > /tmp/changed.txt
echo "== changed files (first 200) =="
sed -n '1,200p' /tmp/changed.txt || true
# Flags
HAS_FULL=0
HAS_HOTPATCH=0
# FULL si build-impacting (ce que tu veux : content/anchors/pages/scripts)
if grep -qE '^(src/content/|src/anchors/|src/pages/|scripts/)' /tmp/changed.txt; then
HAS_FULL=1
fi
# HOTPATCH si annotations/media touchés
if grep -qE '^(src/annotations/|public/media/)' /tmp/changed.txt; then
HAS_HOTPATCH=1
fi
echo "Gate flags: HAS_FULL=${HAS_FULL} HAS_HOTPATCH=${HAS_HOTPATCH}"
# Décision
if [[ "${FORCE}" == "1" ]]; then
GO=1
MODE="full"
echo "✅ force=1 -> MODE=full (rebuild+restart)"
elif [[ "${HAS_FULL}" == "1" ]]; then
GO=1
MODE="full"
echo "✅ build-impacting change -> MODE=full (rebuild+restart)"
elif [[ "${HAS_HOTPATCH}" == "1" ]]; then
GO=1
MODE="hotpatch"
echo "✅ annotations/media change -> MODE=hotpatch"
else
GO=0
MODE="skip"
echo " no relevant change -> skip deploy"
fi
echo "GO=${GO}" >> /tmp/deploy.env
echo "MODE='${MODE}'" >> /tmp/deploy.env
- name: Toolchain sanity + resolve COMPOSE_PROJECT_NAME
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
# tools are prebaked in the image
git --version
docker version
docker compose version
python3 -c 'import yaml; print("PyYAML OK")'
# Reuse existing compose project name if containers already exist
PROJ="$(docker inspect archicratie-web-blue --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
if [[ -z "${PROJ:-}" ]]; then
PROJ="$(docker inspect archicratie-web-green --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
fi
if [[ -z "${PROJ:-}" ]]; then PROJ="archicratie-web"; fi
echo "COMPOSE_PROJECT_NAME='$PROJ'" >> /tmp/deploy.env
echo "✅ Using COMPOSE_PROJECT_NAME=$PROJ"
# Assert target containers exist (hotpatch needs them)
for c in archicratie-web-blue archicratie-web-green; do
docker inspect "$c" >/dev/null 2>&1 || { echo "❌ missing container $c"; exit 5; }
done
- name: Assert required vars (PUBLIC_GITEA_*) — only needed for MODE=full
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; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo " hotpatch mode -> vars not required"; 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: Assert deploy files exist — only needed for MODE=full
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo " hotpatch mode -> files not required"; exit 0; }
test -f docker-compose.yml
test -f Dockerfile
test -f nginx.conf
echo "✅ deploy files OK"
- name: FULL — Build + deploy staging (blue) then warmup+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; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo " MODE=$MODE -> skip full rebuild"; exit 0; }
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
wait_url() {
local url="$1"
local label="$2"
local tries="${3:-60}"
for i in $(seq 1 "$tries"); do
if curl -fsS --max-time 4 "$url" >/dev/null; then
echo "✅ $label OK ($url)"
return 0
fi
echo "… warmup $label ($i/$tries)"
sleep 1
done
echo "❌ timeout $label ($url)"
return 1
}
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
docker compose -p "$PROJ" -f docker-compose.yml build web_blue
docker rm -f archicratie-web-blue || true
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_blue
# warmup endpoints
wait_url "http://127.0.0.1:8081/para-index.json" "blue para-index"
wait_url "http://127.0.0.1:8081/annotations-index.json" "blue annotations-index"
wait_url "http://127.0.0.1:8081/pagefind/pagefind.js" "blue pagefind.js"
CANON="$(curl -fsS --max-time 6 "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"
docker logs --tail 120 archicratie-web-blue || true
exit 3
}
echo "✅ staging OK"
- name: FULL — Build + deploy live (green) then warmup+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; }
[[ "${MODE:-hotpatch}" == "full" ]] || { echo " MODE=$MODE -> skip full rebuild"; exit 0; }
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}"
wait_url() {
local url="$1"
local label="$2"
local tries="${3:-60}"
for i in $(seq 1 "$tries"); do
if curl -fsS --max-time 4 "$url" >/dev/null; then
echo "✅ $label OK ($url)"
return 0
fi
echo "… warmup $label ($i/$tries)"
sleep 1
done
echo "❌ timeout $label ($url)"
return 1
}
rollback() {
echo "⚠️ rollback green -> previous image tag (best effort)"
docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true
docker rm -f archicratie-web-green || true
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green || true
}
# build/restart green
if ! docker compose -p "$PROJ" -f docker-compose.yml build web_green; then
echo "❌ build green failed"; rollback; exit 4
fi
docker rm -f archicratie-web-green || true
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green
# warmup endpoints
if ! wait_url "http://127.0.0.1:8082/para-index.json" "green para-index"; then rollback; exit 4; fi
if ! wait_url "http://127.0.0.1:8082/annotations-index.json" "green annotations-index"; then rollback; exit 4; fi
if ! wait_url "http://127.0.0.1:8082/pagefind/pagefind.js" "green pagefind.js"; then rollback; exit 4; fi
CANON="$(curl -fsS --max-time 6 "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"
docker logs --tail 120 archicratie-web-green || true
rollback
exit 4
}
echo "✅ live OK"
- name: HOTPATCH — deep merge shards -> annotations-index + copy changed media into blue+green
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
python3 - <<'PY'
import os, re, json, glob
import yaml
import datetime as dt
ROOT = os.getcwd()
ANNO_ROOT = os.path.join(ROOT, "src", "annotations")
def is_obj(x): return isinstance(x, dict)
def is_arr(x): return isinstance(x, list)
def iso_dt(x):
if isinstance(x, dt.datetime):
if x.tzinfo is None:
return x.isoformat()
return x.astimezone(dt.timezone.utc).isoformat().replace("+00:00","Z")
if isinstance(x, dt.date):
return x.isoformat()
return None
def normalize(x):
s = iso_dt(x)
if s is not None: return s
if isinstance(x, dict):
return {str(k): normalize(v) for k, v in x.items()}
if isinstance(x, list):
return [normalize(v) for v in x]
return x
def key_media(it): return str((it or {}).get("src",""))
def key_ref(it):
it = it or {}
return "||".join([str(it.get("url","")), str(it.get("label","")), str(it.get("kind","")), str(it.get("citation",""))])
def key_comment(it): return str((it or {}).get("text","")).strip()
def dedup_extend(dst_list, src_list, key_fn):
seen = set(); out = []
for x in (dst_list or []):
x = normalize(x); k = key_fn(x)
if k and k not in seen: seen.add(k); out.append(x)
for x in (src_list or []):
x = normalize(x); k = key_fn(x)
if k and k not in seen: seen.add(k); out.append(x)
return out
def deep_merge(dst, src):
src = normalize(src)
for k, v in (src or {}).items():
if k in ("media","refs","comments_editorial") and is_arr(v):
if k == "media": dst[k] = dedup_extend(dst.get(k, []), v, key_media)
elif k == "refs": dst[k] = dedup_extend(dst.get(k, []), v, key_ref)
else: dst[k] = dedup_extend(dst.get(k, []), v, key_comment)
continue
if is_obj(v):
if not is_obj(dst.get(k)): dst[k] = {}
deep_merge(dst[k], v)
continue
if is_arr(v):
cur = dst.get(k, [])
if not is_arr(cur): cur = []
seen = set(); out = []
for x in cur:
x = normalize(x)
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
if s not in seen: seen.add(s); out.append(x)
for x in v:
x = normalize(x)
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
if s not in seen: seen.add(s); out.append(x)
dst[k] = out
continue
v = normalize(v)
if k not in dst or dst.get(k) in (None, ""):
dst[k] = v
def para_num(pid):
m = re.match(r"^p-(\d+)-", str(pid))
return int(m.group(1)) if m else 10**9
def sort_lists(entry):
for k in ("media","refs","comments_editorial"):
arr = entry.get(k)
if not is_arr(arr): continue
def ts(x):
x = normalize(x)
try:
s = str((x or {}).get("ts",""))
return dt.datetime.fromisoformat(s.replace("Z","+00:00")).timestamp() if s else 0
except Exception:
return 0
arr = [normalize(x) for x in arr]
arr.sort(key=lambda x: (ts(x), json.dumps(x, sort_keys=True, ensure_ascii=False)))
entry[k] = arr
if not os.path.isdir(ANNO_ROOT):
raise SystemExit(f"Missing annotations root: {ANNO_ROOT}")
pages = {}
errors = []
files = sorted(glob.glob(os.path.join(ANNO_ROOT, "**", "*.yml"), recursive=True))
for fp in files:
try:
with open(fp, "r", encoding="utf-8") as f:
doc = yaml.safe_load(f) or {}
doc = normalize(doc)
if not isinstance(doc, dict) or doc.get("schema") != 1:
continue
page = str(doc.get("page","")).strip().strip("/")
paras = doc.get("paras") or {}
if not page or not isinstance(paras, dict):
continue
pg = pages.setdefault(page, {"paras": {}})
for pid, entry in paras.items():
pid = str(pid)
if pid not in pg["paras"] or not isinstance(pg["paras"].get(pid), dict):
pg["paras"][pid] = {}
if isinstance(entry, dict):
deep_merge(pg["paras"][pid], entry)
sort_lists(pg["paras"][pid])
except Exception as e:
errors.append({"file": os.path.relpath(fp, ROOT), "error": str(e)})
for page, obj in pages.items():
keys = list((obj.get("paras") or {}).keys())
keys.sort(key=lambda k: (para_num(k), k))
obj["paras"] = {k: obj["paras"][k] for k in keys}
out = {
"schema": 1,
"generatedAt": dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc).isoformat().replace("+00:00","Z"),
"pages": pages,
"stats": {
"pages": len(pages),
"paras": sum(len(v.get("paras") or {}) for v in pages.values()),
"errors": len(errors),
},
"errors": errors,
}
with open("/tmp/annotations-index.json", "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False)
print("OK: wrote /tmp/annotations-index.json pages=", out["stats"]["pages"], "paras=", out["stats"]["paras"], "errors=", out["stats"]["errors"])
PY
# patch JSON into running containers
for c in archicratie-web-blue archicratie-web-green; do
echo "== patch annotations-index.json into $c =="
docker cp /tmp/annotations-index.json "${c}:/usr/share/nginx/html/annotations-index.json"
done
# copy changed media files into containers (so new media appears without rebuild)
if [[ -s /tmp/changed.txt ]]; then
while IFS= read -r f; do
[[ -n "$f" ]] || continue
if [[ "$f" == public/media/* ]]; then
dest="/usr/share/nginx/html/${f#public/}" # => /usr/share/nginx/html/media/...
for c in archicratie-web-blue archicratie-web-green; do
echo "== copy media into $c: $f -> $dest =="
docker exec "$c" sh -lc "mkdir -p \"$(dirname "$dest")\""
docker cp "$f" "$c:$dest"
done
fi
done < /tmp/changed.txt
fi
# smoke after patch
for p in 8081 8082; do
echo "== smoke annotations-index on $p =="
curl -fsS --max-time 6 "http://127.0.0.1:${p}/annotations-index.json" \
| python3 -c 'import sys,json; j=json.load(sys.stdin); print("generatedAt:", j.get("generatedAt")); print("pages:", len(j.get("pages") or {})); print("paras:", j.get("stats",{}).get("paras"))'
done
echo "✅ hotpatch done"
- name: Debug on failure (containers status/logs)
if: ${{ failure() }}
run: |
set -euo pipefail
echo "== docker ps =="
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | sed -n '1,80p' || true
for c in archicratie-web-blue archicratie-web-green; do
echo "== logs $c (tail 200) =="
docker logs --tail 200 "$c" || true
done

View File

@@ -0,0 +1,395 @@
name: Proposer Apply (PR)
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to apply (Proposer: correction/fact-check)"
required: true
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
concurrency:
group: proposer-apply-${{ github.event.issue.number || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
apply-proposer:
runs-on: mac-ci
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: 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/proposer.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/proposer.env
- name: Gate on label state/approved
run: |
set -euo pipefail
source /tmp/proposer.env
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
echo " label=$LABEL_NAME => skip"
echo "SKIP=1" >> /tmp/proposer.env
exit 0
fi
echo "✅ proceed (issue=$ISSUE_NUMBER)"
- name: Fetch issue + API-hard gate on (state/approved present + proposer type)
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/issue.json
node --input-type=module - <<'NODE' >> /tmp/proposer.env
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
const title = String(issue.title || "");
const body = String(issue.body || "").replace(/\r\n/g, "\n");
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name||"")).filter(Boolean) : [];
function pickLine(key) {
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = body.match(re);
return m ? m[1].trim() : "";
}
const typeRaw = pickLine("Type");
const type = String(typeRaw || "").trim().toLowerCase();
const hasApproved = labels.includes("state/approved");
const proposer = new Set(["type/correction","type/fact-check"]);
const out = [];
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
out.push(`HAS_APPROVED=${hasApproved ? "1":"0"}`);
if (!hasApproved) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("approved_not_present")}`);
} else if (!type) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
} else if (!proposer.has(type)) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("not_proposer:"+type)}`);
}
process.stdout.write(out.join("\n") + "\n");
NODE
echo "✅ proposer gating:"
grep -E '^(ISSUE_TYPE|HAS_APPROVED|SKIP|SKIP_REASON)=' /tmp/proposer.env || true
- name: Comment issue if skipped
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
[[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0
REASON="${SKIP_REASON:-}"
TYPE="${ISSUE_TYPE:-}"
if [[ "$REASON" == "approved_not_present" ]]; then
MSG=" Proposer Apply: skip — le label **state/approved** n'est pas présent sur le ticket au moment du run (gate API-hard)."
elif [[ "$REASON" == "missing_type" ]]; then
MSG=" Proposer Apply: skip — champ **Type:** manquant/illisible. Attendu: type/correction ou type/fact-check."
else
MSG=" Proposer Apply: skip — Type non-Proposer (${TYPE}). (Ce workflow ne traite que correction/fact-check.)"
fi
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" || true
- name: Checkout default branch
run: |
set -euo pipefail
source /tmp/proposer.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
echo "✅ workspace:"
ls -la | sed -n '1,120p'
- name: Detect app dir (repo-root vs ./site)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
APP_DIR="."
if [[ -d "site" && -f "site/package.json" ]]; then
APP_DIR="site"
fi
echo "APP_DIR=$APP_DIR" >> /tmp/proposer.env
echo "✅ APP_DIR=$APP_DIR"
ls -la "$APP_DIR" | sed -n '1,120p'
test -f "$APP_DIR/package.json" || { echo "❌ package.json missing in APP_DIR=$APP_DIR"; exit 1; }
test -d "$APP_DIR/scripts" || { echo "❌ scripts/ missing in APP_DIR=$APP_DIR"; exit 1; }
- name: NPM harden (reduce flakiness)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
cd "$APP_DIR"
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
- name: Install deps (APP_DIR)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
cd "$APP_DIR"
npm ci --no-audit --no-fund
- name: Build dist baseline (APP_DIR)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
cd "$APP_DIR"
npm run build
- name: Apply ticket (alias + commit) on bot branch
continue-on-error: true
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
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/proposer-${ISSUE_NUMBER}-${TS}"
echo "BRANCH=$BR" >> /tmp/proposer.env
git checkout -b "$BR"
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
export FORGE_BASE="$API_BASE"
LOG="/tmp/proposer-apply.log"
set +e
(cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE_NUMBER" --alias --commit) >"$LOG" 2>&1
RC=$?
set -e
echo "APPLY_RC=$RC" >> /tmp/proposer.env
echo "== apply log (tail) =="
tail -n 200 "$LOG" || true
END_SHA="$(git rev-parse HEAD)"
if [[ "$RC" -ne 0 ]]; then
echo "NOOP=0" >> /tmp/proposer.env
exit 0
fi
if [[ "$START_SHA" == "$END_SHA" ]]; then
echo "NOOP=1" >> /tmp/proposer.env
else
echo "NOOP=0" >> /tmp/proposer.env
echo "END_SHA=$END_SHA" >> /tmp/proposer.env
fi
- name: Push bot branch
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${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; }
[[ -n "${BRANCH:-}" ]] || { echo " BRANCH unset -> 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/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "0" ]] || exit 0
[[ -n "${BRANCH:-}" ]] || { echo " BRANCH unset -> skip PR"; exit 0; }
PR_TITLE="proposer: apply ticket #${ISSUE_NUMBER}"
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA:-unknown}\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 Proposer 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"
- name: Finalize (fail job if apply failed)
if: ${{ always() }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
RC="${APPLY_RC:-0}"
if [[ "$RC" != "0" ]]; then
echo "❌ apply failed (rc=$RC)"
exit "$RC"
fi
echo "✅ apply ok"

View File

@@ -3,7 +3,7 @@ on: [push, workflow_dispatch]
jobs:
smoke:
runs-on: ubuntu-latest
runs-on: mac-ci
steps:
- run: node -v && npm -v
- run: echo "runner OK"

7
.gitignore vendored
View File

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

View File

@@ -12,7 +12,7 @@ ENV npm_config_update_notifier=false \
# (Optionnel mais propre) git + certificats
RUN apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/*
# Déps dabord (cache Docker)
COPY package.json package-lock.json ./
@@ -25,9 +25,21 @@ COPY . .
ARG PUBLIC_GITEA_BASE
ARG PUBLIC_GITEA_OWNER
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 \
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)
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/
RUN find /usr/share/nginx/html -type d -exec chmod 755 {} \; \
&& 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 rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
const must = (name, fn) => {
if (typeof fn !== "function") {
throw new Error(`[astro.config] rehype plugin "${name}" is not a function (export default vs named?)`);
}
return fn;
};
/**
* Cast minimal pour satisfaire @ts-check sans dépendre de types internes Astro/Unified.
* @param {unknown} x
* @returns {any}
*/
const asAny = (x) => /** @type {any} */ (x);
/**
* @param {any} node
* @param {string} cls
* @returns {boolean}
*/
function hasClass(node, cls) {
const cn = node?.properties?.className;
if (Array.isArray(cn)) return cn.includes(cls);
if (typeof cn === "string") return cn.split(/\s+/).includes(cls);
return false;
}
/**
* Rehype plugin: retire les ids dupliqués en gardant en priorité:
* 1) span.details-anchor
* 2) h1..h6
* 3) sinon: premier rencontré
* @returns {(tree: any) => void}
*/
function rehypeDedupeIds() {
/** @param {any} tree */
return (tree) => {
/** @type {Map<string, Array<{node:any, pref:number, idx:number}>>} */
const occ = new Map();
let idx = 0;
/** @param {any} node */
const walk = (node) => {
if (!node || typeof node !== "object") return;
if (node.type === "element") {
const id = node.properties?.id;
if (typeof id === "string" && id) {
let pref = 2;
if (node.tagName === "span" && hasClass(node, "details-anchor")) pref = 0;
else if (/^h[1-6]$/.test(String(node.tagName || ""))) pref = 1;
const arr = occ.get(id) || [];
arr.push({ node, pref, idx: idx++ });
occ.set(id, arr);
}
const children = node.children;
if (Array.isArray(children)) for (const c of children) walk(c);
} else if (Array.isArray(node.children)) {
for (const c of node.children) walk(c);
}
};
walk(tree);
for (const [id, items] of occ.entries()) {
if (items.length <= 1) continue;
items.sort((a, b) => (a.pref - b.pref) || (a.idx - b.idx));
const keep = items[0];
for (let i = 1; i < items.length; i++) {
const n = items[i].node;
if (n?.properties?.id === id) delete n.properties.id;
}
// safety: on s'assure qu'un seul garde bien l'id
if (keep?.node?.properties) keep.node.properties.id = id;
}
};
}
export default defineConfig({
output: "static",
trailingSlash: "always",
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
integrations: [
mdx(),
// Important: MDX hérite du pipeline markdown (ids p-… + autres plugins)
mdx({ extendMarkdownConfig: true }),
sitemap({
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"),
}),
],
// ✅ Plugins appliqués AU MDX
mdx: {
// ✅ MDX hérite déjà de markdown.rehypePlugins
// donc ici on ne met QUE le spécifique MDX
rehypePlugins: [
must("rehype-details-sections", rehypeDetailsSections),
],
},
// ✅ Plugins appliqués au Markdown non-MDX
markdown: {
rehypePlugins: [
must("rehype-slug", rehypeSlug),
[must("rehype-autolink-headings", rehypeAutolinkHeadings), { behavior: "append" }],
must("rehype-paragraph-ids", rehypeParagraphIds),
asAny(rehypeSlug),
[asAny(rehypeAutolinkHeadings), { behavior: "append" }],
asAny(rehypeDetailsSections),
asAny(rehypeParagraphIds),
asAny(rehypeDedupeIds),
],
},
});

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

View File

@@ -1,6 +1,3 @@
# `docs/CONFIG-ENV.md`
```md
# CONFIG-ENV — variables, priorités, injection build
## 0) Ce que la prod doit garantir
@@ -13,6 +10,15 @@ Si un seul de ces 3 paramètres est faux → on obtient :
- 404 / redirect login inattendu
- ou un repo/owner incorrect
# Diagnostic — “Proposer” (résumé)
**Symptôme :** clic “Proposer” → 404 / login / mauvais repo
**Cause la plus fréquente :** `PUBLIC_GITEA_OWNER` (casse sensible) ou `PUBLIC_GITEA_REPO` faux.
➡️ Procédure complète (pas-à-pas + commandes) : voir `docs/TROUBLESHOOTING.md#proposer-404`.
## 1) Variables utilisées (publique, côté build Astro)
- `PUBLIC_GITEA_BASE`

View File

@@ -1,5 +1,15 @@
# 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
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
done
En cas déchec :
- 404 / login loop / mauvais repo → `docs/TROUBLESHOOTING.md#proposer-404`
- double onglet → `docs/TROUBLESHOOTING.md#proposer-double-onglet`
## Diagnostic — “Proposer” (résumé)
**Symptôme :** clic “Proposer” → 404 / login / mauvais repo
**Cause la plus fréquente :** `PUBLIC_GITEA_OWNER` (casse sensible) ou `PUBLIC_GITEA_REPO` faux.
➡️ Procédure complète (pas-à-pas + commandes) : voir `docs/TROUBLESHOOTING.md#proposer-404`.
## 5) Reverse Proxy DSM (le point clé)
### DSM 7.3 :

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

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

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

View File

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

View File

@@ -4,7 +4,7 @@ Document “pivot” : liens, invariants, conventions, commandes réflexes.
## 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.
- **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).

View File

@@ -1,5 +1,15 @@
# OPS Runbook — Archicratie Web (NAS Synology DS220 + Gitea)
> 🟦 **ALIAS (résumé)** — Runbook 1-page pour opérer vite sans se tromper.
> La procédure détaillée **canonique** est : docs/DEPLOY_PROD_SYNOLOGY_DS220.md.
> Source Git : Gitea/main ; déploiement = rebuild depuis main ; pas de hotfix non versionné.
> Déploiement : build sur slot inactif → smoke → bascule DSM → rollback si besoin.
> Incidents connus : voir docs/TROUBLESHOOTING.md.
## 0. Objectif
Ce document décrit la procédure **exacte** pour :
- maintenir un état cohérent entre **Local (Mac Studio)**, **Gitea**, **NAS (prod)** ;

View File

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

View File

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

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)

217
docs/TROUBLESHOOTING.md Normal file
View File

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

View File

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

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

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

View File

@@ -0,0 +1,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",
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"astro": "^5.16.11"
"astro": "^5.17.3"
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.0",
@@ -1905,9 +1905,9 @@
}
},
"node_modules/astro": {
"version": "5.16.11",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz",
"integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==",
"version": "5.17.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
"integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.13.0",
@@ -1933,7 +1933,7 @@
"dlv": "^1.1.3",
"dset": "^3.1.4",
"es-module-lexer": "^1.7.0",
"esbuild": "^0.25.0",
"esbuild": "^0.27.3",
"estree-walker": "^3.0.3",
"flattie": "^1.1.1",
"fontace": "~0.4.0",
@@ -1954,16 +1954,16 @@
"prompts": "^2.4.2",
"rehype": "^13.0.2",
"semver": "^7.7.3",
"shiki": "^3.20.0",
"shiki": "^3.21.0",
"smol-toml": "^1.6.0",
"svgo": "^4.0.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tsconfck": "^3.1.6",
"ultrahtml": "^1.6.0",
"unifont": "~0.7.1",
"unifont": "~0.7.3",
"unist-util-visit": "^5.0.0",
"unstorage": "^1.17.3",
"unstorage": "^1.17.4",
"vfile": "^6.0.3",
"vite": "^6.4.1",
"vitefu": "^1.1.1",
@@ -1990,6 +1990,463 @@
"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": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",

View File

@@ -4,32 +4,29 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"dev": "node scripts/write-dev-whoami.mjs && astro dev",
"preview": "astro preview",
"astro": "astro",
"clean": "rm -rf dist",
"build": "astro build",
"build:clean": "npm run clean && npm run build",
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && node scripts/build-para-index.mjs && node scripts/build-annotations-index.mjs && node scripts/purge-dist-dev-whoami.mjs && npx pagefind --site dist",
"import": "node scripts/import-docx.mjs",
"apply:ticket": "node scripts/apply-ticket.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:anchors": "node scripts/check-anchors.mjs",
"test:anchors:update": "node scripts/check-anchors.mjs --update",
"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": "node scripts/check-annotations.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"
},
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"astro": "^5.16.11"
"astro": "^5.17.3"
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

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,899 @@
#!/usr/bin/env node
// scripts/apply-annotation-ticket.mjs
//
// Applique un ticket Gitea "type/media | type/reference | type/comment" vers:
//
// ✅ src/annotations/<oeuvre>/<chapitre>/<paraId>.yml (sharding par paragraphe)
// ✅ public/media/<oeuvre>/<chapitre>/<paraId>/<file>
//
// Compat rétro : lit (si présent) l'ancien monolithe:
// src/annotations/<oeuvre>/<chapitre>.yml
// et deep-merge NON destructif dans le shard lors d'une nouvelle application,
// pour permettre une migration progressive sans perte.
//
// Robuste, idempotent, non destructif.
// DRY RUN 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/ (shard par paragraphe)
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 : vérifie que (page, ancre) existent (dist/para-index.json si dispo, sinon baseline)
--strict : refuse si URL ref invalide (http/https) OU caption media vide OU verify impossible
--commit : git add + git commit (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)
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(/\/+$/, "");
/* --------------------------------- helpers -------------------------------- */
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;
}
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) {
// ex: /archicrat-ia/chapitre-4/ => archicrat-ia/chapitre-4
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);
}
function paraIndexFromId(id) {
const m = String(id).match(/^p-(\d+)-/i);
return m ? Number(m[1]) : Number.NaN;
}
function isHttpUrl(u) {
try {
const x = new URL(String(u));
return x.protocol === "http:" || x.protocol === "https:";
} catch {
return false;
}
}
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 normPage(s) {
let x = String(s || "").trim();
if (!x) return "";
// retire origin si on a une URL complète
x = x.replace(/^https?:\/\/[^/]+/i, "");
// enlève query/hash
x = x.split("#")[0].split("?")[0];
// enlève index.html
x = x.replace(/index\.html$/i, "");
// enlève slashs de bord
x = x.replace(/^\/+/, "").replace(/\/+$/, "");
return x;
}
/* ------------------------------ para-index (verify + order) ------------------------------ */
async function loadParaOrderFromDist(pageKey) {
const distIdx = path.join(CWD, "dist", "para-index.json");
if (!(await exists(distIdx))) return null;
let j;
try {
j = JSON.parse(await fs.readFile(distIdx, "utf8"));
} catch {
return null;
}
const want = normPage(pageKey);
// Support A) { items:[{id,page,...}, ...] } (ou variantes)
const items = Array.isArray(j?.items)
? j.items
: Array.isArray(j?.index?.items)
? j.index.items
: null;
if (items) {
const ids = [];
for (const it of items) {
// page peut être dans plein de clés différentes
const pageCand = normPage(
it?.page ??
it?.pageKey ??
it?.path ??
it?.route ??
it?.href ??
it?.url ??
""
);
// id peut être dans plein de clés différentes
let id = String(it?.id ?? it?.paraId ?? it?.anchorId ?? it?.anchor ?? "");
if (id.startsWith("#")) id = id.slice(1);
if (pageCand === want && id) ids.push(id);
}
if (ids.length) return ids;
}
// Support B) { byId: { "p-...": { page:"...", ... }, ... } }
if (j?.byId && typeof j.byId === "object") {
const ids = Object.keys(j.byId)
.filter((id) => {
const meta = j.byId[id] || {};
const pageCand = normPage(meta.page ?? meta.pageKey ?? meta.path ?? meta.route ?? meta.url ?? "");
return pageCand === want;
});
if (ids.length) {
ids.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));
});
return ids;
}
}
// Support C) { pages: { "archicrat-ia/chapitre-4": { ids:[...] } } } (ou variantes)
if (j?.pages && typeof j.pages === "object") {
// essaie de trouver la bonne clé même si elle est /.../ ou .../index.html
const keys = Object.keys(j.pages);
const hit = keys.find((k) => normPage(k) === want);
if (hit) {
const pg = j.pages[hit];
if (Array.isArray(pg?.ids)) return pg.ids.map(String);
if (Array.isArray(pg?.paras)) return pg.paras.map(String);
}
}
return null;
}
async function tryVerifyAnchor(pageKey, anchorId) {
// 1) dist/para-index.json : order complet si possible
const order = await loadParaOrderFromDist(pageKey);
if (order) return order.includes(anchorId);
// 1bis) dist/para-index.json : fallback “best effort” => recherche brute (IDs quasi uniques)
const distIdx = path.join(CWD, "dist", "para-index.json");
if (await exists(distIdx)) {
try {
const raw = await fs.readFile(distIdx, "utf8");
if (raw.includes(`"${anchorId}"`) || raw.includes(`"#${anchorId}"`)) {
return true;
}
} catch {
// ignore
}
}
// 2) tests/anchors-baseline.json (fallback)
const base = path.join(CWD, "tests", "anchors-baseline.json");
if (await exists(base)) {
try {
const j = JSON.parse(await fs.readFile(base, "utf8"));
const candidates = [];
if (j?.pages && typeof j.pages === "object") {
for (const [k, v] of Object.entries(j.pages)) {
if (!Array.isArray(v)) continue;
if (normPage(k).includes(normPage(pageKey))) candidates.push(...v);
}
}
if (Array.isArray(j?.entries)) {
for (const it of j.entries) {
const p = String(it?.page || "");
const ids = it?.ids;
if (Array.isArray(ids) && normPage(p).includes(normPage(pageKey))) candidates.push(...ids);
}
}
if (candidates.length) return candidates.some((x) => String(x) === anchorId);
} catch {
// ignore
}
}
return null; // cannot verify
}
/* ----------------------------- deep merge helpers (non destructive) ----------------------------- */
function keyMedia(x) {
return String(x?.src || "");
}
function keyRef(x) {
return `${x?.url || ""}||${x?.label || ""}||${x?.kind || ""}||${x?.citation || ""}`;
}
function keyComment(x) {
return String(x?.text || "").trim();
}
function uniqUnion(dstArr, srcArr, keyFn) {
const out = Array.isArray(dstArr) ? [...dstArr] : [];
const seen = new Set(out.map((x) => keyFn(x)));
for (const it of (Array.isArray(srcArr) ? srcArr : [])) {
const k = keyFn(it);
if (!k) continue;
if (!seen.has(k)) {
seen.add(k);
out.push(it);
}
}
return out;
}
function deepMergeEntry(dst, src) {
if (!isPlainObject(dst) || !isPlainObject(src)) return;
for (const [k, v] of Object.entries(src)) {
if (k === "media" && Array.isArray(v)) {
dst.media = uniqUnion(dst.media, v, keyMedia);
continue;
}
if (k === "refs" && Array.isArray(v)) {
dst.refs = uniqUnion(dst.refs, v, keyRef);
continue;
}
if (k === "comments_editorial" && Array.isArray(v)) {
dst.comments_editorial = uniqUnion(dst.comments_editorial, v, keyComment);
continue;
}
if (isPlainObject(v)) {
if (!isPlainObject(dst[k])) dst[k] = {};
deepMergeEntry(dst[k], v);
continue;
}
if (Array.isArray(v)) {
const cur = Array.isArray(dst[k]) ? dst[k] : [];
const seen = new Set(cur.map((x) => JSON.stringify(x)));
const out = [...cur];
for (const it of v) {
const s = JSON.stringify(it);
if (!seen.has(s)) {
seen.add(s);
out.push(it);
}
}
dst[k] = out;
continue;
}
// scalar: set only if missing/empty
if (!(k in dst) || dst[k] == null || dst[k] === "") {
dst[k] = v;
}
}
}
/* ----------------------------- annotations I/O ----------------------------- */
async function loadAnnoDocYaml(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`, 2);
assert(doc.schema === 1, `${path.relative(CWD, fileAbs)}: schema must be 1`, 2);
assert(isPlainObject(doc.paras), `${path.relative(CWD, fileAbs)}: missing object key "paras"`, 2);
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}")`, 2);
} else {
doc.page = pageKey;
}
return doc;
}
function sortParasObject(paras, order) {
const keys = Object.keys(paras || {});
const idx = new Map();
if (Array.isArray(order)) order.forEach((id, i) => idx.set(String(id), i));
keys.sort((a, b) => {
const ha = idx.has(a);
const hb = idx.has(b);
if (ha && hb) return idx.get(a) - idx.get(b);
if (ha && !hb) return -1;
if (!ha && hb) return 1;
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, order = null) {
await fs.mkdir(path.dirname(fileAbs), { recursive: true });
doc.paras = sortParasObject(doc.paras, order);
for (const e of Object.values(doc.paras || {})) {
if (!isPlainObject(e)) continue;
stableSortByTs(e.media);
stableSortByTs(e.refs);
stableSortByTs(e.comments_editorial);
}
const out = YAML.stringify(doc);
await fs.writeFile(fileAbs, out, "utf8");
}
/* ------------------------------ 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}`);
}
}
/* ------------------------------ media helpers ------------------------------ */
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);
}
async function downloadToFile(url, token, destAbs) {
const res = await fetch(url, {
headers: {
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;
}
/* ------------------------------ type parsers ------------------------------ */
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 || "",
};
}
/* ----------------------------------- 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);
const paraOrder = DO_VERIFY ? await loadParaOrderFromDist(pageKey) : null;
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) {
if (STRICT) {
throw Object.assign(
new Error(`Ticket verify (strict): impossible de vérifier (pas de dist/para-index.json ou baseline)`),
{ __exitCode: 2 }
);
}
console.warn("⚠️ verify: impossible de vérifier (pas de dist/para-index.json ou baseline) — on continue.");
}
}
// ✅ shard path: src/annotations/<pageKey>/<paraId>.yml
const shardAbs = path.join(ANNO_DIR, ...pageKey.split("/"), `${ancre}.yml`);
const shardRel = path.relative(CWD, shardAbs).replace(/\\/g, "/");
// legacy monolith: src/annotations/<pageKey>.yml (read-only, for migration)
const legacyAbs = path.join(ANNO_DIR, `${pageKey}.yml`);
console.log("✅ Parsed:", { type, chemin, ancre: `#${ancre}`, pageKey, annoFile: shardRel });
// load shard doc
const doc = await loadAnnoDocYaml(shardAbs, pageKey);
if (!isPlainObject(doc.paras[ancre])) doc.paras[ancre] = {};
const entry = doc.paras[ancre];
// merge legacy entry into shard in-memory (non destructive) to keep compat + enable progressive migration
if (await exists(legacyAbs)) {
try {
const legacy = await loadAnnoDocYaml(legacyAbs, pageKey);
const legacyEntry = legacy?.paras?.[ancre];
if (isPlainObject(legacyEntry)) {
deepMergeEntry(entry, legacyEntry);
}
} catch {
// ignore legacy parse issues; shard still applies new data
}
}
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 before = entry.comments_editorial.length;
entry.comments_editorial = uniqUnion(entry.comments_editorial, [item], keyComment);
if (entry.comments_editorial.length !== before) {
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 before = entry.refs.length;
entry.refs = uniqUnion(entry.refs, [item], keyRef);
if (entry.refs.length !== before) {
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 caption = (title || "").trim();
if (STRICT && !caption) {
throw Object.assign(new Error("Ticket media (strict): caption vide (titre de ticket requis)."), { __exitCode: 2 });
}
const captionFinal = caption || ".";
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; }
const mediaDirAbs = path.join(PUBLIC_DIR, "media", ...pageKey.split("/"), 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, "/"));
changed = true;
} else {
notes.push(`(dry) would download ${name} -> ${urlPath}`);
changed = true;
}
const item = {
type: inferMediaTypeFromFilename(name),
src: urlPath,
caption: captionFinal,
credit: "",
ts: nowIso,
fromIssue: issueNum,
};
const before = entry.media.length;
entry.media = uniqUnion(entry.media, [item], keyMedia);
if (entry.media.length !== before) 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: ${shardRel}`);
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(shardAbs, doc, paraOrder);
touchedFiles.unshift(shardRel);
console.log(`✅ Updated: ${shardRel}`);
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,246 @@
#!/usr/bin/env node
// scripts/build-annotations-index.mjs
// Construit dist/annotations-index.json à partir de src/annotations/**/*.yml
// Supporte:
// - monolith : src/annotations/<pageKey>.yml
// - shard : src/annotations/<pageKey>/<paraId>.yml (paraId = p-<n>-...)
// Invariants:
// - doc.schema === 1
// - doc.page (si présent) == pageKey déduit du chemin
// - shard: doc.paras doit contenir EXACTEMENT la clé paraId (sinon fail)
//
// Deep-merge non destructif (media/refs/comments dédupliqués), tri stable.
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
const ROOT = process.cwd();
const ANNO_ROOT = path.join(ROOT, "src", "annotations");
const DIST_DIR = path.join(ROOT, "dist");
const OUT = path.join(DIST_DIR, "annotations-index.json");
function assert(cond, msg) {
if (!cond) throw new Error(msg);
}
function isObj(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function isArr(x) {
return Array.isArray(x);
}
function normPath(s) {
return String(s || "")
.replace(/\\/g, "/")
.replace(/^\/+|\/+$/g, "");
}
function paraNum(pid) {
const m = String(pid).match(/^p-(\d+)-/i);
return m ? Number(m[1]) : Number.POSITIVE_INFINITY;
}
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 keyMedia(x) { return String(x?.src || ""); }
function keyRef(x) {
return `${x?.url || ""}||${x?.label || ""}||${x?.kind || ""}||${x?.citation || ""}`;
}
function keyComment(x) { return String(x?.text || "").trim(); }
function uniqUnion(dst, src, keyFn) {
const out = isArr(dst) ? [...dst] : [];
const seen = new Set(out.map((x) => keyFn(x)));
for (const it of (isArr(src) ? src : [])) {
const k = keyFn(it);
if (!k) continue;
if (!seen.has(k)) {
seen.add(k);
out.push(it);
}
}
return out;
}
function deepMergeEntry(dst, src) {
if (!isObj(dst) || !isObj(src)) return;
for (const [k, v] of Object.entries(src)) {
if (k === "media" && isArr(v)) { dst.media = uniqUnion(dst.media, v, keyMedia); continue; }
if (k === "refs" && isArr(v)) { dst.refs = uniqUnion(dst.refs, v, keyRef); continue; }
if (k === "comments_editorial" && isArr(v)) { dst.comments_editorial = uniqUnion(dst.comments_editorial, v, keyComment); continue; }
if (isObj(v)) {
if (!isObj(dst[k])) dst[k] = {};
deepMergeEntry(dst[k], v);
continue;
}
if (isArr(v)) {
const cur = isArr(dst[k]) ? dst[k] : [];
const seen = new Set(cur.map((x) => JSON.stringify(x)));
const out = [...cur];
for (const it of v) {
const s = JSON.stringify(it);
if (!seen.has(s)) { seen.add(s); out.push(it); }
}
dst[k] = out;
continue;
}
// scalar: set only if missing/empty
if (!(k in dst) || dst[k] == null || dst[k] === "") dst[k] = v;
}
}
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 if (e.isFile() && /\.ya?ml$/i.test(e.name)) out.push(p);
}
return out;
}
function inferExpectedFromRel(relNoExt) {
const parts = relNoExt.split("/").filter(Boolean);
const last = parts.at(-1) || "";
const isShard = parts.length > 1 && /^p-\d+-/i.test(last); // ✅ durcissement
const pageKey = isShard ? parts.slice(0, -1).join("/") : relNoExt;
const paraId = isShard ? last : null;
return { isShard, pageKey, paraId };
}
function validateAndNormalizeDoc(doc, relFile, expectedPageKey, expectedParaId) {
assert(isObj(doc), `${relFile}: doc must be an object`);
assert(doc.schema === 1, `${relFile}: schema must be 1`);
assert(isObj(doc.paras), `${relFile}: missing object key "paras"`);
const gotPage = doc.page != null ? normPath(doc.page) : "";
const expPage = normPath(expectedPageKey);
if (gotPage) {
assert(
gotPage === expPage,
`${relFile}: page mismatch (page="${doc.page}" vs path="${expectedPageKey}")`
);
} else {
doc.page = expPage;
}
if (expectedParaId) {
const keys = Object.keys(doc.paras || {}).map(String);
assert(
keys.includes(expectedParaId),
`${relFile}: shard mismatch: must contain paras["${expectedParaId}"]`
);
assert(
keys.length === 1 && keys[0] === expectedParaId,
`${relFile}: shard invariant violated: shard file must contain ONLY paras["${expectedParaId}"] (got: ${keys.join(", ")})`
);
}
return doc;
}
async function main() {
const pages = {};
const errors = [];
await fs.mkdir(DIST_DIR, { recursive: true });
const files = await walk(ANNO_ROOT);
for (const fp of files) {
const rel = normPath(path.relative(ANNO_ROOT, fp));
const relNoExt = rel.replace(/\.ya?ml$/i, "");
const { isShard, pageKey, paraId } = inferExpectedFromRel(relNoExt);
try {
const raw = await fs.readFile(fp, "utf8");
const doc = YAML.parse(raw) || {};
if (!isObj(doc) || doc.schema !== 1) continue;
validateAndNormalizeDoc(
doc,
`src/annotations/${rel}`,
pageKey,
isShard ? paraId : null
);
const pg = (pages[pageKey] ??= { paras: {} });
if (isShard) {
const entry = doc.paras[paraId];
if (!isObj(pg.paras[paraId])) pg.paras[paraId] = {};
if (isObj(entry)) deepMergeEntry(pg.paras[paraId], entry);
stableSortByTs(pg.paras[paraId].media);
stableSortByTs(pg.paras[paraId].refs);
stableSortByTs(pg.paras[paraId].comments_editorial);
} else {
for (const [pid, entry] of Object.entries(doc.paras || {})) {
const p = String(pid);
if (!isObj(pg.paras[p])) pg.paras[p] = {};
if (isObj(entry)) deepMergeEntry(pg.paras[p], entry);
stableSortByTs(pg.paras[p].media);
stableSortByTs(pg.paras[p].refs);
stableSortByTs(pg.paras[p].comments_editorial);
}
}
} catch (e) {
errors.push({ file: `src/annotations/${rel}`, error: String(e?.message || e) });
}
}
for (const [pageKey, pg] of Object.entries(pages)) {
const keys = Object.keys(pg.paras || {});
keys.sort((a, b) => {
const ia = paraNum(a);
const ib = paraNum(b);
if (Number.isFinite(ia) && Number.isFinite(ib) && ia !== ib) return ia - ib;
return String(a).localeCompare(String(b));
});
const next = {};
for (const k of keys) next[k] = pg.paras[k];
pg.paras = next;
}
const out = {
schema: 1,
generatedAt: new Date().toISOString(),
pages,
stats: {
pages: Object.keys(pages).length,
paras: Object.values(pages).reduce((n, p) => n + Object.keys(p.paras || {}).length, 0),
errors: errors.length,
},
errors,
};
if (errors.length) {
throw new Error(`${errors[0].file}: ${errors[0].error}`);
}
await fs.writeFile(OUT, JSON.stringify(out), "utf8");
console.log(`✅ annotations-index: pages=${out.stats.pages} paras=${out.stats.paras} -> dist/annotations-index.json`);
}
main().catch((e) => {
console.error(`FAIL: build-annotations-index crashed: ${e?.stack || e?.message || 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,104 @@
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 = [];
// Optim: éviter de vérifier 100 fois le même fichier media
const seenMedia = new Set(); // src string
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
// dédupe
if (seenMedia.has(src)) continue;
seenMedia.add(src);
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,224 @@
// 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 normalizePageKey(s) {
return String(s || "").replace(/^\/+/, "").replace(/\/+$/, "");
}
function isPlainObject(x) {
return !!x && typeof x === "object" && !Array.isArray(x);
}
function isParaId(s) {
return /^p-\d+-/i.test(String(s || ""));
}
/**
* Supporte:
* - monolith: src/annotations/<pageKey>.yml -> pageKey = rel sans ext
* - shard : src/annotations/<pageKey>/<paraId>.yml -> pageKey = dirname(rel), paraId = basename
*
* shard seulement si le fichier est dans un sous-dossier (anti cas pathologique).
*/
function inferFromFile(fileAbs) {
const rel = path.relative(ANNO_DIR, fileAbs).replace(/\\/g, "/");
const relNoExt = rel.replace(/\.(ya?ml|json)$/i, "");
const parts = relNoExt.split("/").filter(Boolean);
const base = parts[parts.length - 1] || "";
const dirParts = parts.slice(0, -1);
const isShard = dirParts.length > 0 && isParaId(base);
const pageKey = isShard ? dirParts.join("/") : relNoExt;
const paraId = isShard ? base : "";
return { pageKey: normalizePageKey(pageKey), paraId };
}
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));
// perf: cache HTML par page (shards = beaucoup de fichiers pour 1 page)
const htmlCache = new Map(); // pageKey -> html
const missingDistPage = new Set(); // pageKey
let pagesSeen = new Set();
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, paraId: shardParaId } = inferFromFile(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;
}
// shard invariant (fort) : doit contenir paras[paraId]
if (shardParaId) {
if (!Object.prototype.hasOwnProperty.call(doc.paras, shardParaId)) {
failures++;
notes.push(`- SHARD MISMATCH: ${rel} (expected paras["${shardParaId}"] present)`);
continue;
}
// si extras -> warning (non destructif)
const keys = Object.keys(doc.paras);
if (!(keys.length === 1 && keys[0] === shardParaId)) {
notes.push(`- WARN shard has extra paras: ${rel} (expected only "${shardParaId}", got ${keys.join(", ")})`);
}
}
pagesSeen.add(pageKey);
const distFile = path.join(DIST_DIR, pageKey, "index.html");
if (!(await exists(distFile))) {
if (!missingDistPage.has(pageKey)) {
missingDistPage.add(pageKey);
failures++;
notes.push(`- MISSING PAGE: dist/${pageKey}/index.html (from ${rel})`);
} else {
notes.push(`- WARN missing page already reported: dist/${pageKey}/index.html (from ${rel})`);
}
continue;
}
let html = htmlCache.get(pageKey);
if (!html) {
html = await fs.readFile(distFile, "utf8");
htmlCache.set(pageKey, html);
}
for (const paraId of Object.keys(doc.paras)) {
checked++;
if (!isParaId(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"));
const pages = pagesSeen.size;
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

@@ -114,7 +114,6 @@ async function runMammoth(docxPath, assetsOutDirWebRoot) {
);
let html = result.value || "";
// Mammoth gives relative src="image-xx.png" ; we will prefix later
return html;
}
@@ -182,6 +181,25 @@ async function exists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
/**
* ✅ compat:
* - ancien : collection="archicratie" + slug="archicrat-ia/chapitre-3"
* - nouveau : collection="archicrat-ia" + slug="chapitre-3"
*
* But : toujours écrire dans src/content/archicrat-ia/<slugSansPrefix>.mdx
*/
function normalizeDest(collection, slug) {
let outCollection = String(collection || "").trim();
let outSlug = String(slug || "").trim().replace(/^\/+|\/+$/g, "");
if (outCollection === "archicratie" && outSlug.startsWith("archicrat-ia/")) {
outCollection = "archicrat-ia";
outSlug = outSlug.replace(/^archicrat-ia\//, "");
}
return { outCollection, outSlug };
}
async function main() {
const args = parseArgs(process.argv);
const manifestPath = path.resolve(args.manifest);
@@ -203,11 +221,14 @@ async function main() {
for (const it of selected) {
const docxPath = path.resolve(it.source);
const outFile = path.resolve("src/content", it.collection, `${it.slug}.mdx`);
const { outCollection, outSlug } = normalizeDest(it.collection, it.slug);
const outFile = path.resolve("src/content", outCollection, `${outSlug}.mdx`);
const outDir = path.dirname(outFile);
const assetsPublicDir = path.posix.join("/imported", it.collection, it.slug);
const assetsDiskDir = path.resolve("public", "imported", it.collection, it.slug);
const assetsPublicDir = path.posix.join("/imported", outCollection, outSlug);
const assetsDiskDir = path.resolve("public", "imported", outCollection, outSlug);
if (!(await exists(docxPath))) {
throw new Error(`Missing source docx: ${docxPath}`);
@@ -241,18 +262,20 @@ async function main() {
html = rewriteLocalImageLinks(html, assetsPublicDir);
body = html.trim() ? html : "<p>(Import vide)</p>";
}
const defaultVersion = process.env.PUBLIC_RELEASE || "0.1.0";
// ✅ IMPORTANT: archicrat-ia partage edition/status avec archicratie (pas de migration frontmatter)
const schemaDefaultsByCollection = {
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
ia: { edition: "ia", status: "cas_pratique", level: 1 },
traite: { edition: "traite", status: "ontodynamique", level: 1 },
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
atlas: { edition: "atlas", status: "atlas", level: 1 },
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
"archicrat-ia": { edition: "archicrat-ia", status: "essai_these", level: 1 },
ia: { edition: "ia", status: "cas_pratique", level: 1 },
traite: { edition: "traite", status: "ontodynamique", level: 1 },
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
atlas: { edition: "atlas", status: "atlas", level: 1 },
};
const defaults = schemaDefaultsByCollection[it.collection] || { edition: it.collection, status: "draft", level: 1 };
const defaults = schemaDefaultsByCollection[outCollection] || { edition: outCollection, status: "draft", level: 1 };
const fm = [
"---",
@@ -282,4 +305,4 @@ async function main() {
main().catch((e) => {
console.error("\nERROR:", e?.message || e);
process.exit(1);
});
});

View File

@@ -14,6 +14,24 @@ const STRICT = argv.includes("--strict") || process.env.CI === "1" || process.en
function escRe(s) {
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) {
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "gi");
let c = 0;
@@ -22,7 +40,6 @@ function countIdAttr(html, id) {
}
function findStartTagWithId(html, id) {
// 1er élément qui porte id="..."
const re = new RegExp(
`<([a-zA-Z0-9:-]+)\\b[^>]*\\bid=(["'])${escRe(id)}\\2[^>]*>`,
"i"
@@ -36,34 +53,10 @@ function isInjectedAliasSpan(html, id) {
const found = findStartTagWithId(html, id);
if (!found) return false;
if (found.tagName !== "span") return false;
// class="... para-alias ..."
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) {
// insère juste avant la balise qui porte id="newId"
const re = new RegExp(
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
"i"
@@ -82,6 +75,7 @@ async function main() {
}
const raw = await fs.readFile(ALIASES_PATH, "utf-8");
/** @type {Record<string, Record<string,string>>} */
let aliases;
try {
@@ -89,6 +83,7 @@ async function main() {
} catch (e) {
throw new Error(`JSON invalide: ${ALIASES_PATH} (${e?.message || e})`);
}
if (!aliases || typeof aliases !== "object" || Array.isArray(aliases)) {
throw new Error(`Format invalide: attendu { route: { oldId: newId } } dans ${ALIASES_PATH}`);
}
@@ -114,10 +109,10 @@ async function main() {
console.log(msg);
warnCount++;
}
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");
if (!(await exists(htmlPath))) {
@@ -135,24 +130,8 @@ async function main() {
if (!oldId || !newId) continue;
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">)
// => 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
// ✅ déjà injecté => idempotent
if (oldCount > 0 && isInjectedAliasSpan(html, oldId)) {
if (STRICT && oldCount !== 1) {
throw new Error(`oldId dupliqué (${oldCount}) alors qu'il est censé être unique: ${route} id=${oldId}`);
@@ -160,18 +139,23 @@ async function main() {
continue;
}
// avant l'injection, après hasId(newId)
const newCount = countIdAttr(html, newId);
if (newCount !== 1) {
const msg = `⚠️ newId non-unique (${newCount}) : ${route} new=${newId} (injection ambiguë)`;
// ⛔️ oldId existe déjà "en vrai" => alias inutile/inversé
if (oldCount > 0) {
const found = findStartTagWithId(html, oldId);
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);
console.log(msg);
warnCount++;
continue;
}
if (!hasId(html, newId)) {
const msg = `⚠️ newId introuvable: ${route} old=${oldId} -> new=${newId}`;
// newId doit exister UNE fois (sinon injection ambiguë)
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);
console.log(msg);
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,
htmlPath,
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}`,
});
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

@@ -1,2 +1,5 @@
{}
{
"/archicrat-ia/chapitre-3/": {
"p-1-60c7ea48": "p-1-a21087b0"
}
}

View File

@@ -0,0 +1,10 @@
schema: 1
page: archicrat-ia/chapitre-1
paras:
p-0-8d27a7f5:
refs:
- url: https://auth.archicratie.trans-hands.synology.me/authenticated
label: Lien web
kind: (livre / article / vidéo / site / autre) Site
ts: 2026-02-27T12:34:31.704Z
fromIssue: 142

View File

@@ -0,0 +1,9 @@
schema: 1
page: archicrat-ia/chapitre-1
paras:
p-1-8a6c18bf:
comments_editorial:
- text: Yeaha
status: new
ts: 2026-02-27T12:40:39.462Z
fromIssue: 143

View File

@@ -0,0 +1,18 @@
schema: 1
page: archicrat-ia/chapitre-3
paras:
p-0-ace27175:
media:
- type: image
src: /media/archicrat-ia/chapitre-3/p-0-ace27175/Capture_d_e_cran_2025-05-05_a_19.20.40.png
caption: "[Media] p-0-ace27175 — Chapitre 3 — Philosophies du pouvoir et
archicration"
credit: ""
ts: 2026-02-27T12:43:14.259Z
fromIssue: 144
refs:
- url: https://gitea.archicratie.trans-hands.synology.me
label: Gitea
kind: (livre / article / vidéo / site / autre) Site
ts: 2026-03-02T19:53:21.252Z
fromIssue: 169

View File

@@ -0,0 +1,11 @@
schema: 1
page: archicrat-ia/chapitre-3
paras:
p-1-60c7ea48:
refs:
- url: https://gitea.archicratie.trans-hands.synology.me
label: Gitea
kind: (livre / article / vidéo / site / autre) Site
ts: 2026-03-02T20:01:55.858Z
fromIssue: 172
# testB: hotpatch-auto gate proof

View File

@@ -0,0 +1,30 @@
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
p-11-67c14c09:
media:
- type: image
src: /media/archicrat-ia/chapitre-4/p-11-67c14c09/Capture_d_e_cran_2026-02-16_a_13.07.35.png
caption: "[Media] p-11-67c14c09 — Chapitre 4 — Histoire archicratique des
révolutions industrielles"
credit: ""
ts: 2026-02-26T13:17:41.286Z
fromIssue: 129

View File

@@ -0,0 +1,19 @@
schema: 1
page: archicrat-ia/chapitre-4
paras:
p-11-67c14c09:
media:
- type: image
src: /media/archicrat-ia/chapitre-4/p-11-67c14c09/Capture_d_e_cran_2026-02-16_a_13.07.35.png
caption: "[Media] p-11-67c14c09 — Chapitre 4 — Histoire archicratique des
révolutions industrielles"
credit: ""
ts: 2026-02-26T13:17:41.286Z
fromIssue: 129
- type: image
src: /media/archicrat-ia/chapitre-4/p-11-67c14c09/Capture_d_e_cran_2025-05-05_a_19.20.40.png
caption: "[Media] p-11-67c14c09 — Chapitre 4 — Histoire archicratique des
révolutions industrielles"
credit: ""
ts: 2026-02-27T09:17:04.386Z
fromIssue: 127

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

@@ -3,11 +3,11 @@ import { getCollection } from "astro:content";
const { currentSlug } = Astro.props;
const entries = (await getCollection("archicratie"))
.filter((e) => e.slug.startsWith("archicrat-ia/"))
// ✅ Après migration : TOC = collection "archicrat-ia"
const entries = (await getCollection("archicrat-ia"))
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
const href = (slug) => `/archicratie/${slug}/`;
const href = (slug) => `/archicrat-ia/${slug}/`;
---
<nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA">
@@ -66,7 +66,6 @@ const href = (slug) => `/archicratie/${slug}/`;
opacity: .88;
}
/* On garde <ol> mais on neutralise tout marker/numéro */
.toc-global__list{
list-style: none;
margin: 0;
@@ -148,7 +147,6 @@ const href = (slug) => `/archicratie/${slug}/`;
scrollbar-gutter: stable;
}
@media (prefers-color-scheme: dark){
.toc-global{ background: rgba(255,255,255,0.04); }
.toc-link:hover{ background: rgba(255,255,255,0.06); }
@@ -162,4 +160,4 @@ const href = (slug) => `/archicratie/${slug}/`;
const active = document.querySelector(".toc-global .toc-item.is-active");
if (active) active.scrollIntoView({ block: "nearest" });
})();
</script>
</script>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<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="/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="/ia/">Cas IA</a><span aria-hidden="true"> · </span>
<a href="/glossaire/">Glossaire</a><span aria-hidden="true"> · </span>

View File

@@ -11,10 +11,11 @@ summary: ""
source:
kind: docx
path: "sources/docx/archicrat-ia/Chapitre_3—Philosophies_du_pouvoir_et_Archicration-pour_une_topologie_differenciee_des_regimes_regulateurs-version_officielle.docx"
<!-- testA: full-auto gate proof -->
---
Ce chapitre se tient à un point nodal de notre essai-thèse : il ouvre un espace dexploration systématique des formes conceptuelles et philosophiques à travers lesquelles le pouvoir se configure comme régime de régulation. Il ne sagit pas ici de revenir une nouvelle fois sur les fondements de lautorité, ni dinterroger la légitimité politique au sens classique du terme, ni même denquêter sur la genèse des institutions. Lambition est autre, structurelle, transversale, morphologique, elle tentera darpenter, à même les dispositifs, les pensées, les théorisations et les expériences, les modalités différentiées par lesquelles sinstaurent, séprouvent et se disputent les formes de régulation du vivre-ensemble.
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point dorigine, ne prétend restituer aucune ontologie stable du politique. Ce quil donne à lire, cest une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières.
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point dorigine, ne prétend restituer aucune ontologie stable du politique. Ce quil donne à lire, cest une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières, et souvent complémentaires.
Ainsi, loin dêtre une galerie illustrative de théories politiques juxtaposées, le chapitre sagence comme une topologie critique, une plongée stratigraphique dans les scènes où sarticule la régulation — entendue ici non comme stabilisation externe ou ajustement technico-fonctionnel, mais comme dispositif instituant, tension structurante, scène traversée de conflictualité et dexigence normative. Car à nos yeux, la régulation nest pas ce qui vient après le pouvoir, elle en est la forme même constitutive — son architecture, son rythme, son épaisseur. Elle est ce par quoi le pouvoir ne se contente pas dêtre exercé, mais sinstitue, se justifie, se dispute, se recompose.

Some files were not shown because too many files have changed in this diff Show More