Compare commits

...

322 Commits

Author SHA1 Message Date
99cf0947da fix(glossaire): align reading follow top actions with glossary navigation
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-26 22:30:05 +01:00
7033354011 fix(glossaire): include standalone relation headings in reading follow
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-26 21:29:26 +01:00
7345730e3c Merge pull request 'fix(glossaire): expose relations heading to reading follow' (#303) from fix/glossaire-relations-follow-heading into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 24s
CI / build-and-anchors (push) Successful in 50s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 9m19s
Reviewed-on: #303
2026-03-26 20:47:02 +01:00
cea94c56db fix(glossaire): expose relations heading to reading follow
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-26 20:42:01 +01:00
c1e24736e3 Merge pull request 'feat(glossaire): deduplicate entry aside relation groups' (#302) from feat/glossaire-entry-aside-dedup into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 21s
CI / build-and-anchors (push) Successful in 49s
SMOKE / smoke (push) Successful in 4s
Deploy staging+live (annotations) / deploy (push) Successful in 9m41s
Reviewed-on: #302
2026-03-26 20:27:31 +01:00
24bbfbc17f feat(glossaire): deduplicate entry aside relation groups
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 46s
2026-03-26 20:24:49 +01:00
a11e2f1d18 Merge pull request 'fix(glossaire): compact sticky entry hero on glossary pages' (#301) from fix/glossaire-entry-sticky-hero-collapse into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 32s
CI / build-and-anchors (push) Successful in 45s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 9m18s
Reviewed-on: #301
2026-03-26 18:32:19 +01:00
630b146d02 fix(glossaire): compact sticky entry hero on glossary pages
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 50s
CI / build-and-anchors (pull_request) Successful in 46s
2026-03-26 18:30:34 +01:00
551360db83 Merge pull request 'fix(ci): use local pagefind binary instead of npx wrapper' (#300) from fix/ci-pagefind-local-bin into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 26s
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 9m45s
Reviewed-on: #300
2026-03-26 14:45:29 +01:00
a96c282780 fix(ci): use local pagefind binary instead of npx wrapper
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 46s
2026-03-26 14:41:02 +01:00
d2e0f147c2 Merge pull request 'audit(glossaire): tighten portal exposure and cross-page coherence' (#299) from audit/glossaire-transverse-coherence-fix into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 33s
SMOKE / smoke (push) Successful in 10s
Deploy staging+live (annotations) / deploy (push) Successful in 10m22s
CI / build-and-anchors (push) Successful in 45s
Reviewed-on: #299
2026-03-26 14:20:37 +01:00
ad95364021 audit(glossaire): tighten portal exposure and cross-page coherence
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 1m11s
CI / build-and-anchors (pull_request) Successful in 1m17s
2026-03-26 14:16:05 +01:00
e48e322363 Merge pull request 'feat(glossaire): harmonize portal pages and sticky reading ux' (#298) from feat/glossaire-portal-polish-final into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 27s
Deploy staging+live (annotations) / deploy (push) Successful in 10m51s
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 43s
Reviewed-on: #298
2026-03-26 13:01:46 +01:00
a9f2a5bbd4 feat(glossaire): harmonize portal pages and sticky reading ux
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 54s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-26 12:58:17 +01:00
0cba8f868e Merge pull request 'feat(glossaire): harmonize portal pages with shared components' (#297) from feat/glossaire-portals-harmonization into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 28s
CI / build-and-anchors (push) Successful in 43s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 10m36s
Reviewed-on: #297
2026-03-25 23:52:36 +01:00
f8e3ee4cca feat(glossaire): harmonize portal pages with shared components
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 46s
2026-03-25 23:49:00 +01:00
92e0ad01c6 Merge pull request 'refactor(glossaire): componentize glossary entry page' (#296) from refactor/glossaire-entry-componentization into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 22s
CI / build-and-anchors (push) Successful in 48s
SMOKE / smoke (push) Successful in 6s
Deploy staging+live (annotations) / deploy (push) Successful in 9m57s
Reviewed-on: #296
2026-03-25 19:28:10 +01:00
e6c18d6b16 refactor(glossaire): componentize glossary entry page
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-25 19:18:58 +01:00
a3092f5d5b Merge pull request 'refactor(glossaire): componentize glossary home sections' (#295) from refactor/glossaire-home-componentization into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 32s
Deploy staging+live (annotations) / deploy (push) Successful in 11m41s
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 46s
Reviewed-on: #295
2026-03-25 18:30:14 +01:00
7187b69935 refactor(glossaire): componentize glossary home sections
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 48s
2026-03-25 18:26:43 +01:00
4ba4453661 Merge pull request 'refactor(glossaire): centralize aside and home data' (#294) from feat/glossaire-relational-asides-and-home into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 21s
CI / build-and-anchors (push) Successful in 42s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 9m5s
Reviewed-on: #294
2026-03-25 16:50:31 +01:00
ee42e391e3 refactor(glossaire): centralize aside and home data
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-25 16:48:43 +01:00
f7756be59e Merge pull request 'feat/glossaire-entry-relations-rendering' (#293) from feat/glossaire-entry-relations-rendering into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 27s
CI / build-and-anchors (push) Successful in 42s
SMOKE / smoke (push) Successful in 6s
Deploy staging+live (annotations) / deploy (push) Successful in 9m40s
Reviewed-on: #293
2026-03-25 15:39:41 +01:00
4abe70e10e refactor(glossaire): extract entry relations rendering
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-25 15:30:51 +01:00
b2b4ec35c0 refactor(glossaire): preserve editorial order for entry relations 2026-03-25 15:20:39 +01:00
b255436958 Merge pull request 'refactor(glossaire): centralize glossary relation helpers' (#292) from feat/glossaire-ui-relations-foundation into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 24s
Deploy staging+live (annotations) / deploy (push) Successful in 9m2s
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 43s
Reviewed-on: #292
2026-03-25 14:17:11 +01:00
ad06b34a85 refactor(glossaire): centralize glossary relation helpers
All checks were successful
CI / build-and-anchors (pull_request) Successful in 42s
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 41s
2026-03-25 14:15:39 +01:00
a38f585f3d Merge pull request 'feat(glossaire): strengthen paradigms and support theories cross-links' (#291) from chore/glossaire-paradigmes-and-support-theories-mesh into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 14s
Deploy staging+live (annotations) / deploy (push) Successful in 9m54s
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 48s
Reviewed-on: #291
2026-03-25 10:30:37 +01:00
bf0dc125d1 feat(glossaire): strengthen paradigms and support theories cross-links
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 1m19s
2026-03-25 10:28:42 +01:00
f61dc15b47 Merge pull request 'feat(glossaire): strengthen meta-regimes and archicrations cross-links' (#290) from chore/glossaire-meta-regimes-and-archicrations-mesh into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 16s
Deploy staging+live (annotations) / deploy (push) Successful in 8m31s
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 45s
Reviewed-on: #290
2026-03-24 20:41:52 +01:00
1ac3d91a19 fix(glossaire): repair malformed normativo-politiques frontmatter
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-24 20:40:10 +01:00
100ba10409 feat(glossaire): strengthen meta-regimes and archicrations cross-links
Some checks failed
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Failing after 27s
2026-03-24 20:33:39 +01:00
5f14785abb Merge pull request 'chore/glossaire-pathologies-figures-and-reinstitution-mesh' (#289) from chore/glossaire-pathologies-figures-and-reinstitution-mesh into main
All checks were successful
CI / build-and-anchors (push) Successful in 47s
Proposer Apply (Queue) / apply-proposer (push) Successful in 32s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 11m12s
Reviewed-on: #289
2026-03-24 18:35:06 +01:00
c7043ae9d5 fix(glossaire): repair malformed seeAlso frontmatter
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 51s
2026-03-24 18:32:55 +01:00
bd1235f8c3 feat(glossaire): strengthen pathologies, figures, and reinstitution cross-links 2026-03-24 18:32:12 +01:00
7ae7b4dca3 Merge pull request 'chore/glossaire-scenes-topologies-audit-and-mesh2' (#288) from chore/glossaire-scenes-topologies-audit-and-mesh into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 18s
Deploy staging+live (annotations) / deploy (push) Successful in 8m20s
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 43s
Reviewed-on: #288
2026-03-24 17:57:50 +01:00
f088db57d4 feat(glossaire): strengthen scenes, topologies, and IA audit cross-links
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-24 17:56:13 +01:00
311e94ed91 Merge pull request 'feat(glossaire): strengthen tensions cross-links' (#287) from chore/glossaire-tensions-audit-and-mesh into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 18s
Deploy staging+live (annotations) / deploy (push) Successful in 9m53s
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 41s
Reviewed-on: #287
2026-03-24 14:37:57 +01:00
e078f3f9ab feat(glossaire): strengthen tensions cross-links
All checks were successful
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 45s
SMOKE / smoke (push) Successful in 5s
2026-03-24 14:32:44 +01:00
7c4bb5a2cf Merge pull request 'fix(glossaire): sanitize imported artefacts and relation metadata' (#286) from chore/glossaire-sanitize-corpus into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 30s
Deploy staging+live (annotations) / deploy (push) Successful in 11m10s
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 41s
Reviewed-on: #286
2026-03-24 13:37:47 +01:00
214e174635 fix(glossaire): sanitize imported artefacts and relation metadata
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 47s
2026-03-24 13:34:03 +01:00
f1b2f4605f Merge pull request 'feat/glossaire-sticky-entry-and-aside-polish-20260324' (#285) from feat/glossaire-sticky-entry-and-aside-polish-20260324 into main
All checks were successful
CI / build-and-anchors (push) Successful in 48s
Proposer Apply (Queue) / apply-proposer (push) Successful in 34s
SMOKE / smoke (push) Successful in 16s
Deploy staging+live (annotations) / deploy (push) Successful in 10m2s
Reviewed-on: #285
2026-03-24 00:32:36 +01:00
87955adf5d feat(glossaire): polish sticky entry flow and aside navigation
All checks were successful
SMOKE / smoke (push) Successful in 18s
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 48s
2026-03-24 00:29:39 +01:00
e39a0c547d feat(glossaire): compress paradigme hero when reading follow is active 2026-03-23 10:27:28 +01:00
c89ddf7237 chore(glossaire): checkpoint before portal hero compression 2026-03-23 00:33:19 +01:00
615effe8bf Merge pull request 'fix(glossaire): description précise du correctif' (#284) from fix/nom-court-et-clair into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 15s
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 8m29s
Reviewed-on: #284
2026-03-21 21:41:19 +01:00
e952b344a0 fix(glossaire): description précise du correctif
All checks were successful
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-21 21:39:02 +01:00
bb0572cc1a Merge pull request 'fix(glossaire): harmonize portal sticky hero and follow behavior' (#283) from fix/glossaire-portails-sticky-follow-20260321 into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 20s
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 9s
Deploy staging+live (annotations) / deploy (push) Successful in 9m19s
Reviewed-on: #283
2026-03-21 20:22:43 +01:00
f6a2347278 fix(glossaire): harmonize portal sticky hero and follow behavior
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-21 20:15:13 +01:00
d902c2bf98 Merge pull request 'feat(glossaire): refine portal pages and contextual asides' (#282) from feat/glossaire-portails-asides-polish into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 17s
CI / build-and-anchors (push) Successful in 48s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 8m30s
Reviewed-on: #282
2026-03-20 17:46:21 +01:00
baa2082f51 feat(glossaire): refine portal pages and contextual asides
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 45s
2026-03-20 17:42:09 +01:00
2f249b420f Merge pull request 'feat(glossaire): enrich entries and refine glossary navigation' (#281) from feat/glossaire-enrich-navigation into main
All checks were successful
CI / build-and-anchors (push) Successful in 48s
Proposer Apply (Queue) / apply-proposer (push) Successful in 29s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 10m44s
Reviewed-on: #281
2026-03-19 22:18:49 +01:00
d6b4eb82f4 feat(glossaire): enrich entries and refine glossary navigation
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 55s
CI / build-and-anchors (pull_request) Successful in 45s
2026-03-19 21:53:33 +01:00
bfa44fecda Merge pull request 'Add archicrations-esthetico-symboliques.md' (#280) from chore/add-archicrations-esthetico-symboliques into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 23s
CI / build-and-anchors (push) Successful in 47s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 10m53s
Reviewed-on: #280
2026-03-18 23:53:32 +01:00
e329235aa9 Add archicrations-esthetico-symboliques.md
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 51s
CI / build-and-anchors (pull_request) Successful in 45s
2026-03-18 23:51:48 +01:00
8cbaa5117c Merge pull request 'Document cockpit local, NAS supervision, and ops workflow' (#279) from docs-and-ops/cockpit-finalization into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 28s
Deploy staging+live (annotations) / deploy (push) Successful in 49s
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 4s
Reviewed-on: #279
2026-03-18 18:24:44 +01:00
3086f333ed Document cockpit local, NAS supervision, and ops workflow
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-18 18:21:52 +01:00
c1c3c19d13 Merge pull request 'Use Europe/Paris build time for ops health manifest' (#278) from ops/builtat-paris-time into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 30s
Deploy staging+live (annotations) / deploy (push) Successful in 46s
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 2s
Reviewed-on: #278
2026-03-18 11:14:11 +01:00
ddcd0acd4d Use Europe/Paris build time for ops health manifest
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 49s
2026-03-18 11:11:54 +01:00
9bc4eeb3e7 Merge pull request 'Add ops health manifest to deploy pipeline' (#277) from ops/ops-health-deploy into main
All checks were successful
CI / build-and-anchors (push) Successful in 44s
Proposer Apply (Queue) / apply-proposer (push) Successful in 19s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 11m8s
Reviewed-on: #277
2026-03-18 10:31:32 +01:00
7a9a5319ac Add ops health manifest to deploy pipeline
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 58s
CI / build-and-anchors (pull_request) Successful in 1m3s
2026-03-18 10:28:13 +01:00
7d75de5c9f Merge pull request 'docs: formalize localhost auto-sync architecture' (#276) from docs/localhost-auto-sync into main
All checks were successful
CI / build-and-anchors (push) Successful in 51s
Proposer Apply (Queue) / apply-proposer (push) Successful in 19s
SMOKE / smoke (push) Successful in 6s
Deploy staging+live (annotations) / deploy (push) Successful in 46s
Reviewed-on: #276
2026-03-16 21:19:51 +01:00
69c91cb661 docs: formalize localhost auto-sync architecture
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 45s
2026-03-16 21:14:41 +01:00
a1bfbf4405 Merge pull request 'proposer: apply 2 tickets on /archicrat-ia/prologue/' (#275) from bot/proposer-273-ce91b3d8cf02 into main
All checks were successful
CI / build-and-anchors (push) Successful in 44s
Proposer Apply (Queue) / apply-proposer (push) Successful in 33s
SMOKE / smoke (push) Successful in 12s
Deploy staging+live (annotations) / deploy (push) Successful in 9m57s
Reviewed-on: #275
2026-03-16 16:46:20 +01:00
archicratie-bot
be26b425d8 edit: apply ticket #274 (/archicrat-ia/prologue/#p-23-d91a7b78)
All checks were successful
CI / build-and-anchors (push) Successful in 51s
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-16 15:43:09 +00:00
archicratie-bot
abf88e7037 edit: apply ticket #273 (/archicrat-ia/prologue/#p-22-a416d473) 2026-03-16 15:42:47 +00:00
04fee32fdb Merge pull request 'proposer: apply 2 tickets on /archicrat-ia/prologue/' (#272) from bot/proposer-270-39655773c199 into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 26s
CI / build-and-anchors (push) Successful in 43s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 9m20s
Reviewed-on: #272
2026-03-16 13:57:16 +01:00
archicratie-bot
fbddf5c3fc edit: apply ticket #271 (/archicrat-ia/prologue/#p-17-b8c5bf21)
All checks were successful
CI / build-and-anchors (push) Successful in 50s
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (pull_request) Successful in 46s
2026-03-16 12:54:46 +00:00
archicratie-bot
bad748df3a edit: apply ticket #270 (/archicrat-ia/prologue/#p-7-64a0ca9c) 2026-03-16 12:54:21 +00:00
0066cf8601 Merge pull request 'fix(actions): harden proposer queue against duplicate batch PRs' (#269) from hotfix/proposer-close-verify into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 21s
Deploy staging+live (annotations) / deploy (push) Successful in 33s
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 4s
Reviewed-on: #269
2026-03-16 13:42:52 +01:00
5d3473d66c fix(actions): harden proposer queue against duplicate batch PRs
All checks were successful
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-16 13:39:09 +01:00
f9d34110e4 Merge pull request 'proposer: apply 2 tickets on /archicrat-ia/prologue/' (#266) from bot/proposer-264-20260316-120249 into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 20s
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 7m47s
Reviewed-on: #266
2026-03-16 13:18:27 +01:00
archicratie-bot
84e9c3ead4 edit: apply ticket #265 (/archicrat-ia/prologue/#p-5-85126fa5)
All checks were successful
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 39s
2026-03-16 12:03:37 +00:00
archicratie-bot
72e59175fc edit: apply ticket #264 (/archicrat-ia/prologue/#p-4-8ed4f807) 2026-03-16 12:03:13 +00:00
81b69ac6d5 Merge pull request 'fix(actions): verify proposer issue closure after PR creation' (#263) from hotfix/proposer-close-verify into main
All checks were successful
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 47s
Proposer Apply (Queue) / apply-proposer (push) Successful in 33s
SMOKE / smoke (push) Successful in 2s
Reviewed-on: #263
2026-03-16 12:54:26 +01:00
513ae72e85 fix(actions): verify proposer issue closure after PR creation
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 47s
2026-03-16 12:52:47 +01:00
4c4dd1c515 Merge pull request 'fix(actions): remove fragile heredocs from proposer PR step' (#262) from hotfix/proposer-no-heredoc-pr-step into main
All checks were successful
Deploy staging+live (annotations) / deploy (push) Successful in 39s
CI / build-and-anchors (push) Successful in 46s
SMOKE / smoke (push) Successful in 7s
Proposer Apply (Queue) / apply-proposer (push) Successful in 1m30s
Reviewed-on: #262
2026-03-16 12:34:12 +01:00
46b15ed6ab fix(actions): remove fragile heredocs from proposer PR step
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 40s
2026-03-16 12:30:43 +01:00
a015e72f7c Merge pull request 'proposer: apply ticket #257' (#261) from bot/proposer-257-20260316-111742 into main
All checks were successful
CI / build-and-anchors (push) Successful in 42s
SMOKE / smoke (push) Successful in 8s
Proposer Apply (Queue) / apply-proposer (push) Successful in 1m26s
Deploy staging+live (annotations) / deploy (push) Successful in 7m52s
Reviewed-on: #261
2026-03-16 12:21:55 +01:00
archicratie-bot
d5df7d77a0 edit: apply ticket #257 (/archicrat-ia/prologue/#p-0-d7974f88)
All checks were successful
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 4s
2026-03-16 11:18:06 +00:00
ec3ceee862 Merge pull request 'fix(actions): tolerate empty label payload in proposer gate' (#260) from debug/proposer-257 into main
Some checks failed
CI / build-and-anchors (push) Successful in 46s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
SMOKE / smoke (push) Successful in 2s
Proposer Apply (Queue) / apply-proposer (push) Failing after 1m39s
Reviewed-on: #260
2026-03-16 12:16:12 +01:00
867475c3ff fix(actions): tolerate empty label payload in proposer gate
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-16 12:14:01 +01:00
b024c5557c Merge pull request 'fix(editorial): preserve frontmatter in apply-ticket' (#259) from hotfix/preserve-frontmatter-apply-ticket into main
All checks were successful
CI / build-and-anchors (push) Successful in 47s
Proposer Apply (Queue) / apply-proposer (push) Successful in 38s
SMOKE / smoke (push) Successful in 11s
Deploy staging+live (annotations) / deploy (push) Successful in 9m38s
Reviewed-on: #259
2026-03-16 11:52:07 +01:00
93306f360d fix(editorial): preserve frontmatter in apply-ticket
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 51s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-16 11:48:08 +01:00
52847d999d Merge pull request 'fix(actions): silence anno workflow on proposer tickets' (#258) from hotfix/fix-proposer-runtime-v2 into main
Some checks failed
Deploy staging+live (annotations) / deploy (push) Successful in 51s
CI / build-and-anchors (push) Successful in 49s
SMOKE / smoke (push) Successful in 5s
Proposer Apply (Queue) / apply-proposer (push) Failing after 48s
Reviewed-on: #258
2026-03-16 11:11:28 +01:00
b9629b43ff fix(actions): silence anno workflow on proposer tickets
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-16 11:06:11 +01:00
06482a9f8d Merge pull request 'fix(actions): make proposer queue runtime-safe' (#254) from hotfix/fix-proposer-runtime-v2 into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 21s
CI / build-and-anchors (push) Successful in 46s
SMOKE / smoke (push) Successful in 6s
Deploy staging+live (annotations) / deploy (push) Successful in 42s
Reviewed-on: #254
2026-03-16 00:59:50 +01:00
f2e4ae5ac2 fix(actions): make proposer queue runtime-safe
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 42s
2026-03-16 00:58:10 +01:00
71baf0f6da Merge pull request 'fix(actions): repair proposer workflow yaml' (#253) from hotfix/fix-proposer-workflow into main
Some checks failed
CI / build-and-anchors (push) Successful in 48s
Deploy staging+live (annotations) / deploy (push) Successful in 1m0s
SMOKE / smoke (push) Successful in 4s
Proposer Apply (Queue) / apply-proposer (push) Failing after 4s
Reviewed-on: #253
2026-03-16 00:42:17 +01:00
d02b6fc347 fix(actions): repair proposer workflow yaml
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 45s
2026-03-16 00:38:46 +01:00
431f1e347b Merge pull request 'chore(editorial): harden proposer queue and apply-ticket' (#252) from chore/editorial-hardening-v1-clean into main
Some checks are pending
Deploy staging+live (annotations) / deploy (push) Waiting to run
CI / build-and-anchors (push) Successful in 1m6s
SMOKE / smoke (push) Successful in 39s
Reviewed-on: #252
2026-03-16 00:02:31 +01:00
ab6f45ed5c chore(editorial): harden proposer queue and apply-ticket
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 46s
2026-03-15 23:58:11 +01:00
02c060d239 Merge pull request 'chore/reset-from-docx-clean-base' (#251) from chore/reset-from-docx-clean-base into main
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 9m50s
Reviewed-on: #251
2026-03-15 21:19:24 +01:00
be2029de82 ci(anchors): accept intentional chapitre 1 anchor reset
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-15 21:11:20 +01:00
e148eaeaf3 content(archicrat-ia): refresh chapitre 1 from official docx
Some checks failed
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Failing after 42s
2026-03-15 20:50:35 +01:00
c63a1e6ce4 chore(reset): reimport chapitre 1 from docx clean base
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 44s
2026-03-15 20:33:49 +01:00
b3a73a7781 Merge pull request 'chore(reset): reimport chapitre 1 from docx clean base' (#250) from chore/reset-from-docx-clean-base into main
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 9m32s
Reviewed-on: #250
2026-03-15 15:31:37 +01:00
1968585d0f chore(reset): reimport chapitre 1 from docx clean base
All checks were successful
CI / build-and-anchors (push) Successful in 57s
CI / build-and-anchors (pull_request) Successful in 50s
SMOKE / smoke (push) Successful in 5s
2026-03-15 15:29:09 +01:00
b33c758411 Merge pull request 'proposer: apply ticket #236' (#243) from bot/proposer-236-20260315-112808 into main
All checks were successful
Deploy staging+live (annotations) / deploy (push) Successful in 12m1s
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 43s
Reviewed-on: #243
2026-03-15 12:55:33 +01:00
afa543125c Merge pull request 'proposer: apply ticket #235' (#242) from bot/proposer-235-20260315-112515 into main
Some checks are pending
Deploy staging+live (annotations) / deploy (push) Has started running
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 51s
Reviewed-on: #242
2026-03-15 12:47:26 +01:00
archicratie-bot
0d0252cac0 edit: apply ticket #236 (/archicrat-ia/chapitre-1/#p-400-8959d62e)
All checks were successful
CI / build-and-anchors (push) Successful in 45s
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-15 11:28:36 +00:00
archicratie-bot
a8bd9aeed5 edit: apply ticket #235 (/archicrat-ia/chapitre-1/#p-139-caa4e99d)
All checks were successful
CI / build-and-anchors (pull_request) Successful in 47s
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 3s
2026-03-15 11:25:43 +00:00
d277c61afd Merge pull request 'proposer: apply ticket #229' (#232) from bot/proposer-229-20260315-105537 into main
Some checks failed
CI / build-and-anchors (push) Successful in 51s
SMOKE / smoke (push) Successful in 10s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
Reviewed-on: #232
2026-03-15 12:24:34 +01:00
archicratie-bot
86479952d1 edit: apply ticket #229 (/archicrat-ia/chapitre-1/#p-10-1a706744)
All checks were successful
CI / build-and-anchors (pull_request) Successful in 42s
CI / build-and-anchors (push) Successful in 42s
SMOKE / smoke (push) Successful in 5s
2026-03-15 10:56:03 +00:00
c94024a8ae Merge pull request 'feat(glossaire): add advanced external paradigms from chapter 3' (#228) from feat/glossaire-archicrations-cartographie into main
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 9m53s
Reviewed-on: #228
2026-03-15 10:08:48 +01:00
70611d16f8 feat(glossaire): add advanced external paradigms from chapter 3
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-15 10:05:32 +01:00
354db231b8 Merge pull request 'feat(glossaire): enrichit la cartographie archicratique et les archicrations' (#227) from feat/glossaire-archicrations-cartographie into main
All checks were successful
SMOKE / smoke (push) Successful in 14s
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 9m37s
Reviewed-on: #227
2026-03-14 19:57:38 +01:00
9d8d60d00f feat(glossaire): enrichit la cartographie archicratique et les archicrations
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 38s
2026-03-14 19:55:16 +01:00
f5d25abbec Merge pull request 'feat(glossaire): add advanced external paradigms from chapter 3' (#226) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 7m13s
Reviewed-on: #226
2026-03-13 18:55:10 +01:00
8e9f7314f5 feat(glossaire): add advanced external paradigms from chapter 3
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 40s
2026-03-13 18:48:33 +01:00
03b88b944d Merge pull request 'feat(glossaire): integrate paradigms portal into glossary navigation' (#225) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 21s
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 7m38s
Reviewed-on: #225
2026-03-13 18:06:43 +01:00
385c36f660 feat(glossaire): integrate paradigms portal into glossary navigation
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 41s
2026-03-13 18:04:13 +01:00
cfa092cd38 Merge pull request 'feat(glossaire): introduce doctrine as ontology type' (#224) from feat/glossaire-paradigmes-externes 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 8m36s
Reviewed-on: #224
2026-03-13 16:47:52 +01:00
1a762f8f54 feat(glossaire): introduce doctrine as ontology type
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 38s
2026-03-13 16:45:29 +01:00
fbdaf72775 Merge pull request 'refactor(glossaire): introduce dedicated GlossaryLayout wrapper' (#223) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 8m12s
Reviewed-on: #223
2026-03-13 15:15:32 +01:00
67128a9ca1 refactor(glossaire): introduce dedicated GlossaryLayout wrapper
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-13 15:11:39 +01:00
898759db3d Merge pull request 'feat(glossaire): refactor core archicratic concepts' (#222) from feat/glossaire-paradigmes-externes 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 8m6s
Reviewed-on: #222
2026-03-13 14:36:40 +01:00
4f009a9557 feat(glossaire): refactor core archicratic concepts
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 37s
2026-03-13 14:32:41 +01:00
378d0981f0 Merge pull request 'refactor(glossaire): disable reading-only controls in glossary' (#221) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 20s
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 9m36s
Reviewed-on: #221
2026-03-12 19:27:16 +01:00
8f3702f803 refactor(glossaire): disable reading-only controls in glossary
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-12 19:23:50 +01:00
cfd303fc85 Merge pull request 'Nouveaux Ajouts - Glossaire' (#220) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 8m26s
Reviewed-on: #220
2026-03-12 17:45:03 +01:00
0fc0976f8a refactor(glossaire): polish contextual aside navigation
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-12 17:42:16 +01:00
e247ea8ead Merge pull request 'feat(glossaire): strengthen conceptual and paradigmatic cross-links' (#219) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 7m49s
Reviewed-on: #219
2026-03-12 14:44:17 +01:00
0c57c4bc6d feat(glossaire): strengthen conceptual and paradigmatic cross-links
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 37s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-12 14:39:46 +01:00
9b7998e1c3 Merge pull request 'feat(glossaire): add external paradigms and theoretical landscape' (#218) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 7m44s
Reviewed-on: #218
2026-03-12 13:57:00 +01:00
8997a00413 feat(glossaire): add external paradigms and theoretical landscape
All checks were successful
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-12 13:53:11 +01:00
a2e6f6185f Merge pull request 'fix(glossaire): adjust section anchor offset for sticky header' (#217) from feat/glossaire-paradigmes-externes into main
All checks were successful
SMOKE / smoke (push) Successful in 13s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 9m8s
Reviewed-on: #217
2026-03-12 13:35:39 +01:00
c2715b01d7 fix(glossaire): adjust section anchor offset for sticky header
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-12 13:31:07 +01:00
6f09dfcd12 Merge pull request 'feat(glossaire): extend taxonomy and align Astro 6 content config' (#216) from feat/glossaire-paradigmes into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 8m58s
Reviewed-on: #216
2026-03-12 12:08:15 +01:00
bb9f55a3b5 feat(glossaire): extend taxonomy and align Astro 6 content config
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-12 12:04:46 +01:00
298ee7492c Merge pull request 'EditionToc Modif' (#215) from chore/astro6-upgrade into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 7m16s
Reviewed-on: #215
2026-03-11 17:58:17 +01:00
37cb836246 chore(astro): upgrade to Astro 6 and fix dynamic collection routes
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 44s
2026-03-11 17:56:53 +01:00
19e3318125 Merge pull request 'chore(astro): upgrade to astro 6 with legacy content compatibility' (#214) from chore/astro6-upgrade into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 47s
Deploy staging+live (annotations) / deploy (push) Successful in 9m33s
Reviewed-on: #214
2026-03-11 17:19:04 +01:00
683b02f4a0 chore(astro): upgrade to astro 6 with legacy content compatibility
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 38s
2026-03-11 17:17:02 +01:00
20aecc30b1 Merge pull request 'feat(glossary): add core glossarial taxonomy and foundational entries' (#213) from feat/glossaire-core2 into main
All checks were successful
SMOKE / smoke (push) Successful in 18s
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 8m42s
Reviewed-on: #213
2026-03-11 13:04:24 +01:00
daf57aa152 feat(glossary): add core glossarial taxonomy and foundational entries
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 45s
2026-03-11 12:50:17 +01:00
bfd693de92 Merge pull request 'refactor(import): unify core editorial manifest' (#212) from refactor/unify-manifest-core into main
All checks were successful
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Successful in 8m0s
Reviewed-on: #212
2026-03-11 11:36:47 +01:00
ea2ad0017b refactor(import): unify core editorial manifest
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 35s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-11 11:33:37 +01:00
82e7473cac Merge pull request 'fix(content): declare commencer collection and remove implicit ia collection' (#211) from fix/content-collections-stability into main
All checks were successful
SMOKE / smoke (push) Successful in 17s
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 8m57s
Reviewed-on: #211
2026-03-11 11:09:39 +01:00
315523e80f refactor(site): depublish non-core sections and refresh anchors baseline
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 40s
2026-03-11 11:07:21 +01:00
569b6de154 fix(content): declare commencer collection and remove implicit ia collection
Some checks failed
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Failing after 41s
CI / build-and-anchors (pull_request) Failing after 46s
2026-03-11 10:54:27 +01:00
95f8159554 Merge pull request 'proposer: apply ticket #209' (#210) from bot/proposer-209-20260311-090431 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 8m16s
Reviewed-on: #210
2026-03-11 10:06:49 +01:00
archicratie-bot
5698c494f1 edit: apply ticket #209 (/cas-ia/introduction/#p-16-615e3d61)
All checks were successful
CI / build-and-anchors (pull_request) Successful in 43s
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 37s
2026-03-11 09:04:53 +00:00
e640e66b8d Merge pull request 'proposer: apply ticket #207' (#208) from bot/proposer-207-20260311-082736 into main
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 7m40s
Reviewed-on: #208
2026-03-11 09:29:21 +01:00
archicratie-bot
9be7d170c6 edit: apply ticket #207 (/cas-ia/introduction/#p-10-ceba29a2)
All checks were successful
CI / build-and-anchors (pull_request) Successful in 40s
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (push) Successful in 41s
2026-03-11 08:27:58 +00:00
c2c98c516b Merge pull request 'refactor(editorial): recentrer le site sur le noyau archicratique' (#206) from refactor/recentrage-noyau-archicratique into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 8m54s
Reviewed-on: #206
2026-03-10 20:43:16 +01:00
32554f5998 refactor(editorial): recentrer le site sur le noyau archicratique
All checks were successful
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 41s
2026-03-10 20:36:33 +01:00
308f4f92bc Merge pull request 'chore: merge main into fix branch' (#205) from fix/annotations-index-fail-open-20260304-223909 into main
All checks were successful
Deploy staging+live (annotations) / deploy (push) Successful in 53s
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 35s
Reviewed-on: #205
2026-03-06 11:18:04 +01:00
4dfd3b026b chore: merge main into fix branch
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 38s
2026-03-06 11:14:48 +01:00
c93f274f41 Merge pull request 'fix(annotations): fail-open when src/annotations is missing' (#204) from fix/annotations-index-fail-open-20260304-223909 into main
All checks were successful
SMOKE / smoke (push) Successful in 14s
CI / build-and-anchors (push) Successful in 46s
Deploy staging+live (annotations) / deploy (push) Successful in 8m58s
Reviewed-on: #204
2026-03-04 22:42:13 +01:00
dfa311fb5b fix(annotations): fail-open when src/annotations is missing
All checks were successful
SMOKE / smoke (push) Successful in 18s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-04 22:39:09 +01:00
3ef1dc2801 Merge pull request 'fix(annotations): fail-open when src/annotations missing + keep dir tracked' (#203) from chore/remove-test-anno-media-20260304-204810 into main
All checks were successful
SMOKE / smoke (push) Successful in 13s
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 8m47s
Reviewed-on: #203
2026-03-04 21:39:04 +01:00
435e41ed4d fix(annotations): fail-open when src/annotations missing + keep dir tracked
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 41s
2026-03-04 21:31:53 +01:00
8825932159 Merge pull request 'chore: keep public/media directory (gitkeep)' (#202) from chore/remove-test-anno-media-20260304-204810 into main
All checks were successful
SMOKE / smoke (push) Successful in 10s
Deploy staging+live (annotations) / deploy (push) Successful in 34s
CI / build-and-anchors (push) Successful in 45s
Reviewed-on: #202
2026-03-04 21:06:45 +01:00
b55decbea4 chore: keep public/media directory (gitkeep)
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 46s
CI / build-and-anchors (pull_request) Successful in 38s
2026-03-04 21:03:55 +01:00
414a848db3 Merge pull request 'chore: keep src/annotations directory (gitkeep)' (#201) from chore/remove-test-anno-media-20260304-204810 into main
All checks were successful
SMOKE / smoke (push) Successful in 5s
Deploy staging+live (annotations) / deploy (push) Successful in 39s
CI / build-and-anchors (push) Successful in 44s
Reviewed-on: #201
2026-03-04 21:03:39 +01:00
cbd4f3a57f chore: keep src/annotations directory (gitkeep)
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 36s
CI / build-and-anchors (pull_request) Successful in 36s
2026-03-04 21:02:19 +01:00
49f8d6a95e Merge pull request 'chore: remove test annotations and media' (#200) from chore/remove-test-anno-media-20260304-204810 into main
Some checks failed
CI / build-and-anchors (push) Failing after 34s
Deploy staging+live (annotations) / deploy (push) Successful in 47s
SMOKE / smoke (push) Successful in 20s
Reviewed-on: #200
2026-03-04 20:59:21 +01:00
5afa5cbfda chore: remove test annotations and media
Some checks failed
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Failing after 32s
CI / build-and-anchors (pull_request) Failing after 34s
2026-03-04 20:48:26 +01:00
a1b1df38ba Merge pull request 'chore: remove test annotations and media' (#196) from chore/remove-test-anno-media-20260304-202811 into main
Some checks failed
Deploy staging+live (annotations) / deploy (push) Failing after 54s
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Failing after 33s
CI / build-and-anchors (pull_request) Failing after 30s
Reviewed-on: #196
2026-03-04 20:35:47 +01:00
d3f7d74da7 Merge pull request 'chore: remove test annotations and media' (#197) from chore/remove-test-anno-media-20260304-202451 into main
Some checks are pending
Deploy staging+live (annotations) / deploy (push) Has started running
CI / build-and-anchors (push) Has started running
SMOKE / smoke (push) Successful in 8s
Reviewed-on: #197
2026-03-04 20:35:40 +01:00
6919190107 chore: remove test annotations and media
Some checks failed
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Failing after 36s
CI / build-and-anchors (pull_request) Failing after 36s
2026-03-04 20:28:29 +01:00
021ef5abd7 chore: remove test annotations and media
Some checks failed
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Failing after 31s
CI / build-and-anchors (pull_request) Failing after 38s
2026-03-04 20:25:03 +01:00
76cdc85f9c Merge pull request 'ci(deploy): treat src/public/package changes as FULL rebuild' (#193) from chore/supprimer-annotations-20260304-191252 into main
Some checks failed
SMOKE / smoke (push) Successful in 16s
CI / build-and-anchors (push) Failing after 35s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #193
2026-03-04 19:22:36 +01:00
f2f6df2127 ci(deploy): treat src/public/package changes as FULL rebuild
Some checks failed
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Failing after 36s
CI / build-and-anchors (pull_request) Failing after 37s
2026-03-04 19:13:25 +01:00
dfe13757f7 Merge pull request 'ci(deploy): treat src/public/package changes as FULL rebuild' (#191) from chore/fix-deploy-gate-full-src-public-20260304-181357 into main
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 34s
Reviewed-on: #191
2026-03-04 18:16:30 +01:00
148ac997df ci(deploy): treat src/public/package changes as FULL rebuild
All checks were successful
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 32s
2026-03-04 18:14:48 +01:00
84492d2741 Merge pull request 'style(mobile): widen reading in landscape on small screens (css-only)' (#190) from feat/mobile-landscape-reading-YYYYMMDD into main
All checks were successful
SMOKE / smoke (push) Successful in 16s
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 34s
Reviewed-on: #190
2026-03-04 17:39:11 +01:00
81baadd57f style(mobile): widen reading in landscape on small screens (css-only)
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 36s
2026-03-04 17:36:56 +01:00
63d0ffc5fc Merge pull request 'chore: <résumé clair de la modif>' (#189) from chore/xxx-20260303-221031 into main
All checks were successful
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 29s
CI / build-and-anchors (push) Successful in 39s
Reviewed-on: #189
2026-03-03 22:15:31 +01:00
24143fc2c4 chore: <résumé clair de la modif>
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 36s
CI / build-and-anchors (pull_request) Successful in 37s
2026-03-03 22:12:01 +01:00
55370b704f Merge pull request 'chore: cleanup testA/testB markers' (#188) from chore/cleanup-testA-testB-20260303-213115 into main
All checks were successful
SMOKE / smoke (push) Successful in 13s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 8m35s
Reviewed-on: #188
2026-03-03 21:38:00 +01:00
b8a3ce1337 chore: cleanup testA/testB markers
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 40s
2026-03-03 21:36:26 +01:00
7f9baedf41 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#187) from testB-hotpatch-auto-20260303-211919 into main
All checks were successful
SMOKE / smoke (push) Successful in 10s
Deploy staging+live (annotations) / deploy (push) Successful in 38s
CI / build-and-anchors (push) Successful in 37s
Reviewed-on: #187
2026-03-03 21:22:04 +01:00
1adbe1c7a3 test: B hotpatch-auto gate (touch src/annotations)
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 37s
CI / build-and-anchors (pull_request) Successful in 37s
2026-03-03 21:20:07 +01:00
107a26352f Merge pull request 'test: A full-auto gate (touch src/content)' (#186) from testA-full-auto-20260303-205324 into main
All checks were successful
SMOKE / smoke (push) Successful in 16s
CI / build-and-anchors (push) Successful in 46s
Deploy staging+live (annotations) / deploy (push) Successful in 7m5s
Reviewed-on: #186
2026-03-03 20:57:09 +01:00
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
be99460d4d Merge pull request 'ci(deploy): fix gate bash + robust merge-proof diff + full/hotpatch auto' (#185) from chore/fix-deploy-gate-bash-20260303-204603 into main
All checks were successful
SMOKE / smoke (push) Successful in 13s
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Successful in 44s
Reviewed-on: #185
2026-03-03 20:48:02 +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
941fbf5845 Merge pull request 'ci(deploy): make gate merge-proof (diff before..after)' (#184) from chore/deploy-gate-mergeproof-20260303-195827 into main
Some checks failed
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Failing after 35s
Reviewed-on: #184
2026-03-03 20:00:37 +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
292 changed files with 42436 additions and 2300 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,450 @@
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");
}
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"
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 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 allowedAnno = new Set(["type/media", "type/reference", "type/comment"]);
const proposerTypes = new Set(["type/correction", "type/fact-check"]);
const out = [];
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
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 (allowedAnno.has(type)) {
// proceed
} else if (proposerTypes.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 (unsupported / missing Type only)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
echo "skip reason=${SKIP_REASON} -> no comment"
exit 0
fi
if [[ "${SKIP_REASON:-}" == proposer_type:* ]]; then
echo "proposer ticket detected -> anno stays silent"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
REASON="${SKIP_REASON:-}"
TYPE="${ISSUE_TYPE:-}"
if [[ "$REASON" == unsupported_type:* ]]; then
MSG="Ticket #${ISSUE_NUMBER} ignored: unsupported Type (${TYPE}). Supported types: type/media, type/reference, type/comment."
else
MSG="Ticket #${ISSUE_NUMBER} ignored: missing or unreadable 'Type:'. Expected: 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 failed (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 created for 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,613 @@
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
# HOTPATCH si annotations/media touchés
if grep -qE '^(src/annotations/|public/media/)' /tmp/changed.txt; then
HAS_HOTPATCH=1
fi
# FULL si build-impacting (robuste)
# 1) Tout src/ SAUF src/annotations/
if grep -qE '^src/' /tmp/changed.txt && grep -qEv '^src/annotations/' /tmp/changed.txt; then
HAS_FULL=1
fi
# 2) scripts/
if grep -qE '^scripts/' /tmp/changed.txt; then
HAS_FULL=1
fi
# 3) Tout public/ SAUF public/media/
if grep -qE '^public/' /tmp/changed.txt && grep -qEv '^public/media/' /tmp/changed.txt; then
HAS_FULL=1
fi
# 4) fichiers racine qui changent le build / limage
if grep -qE '^(package\.json|package-lock\.json|astro\.config\.mjs|tsconfig\.json|\.npmrc|\.nvmrc|Dockerfile|docker-compose\.yml|nginx\.conf)$' /tmp/changed.txt; then
HAS_FULL=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
BUILD_TIME_RAW="$(TZ=Europe/Paris date '+%Y-%m-%dT%H:%M:%S%z')"
BUILD_TIME="${BUILD_TIME_RAW:0:${#BUILD_TIME_RAW}-2}:${BUILD_TIME_RAW:${#BUILD_TIME_RAW}-2}"
PUBLIC_OPS_ENV=staging \
PUBLIC_OPS_UPSTREAM=web_blue \
PUBLIC_BUILD_SHA="${AFTER}" \
PUBLIC_BUILD_TIME="${BUILD_TIME}" \
node scripts/write-ops-health.mjs
test -f public/__ops/health.json
echo "=== public/__ops/health.json (blue/staging) ==="
cat public/__ops/health.json
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"
wait_url "http://127.0.0.1:8081/__ops/health.json" "blue ops health"
curl -fsS --max-time 6 "http://127.0.0.1:8081/__ops/health.json" \
| python3 -c 'import sys, json; j=json.load(sys.stdin); print("env=", j.get("env")); print("upstream=", j.get("upstream")); print("buildSha=", j.get("buildSha")); print("builtAt=", j.get("builtAt"))'
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_TIME_RAW="$(TZ=Europe/Paris date '+%Y-%m-%dT%H:%M:%S%z')"
BUILD_TIME="${BUILD_TIME_RAW:0:${#BUILD_TIME_RAW}-2}:${BUILD_TIME_RAW:${#BUILD_TIME_RAW}-2}"
PUBLIC_OPS_ENV=prod \
PUBLIC_OPS_UPSTREAM=web_green \
PUBLIC_BUILD_SHA="${AFTER}" \
PUBLIC_BUILD_TIME="${BUILD_TIME}" \
node scripts/write-ops-health.mjs
test -f public/__ops/health.json
echo "=== public/__ops/health.json (green/prod) ==="
cat public/__ops/health.json
# 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
if ! wait_url "http://127.0.0.1:8082/__ops/health.json" "green ops health"; then rollback; exit 4; fi
curl -fsS --max-time 6 "http://127.0.0.1:8082/__ops/health.json" \
| python3 -c 'import sys, json; j=json.load(sys.stdin); print("env=", j.get("env")); print("upstream=", j.get("upstream")); print("buildSha=", j.get("buildSha")); print("builtAt=", j.get("builtAt"))'
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,788 @@
name: Proposer Apply (Queue)
on:
issues:
types: [labeled]
push:
branches: [main]
workflow_dispatch:
inputs:
issue:
description: "Issue number to prioritize (optional)"
required: false
default: ""
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
concurrency:
group: proposer-queue-main
cancel-in-progress: false
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 / push)
env:
INPUT_ISSUE: ${{ inputs.issue }}
EVENT_NAME_IN: ${{ github.event_name }}
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) ||
0;
const labelName =
ev?.label?.name ||
(typeof ev?.label === "string" ? ev.label : "") ||
"";
const eventName =
String(process.env.EVENT_NAME_IN || "").trim() ||
(ev?.issue ? "issues" : (ev?.before || ev?.after ? "push" : "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)}`,
`EVENT_NAME=${sh(eventName)}`,
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
echo "Context:"
sed -n '1,200p' /tmp/proposer.env
- name: Early gate (tolerant on empty issue label payload)
run: |
set -euo pipefail
source /tmp/proposer.env
echo "event=$EVENT_NAME label=${LABEL_NAME:-<empty>}"
if [[ "$EVENT_NAME" == "issues" ]]; then
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/approved" ]]; then
echo "issues/labeled with explicit non-approved label=$LABEL_NAME -> skip"
echo 'SKIP=1' >> /tmp/proposer.env
echo 'SKIP_REASON="label_not_state_approved_event"' >> /tmp/proposer.env
exit 0
fi
fi
echo "Proceed to API-based selection/gating"
- 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
- 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"
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: Select next proposer batch (by path)
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
}
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
export FORGE_API="$API_BASE"
cd "$APP_DIR"
test -f scripts/pick-proposer-issue.mjs || {
echo "missing scripts/pick-proposer-issue.mjs in APP_DIR=$APP_DIR"
ls -la scripts | sed -n '1,200p' || true
exit 1
}
node scripts/pick-proposer-issue.mjs "${ISSUE_NUMBER:-0}" > /tmp/proposer.pick.env
cat /tmp/proposer.pick.env >> /tmp/proposer.env
source /tmp/proposer.pick.env
if [[ "${TARGET_FOUND:-0}" != "1" ]]; then
echo 'SKIP=1' >> /tmp/proposer.env
echo "SKIP_REASON=${TARGET_REASON:-no_target}" >> /tmp/proposer.env
echo "No target batch"
exit 0
fi
echo "Target batch:"
grep -E '^(TARGET_PRIMARY_ISSUE|TARGET_ISSUES|TARGET_COUNT|TARGET_CHEMIN)=' /tmp/proposer.env
- name: Derive deterministic batch identity
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
export TARGET_ISSUES TARGET_CHEMIN
node --input-type=module - <<'NODE'
import fs from "node:fs";
import crypto from "node:crypto";
const issues = String(process.env.TARGET_ISSUES || "")
.trim()
.split(/\s+/)
.filter(Boolean)
.sort((a, b) => Number(a) - Number(b));
const chemin = String(process.env.TARGET_CHEMIN || "").trim();
const keySource = `${chemin}::${issues.join(",")}`;
const hash = crypto.createHash("sha1").update(keySource).digest("hex").slice(0, 12);
const primary = issues[0] || "0";
const batchBranch = `bot/proposer-${primary}-${hash}`;
fs.appendFileSync(
"/tmp/proposer.env",
[
`BATCH_KEY=${JSON.stringify(keySource)}`,
`BATCH_HASH=${JSON.stringify(hash)}`,
`BATCH_BRANCH=${JSON.stringify(batchBranch)}`
].join("\n") + "\n"
);
NODE
echo "Batch identity:"
grep -E '^(BATCH_KEY|BATCH_HASH|BATCH_BRANCH)=' /tmp/proposer.env
- name: Inspect open proposer PRs
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls?state=open&limit=100" \
-o /tmp/open_pulls.json
export TARGET_ISSUES="${TARGET_ISSUES:-}"
export BATCH_BRANCH="${BATCH_BRANCH:-}"
export BATCH_KEY="${BATCH_KEY:-}"
node --input-type=module - <<'NODE' >> /tmp/proposer.env
import fs from "node:fs";
const pulls = JSON.parse(fs.readFileSync("/tmp/open_pulls.json", "utf8"));
const issues = String(process.env.TARGET_ISSUES || "")
.trim()
.split(/\s+/)
.filter(Boolean);
const batchBranch = String(process.env.BATCH_BRANCH || "");
const batchKey = String(process.env.BATCH_KEY || "");
const proposerOpen = Array.isArray(pulls)
? pulls.filter((pr) => String(pr?.head?.ref || "").startsWith("bot/proposer-"))
: [];
const sameBatch = proposerOpen.find((pr) => {
const ref = String(pr?.head?.ref || "");
const title = String(pr?.title || "");
const body = String(pr?.body || "");
if (batchBranch && ref === batchBranch) return true;
if (batchKey && body.includes(`Batch-Key: ${batchKey}`)) return true;
return issues.some((n) =>
ref.startsWith(`bot/proposer-${n}-`) ||
title.includes(`#${n}`) ||
body.includes(`#${n}`) ||
body.includes(`ticket #${n}`)
);
});
const out = [];
if (sameBatch) {
out.push("SKIP=1");
out.push(`SKIP_REASON=${JSON.stringify("issue_already_has_open_pr")}`);
out.push(`OPEN_PR_URL=${JSON.stringify(String(sameBatch.html_url || sameBatch.url || ""))}`);
out.push(`OPEN_PR_BRANCH=${JSON.stringify(String(sameBatch?.head?.ref || ""))}`);
} else if (proposerOpen.length > 0) {
const first = proposerOpen[0];
out.push("SKIP=1");
out.push(`SKIP_REASON=${JSON.stringify("queue_busy_open_proposer_pr")}`);
out.push(`OPEN_PR_URL=${JSON.stringify(String(first.html_url || first.url || ""))}`);
out.push(`OPEN_PR_BRANCH=${JSON.stringify(String(first?.head?.ref || ""))}`);
}
process.stdout.write(out.join("\n") + (out.length ? "\n" : ""));
NODE
- name: Guard on remote batch branch before heavy work
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
if git ls-remote --exit-code --heads origin "$BATCH_BRANCH" >/dev/null 2>&1; then
echo 'SKIP=1' >> /tmp/proposer.env
echo 'SKIP_REASON="batch_branch_exists_without_pr"' >> /tmp/proposer.env
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
echo "Remote batch branch already exists -> skip duplicate materialization"
exit 0
fi
echo "Remote batch branch is free"
- name: Comment issue if queued / skipped
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
[[ "${EVENT_NAME:-}" != "push" ]] || exit 0
if [[ "${SKIP_REASON:-}" == "label_not_state_approved_event" || "${SKIP_REASON:-}" == "label_not_state_approved" ]]; then
echo "Skip reason=${SKIP_REASON} -> no comment"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
ISSUE_TO_COMMENT="${ISSUE_NUMBER:-0}"
if [[ "$ISSUE_TO_COMMENT" == "0" || -z "$ISSUE_TO_COMMENT" ]]; then
ISSUE_TO_COMMENT="${TARGET_PRIMARY_ISSUE:-0}"
fi
[[ "$ISSUE_TO_COMMENT" != "0" ]] || exit 0
case "${SKIP_REASON:-}" in
queue_busy_open_proposer_pr)
MSG="Ticket queued in proposer queue. An open proposer PR already exists: ${OPEN_PR_URL:-"(URL unavailable)"}. The workflow will resume after merge on main."
;;
issue_already_has_open_pr)
MSG="This batch already has an open proposer PR: ${OPEN_PR_URL:-"(URL unavailable)"}"
;;
batch_branch_exists_without_pr)
MSG="This batch already has a remote batch branch (${OPEN_PR_BRANCH:-"(unknown branch)"}). Manual inspection is required before any new proposer PR is created."
;;
batch_branch_already_materialized)
MSG="This batch was already materialized by another run on branch ${OPEN_PR_BRANCH:-"(unknown branch)"}. No duplicate PR was created."
;;
explicit_issue_missing_chemin)
MSG="Proposer Apply: cannot process this ticket automatically because field Chemin is missing or unreadable."
;;
explicit_issue_missing_type)
MSG="Proposer Apply: cannot process this ticket automatically because field Type is missing or unreadable."
;;
explicit_issue_not_approved)
MSG="Proposer Apply: this ticket is not currently labeled state/approved."
;;
explicit_issue_rejected)
MSG="Proposer Apply: this ticket has state/rejected and is not eligible for the proposer queue."
;;
no_open_approved_proposer_issue)
MSG="No approved proposer ticket is currently waiting."
;;
*)
MSG="Proposer Apply: skip - ${SKIP_REASON:-unspecified reason}."
;;
esac
export MSG
node --input-type=module - <<'NODE' > /tmp/proposer.skip.comment.json
const msg = process.env.MSG || "";
process.stdout.write(JSON.stringify({ body: msg }));
NODE
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_TO_COMMENT/comments" \
--data-binary @/tmp/proposer.skip.comment.json || true
- name: NPM harden
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
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
cd "$APP_DIR"
npm ci --no-audit --no-fund
- name: Build dist baseline
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
cd "$APP_DIR"
npm run build
- name: Apply proposer batch 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 }}
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)"
BR="$BATCH_BRANCH"
echo "BRANCH=$BR" >> /tmp/proposer.env
git checkout -b "$BR"
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
export FORGE_API="$API_BASE"
LOG="/tmp/proposer-apply.log"
: > "$LOG"
RC=0
FAILED_ISSUE=""
for ISSUE in $TARGET_ISSUES; do
echo "" >> "$LOG"
echo "== ticket #$ISSUE ==" >> "$LOG"
set +e
(cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE" --alias --commit) >> "$LOG" 2>&1
STEP_RC=$?
set -e
if [[ "$STEP_RC" -ne 0 ]]; then
RC="$STEP_RC"
FAILED_ISSUE="$ISSUE"
break
fi
done
echo "APPLY_RC=$RC" >> /tmp/proposer.env
echo "FAILED_ISSUE=${FAILED_ISSUE}" >> /tmp/proposer.env
echo "Apply log (tail):"
tail -n 220 "$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: Rebase bot branch on latest main
continue-on-error: true
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
LOG="/tmp/proposer-apply.log"
git fetch origin "$DEFAULT_BRANCH"
set +e
git rebase "origin/$DEFAULT_BRANCH" >> "$LOG" 2>&1
RC=$?
set -e
if [[ "$RC" -ne 0 ]]; then
git rebase --abort || true
fi
echo "REBASE_RC=$RC" >> /tmp/proposer.env
echo "Rebase log (tail):"
tail -n 220 "$LOG" || true
- name: Comment issues on failure
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
APPLY_RC="${APPLY_RC:-0}"
REBASE_RC="${REBASE_RC:-0}"
if [[ "$APPLY_RC" == "0" && "$REBASE_RC" == "0" ]]; then
echo "No failure detected"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
if [[ -f /tmp/proposer-apply.log ]]; then
BODY="$(tail -n 160 /tmp/proposer-apply.log | sed 's/\r$//')"
else
BODY="(no proposer log found)"
fi
export BODY APPLY_RC REBASE_RC FAILED_ISSUE
if [[ "$APPLY_RC" != "0" ]]; then
export FAILURE_KIND="apply"
else
export FAILURE_KIND="rebase"
fi
node --input-type=module - <<'NODE' > /tmp/proposer.failure.comment.json
const body = process.env.BODY || "";
const applyRc = process.env.APPLY_RC || "0";
const rebaseRc = process.env.REBASE_RC || "0";
const failedIssue = process.env.FAILED_ISSUE || "unknown";
const kind = process.env.FAILURE_KIND || "apply";
const msg =
kind === "apply"
? `Batch proposer failed on ticket #${failedIssue} (rc=${applyRc}).\n\n\`\`\`\n${body}\n\`\`\`\n`
: `Rebase proposer failed on main (rc=${rebaseRc}).\n\n\`\`\`\n${body}\n\`\`\`\n`;
process.stdout.write(JSON.stringify({ body: msg }));
NODE
for ISSUE in ${TARGET_ISSUES:-}; do
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE/comments" \
--data-binary @/tmp/proposer.failure.comment.json || true
done
- name: Late guard against duplicate batch materialization
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${REBASE_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "0" ]] || exit 0
REMOTE_SHA="$(git ls-remote --heads origin "$BATCH_BRANCH" | awk 'NR==1 {print $1}')"
if [[ -n "${REMOTE_SHA:-}" && "${REMOTE_SHA}" != "${END_SHA:-}" ]]; then
echo 'SKIP=1' >> /tmp/proposer.env
echo 'SKIP_REASON="batch_branch_already_materialized"' >> /tmp/proposer.env
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
echo "Remote batch branch already exists at $REMOTE_SHA -> skip duplicate push/PR"
exit 0
fi
echo "Late guard OK"
- 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; }
[[ "${REBASE_RC:-0}" == "0" ]] || { echo "Rebase 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 issues + close issues
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
[[ "${REBASE_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "0" ]] || exit 0
[[ -n "${BRANCH:-}" ]] || { echo "BRANCH unset -> skip PR"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "Missing FORGE_TOKEN"; exit 1; }
OPEN_PRS_JSON="$(curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls?state=open&limit=100")"
export OPEN_PRS_JSON BATCH_BRANCH BATCH_KEY
EXISTING_PR_URL="$(node --input-type=module -e '
const pulls = JSON.parse(process.env.OPEN_PRS_JSON || "[]");
const branch = String(process.env.BATCH_BRANCH || "");
const key = String(process.env.BATCH_KEY || "");
const current = Array.isArray(pulls)
? pulls.find((pr) => {
const ref = String(pr?.head?.ref || "");
const body = String(pr?.body || "");
return (branch && ref === branch) || (key && body.includes(`Batch-Key: ${key}`));
})
: null;
process.stdout.write(current ? String(current.html_url || current.url || "") : "");
')"
if [[ -n "${EXISTING_PR_URL:-}" ]]; then
echo "PR already exists for this batch: $EXISTING_PR_URL"
exit 0
fi
if [[ "${TARGET_COUNT:-0}" == "1" ]]; then
PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}"
else
PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}"
fi
export PR_TITLE TARGET_CHEMIN TARGET_ISSUES BRANCH END_SHA DEFAULT_BRANCH OWNER BATCH_KEY
node --input-type=module -e '
import fs from "node:fs";
const issues = String(process.env.TARGET_ISSUES || "")
.trim()
.split(/\s+/)
.filter(Boolean);
const body = [
`PR auto depuis ticket${issues.length > 1 ? "s" : ""} ${issues.map((n) => `#${n}`).join(", ")} (state/approved).`,
"",
`- Chemin: ${process.env.TARGET_CHEMIN || "(inconnu)"}`,
"- Tickets:",
...issues.map((n) => ` - #${n}`),
`- Branche: ${process.env.BRANCH || ""}`,
`- Commit: ${process.env.END_SHA || "unknown"}`,
`- Batch-Key: ${process.env.BATCH_KEY || ""}`,
"",
"Merge si CI OK."
].join("\n");
fs.writeFileSync(
"/tmp/proposer.pr.json",
JSON.stringify({
title: process.env.PR_TITLE || "proposer: apply tickets",
body,
base: process.env.DEFAULT_BRANCH || "main",
head: `${process.env.OWNER}:${process.env.BRANCH}`,
allow_maintainer_edit: true
})
);
'
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 @/tmp/proposer.pr.json)"
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
}
for ISSUE in $TARGET_ISSUES; do
export ISSUE PR_URL
node --input-type=module -e '
import fs from "node:fs";
const issue = process.env.ISSUE || "";
const url = process.env.PR_URL || "";
const msg =
`PR proposer creee pour le ticket #${issue} : ${url}\n\n` +
`Le ticket est cloture automatiquement ; la discussion peut se poursuivre dans la PR.`;
fs.writeFileSync(
"/tmp/proposer.issue.close.comment.json",
JSON.stringify({ body: msg })
);
'
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE/comments" \
--data-binary @/tmp/proposer.issue.close.comment.json
curl -fsS -X PATCH \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \
--data-binary '{"state":"closed"}'
ISSUE_STATE="$(curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" | \
node --input-type=module -e 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { const j = JSON.parse(s || "{}"); process.stdout.write(String(j.state || "")); });')"
[[ "$ISSUE_STATE" == "closed" ]] || {
echo "Issue #$ISSUE is still not closed after PATCH"
exit 1
}
done
echo "PR: $PR_URL"
- name: Finalize
if: ${{ always() }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
if [[ "${APPLY_RC:-0}" != "0" ]]; then
echo "Apply failed (rc=${APPLY_RC})"
exit "${APPLY_RC}"
fi
if [[ "${REBASE_RC:-0}" != "0" ]]; then
echo "Rebase failed (rc=${REBASE_RC})"
exit "${REBASE_RC}"
fi
echo "Proposer queue 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"

11
.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,10 @@ dist/
# local backups
Dockerfile.bak.*
public/favicon_io.zip
# macOS
.DS_Store
# local temp workspace
.tmp/
public/__ops/health.json

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,105 @@ 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({
legacy: {
collectionsBackwardsCompat: true,
},
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

@@ -0,0 +1,5 @@
{
"accepted_resets": {
"archicrat-ia/chapitre-1/index.html": "Reset intentionnel des ancres après révision doctrinale substantielle du chapitre 1. Site neuf, sans annotations ni compatibilité descendante à préserver."
}
}

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

@@ -10,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

View File

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

View File

@@ -1,6 +1,59 @@
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).
## Mise à jour (2026-03-03) — Gate CI de déploiement (SKIP / HOTPATCH / FULL) + preuves A/B
La procédure de déploiement “vivante” est désormais pilotée par **Gitea Actions** via le workflow :
- `.gitea/workflows/deploy-staging-live.yml`
Ce workflow décide automatiquement :
- **FULL** (rebuild + restart blue + green) dès quun changement impacte le build (ex: `src/content/`, `src/pages/`, `scripts/`, `src/anchors/`, etc.)
- **HOTPATCH** (patch JSON + copie media) quand le changement ne concerne que `src/annotations/` et/ou `public/media/`
- **SKIP** sinon
Les preuves et la procédure de test reproductible A/B sont documentées dans :
➡️ `docs/runbooks/DEPLOY-BLUE-GREEN.md` → section “CI Deploy gate (merge-proof) + Tests A/B + preuve alias injection”.
## 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 +436,5 @@ Fix standard (dans le vrai dossier site/) :
rm -rf node_modules .astro dist
npm ci
npm run dev
</details>

File diff suppressed because it is too large Load Diff

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

@@ -202,4 +202,33 @@ docker compose logs --tail=200 web_blue
docker compose logs --tail=200 web_green
# Si tu veux suivre en live :
docker compose logs -f web_green
docker compose logs -f web_green
## Historique synthétique (2026-03-03) — Stabilisation CI/CD “zéro surprise”
### Problème initial observé
- Déploiement parfois lancé en “hotpatch” alors quun rebuild était nécessaire.
- Sur merge commits, la détection de fichiers modifiés pouvait être ambiguë.
- Résultat : besoin de `force=1` manuel pour éviter des incohérences.
### Correctif appliqué
- Gate CI rendu **merge-proof** :
- lecture de `BEFORE` et `AFTER` depuis `event.json`
- calcul des fichiers modifiés via `git diff --name-only BEFORE AFTER`
- Politique de décision stabilisée :
- FULL auto dès quun changement impacte build/runtime (content/pages/scripts/anchors/etc.)
- HOTPATCH auto uniquement pour annotations/media
### Preuves
- Test A (touch src/content) :
- Gate flags: HAS_FULL=1 HAS_HOTPATCH=0 → MODE=full
- Test B (touch src/annotations) :
- Gate flags: HAS_FULL=0 HAS_HOTPATCH=1 → MODE=hotpatch
### Audit post-déploiement (preuves côté NAS)
- 8081 + 8082 répondent HTTP 200
- `/para-index.json` + `/annotations-index.json` OK
- Aliases injectés visibles dans HTML via `.para-alias` quand alias présent

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.

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

@@ -0,0 +1,683 @@
# START-HERE — Archicratie / Édition Web (v3)
> Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO → localhost auto-sync)
## 0) TL;DR (la règle dor)
- **Gitea = source canonique**.
- **`main` est protégée** : toute modification passe par **branche → PR → CI → merge**.
- **Le NAS nest pas la source** : si un hotfix est fait sur NAS, il doit être **backporté immédiatement** via PR.
- **Le site est statique Astro** : la prod sert du HTML via nginx ; laccès est contrôlé au niveau reverse-proxy (Traefik + Authelia).
- **Le localhost automatique nest pas le repo de dev** : il tourne depuis un **worktree dédié**, synchronisé sur `origin/main`.
---
## 1) Architecture mentale (ultra simple)
- **DEV canonique (Mac Studio)** : édition, dev, tests, commits, pushes
- **Gitea** : dépôt canonique, PR, CI, workflows éditoriaux
- **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)** : routage des hosts
- `staging.archicratie...` → 8081
- `archicratie...` → 8082
- **Authelia** devant, via middleware `chain-auth@file`
- **Localhost auto-sync**
- un **repo canonique de développement**
- un **worktree localhost miroir de `origin/main`**
- un **agent de sync**
- un **agent Astro**
---
## 2) Répertoires & conventions (repo)
### 2.1 Contenu canon (édition)
- `src/content/**` : contenu MD / MDX canon
- `src/pages/**` : routes Astro
- `src/components/**` : composants UI
- `src/layouts/**` : layouts
- `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` : retrait IDs dupliqués
- `scripts/build-para-index.mjs` : index paragraphes
- `scripts/build-annotations-index.mjs` : index annotations
- `scripts/check-anchors.mjs` : contrat stabilité dancres
- `scripts/check-annotations*.mjs` : sanity YAML + médias
> Important : ces scripts ne sont pas accessoires.
> Ils font partie du contrat de stabilité éditoriale.
---
## 3) Les trois espaces à ne jamais confondre
### 3.1 Repo canonique de développement
```text
/Volumes/FunIA/dev/archicratie-edition/site
```
Usage :
- développement normal
- branches de travail
- nouvelles fonctionnalités
- corrections manuelles
- commits
- pushes
- PR
### 3.2 Worktree localhost miroir de `main`
```text
/Users/s-funia/ops-local/archicratie/localhost-worktree
```
Branche attendue :
```text
localhost-sync
```
Usage :
- exécuter le localhost automatique
- refléter `origin/main`
- ne jamais servir despace de développement
### 3.3 Ops local hors repo
```text
/Users/s-funia/ops-local/archicratie
```
Usage :
- scripts dexploitation
- état
- logs
- automatisation `launchd`
---
## 4) Pourquoi cette séparation existe
Il ne faut pas utiliser le repo canonique de développement comme serveur localhost permanent.
Sinon on mélange :
- travail en cours
- commits non poussés
- essais temporaires
- état réellement publié sur `main`
Le résultat devient ambigu.
La séparation retenue est donc :
- **repo canonique** = espace de développement
- **worktree localhost** = miroir exécutable de `origin/main`
- **ops local** = scripts et automatisation
Cest cette séparation qui rend le système lisible, robuste et opérable.
---
## 5) Workflow Git “pro” (main protégée)
### 5.1 Cycle standard (toute modif)
```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"
```
### 5.2 PR vers `main`
- ouvrir une PR dans Gitea
- attendre une CI verte
- merger
- laisser les workflows faire le reste
### 5.3 Cas spécial : hotfix prod (NAS)
On peut faire un hotfix durgence côté NAS si nécessaire.
Mais létat final doit toujours revenir dans Gitea :
- branche
- PR
- CI
- merge
---
## 6) Déploiement (NAS) — principe
### 6.1 Release pack
On génère un pack reproductible, puis on déploie.
### 6.2 Blue/Green
- `web_blue` = staging (`8081`)
- `web_green` = live (`8082`)
Le reverse-proxy choisit lupstream selon le host demandé.
---
## 7) Happy path complet
### 7.1 DEV (Mac)
```bash
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
```
### 7.2 Push + PR
```bash
git add -A
git commit -m "chore: my change"
git push -u origin chore/my-change-YYYYMMDD
```
Puis ouvrir la PR dans Gitea.
### 7.3 Déploiement NAS
Voir :
```text
docs/runbooks/DEPLOY-BLUE-GREEN.md
```
---
## 8) Localhost auto-sync — ce quil faut retenir
Le localhost automatique sert à voir **la vérité de `main`**, pas à développer du neuf.
### 8.1 Scripts principaux
#### Script de sync
```text
~/ops-local/archicratie/auto-sync-localhost.sh
```
Rôle :
- fetch `origin/main`
- réaligner le worktree localhost
- lancer `npm ci` si besoin
- redéclencher lagent Astro si nécessaire
#### Script Astro
```text
~/ops-local/archicratie/run-astro-localhost.sh
```
Rôle :
- lancer `astro dev`
- depuis le bon worktree
- avec le bon runtime Node
- sur `127.0.0.1:4321`
> Oui : ce script est nécessaire.
> Il isole proprement le lancement du serveur Astro dans un contexte `launchd` stable.
### 8.2 LaunchAgents
#### Agent sync
```text
~/Library/LaunchAgents/me.archicratie.localhost-sync.plist
```
#### Agent Astro
```text
~/Library/LaunchAgents/me.archicratie.localhost-astro.plist
```
### 8.3 Document de référence
Pour tout le détail dexploitation du localhost automatique, lire :
```text
docs/OPS-LOCALHOST-AUTO-SYNC.md
```
---
## 9) Règle dor : il y a deux usages locaux distincts
### 9.1 Voir ce qui est réellement sur `main`
Utiliser :
```text
http://127.0.0.1:4321
```
Ce localhost doit être considéré comme :
**un miroir local exécutable de `origin/main`**
### 9.2 Développer / tester une nouvelle fonctionnalité
Utiliser le repo canonique :
```bash
cd /Volumes/FunIA/dev/archicratie-edition/site
npm run dev
```
Donc :
- **localhost auto-sync** = vérité de `main`
- **localhost de dev manuel** = expérimentation en cours
Il ne faut pas les confondre.
---
## 10) Ce quil ne faut pas faire
### 10.1 Ne pas développer dans le worktree localhost
Le worktree localhost est piloté automatiquement.
Il peut être :
- réaligné
- nettoyé
- redémarré
Donc :
- pas de commits dedans
- pas de dev feature dedans
- pas dexpérimentation de fond dedans
### 10.2 Ne pas utiliser le repo canonique comme miroir auto-sync
Sinon on mélange :
- espace de dev
- état publié
- serveur local permanent
### 10.3 Ne pas remettre les scripts ops sur un volume externe
Les scripts dops doivent rester sous `HOME`.
Le fait de les mettre sous `/Volumes/...` a déjà provoqué des erreurs du type :
```text
Operation not permitted
```
### 10.4 Ne pas supprimer `run-astro-localhost.sh`
Ce script fait partie de larchitecture actuelle.
Le supprimer reviendrait à réintroduire le flou entre sync Git et exécution dAstro.
---
## 11) Commandes de contrôle essentielles
### 11.1 État global
```bash
~/ops-local/archicratie/doctor-localhost.sh
```
### 11.2 État Git
```bash
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
git -C ~/ops-local/archicratie/localhost-worktree branch --show-current
```
### 11.3 État LaunchAgents
```bash
launchctl print "gui/$(id -u)/me.archicratie.localhost-sync" | sed -n '1,160p'
launchctl print "gui/$(id -u)/me.archicratie.localhost-astro" | sed -n '1,160p'
```
### 11.4 État logs
```bash
tail -n 120 ~/ops-local/archicratie/logs/auto-sync-localhost.log
tail -n 120 ~/ops-local/archicratie/logs/astro-localhost.log
tail -n 80 ~/Library/Logs/archicratie-localhost-sync.err.log
tail -n 80 ~/Library/Logs/archicratie-localhost-astro.err.log
```
### 11.5 État serveur
```bash
lsof -nP -iTCP:4321 -sTCP:LISTEN
PID="$(lsof -tiTCP:4321 -sTCP:LISTEN | head -n 1)"
ps -p "$PID" -o pid=,command=
lsof -a -p "$PID" -d cwd
```
### 11.6 Vérification contenu
```bash
curl -s http://127.0.0.1:4321/archicrat-ia/prologue/ | grep -n "taxe Zucman"
```
---
## 12) Problèmes classiques + diagnostic
### 12.1 “Le staging ne ressemble pas au local”
Comparer les upstream directs :
```bash
curl -sS http://127.0.0.1:8081/ | head -n 2
curl -sS http://127.0.0.1:8082/ | head -n 2
```
Vérifier le routeur edge :
```bash
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router'
```
Voir :
```text
docs/runbooks/EDGE-TRAEFIK.md
```
### 12.2 Canonical incorrect
Cause probable : `PUBLIC_SITE` mal injecté au build.
Test :
```bash
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1
```
Voir :
```text
docs/runbooks/ENV-PUBLIC_SITE.md
```
### 12.3 Contrat anchors en échec après migration dURL
Procédure safe :
```bash
cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S)
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
npm run test:anchors
```
### 12.4 “Le localhost auto-sync ne montre pas les dernières modifs”
Commande réflexe :
```bash
~/ops-local/archicratie/doctor-localhost.sh
```
Puis :
```bash
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
```
Si les SHA diffèrent :
- le sync na pas tourné
- ou lagent sync a un problème
### 12.5 “Le SHA est bon mais le contenu web est faux”
Vérifier quel Astro écoute réellement :
```bash
lsof -nP -iTCP:4321 -sTCP:LISTEN
PID="$(lsof -tiTCP:4321 -sTCP:LISTEN | head -n 1)"
ps -p "$PID" -o pid=,command=
lsof -a -p "$PID" -d cwd
```
Attendu :
- commande contenant `astro dev`
- cwd = `~/ops-local/archicratie/localhost-worktree`
### 12.6 Erreur `EBADENGINE`
Cause probable :
- Node 23 utilisé au lieu de Node 22
Résolution :
- forcer `node@22` dans les scripts et les LaunchAgents
### 12.7 Erreur `Operation not permitted`
Cause probable :
- scripts dops placés sous `/Volumes/...`
Résolution :
- garder les scripts sous :
```text
~/ops-local/archicratie
```
### 12.8 Erreur `EPERM` sur `astro.mjs`
Cause probable :
- ancien worktree sur volume externe
- ancien chemin résiduel
- Astro lancé depuis un mauvais emplacement
Résolution :
- worktree localhost sous :
```text
~/ops-local/archicratie/localhost-worktree
```
- scripts cohérents avec ce chemin
- réinstallation propre via :
```bash
~/ops-local/archicratie/install-localhost-sync.sh
```
---
## 13) Redémarrage machine
Après reboot, le comportement attendu est :
1. le LaunchAgent sync se recharge
2. le LaunchAgent Astro se recharge
3. le worktree localhost est réaligné
4. Astro redémarre sur `127.0.0.1:4321`
### Vérification rapide après reboot
```bash
~/ops-local/archicratie/doctor-localhost.sh
```
Si nécessaire :
```bash
~/ops-local/archicratie/install-localhost-sync.sh
```
---
## 14) Procédure de secours manuelle
### Forcer un sync
```bash
~/ops-local/archicratie/auto-sync-localhost.sh
```
### Réinstaller proprement le dispositif local
```bash
~/ops-local/archicratie/install-localhost-sync.sh
```
### Diagnostic complet
```bash
~/ops-local/archicratie/doctor-localhost.sh
```
---
## 15) Décision dexploitation finale
La politique retenue est la suivante :
- **repo canonique** = espace de développement
- **worktree localhost** = miroir automatique de `main`
- **ops sous HOME** = scripts, logs, automation
- **LaunchAgent sync** = réalignement Git
- **LaunchAgent Astro** = exécution stable du serveur local
- **Astro local** = lancé uniquement depuis le worktree localhost
Cette séparation rend le dispositif plus :
- lisible
- robuste
- opérable
- antifragile
---
## 16) Résumé opératoire
### Pour voir la vérité de `main`
Ouvrir :
```text
http://127.0.0.1:4321
```
Le serveur doit provenir de :
```text
/Users/s-funia/ops-local/archicratie/localhost-worktree
```
### Pour développer
Travailler dans :
```text
/Volumes/FunIA/dev/archicratie-edition/site
```
avec les commandes habituelles.
### Pour réparer vite
```bash
~/ops-local/archicratie/doctor-localhost.sh
~/ops-local/archicratie/auto-sync-localhost.sh
```
---
## 17) Mémoire courte
Si un jour plus rien nest clair, repartir de ces commandes :
```bash
~/ops-local/archicratie/doctor-localhost.sh
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
lsof -nP -iTCP:4321 -sTCP:LISTEN
```
Puis lire :
```bash
tail -n 120 ~/ops-local/archicratie/logs/auto-sync-localhost.log
tail -n 120 ~/ops-local/archicratie/logs/astro-localhost.log
```
---
## 18) Statut actuel visé
Quand tout fonctionne correctement :
- le worktree localhost pointe sur le même SHA que `origin/main`
- `astro dev` écoute sur `127.0.0.1:4321`
- son cwd est `~/ops-local/archicratie/localhost-worktree`
- le contenu servi correspond au contenu mergé sur `main`
Cest létat de référence à préserver.

View File

@@ -15,6 +15,7 @@ Toujours isoler : **Local**, **Gitea**, **NAS**, **Navigateur**.
---
<a id="proposer-404"></a>
## 1) “Proposer” ouvre Gitea mais retourne 404 / non autorisé
### Symptôme
@@ -38,6 +39,7 @@ Puis rebuild + restart du container + smoke.
---
<a id="proposer-double-onglet"></a>
## 2) Double onglet à la validation du flow “Proposer”
### Symptôme
@@ -64,7 +66,9 @@ 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

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,546 @@
# 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).
## 10) CI Deploy (Gitea Actions) — Gate SKIP / HOTPATCH / FULL (merge-proof) + preuves
Cette section documente le comportement **canonique** du workflow :
- `.gitea/workflows/deploy-staging-live.yml`
Objectif : **zéro surprise**.
On ne veut plus “penser à force=1”.
Le gate doit décider automatiquement, y compris sur des **merge commits**.
### 10.1 — Principe (ce que fait réellement le gate)
Le job `deploy` calcule les fichiers modifiés entre :
- `BEFORE` = commit précédent (avant le push sur main)
- `AFTER` = commit actuel (après le push / merge sur main)
Puis il classe le déploiement dans un mode :
- **MODE=full**
- rebuild image + restart `archicratie-web-blue` (8081) + `archicratie-web-green` (8082)
- warmup endpoints (para-index, annotations-index, pagefind.js)
- vérification canonical staging + live
- **MODE=hotpatch**
- rebuild dun `annotations-index.json` consolidé depuis `src/annotations/**`
- patch direct dans les conteneurs en cours dexécution (blue+green)
- copie des médias modifiés `public/media/**` vers `/usr/share/nginx/html/media/**`
- smoke sur `/annotations-index.json` des deux ports
- **MODE=skip**
- pas de déploiement (on évite le bruit)
⚠️ Important : le mode “hotpatch” **ne rebuild pas** Astro.
Donc toute modification de contenu, routes, scripts, anchors, etc. doit déclencher **full**.
### 10.2 — Matrice de décision (règles officielles)
Le gate définit deux flags :
- `HAS_FULL=1` si changement “build-impacting”
- `HAS_HOTPATCH=1` si changement “annotations/media only”
Règle de priorité :
1) Si `HAS_FULL=1`**MODE=full**
2) Sinon si `HAS_HOTPATCH=1`**MODE=hotpatch**
3) Sinon → **MODE=skip**
#### 10.2.1 — Changements qui déclenchent FULL (build-impacting)
Exemples typiques (non exhaustif, mais on couvre le cœur) :
- `src/content/**` (contenu MD/MDX)
- `src/pages/**` (routes Astro)
- `src/anchors/**` (aliases dancres)
- `scripts/**` (tooling postbuild : injection, index, tests)
- `src/layouts/**`, `src/components/**`, `src/styles/**` (rendu et scripts inline)
- `astro.config.mjs`, `package.json`, `package-lock.json`
- `Dockerfile`, `docker-compose.yml`, `nginx.conf`
- `.gitea/workflows/**` (changement infra CI/CD)
=> On veut **full** pour garantir cohérence et éviter “site partiellement mis à jour”.
#### 10.2.2 — Changements qui déclenchent HOTPATCH (sans rebuild)
Uniquement :
- `src/annotations/**` (shards YAML)
- `public/media/**` (assets média)
=> On veut hotpatch pour vitesse et éviter rebuild NAS.
### 10.3 — “Merge-proof” : pourquoi on ne lit PAS seulement `git show $SHA`
Sur un merge commit, `git show --name-only $SHA` peut être trompeur selon le contexte.
La méthode robuste est :
- utiliser `event.json` (Gitea Actions) pour récupérer `before` et `after`
- calculer `git diff --name-only BEFORE AFTER`
Cest ce qui rend le gate **merge-proof**.
### 10.4 — Tests de preuve A/B (reproductibles)
Ces tests valident le gate sans ambiguïté.
But : vérifier que le mode choisi est EXACTEMENT celui attendu.
#### Test A — toucher `src/content/...` (FULL auto)
1) Créer une branche test
2) Modifier 1 fichier dans `src/content/` (ex : ajouter une ligne de commentaire non destructive)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=1 HAS_HOTPATCH=0`
- `✅ build-impacting change -> MODE=full (rebuild+restart)`
- Les étapes FULL (blue puis green) sexécutent réellement
#### Test B — toucher `src/annotations/...` uniquement (HOTPATCH auto)
1) Créer une branche test
2) Modifier 1 fichier sous `src/annotations/**` (ex: un champ comment, ts, etc.)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=0 HAS_HOTPATCH=1`
- `✅ annotations/media change -> MODE=hotpatch`
- Les étapes FULL sont “skip” (durée 0s)
- Létape HOTPATCH sexécute réellement
### 10.5 — Preuve opérationnelle côté NAS (2 URLs + 2 commandes)
But : prouver que staging+live servent bien les endpoints essentiels (et que le déploiement na pas “fait semblant”).
#### 10.5.1 — Deux URLs à vérifier (staging et live)
- Staging (blue) : `http://127.0.0.1:8081/`
- Live (green) : `http://127.0.0.1:8082/`
#### 10.5.2 — Deux commandes minimales (zéro débat)
```bash
curl -fsSI http://127.0.0.1:8081/ | head -n 1
curl -fsSI http://127.0.0.1:8082/ | head -n 1
---
## 10) CI Deploy (Gitea Actions) — Gate SKIP / HOTPATCH / FULL (merge-proof) + preuves
Cette section documente le comportement **canonique** du workflow :
- `.gitea/workflows/deploy-staging-live.yml`
Objectif : **zéro surprise**.
On ne veut plus “penser à force=1”.
Le gate doit décider automatiquement, y compris sur des **merge commits**.
### 10.1 — Principe (ce que fait réellement le gate)
Le job `deploy` calcule les fichiers modifiés entre :
- `BEFORE` = commit précédent (avant le push sur main)
- `AFTER` = commit actuel (après le push / merge sur main)
Puis il classe le déploiement dans un mode :
- **MODE=full**
- rebuild image + restart `archicratie-web-blue` (8081) + `archicratie-web-green` (8082)
- warmup endpoints (para-index, annotations-index, pagefind.js)
- vérification canonical staging + live
- **MODE=hotpatch**
- rebuild dun `annotations-index.json` consolidé depuis `src/annotations/**`
- patch direct dans les conteneurs en cours dexécution (blue+green)
- copie des médias modifiés `public/media/**` vers `/usr/share/nginx/html/media/**`
- smoke sur `/annotations-index.json` des deux ports
- **MODE=skip**
- pas de déploiement (on évite le bruit)
⚠️ Important : le mode “hotpatch” **ne rebuild pas** Astro.
Donc toute modification de contenu, routes, scripts, anchors, etc. doit déclencher **full**.
### 10.2 — Matrice de décision (règles officielles)
Le gate définit deux flags :
- `HAS_FULL=1` si changement “build-impacting”
- `HAS_HOTPATCH=1` si changement “annotations/media only”
Règle de priorité :
1) Si `HAS_FULL=1` → **MODE=full**
2) Sinon si `HAS_HOTPATCH=1` → **MODE=hotpatch**
3) Sinon → **MODE=skip**
#### 10.2.1 — Changements qui déclenchent FULL (build-impacting)
Exemples typiques (non exhaustif, mais on couvre le cœur) :
- `src/content/**` (contenu MD/MDX)
- `src/pages/**` (routes Astro)
- `src/anchors/**` (aliases dancres)
- `scripts/**` (tooling postbuild : injection, index, tests)
- `src/layouts/**`, `src/components/**`, `src/styles/**` (rendu et scripts inline)
- `astro.config.mjs`, `package.json`, `package-lock.json`
- `Dockerfile`, `docker-compose.yml`, `nginx.conf`
- `.gitea/workflows/**` (changement infra CI/CD)
=> On veut **full** pour garantir cohérence et éviter “site partiellement mis à jour”.
#### 10.2.2 — Changements qui déclenchent HOTPATCH (sans rebuild)
Uniquement :
- `src/annotations/**` (shards YAML)
- `public/media/**` (assets média)
=> On veut hotpatch pour vitesse et éviter rebuild NAS.
### 10.3 — “Merge-proof” : pourquoi on ne lit PAS seulement `git show $SHA`
Sur un merge commit, `git show --name-only $SHA` peut être trompeur selon le contexte.
La méthode robuste est :
- utiliser `event.json` (Gitea Actions) pour récupérer `before` et `after`
- calculer `git diff --name-only BEFORE AFTER`
Cest ce qui rend le gate **merge-proof**.
### 10.4 — Tests de preuve A/B (reproductibles)
Ces tests valident le gate sans ambiguïté.
But : vérifier que le mode choisi est EXACTEMENT celui attendu.
#### Test A — toucher `src/content/...` (FULL auto)
1) Créer une branche test
2) Modifier 1 fichier dans `src/content/` (ex : ajouter une ligne de commentaire non destructive)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=1 HAS_HOTPATCH=0`
- `✅ build-impacting change -> MODE=full (rebuild+restart)`
- Les étapes FULL (blue puis green) sexécutent réellement
#### Test B — toucher `src/annotations/...` uniquement (HOTPATCH auto)
1) Créer une branche test
2) Modifier 1 fichier sous `src/annotations/**` (ex: un champ comment, ts, etc.)
3) PR → merge dans `main`
4) Vérifier dans `deploy-staging-live.yml` :
Attendus :
- `Gate flags: HAS_FULL=0 HAS_HOTPATCH=1`
- `✅ annotations/media change -> MODE=hotpatch`
- Les étapes FULL sont “skip” (durée 0s)
- Létape HOTPATCH sexécute réellement
### 10.5 — Preuve opérationnelle côté NAS (2 URLs + 2 commandes)
But : prouver que staging+live servent bien les endpoints essentiels (et que le déploiement na pas “fait semblant”).
#### 10.5.1 — Deux URLs à vérifier (staging et live)
- Staging (blue) : `http://127.0.0.1:8081/`
- Live (green) : `http://127.0.0.1:8082/`
#### 10.5.2 — Deux commandes minimales (zéro débat)
en bash :
curl -fsSI http://127.0.0.1:8081/ | head -n 1
curl -fsSI http://127.0.0.1:8082/ | head -n 1
Attendu : HTTP/1.1 200 OK des deux côtés.
10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page
Contexte : lorsquun paragraphe change (ex: ticket “Proposer” appliqué),
lID de paragraphe peut changer, mais on doit préserver les liens anciens via :
src/anchors/anchor-aliases.json
injection build-time dans dist (span .para-alias)
10.6.1 — Check rapide (staging + live)
Remplacer OLD/NEW par tes ids réels :
Attendu : HTTP/1.1 200 OK des deux côtés.
10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page
Contexte : lorsquun paragraphe change (ex: ticket “Proposer” appliqué),
lID de paragraphe peut changer, mais on doit préserver les liens anciens via :
src/anchors/anchor-aliases.json
injection build-time dans dist (span .para-alias)
10.6.1 — Check rapide (staging + live)
Remplacer OLD/NEW par tes ids réels :
OLD="p-1-60c7ea48"
NEW="p-1-a21087b0"
for P in 8081 8082; do
echo "=== $P ==="
HTML="$(curl -fsS "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/" | tr -d '\r')"
echo "OLD count: $(printf '%s' "$HTML" | grep -o "$OLD" | wc -l | tr -d ' ')"
echo "NEW count: $(printf '%s' "$HTML" | grep -o "$NEW" | wc -l | tr -d ' ')"
printf '%s\n' "$HTML" | grep -nE "$OLD|$NEW|class=\"para-alias\"" | head -n 40 || true
done
Attendu :
présence dun alias : <span id="$OLD" class="para-alias"...>
présence du nouveau paragraphe : <p id="$NEW">...
10.6.2 — Check “lien ancien ne casse pas” (HTTP 200)
for P in 8081 8082; do
curl -fsSI "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/#${OLD}" | head -n 1
done
Attendu : HTTP/1.1 200 OK et navigation fonctionnelle côté navigateur.
10.7 — Troubleshooting gate (symptômes typiques)
Symptom 1 : job bloqué “Set up job” très longtemps
Causes fréquentes :
runner indisponible / capacity saturée
runner ne récupère pas les tâches (fetch_timeout trop court + réseau instable)
erreur dans “Gate — decide …” qui casse bash (et donne limpression dun hang)
Commandes NAS (diagnostic rapide) :
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | grep -E 'gitea-act-runner|registry|archicratie-web'
docker logs --since 30m --tail 400 gitea-act-runner | tail -n 200
Symptom 2 : conditional binary operator expected
Cause :
test bash du type [[ "$X" == "1" && "$Y" == "2" ]] mal formé
variable vide non quotée
usage dun opérateur non supporté dans la shell effective
Fix :
set -euo pipefail
toujours quoter : [[ "${VAR:-}" == "..." ]]
logguer BEFORE/AFTER/FORCE et sassurer quils ne sont pas vides
Symptom 3 : le gate liste “trop de fichiers” alors quon a changé 1 seul fichier
Cause :
comparaison faite sur le mauvais range (ex: git show sur merge, ou mauvais parent)
Fix :
toujours utiliser git diff --name-only "$BEFORE" "$AFTER" (merge-proof)
confirmer dans le log : Gate ctx: BEFORE=... AFTER=...

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.

1330
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,35 +4,33 @@
"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",
"build:search": "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 && npm run build:search",
"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"
"@astrojs/mdx": "^5.0.0",
"astro": "^6.0.2"
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.0",
"@astrojs/sitemap": "^3.7.1",
"mammoth": "^1.11.0",
"pagefind": "^1.4.0",
"rehype-autolink-headings": "^7.1.0",

View File

@@ -1 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone","orientation":"any"}

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

@@ -9,8 +9,9 @@ import { spawnSync } from "node:child_process";
*
* Conçu pour:
* - prendre un ticket [Correction]/[Fact-check] (issue) avec Chemin + Ancre + Proposition
* - retrouver le bon paragraphe dans le .mdx
* - retrouver le bon paragraphe dans le .mdx/.md
* - remplacer proprement
* - ne JAMAIS toucher au frontmatter
* - optionnel: écrire un alias dancre old->new (build-time) dans src/anchors/anchor-aliases.json
* - optionnel: committer automatiquement
* - optionnel: fermer le ticket (après commit)
@@ -39,7 +40,7 @@ Env (recommandé):
Notes:
- Si dist/<chemin>/index.html est absent, le script lance "npm run build" sauf si --no-build.
- Sauvegarde automatique: <fichier>.bak.issue-<N> (uniquement si on écrit)
- Sauvegarde automatique: .tmp/apply-ticket/<fichier>.bak.issue-<N> (uniquement si on écrit)
- Avec --alias : le script rebuild pour identifier le NOUVEL id, puis écrit l'alias old->new.
- Refuse automatiquement les Pull Requests (PR) : ce ne sont pas des tickets éditoriaux.
`);
@@ -89,6 +90,7 @@ const CWD = process.cwd();
const CONTENT_ROOT = path.join(CWD, "src", "content");
const DIST_ROOT = path.join(CWD, "dist");
const ALIASES_FILE = path.join(CWD, "src", "anchors", "anchor-aliases.json");
const BACKUP_ROOT = path.join(CWD, ".tmp", "apply-ticket");
/* -------------------------- utils texte / matching -------------------------- */
@@ -136,31 +138,26 @@ function scoreText(candidate, targetText) {
let hit = 0;
for (const w of tgtSet) if (blkSet.has(w)) hit++;
// Bonus si un long préfixe ressemble
const tgtNorm = normalizeText(stripMd(targetText));
const blkNorm = normalizeText(stripMd(candidate));
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
// Ratio bonus (0..100)
const ratio = hit / Math.max(1, tgtSet.size);
const ratioBonus = Math.round(ratio * 100);
return prefixBonus + hit + ratioBonus;
}
function bestBlockMatchIndex(blocks, targetText) {
let best = { i: -1, score: -1 };
for (let i = 0; i < blocks.length; i++) {
const sc = scoreText(blocks[i], targetText);
if (sc > best.score) best = { i, score: sc };
}
return best;
}
function splitParagraphBlocks(mdxText) {
const raw = String(mdxText ?? "").replace(/\r\n/g, "\n");
return raw.split(/\n{2,}/);
function rankedBlockMatches(blocks, targetText, limit = 5) {
return blocks
.map((b, i) => ({
i,
score: scoreText(b, targetText),
excerpt: stripMd(b).slice(0, 140),
}))
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
function isLikelyExcerpt(s) {
@@ -172,6 +169,89 @@ function isLikelyExcerpt(s) {
return false;
}
/* --------------------------- frontmatter / structure ------------------------ */
function normalizeNewlines(s) {
return String(s ?? "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
}
function splitMdxFrontmatter(src) {
const text = normalizeNewlines(src);
const m = text.match(/^---\n[\s\S]*?\n---\n?/);
if (!m) {
return {
hasFrontmatter: false,
frontmatter: "",
body: text,
};
}
const frontmatter = m[0];
const body = text.slice(frontmatter.length);
return {
hasFrontmatter: true,
frontmatter,
body,
};
}
function joinMdxFrontmatter(frontmatter, body) {
if (!frontmatter) return String(body ?? "");
return String(frontmatter) + String(body ?? "");
}
function assertFrontmatterIntegrity({ hadFrontmatter, originalFrontmatter, finalText, filePath }) {
if (!hadFrontmatter) return;
const text = normalizeNewlines(finalText);
if (!text.startsWith("---\n")) {
throw new Error(`Frontmatter perdu pendant la mise à jour de ${filePath}`);
}
if (!text.startsWith(originalFrontmatter)) {
throw new Error(`Frontmatter altéré pendant la mise à jour de ${filePath}`);
}
}
function splitParagraphBlocksPreserve(bodyText) {
const text = normalizeNewlines(bodyText);
if (!text) {
return { blocks: [], separators: [] };
}
const blocks = [];
const separators = [];
const re = /(\n{2,})/g;
let last = 0;
let m;
while ((m = re.exec(text))) {
blocks.push(text.slice(last, m.index));
separators.push(m[1]);
last = m.index + m[1].length;
}
blocks.push(text.slice(last));
return { blocks, separators };
}
function joinParagraphBlocksPreserve(blocks, separators) {
if (!Array.isArray(blocks) || blocks.length === 0) return "";
let out = "";
for (let i = 0; i < blocks.length; i++) {
out += blocks[i];
if (i < separators.length) out += separators[i];
}
return out;
}
/* ------------------------------ utils système ------------------------------ */
function run(cmd, args, opts = {}) {
@@ -251,7 +331,9 @@ function pickSection(body, 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);
@@ -266,11 +348,13 @@ function pickSection(body, markers) {
"\n## Proposition",
"\n## Problème",
];
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();
}
@@ -298,8 +382,6 @@ function extractAnchorIdAnywhere(text) {
function extractCheminFromAnyUrl(text) {
const s = String(text || "");
// Exemple: http://localhost:4321/archicratie/prologue/#p-3-xxxx
// ou: /archicratie/prologue/#p-3-xxxx
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
return m ? m[1] : "";
}
@@ -400,7 +482,7 @@ async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"User-Agent": "archicratie-apply-ticket/2.0",
"User-Agent": "archicratie-apply-ticket/2.1",
},
});
if (!res.ok) {
@@ -416,7 +498,7 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
Authorization: `token ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-apply-ticket/2.0",
"User-Agent": "archicratie-apply-ticket/2.1",
};
if (comment) {
@@ -425,7 +507,11 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
}
const url = `${base}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
const res = await fetch(url, { method: "PATCH", headers, body: JSON.stringify({ state: "closed" }) });
const res = await fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify({ state: "closed" }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
@@ -529,10 +615,9 @@ async function main() {
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo}`);
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
// Guard PR (Pull Request = "Demande d'ajout" = pas un ticket éditorial)
if (issue?.pull_request) {
console.error(`❌ #${issueNum} est une Pull Request (demande dajout), pas un ticket éditorial.`);
console.error(`➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.`);
console.error("➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.");
process.exit(2);
}
@@ -553,7 +638,6 @@ async function main() {
ancre = (ancre || "").trim();
if (ancre.startsWith("#")) ancre = ancre.slice(1);
// fallback si ticket mal formé
if (!ancre) ancre = extractAnchorIdAnywhere(title) || extractAnchorIdAnywhere(body);
chemin = normalizeChemin(chemin);
@@ -592,7 +676,6 @@ async function main() {
const distHtmlPath = path.join(DIST_ROOT, chemin.replace(/^\/+|\/+$/g, ""), "index.html");
await ensureBuildIfNeeded(distHtmlPath);
// Texte cible: préférence au texte complet (ticket), sinon dist si extrait probable
let targetText = texteActuel;
let distText = "";
@@ -609,21 +692,24 @@ async function main() {
throw new Error("Impossible de reconstruire le texte du paragraphe (ni texte actuel, ni dist html).");
}
const original = await fs.readFile(contentFile, "utf-8");
const blocks = splitParagraphBlocks(original);
const originalRaw = await fs.readFile(contentFile, "utf-8");
const { hasFrontmatter, frontmatter, body: originalBody } = splitMdxFrontmatter(originalRaw);
const best = bestBlockMatchIndex(blocks, targetText);
const split = splitParagraphBlocksPreserve(originalBody);
const blocks = split.blocks;
const separators = split.separators;
if (!blocks.length) {
throw new Error(`Aucun bloc éditorial exploitable dans ${path.relative(CWD, contentFile)}`);
}
const ranked = rankedBlockMatches(blocks, targetText, 5);
const best = ranked[0] || { i: -1, score: -1, excerpt: "" };
const runnerUp = ranked[1] || null;
// seuil de sécurité
if (best.i < 0 || best.score < 40) {
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
const ranked = blocks
.map((b, i) => ({ i, score: scoreText(b, targetText), excerpt: stripMd(b).slice(0, 140) }))
.sort((a, b) => b.score - a.score)
.slice(0, 5);
console.error("Top candidates:");
for (const r of ranked) {
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
@@ -631,12 +717,34 @@ async function main() {
process.exit(2);
}
if (runnerUp) {
const ambiguityGap = best.score - runnerUp.score;
if (ambiguityGap < 15) {
console.error("❌ Match ambigu: le meilleur candidat est trop proche du second.");
console.error(`➡️ best=${best.score} / second=${runnerUp.score} / gap=${ambiguityGap}`);
console.error("Top candidates:");
for (const r of ranked) {
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
}
process.exit(2);
}
}
const beforeBlock = blocks[best.i];
const afterBlock = proposition.trim();
const nextBlocks = blocks.slice();
nextBlocks[best.i] = afterBlock;
const updated = nextBlocks.join("\n\n");
const updatedBody = joinParagraphBlocksPreserve(nextBlocks, separators);
const updatedRaw = joinMdxFrontmatter(frontmatter, updatedBody);
assertFrontmatterIntegrity({
hadFrontmatter: hasFrontmatter,
originalFrontmatter: frontmatter,
finalText: updatedRaw,
filePath: path.relative(CWD, contentFile),
});
console.log(`🧩 Matched block #${best.i + 1}/${blocks.length} score=${best.score}`);
@@ -650,13 +758,15 @@ async function main() {
return;
}
// backup uniquement si on écrit
const bakPath = `${contentFile}.bak.issue-${issueNum}`;
const relContentFile = path.relative(CWD, contentFile);
const bakPath = path.join(BACKUP_ROOT, `${relContentFile}.bak.issue-${issueNum}`);
await fs.mkdir(path.dirname(bakPath), { recursive: true });
if (!(await fileExists(bakPath))) {
await fs.writeFile(bakPath, original, "utf-8");
await fs.writeFile(bakPath, originalRaw, "utf-8");
}
await fs.writeFile(contentFile, updated, "utf-8");
await fs.writeFile(contentFile, updatedRaw, "utf-8");
console.log("✅ Applied.");
let aliasChanged = false;
@@ -677,13 +787,13 @@ async function main() {
if (aliasChanged) {
console.log(`✅ Alias ajouté: ${chemin} ${ancre} -> ${newId}`);
// MàJ dist sans rebuild complet (inject seulement)
run("node", ["scripts/inject-anchor-aliases.mjs"], { cwd: CWD });
} else {
console.log(` Alias déjà présent ou inutile (${ancre} -> ${newId}).`);
}
// garde-fous rapides
run("node", ["scripts/check-anchor-aliases.mjs"], { cwd: CWD });
run("node", ["scripts/verify-anchor-aliases-in-dist.mjs"], { cwd: CWD });
run("npm", ["run", "test:anchors"], { cwd: CWD });
run("node", ["scripts/check-inline-js.mjs"], { cwd: CWD });
}
@@ -713,7 +823,6 @@ async function main() {
return;
}
// mode manuel
console.log("Next (manuel) :");
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
console.log(
@@ -730,4 +839,4 @@ async function main() {
main().catch((e) => {
console.error("💥", e?.message || e);
process.exit(1);
});
});

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

@@ -74,7 +74,24 @@ function loadAllowMissing() {
return new Set(arr.map(String));
}
function loadAcceptedResets() {
const p = path.resolve("config/anchor-churn-allowlist.json");
if (!fssync.existsSync(p)) return {};
const raw = fssync.readFileSync(p, "utf8").trim();
if (!raw) return {};
const data = JSON.parse(raw);
if (!data || typeof data !== "object" || Array.isArray(data)) {
throw new Error("anchor-churn-allowlist.json must be an object");
}
const accepted = data.accepted_resets || {};
if (!accepted || typeof accepted !== "object" || Array.isArray(accepted)) {
throw new Error("anchor-churn-allowlist.json: accepted_resets must be an object");
}
return accepted;
}
const ALLOW_MISSING = loadAllowMissing();
const ACCEPTED_RESETS = loadAcceptedResets();
async function buildSnapshot() {
const absDist = path.resolve(DIST_DIR);
@@ -139,6 +156,7 @@ function diffPage(prevIds, curIds) {
let failed = false;
let changedPages = 0;
let acceptedPages = 0;
for (const p of pages) {
const prevIds = base[p] || null;
@@ -172,6 +190,7 @@ function diffPage(prevIds, curIds) {
const prevN = prevIds.length || 1;
const churn = (added.length + removed.length) / prevN;
const removedRatio = removed.length / prevN;
const acceptedReason = ACCEPTED_RESETS[p] || null;
console.log(
`~ ${p} prev=${prevIds.length} now=${curIds.length}` +
@@ -182,11 +201,23 @@ function diffPage(prevIds, curIds) {
console.log(` removed: ${removed.slice(0, 20).join(", ")}${removed.length > 20 ? " …" : ""}`);
}
if (prevIds.length >= MIN_PREV && churn > THRESHOLD) failed = true;
if (prevIds.length >= MIN_PREV && removedRatio > THRESHOLD) failed = true;
const exceeds =
(prevIds.length >= MIN_PREV && churn > THRESHOLD) ||
(prevIds.length >= MIN_PREV && removedRatio > THRESHOLD);
if (exceeds && acceptedReason) {
acceptedPages += 1;
console.log(` ✅ accepted reset: ${acceptedReason}`);
continue;
}
if (exceeds) failed = true;
}
console.log(`\nSummary: pages compared=${pages.length}, pages changed=${changedPages}`);
console.log(
`\nSummary: pages compared=${pages.length}, pages changed=${changedPages}, accepted resets=${acceptedPages}`
);
if (failed) {
console.error(`FAIL: anchor churn above threshold (threshold=${pct(THRESHOLD)} minPrev=${MIN_PREV})`);
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 },
"cas-ia": { edition: "cas-ia", status: "application", 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,241 @@
#!/usr/bin/env node
import process from "node:process";
function getEnv(name, fallback = "") {
return String(process.env[name] ?? fallback).trim();
}
function sh(value) {
return JSON.stringify(String(value ?? ""));
}
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 pickHeadingValue(body, headingKey) {
const re = new RegExp(
`^##\\s*${escapeRegExp(headingKey)}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n\\s*$)`,
"mi"
);
const m = String(body || "").match(re);
if (!m) return "";
const lines = m[1].split(/\r?\n/).map((l) => l.trim());
for (const l of lines) {
if (!l) continue;
if (l.startsWith("<!--")) continue;
return l.replace(/^\/?/, "/").trim();
}
return "";
}
function normalizeChemin(chemin) {
let c = String(chemin || "").trim();
if (!c) return "";
if (!c.startsWith("/")) c = "/" + c;
if (!c.endsWith("/")) c += "/";
return c;
}
function extractCheminFromAnyUrl(text) {
const s = String(text || "");
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
return m ? m[1] : "";
}
function inferType(issue) {
const title = String(issue?.title || "");
const body = String(issue?.body || "").replace(/\r\n/g, "\n");
const fromBody = String(pickLine(body, "Type") || "").trim().toLowerCase();
if (fromBody) return fromBody;
if (title.startsWith("[Correction]")) return "type/correction";
if (title.startsWith("[Fact-check]") || title.startsWith("[Vérification]")) return "type/fact-check";
return "";
}
function inferChemin(issue) {
const title = String(issue?.title || "");
const body = String(issue?.body || "").replace(/\r\n/g, "\n");
return normalizeChemin(
pickLine(body, "Chemin") ||
pickHeadingValue(body, "Chemin") ||
extractCheminFromAnyUrl(body) ||
extractCheminFromAnyUrl(title)
);
}
function labelsOf(issue) {
return Array.isArray(issue?.labels)
? issue.labels.map((l) => String(l?.name || "")).filter(Boolean)
: [];
}
function issueNumber(issue) {
return Number(issue?.number || issue?.index || 0);
}
function parseMeta(issue) {
const labels = labelsOf(issue);
const type = inferType(issue);
const chemin = inferChemin(issue);
const number = issueNumber(issue);
const hasApproved = labels.includes("state/approved");
const hasRejected = labels.includes("state/rejected");
const isProposer = type === "type/correction" || type === "type/fact-check";
const isOpen = String(issue?.state || "open") === "open";
const isPR = Boolean(issue?.pull_request);
const eligible =
number > 0 &&
isOpen &&
!isPR &&
hasApproved &&
!hasRejected &&
isProposer &&
Boolean(chemin);
return {
issue,
number,
type,
chemin,
labels,
hasApproved,
hasRejected,
eligible,
};
}
async function fetchJson(url, token) {
const res = await fetch(url, {
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"User-Agent": "archicratie-pick-proposer-issue/1.0",
},
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`HTTP ${res.status} ${url}\n${t}`);
}
return await res.json();
}
async function fetchIssue(apiBase, owner, repo, token, n) {
const url = `${apiBase}/api/v1/repos/${owner}/${repo}/issues/${n}`;
return await fetchJson(url, token);
}
async function listOpenIssues(apiBase, owner, repo, token) {
const out = [];
let page = 1;
const limit = 100;
while (true) {
const url = `${apiBase}/api/v1/repos/${owner}/${repo}/issues?state=open&page=${page}&limit=${limit}`;
const batch = await fetchJson(url, token);
if (!Array.isArray(batch) || batch.length === 0) break;
out.push(...batch);
if (batch.length < limit) break;
page += 1;
}
return out;
}
function emitNone(reason) {
process.stdout.write(
[
`TARGET_FOUND="0"`,
`TARGET_REASON=${sh(reason)}`,
`TARGET_PRIMARY_ISSUE=""`,
`TARGET_ISSUES=""`,
`TARGET_COUNT="0"`,
`TARGET_CHEMIN=""`,
].join("\n") + "\n"
);
}
async function main() {
const token = getEnv("FORGE_TOKEN");
const owner = getEnv("GITEA_OWNER");
const repo = getEnv("GITEA_REPO");
const apiBase = (getEnv("FORGE_API") || getEnv("FORGE_BASE")).replace(/\/+$/, "");
const explicit = Number(process.argv[2] || 0);
if (!token) throw new Error("Missing FORGE_TOKEN");
if (!owner || !repo) throw new Error("Missing GITEA_OWNER / GITEA_REPO");
if (!apiBase) throw new Error("Missing FORGE_API / FORGE_BASE");
let metas = [];
if (explicit > 0) {
const issue = await fetchIssue(apiBase, owner, repo, token, explicit);
const meta = parseMeta(issue);
if (!meta.eligible) {
emitNone(
!meta.hasApproved
? "explicit_issue_not_approved"
: meta.hasRejected
? "explicit_issue_rejected"
: !meta.type
? "explicit_issue_missing_type"
: !meta.chemin
? "explicit_issue_missing_chemin"
: "explicit_issue_not_eligible"
);
return;
}
const openIssues = await listOpenIssues(apiBase, owner, repo, token);
metas = openIssues.map(parseMeta).filter((m) => m.eligible && m.chemin === meta.chemin);
} else {
const openIssues = await listOpenIssues(apiBase, owner, repo, token);
metas = openIssues.map(parseMeta).filter((m) => m.eligible);
if (metas.length === 0) {
emitNone("no_open_approved_proposer_issue");
return;
}
metas.sort((a, b) => a.number - b.number);
const first = metas[0];
metas = metas.filter((m) => m.chemin === first.chemin);
}
metas.sort((a, b) => a.number - b.number);
if (metas.length === 0) {
emitNone("no_batch_for_path");
return;
}
const primary = metas[0];
const issues = metas.map((m) => String(m.number));
process.stdout.write(
[
`TARGET_FOUND="1"`,
`TARGET_REASON="ok"`,
`TARGET_PRIMARY_ISSUE=${sh(primary.number)}`,
`TARGET_ISSUES=${sh(issues.join(" "))}`,
`TARGET_COUNT=${sh(issues.length)}`,
`TARGET_CHEMIN=${sh(primary.chemin)}`,
].join("\n") + "\n"
);
}
main().catch((e) => {
console.error("💥 pick-proposer-issue:", e?.message || e);
process.exit(1);
});

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

@@ -0,0 +1,20 @@
import fs from "node:fs";
import path from "node:path";
const root = process.cwd();
const outDir = path.join(root, "public", "__ops");
const outFile = path.join(outDir, "health.json");
const payload = {
service: "archicratie-site",
env: process.env.PUBLIC_OPS_ENV || "unknown",
upstream: process.env.PUBLIC_OPS_UPSTREAM || "unknown",
buildSha: process.env.PUBLIC_BUILD_SHA || "unknown",
builtAt: process.env.PUBLIC_BUILD_TIME || new Date().toISOString(),
};
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
console.log(`✅ ops health written: ${outFile}`);
console.log(payload);

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