Compare commits
164 Commits
docs/unwra
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
| 6919190107 | |||
| 76cdc85f9c | |||
| f2f6df2127 | |||
| dfe13757f7 | |||
| 148ac997df | |||
| 84492d2741 | |||
| 81baadd57f | |||
| 63d0ffc5fc | |||
| 24143fc2c4 | |||
| 55370b704f | |||
| b8a3ce1337 | |||
| 7f9baedf41 | |||
| 1adbe1c7a3 | |||
| 107a26352f | |||
| 1c2b9ddbb6 | |||
| be99460d4d | |||
| 9e1b704aa6 | |||
| 941fbf5845 | |||
| 0b4a31a432 | |||
| c617dc3979 | |||
| 1b95161de0 | |||
| ebd976bd46 | |||
| f8d57d8fe0 | |||
| 09a4d2c472 | |||
| 1f6dc874d0 | |||
| 4dd63945ee | |||
| ba64b0694b | |||
| 58e5ceda59 | |||
| 08f826ee01 | |||
| 3358d280ec | |||
| 9cb0d5e416 | |||
| a46f058917 | |||
| 604b2199da | |||
| d153f71be6 | |||
| 8f64e4b098 | |||
| 459bf195d8 | |||
| 0c46b0d19b | |||
| bfbdc7b688 | |||
| 8fd53dd4d2 | |||
|
|
c8bbee4f74 | ||
| 04cdf54eb7 | |||
|
|
d6bf645ae9 | ||
| 1ca6bcbd81 | |||
| dec5f8eba7 | |||
| 716c887045 | |||
| 9b1789a164 | |||
| 17fa39c7ff | |||
| 8132e315f4 | |||
| 8d993915d7 | |||
| 497bddd05d | |||
| 7c8e49c1a9 | |||
| 901d28b89b | |||
| 43e2862c89 | |||
| 73fb38c4d1 | |||
| a81d206aba | |||
| 9801ea3cea | |||
| c11189fe11 | |||
| b47edb24cf | |||
| be191b09a0 | |||
| e06587478d | |||
| 402ffb04cd | |||
| 1cbfc02670 | |||
| 28d2fbbd2f | |||
| 225368a952 | |||
| 3574695041 | |||
| ea68025a1d | |||
| 3a08698003 | |||
| 3d583608c2 | |||
|
|
01ae95ab43 | ||
|
|
0d5821c640 | ||
|
|
2bcea39558 | ||
| af85970d4a | |||
| 210f621487 | |||
| 8ad960dc69 | |||
| d45a8b285f | |||
| b6e04a9138 | |||
| dcf1fc2d0b | |||
| 41b0517c6c | |||
| 6b43eb199d | |||
| d40f24e92d | |||
| 480a61b071 | |||
| a5d68d6a7e | |||
| 390f2c33e5 | |||
| 16485dc4a9 | |||
| a43ce5f188 | |||
| 0519ae2dd0 | |||
| 0d5b790e52 | |||
| 342e21b9ea | |||
| 4dec9e182b | |||
| c7ae883c6a | |||
| 9b4584f70a | |||
| 7b64fb7401 | |||
|
|
57cb23ce8b | ||
| 708b87ff35 | |||
| 577cfd08e8 | |||
| de9edbe532 | |||
| 5e95dc9898 | |||
| 006fec7efd | |||
| 2b612214bb | |||
| 29a6c349aa | |||
|
|
33a227c401 | ||
| 396ad4df7c | |||
|
|
0b39427090 | ||
| 8fcb18cb46 | |||
| d03fc519de | |||
| 97dd3797d6 | |||
| 6c7b7ab6a0 | |||
| 105dfe1b5b | |||
| 82f6453538 | |||
| fe862102d3 | |||
| 6ef538a0c4 | |||
| 689612ff7f | |||
| 7b135a4707 | |||
| 0cb8a54195 | |||
| a7a333397d | |||
| eb1d444776 | |||
| 68c3416594 | |||
| ae809e0152 | |||
| 7444eeb532 | |||
| 9bbebf5886 | |||
| fe7810671d | |||
| 53562025ac | |||
| 2b35315466 | |||
| 1b7f23d0a6 | |||
| 3d1d4d7952 | |||
| 3320563e1b | |||
| 798b2ddd0b | |||
| 31d4896f5d | |||
| 3fda37491d | |||
| 488c02b8b5 | |||
| 672e6d03d0 | |||
| 2881fdaf01 | |||
| db98a3787b | |||
| f9ea3760e2 | |||
| 78eb9cbb58 | |||
| 00e1a1d4b0 | |||
| ab3758bbc2 | |||
| 12d3d81518 | |||
| e2468be522 | |||
| dc2826df08 | |||
| 3e4df18b88 | |||
| a2d1df427d | |||
| ab63511d81 | |||
| e5d831cb61 | |||
| cab9e9cf2d | |||
| c7704ada8a | |||
| 010601be63 | |||
| add688602a | |||
| 3f3c717185 | |||
| b5f32da0c8 | |||
| 6e7ed8e041 | |||
| 90f79a7ee7 | |||
| b5663891a1 | |||
| a74b95e775 | |||
| b78eb4fc7b | |||
| 80c047369f | |||
| d7c158a0fc | |||
| 30f0ef4164 | |||
| d2963673c9 | |||
| d59e10dfc6 | |||
| 214f930e56 | |||
| 9e903607bb | |||
| 8b7cfdfd48 | |||
| c12b6015ab |
3
.env
@@ -1,3 +0,0 @@
|
||||
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
|
||||
PUBLIC_GITEA_OWNER=Archicratia
|
||||
PUBLIC_GITEA_REPO=archicratie-edition
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 -->
|
||||
/...
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
/...
|
||||
|
||||
|
||||
449
.gitea/workflows/anno-apply-pr.yml
Normal file
@@ -0,0 +1,449 @@
|
||||
name: Anno Apply (PR)
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue:
|
||||
description: "Issue number to apply"
|
||||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
concurrency:
|
||||
group: anno-apply-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
apply-approved:
|
||||
runs-on: mac-ci
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
- name: Tools sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Derive context (event.json / workflow_dispatch)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||||
|
||||
node --input-type=module - <<'NODE' > /tmp/anno.env
|
||||
import fs from "node:fs";
|
||||
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const repoObj = ev?.repository || {};
|
||||
|
||||
const cloneUrl =
|
||||
repoObj?.clone_url ||
|
||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
|
||||
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
||||
|
||||
let owner =
|
||||
repoObj?.owner?.login ||
|
||||
repoObj?.owner?.username ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
|
||||
|
||||
let repo =
|
||||
repoObj?.name ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
|
||||
|
||||
if (!owner || !repo) {
|
||||
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
||||
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
||||
}
|
||||
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
|
||||
|
||||
const defaultBranch = repoObj?.default_branch || "main";
|
||||
|
||||
const issueNumber =
|
||||
ev?.issue?.number ||
|
||||
ev?.issue?.index ||
|
||||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
|
||||
|
||||
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
|
||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||
}
|
||||
|
||||
// label name: best-effort (non-bloquant)
|
||||
let labelName = "workflow_dispatch";
|
||||
const lab = ev?.label;
|
||||
if (typeof lab === "string") labelName = lab;
|
||||
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
|
||||
else if (ev?.label?.name) labelName = ev.label.name;
|
||||
|
||||
const u = new URL(cloneUrl);
|
||||
const origin = u.origin;
|
||||
|
||||
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
|
||||
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
|
||||
: origin;
|
||||
|
||||
function sh(s){ return JSON.stringify(String(s)); }
|
||||
|
||||
process.stdout.write([
|
||||
`CLONE_URL=${sh(cloneUrl)}`,
|
||||
`OWNER=${sh(owner)}`,
|
||||
`REPO=${sh(repo)}`,
|
||||
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
|
||||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||||
`LABEL_NAME=${sh(labelName)}`,
|
||||
`API_BASE=${sh(apiBase)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ context:"
|
||||
sed -n '1,120p' /tmp/anno.env
|
||||
|
||||
- name: Early gate (label event fast-skip, but tolerant)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
|
||||
echo "ℹ️ event label = $LABEL_NAME"
|
||||
|
||||
# Fast skip on obvious non-approved label events (avoid noise),
|
||||
# BUT do NOT skip if label payload is weird/unknown.
|
||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip early"
|
||||
echo "SKIP=1" >> /tmp/anno.env
|
||||
echo "SKIP_REASON=\"label_not_approved_event\"" >> /tmp/anno.env
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "✅ continue to API gating (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Fetch issue + hard gate on labels + Type
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
|
||||
curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
-o /tmp/issue.json
|
||||
|
||||
node --input-type=module - <<'NODE' >> /tmp/anno.env
|
||||
import fs from "node:fs";
|
||||
|
||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
||||
const title = String(issue.title || "");
|
||||
const body = String(issue.body || "").replace(/\r\n/g, "\n");
|
||||
|
||||
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
|
||||
const hasApproved = labels.includes("state/approved");
|
||||
|
||||
function pickLine(key) {
|
||||
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
|
||||
const m = body.match(re);
|
||||
return m ? m[1].trim() : "";
|
||||
}
|
||||
|
||||
const typeRaw = pickLine("Type");
|
||||
const type = String(typeRaw || "").trim().toLowerCase();
|
||||
|
||||
const allowed = new Set(["type/media","type/reference","type/comment"]);
|
||||
const proposer = new Set(["type/correction","type/fact-check"]);
|
||||
|
||||
const out = [];
|
||||
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
||||
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
|
||||
|
||||
// HARD gate: must currently have state/approved (avoids depending on event payload)
|
||||
if (!hasApproved) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("not_approved_label_present")}`);
|
||||
process.stdout.write(out.join("\n") + "\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
|
||||
} else if (allowed.has(type)) {
|
||||
// proceed
|
||||
} else if (proposer.has(type)) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("proposer_type:"+type)}`);
|
||||
} else {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("unsupported_type:"+type)}`);
|
||||
}
|
||||
|
||||
process.stdout.write(out.join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ gating result:"
|
||||
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
|
||||
|
||||
- name: Comment issue if skipped (Proposer / unsupported / missing Type)
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env || true
|
||||
|
||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||
|
||||
# IMPORTANT: do NOT comment for "not_approved_label_present" (avoid spam on other label events)
|
||||
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
|
||||
echo "ℹ️ skip reason=${SKIP_REASON} -> no comment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||
|
||||
REASON="${SKIP_REASON:-}"
|
||||
TYPE="${ISSUE_TYPE:-}"
|
||||
|
||||
if [[ "$REASON" == proposer_type:* ]]; then
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors**.\n✅ Aucun traitement automatique."
|
||||
elif [[ "$REASON" == unsupported_type:* ]]; then
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment."
|
||||
else
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\nAjoute : Type: type/media|type/reference|type/comment"
|
||||
fi
|
||||
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$PAYLOAD"
|
||||
|
||||
- name: Checkout default branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git remote add origin "$CLONE_URL"
|
||||
git fetch --depth 1 origin "$DEFAULT_BRANCH"
|
||||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||||
git log -1 --oneline
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
npm ci --no-audit --no-fund
|
||||
|
||||
- name: Check apply script exists
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
test -f scripts/apply-annotation-ticket.mjs || {
|
||||
echo "❌ missing scripts/apply-annotation-ticket.mjs on $DEFAULT_BRANCH"
|
||||
ls -la scripts | sed -n '1,200p' || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Build dist (needed for --verify)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
npm run build
|
||||
|
||||
test -f dist/para-index.json || {
|
||||
echo "❌ missing dist/para-index.json after build"
|
||||
ls -la dist | sed -n '1,200p' || true
|
||||
exit 1
|
||||
}
|
||||
echo "✅ dist/para-index.json present"
|
||||
|
||||
- name: Apply ticket on bot branch (strict+verify, commit)
|
||||
continue-on-error: true
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
|
||||
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
test -d .git || { echo "❌ not a git repo (checkout failed)"; echo "APPLY_RC=90" >> /tmp/anno.env; exit 0; }
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
|
||||
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
|
||||
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
|
||||
|
||||
START_SHA="$(git rev-parse HEAD)"
|
||||
TS="$(date -u +%Y%m%d-%H%M%S)"
|
||||
BR="bot/anno-${ISSUE_NUMBER}-${TS}"
|
||||
echo "BRANCH=$BR" >> /tmp/anno.env
|
||||
git checkout -b "$BR"
|
||||
|
||||
export FORGE_API="$API_BASE"
|
||||
export GITEA_OWNER="$OWNER"
|
||||
export GITEA_REPO="$REPO"
|
||||
|
||||
LOG="/tmp/apply.log"
|
||||
set +e
|
||||
node scripts/apply-annotation-ticket.mjs "$ISSUE_NUMBER" --strict --verify --commit >"$LOG" 2>&1
|
||||
RC=$?
|
||||
set -e
|
||||
|
||||
echo "APPLY_RC=$RC" >> /tmp/anno.env
|
||||
|
||||
echo "== apply log (tail) =="
|
||||
tail -n 180 "$LOG" || true
|
||||
|
||||
END_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
if [[ "$RC" -ne 0 ]]; then
|
||||
echo "NOOP=0" >> /tmp/anno.env
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$START_SHA" == "$END_SHA" ]]; then
|
||||
echo "NOOP=1" >> /tmp/anno.env
|
||||
else
|
||||
echo "NOOP=0" >> /tmp/anno.env
|
||||
echo "END_SHA=$END_SHA" >> /tmp/anno.env
|
||||
fi
|
||||
|
||||
- name: Comment issue on failure (strict/verify/etc)
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
RC="${APPLY_RC:-0}"
|
||||
if [[ "$RC" == "0" ]]; then
|
||||
echo "ℹ️ no failure detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||
|
||||
if [[ -f /tmp/apply.log ]]; then
|
||||
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
|
||||
else
|
||||
BODY="(no apply log found)"
|
||||
fi
|
||||
|
||||
MSG="❌ apply-annotation-ticket a échoué (rc=${RC}).\n\n\`\`\`\n${BODY}\n\`\`\`\n"
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$PAYLOAD"
|
||||
|
||||
- name: Push bot branch
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip push"; exit 0; }
|
||||
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; }
|
||||
test -d .git || { echo "ℹ️ no git repo -> skip push"; exit 0; }
|
||||
|
||||
AUTH_URL="$(node --input-type=module -e '
|
||||
const [clone, tok] = process.argv.slice(1);
|
||||
const u = new URL(clone);
|
||||
u.username = "oauth2";
|
||||
u.password = tok;
|
||||
console.log(u.toString());
|
||||
' "$CLONE_URL" "$FORGE_TOKEN")"
|
||||
|
||||
git remote set-url origin "$AUTH_URL"
|
||||
git push -u origin "$BRANCH"
|
||||
|
||||
- name: Create PR + comment issue
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip PR"; exit 0; }
|
||||
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip PR"; exit 0; }
|
||||
|
||||
PR_TITLE="anno: apply ticket #${ISSUE_NUMBER}"
|
||||
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA}\n\nMerge si CI OK."
|
||||
|
||||
PR_PAYLOAD="$(node --input-type=module -e '
|
||||
const [title, body, base, head] = process.argv.slice(1);
|
||||
console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
|
||||
' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"
|
||||
|
||||
PR_JSON="$(curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
|
||||
--data-binary "$PR_PAYLOAD")"
|
||||
|
||||
PR_URL="$(node --input-type=module -e '
|
||||
const pr = JSON.parse(process.argv[1] || "{}");
|
||||
console.log(pr.html_url || pr.url || "");
|
||||
' "$PR_JSON")"
|
||||
|
||||
test -n "$PR_URL" || { echo "❌ PR URL missing. Raw: $PR_JSON"; exit 1; }
|
||||
|
||||
MSG="✅ PR créée pour ticket #${ISSUE_NUMBER} : ${PR_URL}"
|
||||
C_PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$C_PAYLOAD"
|
||||
|
||||
echo "✅ PR: $PR_URL"
|
||||
|
||||
- name: Finalize (fail job if apply failed)
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env || true
|
||||
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
RC="${APPLY_RC:-0}"
|
||||
if [[ "$RC" != "0" ]]; then
|
||||
echo "❌ apply failed (rc=$RC)"
|
||||
exit "$RC"
|
||||
fi
|
||||
echo "✅ apply ok"
|
||||
181
.gitea/workflows/anno-reject.yml
Normal 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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
577
.gitea/workflows/deploy-staging-live.yml
Normal file
@@ -0,0 +1,577 @@
|
||||
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 / l’image
|
||||
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
|
||||
|
||||
docker compose -p "$PROJ" -f docker-compose.yml build web_blue
|
||||
docker rm -f archicratie-web-blue || true
|
||||
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_blue
|
||||
|
||||
# warmup endpoints
|
||||
wait_url "http://127.0.0.1:8081/para-index.json" "blue para-index"
|
||||
wait_url "http://127.0.0.1:8081/annotations-index.json" "blue annotations-index"
|
||||
wait_url "http://127.0.0.1:8081/pagefind/pagefind.js" "blue pagefind.js"
|
||||
|
||||
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
|
||||
echo "canonical(blue)=$CANON"
|
||||
echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || {
|
||||
echo "❌ staging canonical mismatch"
|
||||
docker logs --tail 120 archicratie-web-blue || true
|
||||
exit 3
|
||||
}
|
||||
|
||||
echo "✅ staging OK"
|
||||
|
||||
- name: FULL — Build + deploy live (green) then warmup+smoke + rollback if needed
|
||||
env:
|
||||
PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }}
|
||||
PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }}
|
||||
PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
[[ "${MODE:-hotpatch}" == "full" ]] || { echo "ℹ️ MODE=$MODE -> skip full rebuild"; exit 0; }
|
||||
|
||||
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
|
||||
TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}"
|
||||
|
||||
wait_url() {
|
||||
local url="$1"
|
||||
local label="$2"
|
||||
local tries="${3:-60}"
|
||||
for i in $(seq 1 "$tries"); do
|
||||
if curl -fsS --max-time 4 "$url" >/dev/null; then
|
||||
echo "✅ $label OK ($url)"
|
||||
return 0
|
||||
fi
|
||||
echo "… warmup $label ($i/$tries)"
|
||||
sleep 1
|
||||
done
|
||||
echo "❌ timeout $label ($url)"
|
||||
return 1
|
||||
}
|
||||
|
||||
rollback() {
|
||||
echo "⚠️ rollback green -> previous image tag (best effort)"
|
||||
docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true
|
||||
docker rm -f archicratie-web-green || true
|
||||
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green || true
|
||||
}
|
||||
|
||||
# build/restart green
|
||||
if ! docker compose -p "$PROJ" -f docker-compose.yml build web_green; then
|
||||
echo "❌ build green failed"; rollback; exit 4
|
||||
fi
|
||||
|
||||
docker rm -f archicratie-web-green || true
|
||||
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green
|
||||
|
||||
# warmup endpoints
|
||||
if ! wait_url "http://127.0.0.1:8082/para-index.json" "green para-index"; then rollback; exit 4; fi
|
||||
if ! wait_url "http://127.0.0.1:8082/annotations-index.json" "green annotations-index"; then rollback; exit 4; fi
|
||||
if ! wait_url "http://127.0.0.1:8082/pagefind/pagefind.js" "green pagefind.js"; then rollback; exit 4; fi
|
||||
|
||||
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
|
||||
echo "canonical(green)=$CANON"
|
||||
echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || {
|
||||
echo "❌ live canonical mismatch"
|
||||
docker logs --tail 120 archicratie-web-green || true
|
||||
rollback
|
||||
exit 4
|
||||
}
|
||||
|
||||
echo "✅ live OK"
|
||||
|
||||
- name: HOTPATCH — deep merge shards -> annotations-index + copy changed media into blue+green
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
python3 - <<'PY'
|
||||
import os, re, json, glob
|
||||
import yaml
|
||||
import datetime as dt
|
||||
|
||||
ROOT = os.getcwd()
|
||||
ANNO_ROOT = os.path.join(ROOT, "src", "annotations")
|
||||
|
||||
def is_obj(x): return isinstance(x, dict)
|
||||
def is_arr(x): return isinstance(x, list)
|
||||
|
||||
def iso_dt(x):
|
||||
if isinstance(x, dt.datetime):
|
||||
if x.tzinfo is None:
|
||||
return x.isoformat()
|
||||
return x.astimezone(dt.timezone.utc).isoformat().replace("+00:00","Z")
|
||||
if isinstance(x, dt.date):
|
||||
return x.isoformat()
|
||||
return None
|
||||
|
||||
def normalize(x):
|
||||
s = iso_dt(x)
|
||||
if s is not None: return s
|
||||
if isinstance(x, dict):
|
||||
return {str(k): normalize(v) for k, v in x.items()}
|
||||
if isinstance(x, list):
|
||||
return [normalize(v) for v in x]
|
||||
return x
|
||||
|
||||
def key_media(it): return str((it or {}).get("src",""))
|
||||
def key_ref(it):
|
||||
it = it or {}
|
||||
return "||".join([str(it.get("url","")), str(it.get("label","")), str(it.get("kind","")), str(it.get("citation",""))])
|
||||
def key_comment(it): return str((it or {}).get("text","")).strip()
|
||||
|
||||
def dedup_extend(dst_list, src_list, key_fn):
|
||||
seen = set(); out = []
|
||||
for x in (dst_list or []):
|
||||
x = normalize(x); k = key_fn(x)
|
||||
if k and k not in seen: seen.add(k); out.append(x)
|
||||
for x in (src_list or []):
|
||||
x = normalize(x); k = key_fn(x)
|
||||
if k and k not in seen: seen.add(k); out.append(x)
|
||||
return out
|
||||
|
||||
def deep_merge(dst, src):
|
||||
src = normalize(src)
|
||||
for k, v in (src or {}).items():
|
||||
if k in ("media","refs","comments_editorial") and is_arr(v):
|
||||
if k == "media": dst[k] = dedup_extend(dst.get(k, []), v, key_media)
|
||||
elif k == "refs": dst[k] = dedup_extend(dst.get(k, []), v, key_ref)
|
||||
else: dst[k] = dedup_extend(dst.get(k, []), v, key_comment)
|
||||
continue
|
||||
|
||||
if is_obj(v):
|
||||
if not is_obj(dst.get(k)): dst[k] = {}
|
||||
deep_merge(dst[k], v)
|
||||
continue
|
||||
|
||||
if is_arr(v):
|
||||
cur = dst.get(k, [])
|
||||
if not is_arr(cur): cur = []
|
||||
seen = set(); out = []
|
||||
for x in cur:
|
||||
x = normalize(x)
|
||||
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
|
||||
if s not in seen: seen.add(s); out.append(x)
|
||||
for x in v:
|
||||
x = normalize(x)
|
||||
s = json.dumps(x, sort_keys=True, ensure_ascii=False)
|
||||
if s not in seen: seen.add(s); out.append(x)
|
||||
dst[k] = out
|
||||
continue
|
||||
|
||||
v = normalize(v)
|
||||
if k not in dst or dst.get(k) in (None, ""):
|
||||
dst[k] = v
|
||||
|
||||
def para_num(pid):
|
||||
m = re.match(r"^p-(\d+)-", str(pid))
|
||||
return int(m.group(1)) if m else 10**9
|
||||
|
||||
def sort_lists(entry):
|
||||
for k in ("media","refs","comments_editorial"):
|
||||
arr = entry.get(k)
|
||||
if not is_arr(arr): continue
|
||||
def ts(x):
|
||||
x = normalize(x)
|
||||
try:
|
||||
s = str((x or {}).get("ts",""))
|
||||
return dt.datetime.fromisoformat(s.replace("Z","+00:00")).timestamp() if s else 0
|
||||
except Exception:
|
||||
return 0
|
||||
arr = [normalize(x) for x in arr]
|
||||
arr.sort(key=lambda x: (ts(x), json.dumps(x, sort_keys=True, ensure_ascii=False)))
|
||||
entry[k] = arr
|
||||
|
||||
if not os.path.isdir(ANNO_ROOT):
|
||||
raise SystemExit(f"Missing annotations root: {ANNO_ROOT}")
|
||||
|
||||
pages = {}
|
||||
errors = []
|
||||
|
||||
files = sorted(glob.glob(os.path.join(ANNO_ROOT, "**", "*.yml"), recursive=True))
|
||||
for fp in files:
|
||||
try:
|
||||
with open(fp, "r", encoding="utf-8") as f:
|
||||
doc = yaml.safe_load(f) or {}
|
||||
doc = normalize(doc)
|
||||
if not isinstance(doc, dict) or doc.get("schema") != 1:
|
||||
continue
|
||||
|
||||
page = str(doc.get("page","")).strip().strip("/")
|
||||
paras = doc.get("paras") or {}
|
||||
if not page or not isinstance(paras, dict):
|
||||
continue
|
||||
|
||||
pg = pages.setdefault(page, {"paras": {}})
|
||||
for pid, entry in paras.items():
|
||||
pid = str(pid)
|
||||
if pid not in pg["paras"] or not isinstance(pg["paras"].get(pid), dict):
|
||||
pg["paras"][pid] = {}
|
||||
if isinstance(entry, dict):
|
||||
deep_merge(pg["paras"][pid], entry)
|
||||
sort_lists(pg["paras"][pid])
|
||||
|
||||
except Exception as e:
|
||||
errors.append({"file": os.path.relpath(fp, ROOT), "error": str(e)})
|
||||
|
||||
for page, obj in pages.items():
|
||||
keys = list((obj.get("paras") or {}).keys())
|
||||
keys.sort(key=lambda k: (para_num(k), k))
|
||||
obj["paras"] = {k: obj["paras"][k] for k in keys}
|
||||
|
||||
out = {
|
||||
"schema": 1,
|
||||
"generatedAt": dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc).isoformat().replace("+00:00","Z"),
|
||||
"pages": pages,
|
||||
"stats": {
|
||||
"pages": len(pages),
|
||||
"paras": sum(len(v.get("paras") or {}) for v in pages.values()),
|
||||
"errors": len(errors),
|
||||
},
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
with open("/tmp/annotations-index.json", "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False)
|
||||
|
||||
print("OK: wrote /tmp/annotations-index.json pages=", out["stats"]["pages"], "paras=", out["stats"]["paras"], "errors=", out["stats"]["errors"])
|
||||
PY
|
||||
|
||||
# patch JSON into running containers
|
||||
for c in archicratie-web-blue archicratie-web-green; do
|
||||
echo "== patch annotations-index.json into $c =="
|
||||
docker cp /tmp/annotations-index.json "${c}:/usr/share/nginx/html/annotations-index.json"
|
||||
done
|
||||
|
||||
# copy changed media files into containers (so new media appears without rebuild)
|
||||
if [[ -s /tmp/changed.txt ]]; then
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] || continue
|
||||
if [[ "$f" == public/media/* ]]; then
|
||||
dest="/usr/share/nginx/html/${f#public/}" # => /usr/share/nginx/html/media/...
|
||||
for c in archicratie-web-blue archicratie-web-green; do
|
||||
echo "== copy media into $c: $f -> $dest =="
|
||||
docker exec "$c" sh -lc "mkdir -p \"$(dirname "$dest")\""
|
||||
docker cp "$f" "$c:$dest"
|
||||
done
|
||||
fi
|
||||
done < /tmp/changed.txt
|
||||
fi
|
||||
|
||||
# smoke after patch
|
||||
for p in 8081 8082; do
|
||||
echo "== smoke annotations-index on $p =="
|
||||
curl -fsS --max-time 6 "http://127.0.0.1:${p}/annotations-index.json" \
|
||||
| python3 -c 'import sys,json; j=json.load(sys.stdin); print("generatedAt:", j.get("generatedAt")); print("pages:", len(j.get("pages") or {})); print("paras:", j.get("stats",{}).get("paras"))'
|
||||
done
|
||||
|
||||
echo "✅ hotpatch done"
|
||||
|
||||
- name: Debug on failure (containers status/logs)
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "== docker ps =="
|
||||
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | sed -n '1,80p' || true
|
||||
for c in archicratie-web-blue archicratie-web-green; do
|
||||
echo "== logs $c (tail 200) =="
|
||||
docker logs --tail 200 "$c" || true
|
||||
done
|
||||
395
.gitea/workflows/proposer-apply-pr.yml
Normal file
@@ -0,0 +1,395 @@
|
||||
name: Proposer Apply (PR)
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue:
|
||||
description: "Issue number to apply (Proposer: correction/fact-check)"
|
||||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
concurrency:
|
||||
group: proposer-apply-${{ github.event.issue.number || inputs.issue || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
apply-proposer:
|
||||
runs-on: mac-ci
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
- name: Tools sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Derive context (event.json / workflow_dispatch)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||||
|
||||
node --input-type=module - <<'NODE' > /tmp/proposer.env
|
||||
import fs from "node:fs";
|
||||
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const repoObj = ev?.repository || {};
|
||||
|
||||
const cloneUrl =
|
||||
repoObj?.clone_url ||
|
||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
|
||||
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
||||
|
||||
let owner =
|
||||
repoObj?.owner?.login ||
|
||||
repoObj?.owner?.username ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
|
||||
|
||||
let repo =
|
||||
repoObj?.name ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
|
||||
|
||||
if (!owner || !repo) {
|
||||
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
||||
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
||||
}
|
||||
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
|
||||
|
||||
const defaultBranch = repoObj?.default_branch || "main";
|
||||
|
||||
const issueNumber =
|
||||
ev?.issue?.number ||
|
||||
ev?.issue?.index ||
|
||||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
|
||||
|
||||
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
|
||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||
}
|
||||
|
||||
const labelName =
|
||||
ev?.label?.name ||
|
||||
ev?.label ||
|
||||
"workflow_dispatch";
|
||||
|
||||
const u = new URL(cloneUrl);
|
||||
const origin = u.origin;
|
||||
|
||||
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
|
||||
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
|
||||
: origin;
|
||||
|
||||
function sh(s){ return JSON.stringify(String(s)); }
|
||||
process.stdout.write([
|
||||
`CLONE_URL=${sh(cloneUrl)}`,
|
||||
`OWNER=${sh(owner)}`,
|
||||
`REPO=${sh(repo)}`,
|
||||
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
|
||||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||||
`LABEL_NAME=${sh(labelName)}`,
|
||||
`API_BASE=${sh(apiBase)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ context:"
|
||||
sed -n '1,120p' /tmp/proposer.env
|
||||
|
||||
- name: Gate on label state/approved
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
|
||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||
echo "SKIP=1" >> /tmp/proposer.env
|
||||
exit 0
|
||||
fi
|
||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Fetch issue + API-hard gate on (state/approved present + proposer type)
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
|
||||
curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
-o /tmp/issue.json
|
||||
|
||||
node --input-type=module - <<'NODE' >> /tmp/proposer.env
|
||||
import fs from "node:fs";
|
||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
||||
const title = String(issue.title || "");
|
||||
const body = String(issue.body || "").replace(/\r\n/g, "\n");
|
||||
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name||"")).filter(Boolean) : [];
|
||||
|
||||
function pickLine(key) {
|
||||
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
|
||||
const m = body.match(re);
|
||||
return m ? m[1].trim() : "";
|
||||
}
|
||||
|
||||
const typeRaw = pickLine("Type");
|
||||
const type = String(typeRaw || "").trim().toLowerCase();
|
||||
|
||||
const hasApproved = labels.includes("state/approved");
|
||||
const proposer = new Set(["type/correction","type/fact-check"]);
|
||||
|
||||
const out = [];
|
||||
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
||||
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
|
||||
out.push(`HAS_APPROVED=${hasApproved ? "1":"0"}`);
|
||||
|
||||
if (!hasApproved) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("approved_not_present")}`);
|
||||
} else if (!type) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
|
||||
} else if (!proposer.has(type)) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("not_proposer:"+type)}`);
|
||||
}
|
||||
process.stdout.write(out.join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ proposer gating:"
|
||||
grep -E '^(ISSUE_TYPE|HAS_APPROVED|SKIP|SKIP_REASON)=' /tmp/proposer.env || true
|
||||
|
||||
- name: Comment issue if skipped
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
|
||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||
[[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0
|
||||
|
||||
REASON="${SKIP_REASON:-}"
|
||||
TYPE="${ISSUE_TYPE:-}"
|
||||
|
||||
if [[ "$REASON" == "approved_not_present" ]]; then
|
||||
MSG="ℹ️ Proposer Apply: skip — le label **state/approved** n'est pas présent sur le ticket au moment du run (gate API-hard)."
|
||||
elif [[ "$REASON" == "missing_type" ]]; then
|
||||
MSG="ℹ️ Proposer Apply: skip — champ **Type:** manquant/illisible. Attendu: type/correction ou type/fact-check."
|
||||
else
|
||||
MSG="ℹ️ Proposer Apply: skip — Type non-Proposer (${TYPE}). (Ce workflow ne traite que correction/fact-check.)"
|
||||
fi
|
||||
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$PAYLOAD" || true
|
||||
|
||||
- name: Checkout default branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git remote add origin "$CLONE_URL"
|
||||
git fetch --depth 1 origin "$DEFAULT_BRANCH"
|
||||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||||
git log -1 --oneline
|
||||
echo "✅ workspace:"
|
||||
ls -la | sed -n '1,120p'
|
||||
|
||||
- name: Detect app dir (repo-root vs ./site)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
APP_DIR="."
|
||||
if [[ -d "site" && -f "site/package.json" ]]; then
|
||||
APP_DIR="site"
|
||||
fi
|
||||
|
||||
echo "APP_DIR=$APP_DIR" >> /tmp/proposer.env
|
||||
echo "✅ APP_DIR=$APP_DIR"
|
||||
ls -la "$APP_DIR" | sed -n '1,120p'
|
||||
test -f "$APP_DIR/package.json" || { echo "❌ package.json missing in APP_DIR=$APP_DIR"; exit 1; }
|
||||
test -d "$APP_DIR/scripts" || { echo "❌ scripts/ missing in APP_DIR=$APP_DIR"; exit 1; }
|
||||
|
||||
- name: NPM harden (reduce flakiness)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
cd "$APP_DIR"
|
||||
npm config set fetch-retries 5
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
npm config set fetch-retry-maxtimeout 120000
|
||||
npm config set registry https://registry.npmjs.org
|
||||
|
||||
- name: Install deps (APP_DIR)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
cd "$APP_DIR"
|
||||
npm ci --no-audit --no-fund
|
||||
|
||||
- name: Build dist baseline (APP_DIR)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
cd "$APP_DIR"
|
||||
npm run build
|
||||
|
||||
- name: Apply ticket (alias + commit) on bot branch
|
||||
continue-on-error: true
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
|
||||
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
|
||||
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
|
||||
|
||||
START_SHA="$(git rev-parse HEAD)"
|
||||
TS="$(date -u +%Y%m%d-%H%M%S)"
|
||||
BR="bot/proposer-${ISSUE_NUMBER}-${TS}"
|
||||
echo "BRANCH=$BR" >> /tmp/proposer.env
|
||||
git checkout -b "$BR"
|
||||
|
||||
export GITEA_OWNER="$OWNER"
|
||||
export GITEA_REPO="$REPO"
|
||||
export FORGE_BASE="$API_BASE"
|
||||
|
||||
LOG="/tmp/proposer-apply.log"
|
||||
set +e
|
||||
(cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE_NUMBER" --alias --commit) >"$LOG" 2>&1
|
||||
RC=$?
|
||||
set -e
|
||||
|
||||
echo "APPLY_RC=$RC" >> /tmp/proposer.env
|
||||
|
||||
echo "== apply log (tail) =="
|
||||
tail -n 200 "$LOG" || true
|
||||
|
||||
END_SHA="$(git rev-parse HEAD)"
|
||||
if [[ "$RC" -ne 0 ]]; then
|
||||
echo "NOOP=0" >> /tmp/proposer.env
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$START_SHA" == "$END_SHA" ]]; then
|
||||
echo "NOOP=1" >> /tmp/proposer.env
|
||||
else
|
||||
echo "NOOP=0" >> /tmp/proposer.env
|
||||
echo "END_SHA=$END_SHA" >> /tmp/proposer.env
|
||||
fi
|
||||
|
||||
- name: Push bot branch
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip push"; exit 0; }
|
||||
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; }
|
||||
[[ -n "${BRANCH:-}" ]] || { echo "ℹ️ BRANCH unset -> skip push"; exit 0; }
|
||||
|
||||
AUTH_URL="$(node --input-type=module -e '
|
||||
const [clone, tok] = process.argv.slice(1);
|
||||
const u = new URL(clone);
|
||||
u.username = "oauth2";
|
||||
u.password = tok;
|
||||
console.log(u.toString());
|
||||
' "$CLONE_URL" "$FORGE_TOKEN")"
|
||||
|
||||
git remote set-url origin "$AUTH_URL"
|
||||
git push -u origin "$BRANCH"
|
||||
|
||||
- name: Create PR + comment issue
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
|
||||
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
||||
[[ -n "${BRANCH:-}" ]] || { echo "ℹ️ BRANCH unset -> skip PR"; exit 0; }
|
||||
|
||||
PR_TITLE="proposer: apply ticket #${ISSUE_NUMBER}"
|
||||
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA:-unknown}\n\nMerge si CI OK."
|
||||
|
||||
PR_PAYLOAD="$(node --input-type=module -e '
|
||||
const [title, body, base, head] = process.argv.slice(1);
|
||||
console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
|
||||
' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"
|
||||
|
||||
PR_JSON="$(curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
|
||||
--data-binary "$PR_PAYLOAD")"
|
||||
|
||||
PR_URL="$(node --input-type=module -e '
|
||||
const pr = JSON.parse(process.argv[1] || "{}");
|
||||
console.log(pr.html_url || pr.url || "");
|
||||
' "$PR_JSON")"
|
||||
|
||||
test -n "$PR_URL" || { echo "❌ PR URL missing. Raw: $PR_JSON"; exit 1; }
|
||||
|
||||
MSG="✅ PR Proposer créée pour ticket #${ISSUE_NUMBER} : ${PR_URL}"
|
||||
C_PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$C_PAYLOAD"
|
||||
|
||||
- name: Finalize (fail job if apply failed)
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
RC="${APPLY_RC:-0}"
|
||||
if [[ "$RC" != "0" ]]; then
|
||||
echo "❌ apply failed (rc=$RC)"
|
||||
exit "$RC"
|
||||
fi
|
||||
echo "✅ apply ok"
|
||||
@@ -3,7 +3,7 @@ on: [push, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: mac-ci
|
||||
steps:
|
||||
- run: node -v && npm -v
|
||||
- run: echo "runner OK"
|
||||
|
||||
7
.gitignore
vendored
@@ -3,6 +3,10 @@
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# dev-only
|
||||
public/_auth/whoami
|
||||
public/_auth/whoami/*
|
||||
|
||||
# --- local backups ---
|
||||
*.bak
|
||||
*.bak.*
|
||||
@@ -21,3 +25,6 @@ dist/
|
||||
# local backups
|
||||
Dockerfile.bak.*
|
||||
public/favicon_io.zip
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
18
Dockerfile
@@ -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 d’abord (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 l’exige
|
||||
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
|
||||
102
astro.config.mjs
@@ -10,41 +10,101 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
import rehypeDetailsSections from "./scripts/rehype-details-sections.mjs";
|
||||
import rehypeParagraphIds from "./src/plugins/rehype-paragraph-ids.js";
|
||||
|
||||
const must = (name, fn) => {
|
||||
if (typeof fn !== "function") {
|
||||
throw new Error(`[astro.config] rehype plugin "${name}" is not a function (export default vs named?)`);
|
||||
}
|
||||
return fn;
|
||||
};
|
||||
/**
|
||||
* Cast minimal pour satisfaire @ts-check sans dépendre de types internes Astro/Unified.
|
||||
* @param {unknown} x
|
||||
* @returns {any}
|
||||
*/
|
||||
const asAny = (x) => /** @type {any} */ (x);
|
||||
|
||||
/**
|
||||
* @param {any} node
|
||||
* @param {string} cls
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasClass(node, cls) {
|
||||
const cn = node?.properties?.className;
|
||||
if (Array.isArray(cn)) return cn.includes(cls);
|
||||
if (typeof cn === "string") return cn.split(/\s+/).includes(cls);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehype plugin: retire les ids dupliqués en gardant en priorité:
|
||||
* 1) span.details-anchor
|
||||
* 2) h1..h6
|
||||
* 3) sinon: premier rencontré
|
||||
* @returns {(tree: any) => void}
|
||||
*/
|
||||
function rehypeDedupeIds() {
|
||||
/** @param {any} tree */
|
||||
return (tree) => {
|
||||
/** @type {Map<string, Array<{node:any, pref:number, idx:number}>>} */
|
||||
const occ = new Map();
|
||||
let idx = 0;
|
||||
|
||||
/** @param {any} node */
|
||||
const walk = (node) => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
|
||||
if (node.type === "element") {
|
||||
const id = node.properties?.id;
|
||||
if (typeof id === "string" && id) {
|
||||
let pref = 2;
|
||||
if (node.tagName === "span" && hasClass(node, "details-anchor")) pref = 0;
|
||||
else if (/^h[1-6]$/.test(String(node.tagName || ""))) pref = 1;
|
||||
|
||||
const arr = occ.get(id) || [];
|
||||
arr.push({ node, pref, idx: idx++ });
|
||||
occ.set(id, arr);
|
||||
}
|
||||
|
||||
const children = node.children;
|
||||
if (Array.isArray(children)) for (const c of children) walk(c);
|
||||
} else if (Array.isArray(node.children)) {
|
||||
for (const c of node.children) walk(c);
|
||||
}
|
||||
};
|
||||
|
||||
walk(tree);
|
||||
|
||||
for (const [id, items] of occ.entries()) {
|
||||
if (items.length <= 1) continue;
|
||||
|
||||
items.sort((a, b) => (a.pref - b.pref) || (a.idx - b.idx));
|
||||
const keep = items[0];
|
||||
|
||||
for (let i = 1; i < items.length; i++) {
|
||||
const n = items[i].node;
|
||||
if (n?.properties?.id === id) delete n.properties.id;
|
||||
}
|
||||
|
||||
// safety: on s'assure qu'un seul garde bien l'id
|
||||
if (keep?.node?.properties) keep.node.properties.id = id;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
output: "static",
|
||||
trailingSlash: "always",
|
||||
|
||||
site: process.env.PUBLIC_SITE ?? "http://localhost:4321",
|
||||
|
||||
integrations: [
|
||||
mdx(),
|
||||
// Important: MDX hérite du pipeline markdown (ids p-… + autres plugins)
|
||||
mdx({ extendMarkdownConfig: true }),
|
||||
sitemap({
|
||||
filter: (page) => !page.includes("/api/") && !page.endsWith("/robots.txt"),
|
||||
}),
|
||||
],
|
||||
|
||||
// ✅ Plugins appliqués AU MDX
|
||||
mdx: {
|
||||
// ✅ MDX hérite déjà de markdown.rehypePlugins
|
||||
// donc ici on ne met QUE le spécifique MDX
|
||||
rehypePlugins: [
|
||||
must("rehype-details-sections", rehypeDetailsSections),
|
||||
],
|
||||
},
|
||||
|
||||
// ✅ Plugins appliqués au Markdown non-MDX
|
||||
markdown: {
|
||||
rehypePlugins: [
|
||||
must("rehype-slug", rehypeSlug),
|
||||
[must("rehype-autolink-headings", rehypeAutolinkHeadings), { behavior: "append" }],
|
||||
must("rehype-paragraph-ids", rehypeParagraphIds),
|
||||
asAny(rehypeSlug),
|
||||
[asAny(rehypeAutolinkHeadings), { behavior: "append" }],
|
||||
asAny(rehypeDetailsSections),
|
||||
asAny(rehypeParagraphIds),
|
||||
asAny(rehypeDedupeIds),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
7
bridge/Dockerfile
Normal file
@@ -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"]
|
||||
15
bridge/docker-compose-bridge.yml
Normal 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
@@ -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
@@ -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");
|
||||
});
|
||||
@@ -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
|
||||
@@ -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`
|
||||
|
||||
@@ -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 :
|
||||
|
||||
327
docs/EDITORIAL-ANNOTATIONS-SPEC.md
Normal 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 d’application côté éditeur (`apply-ticket.mjs` ou équivalent)
|
||||
- génération d’un YAML d’annotations versionné dans Git
|
||||
|
||||
La donnée d’annotation 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
|
||||
- l’arborescence 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>/`.
|
||||
> S’il 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 d’annotations
|
||||
|
||||
### 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 l’existant)
|
||||
- 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 c’est 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 l’item 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 c’est 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 l’id 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 d’implé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 d’ID (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 d’annotation doit suivre le workKey final (archicrat-ia).
|
||||
|
||||
## 9) Ce que l’étape 9 devra implémenter
|
||||
|
||||
pipeline : ticket → YAML (apply-ticket)
|
||||
|
||||
index : build-annotations-index + check-annotations
|
||||
|
||||
tooling : détection médias orphelins / liens cassés
|
||||
|
||||
éventuellement : migration vers sharding par paragraphe (v2) si volume réel le justifie
|
||||
57
docs/FEATURE-PROPOSER.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# FEATURE — “Proposer” (édition par paragraphe → issue Gitea)
|
||||
|
||||
Dernière mise à jour : 2026-02-01
|
||||
|
||||
Cette feature permet à un lecteur de proposer une correction/amélioration d’un paragraphe, en générant une issue pré-remplie dans Gitea.
|
||||
|
||||
---
|
||||
|
||||
## 0) Objectif fonctionnel
|
||||
Depuis une page chapitre :
|
||||
1) clic sur **Proposer** sur un paragraphe
|
||||
2) choix #1 (type)
|
||||
3) choix #2 (state/catégorie selon UI)
|
||||
4) ouverture d’un seul onglet vers Gitea : `/issues/new?...`
|
||||
5) issue pré-remplie avec :
|
||||
- chemin / URL / ancre
|
||||
- texte actuel (citation)
|
||||
- champs “Proposition / Justification”
|
||||
6) l’utilisateur valide, et le runner/CI traite.
|
||||
|
||||
---
|
||||
|
||||
## 1) Dépendances de configuration (critique)
|
||||
|
||||
Le lien Gitea est construit à partir de variables publiques injectées au build Astro :
|
||||
|
||||
- `PUBLIC_GITEA_BASE` (ex: `https://gitea.archicratie.trans-hands.synology.me`)
|
||||
- `PUBLIC_GITEA_OWNER` (**casse sensible**, ex: `Archicratia`)
|
||||
- `PUBLIC_GITEA_REPO` (ex: `archicratie-edition`)
|
||||
|
||||
### Symptômes si mauvais
|
||||
- mauvais repo → 404
|
||||
- redirect login inattendu
|
||||
- création d’issues impossible
|
||||
|
||||
---
|
||||
|
||||
## 2) Contrat “une seule ouverture d’onglet”
|
||||
Le flow ne doit jamais ouvrir deux onglets.
|
||||
|
||||
### Contrat
|
||||
- pas de `window.open(...)`
|
||||
- un seul `a.target="_blank"` (ou équivalent) déclenché
|
||||
- sur click : handler doit neutraliser les propagations parasites
|
||||
|
||||
## Diagnostic (canonique)
|
||||
|
||||
Le diagnostic détaillé est centralisé dans `docs/TROUBLESHOOTING.md` pour éviter les doublons.
|
||||
|
||||
- 404 / non autorisé / redirect login :
|
||||
- voir : `TROUBLESHOOTING.md#proposer-404`
|
||||
- cause la plus fréquente : `PUBLIC_GITEA_OWNER/REPO` faux (souvent casse)
|
||||
|
||||
- Double onglet :
|
||||
- voir : `TROUBLESHOOTING.md#proposer-double-onglet`
|
||||
- cause la plus fréquente : double handler (bubbling) ou `window.open` + `a.click()`
|
||||
|
||||
@@ -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 n’est 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 d’erreur 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 qu’ils ont été consolidés pendant la phase d’implé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 “d’où ça vient”.
|
||||
|
||||
## Ce qu’il faut faire aujourd’hui (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 qu’un 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 l’autre 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>
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)** ;
|
||||
|
||||
122
docs/OPS-SYNC-TRIPLE-SOURCE.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# OPS-SYNC-TRIPLE-SOURCE — Mac Studio / Gitea / NAS (prod)
|
||||
|
||||
Dernière mise à jour : 2026-02-01
|
||||
|
||||
Ce document décrit la synchronisation **sans ambiguïté** entre :
|
||||
- **Local (Mac Studio)** : édition / écriture / préparation PR
|
||||
- **Gitea** : **vérité canonique** (branche `main`)
|
||||
- **NAS Synology DS220+** : déploiement (blue/green) à partir de `main`
|
||||
|
||||
---
|
||||
|
||||
## 0) Invariants (à ne jamais violer)
|
||||
|
||||
1) **Gitea `main` = source de vérité.**
|
||||
2) Le NAS ne doit pas “inventer” du code : pas d’édition manuelle non versionnée en prod (sauf hotfix temporaire immédiatement reporté dans une PR).
|
||||
3) Le bouton “Proposer” dépend de `PUBLIC_GITEA_*` : une valeur fausse → 404 / redirect login / mauvais repo.
|
||||
4) Les secrets (tokens) **ne doivent jamais** entrer dans le repo : `.env*` ignorés, token injecté uniquement via variable d’environnement locale/CI.
|
||||
|
||||
---
|
||||
|
||||
## 1) Topologie réelle (ce que nous avons)
|
||||
|
||||
### 1.1 Local (Mac Studio)
|
||||
- Dev et documentation.
|
||||
- Git complet.
|
||||
- On fait : branches, commits, push, PR, merge.
|
||||
|
||||
### 1.2 Gitea
|
||||
- Repo canonique : `Archicratia/archicratie-edition`.
|
||||
- `main` = défaut + protégée.
|
||||
- Toute modif arrive via PR.
|
||||
|
||||
### 1.3 NAS (prod)
|
||||
- Chemin canonique :
|
||||
- `/volume2/docker/archicratie-web/releases/<timestamp>/app`
|
||||
- `/volume2/docker/archicratie-web/current` → symlink vers la release active
|
||||
- Blue/Green :
|
||||
- `web_blue` sur `127.0.0.1:8081`
|
||||
- `web_green` sur `127.0.0.1:8082`
|
||||
- Reverse proxy DSM : bascule 8081 ↔ 8082.
|
||||
|
||||
---
|
||||
|
||||
## 2) Règle d’or : qui écrit quoi, où ?
|
||||
|
||||
### 2.1 Toute écriture “source” se fait sur Mac Studio
|
||||
- Code Astro
|
||||
- Scripts
|
||||
- Docs `docs/*.md`
|
||||
- `.gitignore`
|
||||
|
||||
### 2.2 Gitea ne reçoit que via PR
|
||||
- Push sur branche feature/docs
|
||||
- PR → CI → merge
|
||||
|
||||
### 2.3 NAS ne fait que :
|
||||
- `git reset --hard origin/main` (alignement)
|
||||
- build image + restart slot blue/green
|
||||
- smoke test
|
||||
- bascule reverse proxy DSM
|
||||
|
||||
---
|
||||
|
||||
## 3) Procédure standard (la seule à utiliser)
|
||||
|
||||
### Étape A — Mac Studio → Gitea (PR)
|
||||
1) `git checkout -b feat/...` ou `docs/...`
|
||||
2) commits propres et atomiques
|
||||
3) `git push -u origin <branch>`
|
||||
4) PR dans Gitea → CI OK → merge dans `main`
|
||||
|
||||
### Étape B — NAS : aligner `current` sur `origin/main`
|
||||
Sur NAS, git n’est pas forcément installé : on utilise un conteneur git.
|
||||
en sh :
|
||||
|
||||
APP="/volume2/docker/archicratie-web/current"
|
||||
U_ID="$(id -u)"; G_ID="$(id -g)"
|
||||
|
||||
sudo docker run --rm --network host \
|
||||
-u "$U_ID:$G_ID" -e HOME=/tmp \
|
||||
-v "$APP":/repo -w /repo \
|
||||
--entrypoint sh alpine/git -lc '
|
||||
set -eu
|
||||
git config --global --add safe.directory /repo
|
||||
git config http.sslVerify false
|
||||
|
||||
git fetch origin --prune
|
||||
git checkout -B main
|
||||
git reset --hard origin/main
|
||||
git status -sb
|
||||
'
|
||||
|
||||
### Étape C — NAS : rebuild du slot inactif + smoke + bascule
|
||||
|
||||
Rebuild de l’image (slot inactif recommandé).
|
||||
|
||||
docker compose up -d --force-recreate --no-build web_green (ou blue)
|
||||
|
||||
smoke test via script ou curl
|
||||
|
||||
bascule DSM vers le port du slot actif
|
||||
|
||||
## 4) Checkpoints rapides (sanity)
|
||||
### 4.1 Vérifier que NAS = origin/main
|
||||
|
||||
git rev-parse --short HEAD sur NAS (via alpine/git)
|
||||
|
||||
doit égaler origin/main.
|
||||
|
||||
### 4.2 Vérifier “Proposer” (points minimum)
|
||||
|
||||
PUBLIC_GITEA_OWNER=Archicratia (casse sensible)
|
||||
|
||||
PUBLIC_GITEA_REPO=archicratie-edition
|
||||
|
||||
Flow : Proposer → choix 1 → choix 2 → onglet Gitea /issues/new?... OK
|
||||
|
||||
## 5) Rollback
|
||||
|
||||
DSM reverse proxy : repasser sur l’autre port (8081/8082).
|
||||
|
||||
En cas de code cassé : réaligner NAS sur origin/main précédent (tag/release) ou repointer /current vers une release précédente.
|
||||
@@ -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 qu’un 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 qu’un 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
|
||||
|
||||
|
||||
122
docs/RUNBOOK-PR-AUTO-GITEA.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# RUNBOOK — Créer une Demande d’ajout (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 d’ajout” (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 n’affiche rien : tu n’as 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 d’ajout”.
|
||||
|
||||
# Chemin B — via l’UI Gitea (si tu veux le bandeau vert)
|
||||
|
||||
Va sur le dépôt
|
||||
|
||||
Onglet “Demandes d’ajout”
|
||||
|
||||
Clique “Nouvelle demande d’ajout”
|
||||
|
||||
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 n’es pas sur la bonne branche
|
||||
|
||||
# Diagnostic :
|
||||
|
||||
git branch --show-current
|
||||
|
||||
Tu regardes l’UI au mauvais endroit
|
||||
|
||||
Solution : utilise le bouton “Nouvelle demande d’ajout” ou le lien du terminal (chemin A).
|
||||
|
||||
Anti-bêtise (optionnel mais recommandé)
|
||||
Empêcher de commit sur main par erreur (hook local)
|
||||
|
||||
# Créer .git/hooks/pre-commit :
|
||||
|
||||
#!/bin/sh
|
||||
b="$(git branch --show-current)"
|
||||
if [ "$b" = "main" ]; then
|
||||
echo "❌ Refus: commit interdit sur main. Crée une branche."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Puis :
|
||||
|
||||
chmod +x .git/hooks/pre-commit
|
||||
|
||||
## Rappel : main protégé
|
||||
|
||||
Si main est protégé, tu ne merges PAS par git push origin main.
|
||||
Tu merges via la PR (UI), après CI verte.
|
||||
|
||||
176
docs/START-HERE.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# START-HERE — Archicratie / Édition Web (v2)
|
||||
> Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO)
|
||||
|
||||
## 0) TL;DR (la règle d’or)
|
||||
- **Gitea = source canonique**.
|
||||
- **main est protégé** : toute modification passe par **branche → PR → CI → merge**.
|
||||
- **Le NAS n’est pas la source** : si un hotfix est fait sur NAS, on **backporte** via PR immédiatement.
|
||||
- **Le site est statique Astro** : la prod sert du HTML (nginx), l’accès est contrôlé au niveau reverse-proxy (Traefik + Authelia).
|
||||
|
||||
## 1) Architecture mentale (ultra simple)
|
||||
- **DEV (Mac Studio)** : édition + tests + commit + push
|
||||
- **Gitea** : dépôt canon + PR + CI (CI.yaml)
|
||||
- **NAS (DS220+)** : déploiement “blue/green”
|
||||
- `web_blue` (staging upstream) → `127.0.0.1:8081`
|
||||
- `web_green` (live upstream) → `127.0.0.1:8082`
|
||||
- **Edge (Traefik)** : route les hosts
|
||||
- `staging.archicratie...` → 8081
|
||||
- `archicratie...` → 8082
|
||||
- **Authelia** devant, via middleware `chain-auth@file`
|
||||
|
||||
## 2) Répertoires & conventions (repo)
|
||||
### 2.1 Contenu canon (édition)
|
||||
- `src/content/**` : contenu MD / MDX canon (Astro content collections)
|
||||
- `src/pages/**` : routes Astro (index, [...slug], etc.)
|
||||
- `src/components/**` : composants UI (SiteNav, TOC, SidePanel, etc.)
|
||||
- `src/layouts/**` : layouts (EditionLayout, SiteLayout)
|
||||
- `src/styles/**` : CSS global
|
||||
|
||||
### 2.2 Annotations (pré-Édition “tickets”)
|
||||
- `src/annotations/<workKey>/<slug>.yml`
|
||||
- Exemple : `src/annotations/archicrat-ia/prologue.yml`
|
||||
- Objectif : stocker “Références / Médias / Commentaires” par page et par paragraphe (`p-...`).
|
||||
|
||||
### 2.3 Scripts (tooling / build)
|
||||
- `scripts/inject-anchor-aliases.mjs` : injection aliases dans dist
|
||||
- `scripts/dedupe-ids-dist.mjs` : retire IDs dupliqués dans dist
|
||||
- `scripts/build-para-index.mjs` : index paragraphes (postbuild / predev)
|
||||
- `scripts/build-annotations-index.mjs` : index annotations (postbuild / predev)
|
||||
- `scripts/check-anchors.mjs` : contrat stabilité d’ancres (CI)
|
||||
- `scripts/check-annotations*.mjs` : sanity YAML + médias
|
||||
|
||||
> Important : les scripts sont **partie intégrante** de la stabilité (IDs/ancres/indexation).
|
||||
> On évite “la magie” : tout est scripté + vérifié.
|
||||
|
||||
## 3) Workflow Git “pro” (main protégé)
|
||||
### 3.1 Cycle standard (toute modif)
|
||||
en bash :
|
||||
|
||||
git checkout main
|
||||
git pull --ff-only
|
||||
|
||||
BR="chore/xxx-$(date +%Y%m%d)"
|
||||
git checkout -b "$BR"
|
||||
|
||||
# dev…
|
||||
npm i
|
||||
npm run build
|
||||
npm run test:anchors
|
||||
|
||||
git add -A
|
||||
git commit -m "xxx: description claire"
|
||||
git push -u origin "$BR"
|
||||
|
||||
### 3.2 PR vers main
|
||||
|
||||
Ouvrir PR dans Gitea
|
||||
|
||||
CI doit être verte
|
||||
|
||||
Merge PR → main
|
||||
|
||||
### 3.3 Cas spécial : hotfix prod (NAS)
|
||||
|
||||
On peut faire un hotfix “urgence” en prod/staging si nécessaire…
|
||||
|
||||
MAIS : l’état final doit revenir dans Gitea : branche → PR → CI → merge.
|
||||
|
||||
## 4) Déploiement (NAS) — principe
|
||||
### 4.1 Release pack
|
||||
|
||||
On génère un pack “reproductible” (source + config + scripts) puis on déploie.
|
||||
|
||||
### 4.2 Blue/Green
|
||||
|
||||
web_blue = staging upstream (8081)
|
||||
|
||||
web_green = live upstream (8082)
|
||||
|
||||
Edge Traefik sélectionne quel host pointe vers quel upstream.
|
||||
|
||||
## 5) Check-list “≤ 10 commandes” (happy path complet)
|
||||
### 5.1 DEV (Mac)
|
||||
|
||||
git checkout main && git pull --ff-only
|
||||
git checkout -b chore/my-change-$(date +%Y%m%d)
|
||||
|
||||
npm i
|
||||
rm -rf .astro node_modules/.vite dist
|
||||
npm run build
|
||||
npm run test:anchors
|
||||
npm run dev
|
||||
|
||||
### 5.2 Push + PR
|
||||
|
||||
git add -A
|
||||
git commit -m "chore: my change"
|
||||
git push -u origin chore/my-change-YYYYMMDD
|
||||
# ouvrir PR dans Gitea
|
||||
|
||||
### 5.3 Déploiement NAS (résumé)
|
||||
|
||||
Voir docs/runbooks/DEPLOY-BLUE-GREEN.md.
|
||||
|
||||
## 6) Problèmes “classiques” + diagnostic rapide
|
||||
### 6.1 “Le staging ne ressemble pas au local”
|
||||
|
||||
# Comparer upstream direct 8081 vs 8082 :
|
||||
|
||||
curl -sS http://127.0.0.1:8081/ | head -n 2
|
||||
curl -sS http://127.0.0.1:8082/ | head -n 2
|
||||
|
||||
# Vérifier quel routeur edge répond (header diag) :
|
||||
|
||||
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
|
||||
| grep -iE 'HTTP/|location:|x-archi-router'
|
||||
|
||||
# Lire docs/runbooks/EDGE-TRAEFIK.md.
|
||||
|
||||
### 6.2 Canonical incorrect (localhost en prod)
|
||||
|
||||
Cause racine : site dans Astro = PUBLIC_SITE non injecté au build.
|
||||
|
||||
Fix canonique : voir docs/runbooks/ENV-PUBLIC_SITE.md.
|
||||
|
||||
Test :
|
||||
|
||||
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1
|
||||
|
||||
### 6.3 Contrat “anchors” en échec après migration d’URL
|
||||
|
||||
Quand on déplace des routes (ex: /archicratie/archicrat-ia/* → /archicrat-ia/*), le test d’ancres peut échouer même si les IDs n’ont pas changé, car les pages ont changé de chemin.
|
||||
|
||||
# Procédure safe :
|
||||
|
||||
Backup baseline :
|
||||
|
||||
cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S)
|
||||
|
||||
Mettre à jour les clés (chemins) sans toucher aux IDs :
|
||||
|
||||
node - <<'NODE'
|
||||
import fs from 'fs';
|
||||
const p='tests/anchors-baseline.json';
|
||||
const j=JSON.parse(fs.readFileSync(p,'utf8'));
|
||||
const out={};
|
||||
for (const [k,v] of Object.entries(j)) {
|
||||
const nk = k.replace(/^archicratie\/archicrat-ia\//, 'archicrat-ia/');
|
||||
out[nk]=v;
|
||||
}
|
||||
fs.writeFileSync(p, JSON.stringify(out,null,2)+'\n');
|
||||
console.log('updated keys:', Object.keys(j).length, '->', Object.keys(out).length);
|
||||
NODE
|
||||
|
||||
Re-run :
|
||||
|
||||
npm run test:anchors
|
||||
|
||||
## 7) Ce que l’étape 9 doit faire (orientation)
|
||||
|
||||
Stabiliser le pipeline “tickets → YAML annotations”
|
||||
|
||||
Formaliser la spec YAML + merge + anti-doublon (voir docs/EDITORIAL-ANNOTATIONS-SPEC.md)
|
||||
|
||||
Durcir l’onboarding (ce START-HERE + runbooks)
|
||||
|
||||
Éviter les régressions par tests (anchors / annotations / smoke)
|
||||
217
docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# TROUBLESHOOTING — Archicratie Web / NAS / Gitea
|
||||
|
||||
Dernière mise à jour : 2026-02-01
|
||||
|
||||
Ce document liste les symptômes rencontrés et les remèdes **concrets**.
|
||||
|
||||
---
|
||||
|
||||
## 0) Réflexe unique
|
||||
Toujours isoler : **Local**, **Gitea**, **NAS**, **Navigateur**.
|
||||
|
||||
- Si ça marche sur `127.0.0.1:8082` mais pas sur le domaine → proxy/cache.
|
||||
- Si ça marche après login Gitea mais pas via “Proposer” → variables `PUBLIC_GITEA_*`.
|
||||
- Si push refusé → branch protection (normal).
|
||||
|
||||
---
|
||||
|
||||
<a id="proposer-404"></a>
|
||||
## 1) “Proposer” ouvre Gitea mais retourne 404 / non autorisé
|
||||
|
||||
### Symptôme
|
||||
Nouvel onglet :
|
||||
- 404 Not Found / “n’existe pas ou pas autorisé”
|
||||
- ou redirect `/user/login`
|
||||
|
||||
### Cause la plus fréquente
|
||||
URL pointe vers **mauvais owner/repo** (casse sensible) :
|
||||
- `archicratia/archicratie-web` au lieu de `Archicratia/archicratie-edition`
|
||||
|
||||
### Diagnostic
|
||||
Sur NAS (ou dans le HTML généré), vérifier l’URL ouverte :
|
||||
- doit contenir : `/Archicratia/archicratie-edition/issues/new`
|
||||
|
||||
### Fix
|
||||
Dans `.env` de build prod (NAS) :
|
||||
- `PUBLIC_GITEA_OWNER=Archicratia`
|
||||
- `PUBLIC_GITEA_REPO=archicratie-edition`
|
||||
Puis rebuild + restart du container + smoke.
|
||||
|
||||
---
|
||||
|
||||
<a id="proposer-double-onglet"></a>
|
||||
## 2) Double onglet à la validation du flow “Proposer”
|
||||
|
||||
### Symptôme
|
||||
Deux onglets s’ouvrent au moment de valider (après choix 1 / choix 2).
|
||||
|
||||
### Causes possibles
|
||||
- handler JS déclenché deux fois (bubbling)
|
||||
- présence d’un `window.open` + `a.click()` simultanément
|
||||
- bouton “Proposer” est un `<a target=_blank>` et un autre handler ouvre aussi.
|
||||
|
||||
### Diagnostic rapide (devtools navigateur)
|
||||
Chercher `window.open` dans la page générée :
|
||||
- la commande doit retourner 0 lignes.
|
||||
|
||||
Sur NAS :
|
||||
en sh :
|
||||
|
||||
curl -fsS http://127.0.0.1:8082/archicratie/archicrat-ia/chapitre-4/ > /tmp/page.html
|
||||
grep -n "window.open" /tmp/page.html | head
|
||||
|
||||
Fix
|
||||
|
||||
garder un seul mécanisme d’ouverture
|
||||
|
||||
sur click : preventDefault() + stopImmediatePropagation()
|
||||
|
||||
<a id="Favicon-504-erreurs"></a>
|
||||
## 3) Favicon 504 / erreurs console sur favicon
|
||||
|
||||
# Symptôme
|
||||
|
||||
Console navigateur : GET /favicon.ico 504
|
||||
|
||||
# Cause fréquente
|
||||
|
||||
Cache du navigateur (ancienne erreur conservée).
|
||||
|
||||
# Diagnostic
|
||||
|
||||
Comparer :
|
||||
|
||||
curl -I http://127.0.0.1:8082/favicon.ico
|
||||
|
||||
curl -kI https://<domaine>/favicon.ico
|
||||
|
||||
Si curl = 200 et navigateur = 504 → cache.
|
||||
|
||||
# Fix
|
||||
|
||||
Désactiver cache dans l’onglet Réseau (devtools)
|
||||
|
||||
hard refresh
|
||||
|
||||
vérifier droits fichiers dans dist/
|
||||
|
||||
## 4) Sur NAS : git: command not found
|
||||
# Symptôme
|
||||
|
||||
git fetch impossible sur le NAS.
|
||||
|
||||
# Cause
|
||||
|
||||
Git non installé sur DSM shell.
|
||||
|
||||
# Fix standard (recommandé)
|
||||
|
||||
Utiliser un conteneur git :
|
||||
|
||||
APP="/volume2/docker/archicratie-web/current"
|
||||
U_ID="$(id -u)"; G_ID="$(id -g)"
|
||||
|
||||
sudo docker run --rm --network host \
|
||||
-u "$U_ID:$G_ID" -e HOME=/tmp \
|
||||
-v "$APP":/repo -w /repo \
|
||||
--entrypoint sh alpine/git -lc '
|
||||
set -eu
|
||||
git config --global --add safe.directory /repo
|
||||
git config http.sslVerify false
|
||||
git fetch origin --prune
|
||||
git status -sb
|
||||
'
|
||||
|
||||
## 5) Git : “dubious ownership in repository”
|
||||
# Symptôme
|
||||
|
||||
fatal: detected dubious ownership
|
||||
|
||||
# Fix
|
||||
|
||||
Dans le conteneur git (ou machine locale) :
|
||||
git config --global --add safe.directory /repo
|
||||
|
||||
## 6) Git : non-fast-forward au push
|
||||
# Symptôme
|
||||
|
||||
rejected (non-fast-forward)
|
||||
|
||||
# Cause
|
||||
|
||||
Ta branche locale est en retard vs remote.
|
||||
|
||||
# Fix
|
||||
|
||||
En général :
|
||||
|
||||
on fait une PR depuis une branche
|
||||
|
||||
ou on rebase/merge origin/main avant push
|
||||
|
||||
Sur une branche de travail :
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
# ou
|
||||
git merge origin/main
|
||||
|
||||
## 7) Gitea : “Not allowed to push to protected branch main”
|
||||
# Symptôme
|
||||
|
||||
pre-receive hook declined
|
||||
|
||||
# Cause
|
||||
|
||||
Protection de branche (normal/attendu).
|
||||
|
||||
# Fix
|
||||
|
||||
Push sur une branche
|
||||
|
||||
Ouvrir PR
|
||||
|
||||
Merger via UI Gitea
|
||||
|
||||
## 8) Docker build : BuildKit / buildx / API version
|
||||
# Symptômes typiques
|
||||
|
||||
the --network option requires BuildKit
|
||||
|
||||
BuildKit is enabled but the buildx component is missing
|
||||
|
||||
client version ... too new. Maximum supported API version ...
|
||||
|
||||
# Fix “robuste” (principe)
|
||||
|
||||
installer buildx si nécessaire
|
||||
|
||||
si DSM/docker API ancienne : définir DOCKER_API_VERSION=<compatible> (selon ton environnement)
|
||||
|
||||
garder le build en --network host si nécessaire
|
||||
|
||||
## 9) Container Manager / Docker : “database is locked” (logging driver db)
|
||||
# Symptôme
|
||||
|
||||
failed to initialize logging driver : database is locked
|
||||
|
||||
# Cause
|
||||
|
||||
Le driver de logs Docker est db (Synology) et sa DB est verrouillée.
|
||||
|
||||
# Fix rapide
|
||||
|
||||
Redémarrer “Container Manager” depuis le centre de paquets DSM.
|
||||
|
||||
Vérifier que le conteneur redémarre ensuite.
|
||||
|
||||
## 10) Checklist “tout marche”
|
||||
|
||||
curl -I http://127.0.0.1:8082/ => 200
|
||||
|
||||
curl -kI https://<domaine>/ => 200
|
||||
|
||||
PUBLIC_GITEA_* corrects
|
||||
|
||||
“Proposer” : 1 onglet, pas de 404, issue pré-remplie
|
||||
|
||||
CI passe sur PR merge
|
||||
@@ -63,7 +63,7 @@ Si l’ID exact n’existe plus :
|
||||
But : éviter les “liens morts” historiques quand une régénération d’IDs a eu lieu.
|
||||
|
||||
Limite : c’est un fallback de dernier recours (moins déterministe qu’un 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 d’ancres générés (ou dérivés) peuvent changer :
|
||||
|
||||
## 2) Le mapping d’alias
|
||||
|
||||
- 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
@@ -0,0 +1,201 @@
|
||||
# Auth Stack — LLDAP + Authelia + Redis (DSM 7.3 / Synology DS220+)
|
||||
|
||||
## Objectif
|
||||
Fournir une pile d’authentification 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é d’Authelia (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 d’abord 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 l’environnement 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 qu’on 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 n’est pas configuré en OIDC vers Authelia, ce n’est pas du SSO.
|
||||
|
||||
Authelia protège l’accès, mais ne crée pas de session Gitea.
|
||||
|
||||
@@ -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/<ts>/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 && 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/<ts>/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 -> 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 |
@@ -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 l’URL 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 & 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">Où</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 l’autre 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 l’URL (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 l’autre 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 l’auth (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">L’un des deux peut rester l’ancien LIVE.</text>
|
||||
<text
|
||||
x="1110"
|
||||
y="590"
|
||||
class="small"
|
||||
id="text57">L’objectif 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 |
@@ -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 |
324
docs/diagrams/archicratie-web-edition-edge-routing-verbatim.svg
Normal 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 |
870
docs/diagrams/archicratie-web-edition-git-ci-workflow-v1.svg
Normal 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,"Liberation Mono","Courier New",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 d’un hotfix appliqué (si besoin), le remettre proprement sous Git (branche → PR → CI → merge),
|
||||
puis produire une release packagée et déployer sans régression.
|
||||
</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 "step8: sync hotfix</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan97"
|
||||
x="92"
|
||||
y="747.84998">(SidePanel/reading)"</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 c’est 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 l’unique 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 d’abord</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 d’or</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 n’est 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 |
537
docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg
Normal 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/<timestamp>/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 & 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 |
828
docs/diagrams/archicratie-web-edition-global-verbatim.svg
Normal 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 d’un 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 l’entré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 (d’aprè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 |
409
docs/diagrams/archicratie-web-edition-machine-editoriale-v2.svg
Normal 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 d’accordé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 |
596
docs/diagrams/archicratie-web-edition-machine-editoriale-v3.svg
Normal 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,"Liberation Mono","Courier New",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 & 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 & 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 l’UI (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 d’import / qualité</text>
|
||||
<text
|
||||
x="92"
|
||||
y="625"
|
||||
class="p"
|
||||
id="text25">Import DOCX, contrôle d’IDs, 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 d’ancres.</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 & 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">• L’ordre 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 |
@@ -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 d’accordé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 |
@@ -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,"Liberation Mono","Courier New",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 & 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 d’issues</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 & qualité)</text>
|
||||
<text
|
||||
x="92"
|
||||
y="655"
|
||||
class="p"
|
||||
id="text23">Import DOCX, checks d’IDs, aliases d’ancres, 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, l’UI 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 l’auth (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">C’est le “contrat” anti-régression : si tu changes l’ordre, 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 & fond explicites (zéro var()).</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -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="/_auth/whoami", 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="/_auth/whoami"</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 n’existe 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 <issue_number> --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"><span id="oldId" class="para-alias"></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 |
631
docs/diagrams/archicratie-web-edition-machine-editoriale.svg
Normal 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="/_auth/whoami", 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 d’ancre 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="/_auth/whoami"</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 n’existe 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 <issue_number> --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"><span id="oldId" class="para-alias"> 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
@@ -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 l’autre ; 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 |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/diagrams/out/archicratie-web-edition-git-ci-workflow-v1.png
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
docs/diagrams/out/archicratie-web-edition-global-verbatim-v2.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 360 KiB |
67
docs/gitea-pr-main-protege.md
Normal 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 d’abord qu’il 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 l’UI de la Pull Request (ou via un mainteneur).
|
||||
|
||||
69
docs/proposer-whoami-gate.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# “Proposer” protégé par groupe (whoami / editors)
|
||||
|
||||
## But
|
||||
Le bouton **Proposer** (création d’issue 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 d’auth (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” n’existe 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.
|
||||
82
docs/runbook-deploiement-web-blue-green.md
Normal 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 : s’assurer qu’un 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
|
||||
71
docs/runbook-gitea-branches-pr.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Runbook — Gitea : Branches, PR, Merge (sans se faire piéger)
|
||||
|
||||
## Règle n°1 (hyper importante)
|
||||
Une PR n’apparaî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 l’UI 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”)
|
||||
|
||||
# C’est 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 l’interdit.
|
||||
|
||||
Conclusion : la voie “pro” = PR + merge UI.
|
||||
67
docs/runbook-proposer-gitea.md
Normal 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 qu’aucune erreur JS n’empêche l’injection des outils paragraphe
|
||||
323
docs/runbooks/DEPLOY-BLUE-GREEN.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 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 n’es pas authentifié, tu verras un 302 vers auth... : c’est 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, c’est que current n’est pas un symlink correct OU que le parent n’est 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 l’ancien 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 d’un `annotations-index.json` consolidé depuis `src/annotations/**`
|
||||
- patch direct dans les conteneurs en cours d’exé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 d’ancres)
|
||||
- `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`
|
||||
|
||||
C’est 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) s’exé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 s’exé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 n’a 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
|
||||
147
docs/runbooks/EDGE-TRAEFIK.md
Normal 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 d’entré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 l’upstream 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 : c’est “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é, l’ordre 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.
|
||||
114
docs/runbooks/ENV-PUBLIC_SITE.md
Normal 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 c’est 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 quelqu’un oublie l’URL 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 l’injection 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.
|
||||
691
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"astro": "^5.16.11"
|
||||
"astro": "^5.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
@@ -1247,9 +1247,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
|
||||
"integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1260,9 +1260,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1273,9 +1273,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1286,9 +1286,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1299,9 +1299,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1312,9 +1312,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1325,9 +1325,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
|
||||
"integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1338,9 +1338,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
|
||||
"integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1351,9 +1351,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1364,9 +1364,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1377,9 +1377,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1390,9 +1390,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1403,9 +1403,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1416,9 +1416,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1429,9 +1429,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1442,9 +1442,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1455,9 +1455,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1468,9 +1468,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1481,9 +1481,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1494,9 +1494,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1507,9 +1507,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1520,9 +1520,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1533,9 +1533,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1546,9 +1546,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1559,9 +1559,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1905,9 +1905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/astro": {
|
||||
"version": "5.16.11",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz",
|
||||
"integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==",
|
||||
"version": "5.18.0",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.18.0.tgz",
|
||||
"integrity": "sha512-CHiohwJIS4L0G6/IzE1Fx3dgWqXBCXus/od0eGUfxrZJD2um2pE7ehclMmgL/fXqbU7NfE1Ze2pq34h2QaA6iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.13.0",
|
||||
@@ -1933,7 +1933,7 @@
|
||||
"dlv": "^1.1.3",
|
||||
"dset": "^3.1.4",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"estree-walker": "^3.0.3",
|
||||
"flattie": "^1.1.1",
|
||||
"fontace": "~0.4.0",
|
||||
@@ -1954,16 +1954,16 @@
|
||||
"prompts": "^2.4.2",
|
||||
"rehype": "^13.0.2",
|
||||
"semver": "^7.7.3",
|
||||
"shiki": "^3.20.0",
|
||||
"shiki": "^3.21.0",
|
||||
"smol-toml": "^1.6.0",
|
||||
"svgo": "^4.0.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tsconfck": "^3.1.6",
|
||||
"ultrahtml": "^1.6.0",
|
||||
"unifont": "~0.7.1",
|
||||
"unifont": "~0.7.3",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"unstorage": "^1.17.3",
|
||||
"unstorage": "^1.17.4",
|
||||
"vfile": "^6.0.3",
|
||||
"vite": "^6.4.1",
|
||||
"vitefu": "^1.1.1",
|
||||
@@ -1990,6 +1990,463 @@
|
||||
"sharp": "^0.34.0"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -2426,9 +2883,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
@@ -5233,9 +5690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -5248,31 +5705,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.55.1",
|
||||
"@rollup/rollup-android-arm64": "4.55.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.55.1",
|
||||
"@rollup/rollup-darwin-x64": "4.55.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.55.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.55.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.55.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.55.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.55.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.55.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.55.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.55.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.55.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -5681,9 +6138,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
19
package.json
@@ -4,32 +4,29 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev": "node scripts/write-dev-whoami.mjs && astro dev",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
|
||||
"clean": "rm -rf dist",
|
||||
"build": "astro build",
|
||||
"build:clean": "npm run clean && npm run build",
|
||||
|
||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
|
||||
|
||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && node scripts/dedupe-ids-dist.mjs && node scripts/build-para-index.mjs && node scripts/build-annotations-index.mjs && node scripts/purge-dist-dev-whoami.mjs && npx pagefind --site dist",
|
||||
"import": "node scripts/import-docx.mjs",
|
||||
"apply:ticket": "node scripts/apply-ticket.mjs",
|
||||
|
||||
"audit:dist": "node scripts/audit-dist.mjs",
|
||||
|
||||
"build:para-index": "node scripts/build-para-index.mjs",
|
||||
"build:annotations-index": "node scripts/build-annotations-index.mjs",
|
||||
"test:aliases": "node scripts/check-anchor-aliases.mjs",
|
||||
"test:anchors": "node scripts/check-anchors.mjs",
|
||||
"test:anchors:update": "node scripts/check-anchors.mjs --update",
|
||||
|
||||
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && node scripts/check-inline-js.mjs",
|
||||
|
||||
"test:annotations": "node scripts/check-annotations.mjs",
|
||||
"test:annotations:media": "node scripts/check-annotations-media.mjs",
|
||||
"test": "npm run test:aliases && npm run build:clean && npm run audit:dist && node scripts/verify-anchor-aliases-in-dist.mjs && npm run test:anchors && npm run test:annotations && npm run test:annotations:media && node scripts/check-inline-js.mjs",
|
||||
"ci": "CI=1 npm test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"astro": "^5.16.11"
|
||||
"astro": "^5.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
|
||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 816 KiB |
|
After Width: | Height: | Size: 822 KiB |
|
After Width: | Height: | Size: 822 KiB |
@@ -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"}
|
||||
899
scripts/apply-annotation-ticket.mjs
Normal 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);
|
||||
});
|
||||
246
scripts/build-annotations-index.mjs
Normal 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);
|
||||
});
|
||||
148
scripts/build-para-index.mjs
Normal 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(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function normalizeSpaces(s) {
|
||||
return decodeEntities(s).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function relPageFromIndexHtml(inDirAbs, fileAbs) {
|
||||
const rel = path.relative(inDirAbs, fileAbs).replace(/\\/g, "/");
|
||||
if (!/index\.html$/i.test(rel)) return null;
|
||||
|
||||
// dist/<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);
|
||||
});
|
||||
104
scripts/check-annotations-media.mjs
Normal 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);
|
||||
});
|
||||
224
scripts/check-annotations.mjs
Normal 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 d’abord `npm run build` (ou `npm test`).");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const aliases = await loadAliases();
|
||||
const files = (await walk(ANNO_DIR)).filter((p) => /\.(ya?ml|json)$/i.test(p));
|
||||
|
||||
// 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
@@ -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 d’id : 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 l’ouverture de tag en supprimant l’attribut 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);
|
||||
});
|
||||
@@ -114,7 +114,6 @@ async function runMammoth(docxPath, assetsOutDirWebRoot) {
|
||||
);
|
||||
|
||||
let html = result.value || "";
|
||||
|
||||
// Mammoth gives relative src="image-xx.png" ; we will prefix later
|
||||
return html;
|
||||
}
|
||||
@@ -182,6 +181,25 @@ async function exists(p) {
|
||||
try { await fs.access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ compat:
|
||||
* - ancien : collection="archicratie" + slug="archicrat-ia/chapitre-3"
|
||||
* - nouveau : collection="archicrat-ia" + slug="chapitre-3"
|
||||
*
|
||||
* But : toujours écrire dans src/content/archicrat-ia/<slugSansPrefix>.mdx
|
||||
*/
|
||||
function normalizeDest(collection, slug) {
|
||||
let outCollection = String(collection || "").trim();
|
||||
let outSlug = String(slug || "").trim().replace(/^\/+|\/+$/g, "");
|
||||
|
||||
if (outCollection === "archicratie" && outSlug.startsWith("archicrat-ia/")) {
|
||||
outCollection = "archicrat-ia";
|
||||
outSlug = outSlug.replace(/^archicrat-ia\//, "");
|
||||
}
|
||||
|
||||
return { outCollection, outSlug };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const manifestPath = path.resolve(args.manifest);
|
||||
@@ -203,11 +221,14 @@ async function main() {
|
||||
|
||||
for (const it of selected) {
|
||||
const docxPath = path.resolve(it.source);
|
||||
const outFile = path.resolve("src/content", it.collection, `${it.slug}.mdx`);
|
||||
|
||||
const { outCollection, outSlug } = normalizeDest(it.collection, it.slug);
|
||||
|
||||
const outFile = path.resolve("src/content", outCollection, `${outSlug}.mdx`);
|
||||
const outDir = path.dirname(outFile);
|
||||
|
||||
const assetsPublicDir = path.posix.join("/imported", it.collection, it.slug);
|
||||
const assetsDiskDir = path.resolve("public", "imported", it.collection, it.slug);
|
||||
const assetsPublicDir = path.posix.join("/imported", outCollection, outSlug);
|
||||
const assetsDiskDir = path.resolve("public", "imported", outCollection, outSlug);
|
||||
|
||||
if (!(await exists(docxPath))) {
|
||||
throw new Error(`Missing source docx: ${docxPath}`);
|
||||
@@ -241,18 +262,20 @@ async function main() {
|
||||
html = rewriteLocalImageLinks(html, assetsPublicDir);
|
||||
body = html.trim() ? html : "<p>(Import vide)</p>";
|
||||
}
|
||||
|
||||
|
||||
const defaultVersion = process.env.PUBLIC_RELEASE || "0.1.0";
|
||||
|
||||
// ✅ IMPORTANT: archicrat-ia partage edition/status avec archicratie (pas de migration frontmatter)
|
||||
const schemaDefaultsByCollection = {
|
||||
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
|
||||
ia: { edition: "ia", status: "cas_pratique", level: 1 },
|
||||
traite: { edition: "traite", status: "ontodynamique", level: 1 },
|
||||
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
|
||||
atlas: { edition: "atlas", status: "atlas", level: 1 },
|
||||
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
|
||||
"archicrat-ia": { edition: "archicrat-ia", status: "essai_these", level: 1 },
|
||||
ia: { edition: "ia", status: "cas_pratique", level: 1 },
|
||||
traite: { edition: "traite", status: "ontodynamique", level: 1 },
|
||||
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
|
||||
atlas: { edition: "atlas", status: "atlas", level: 1 },
|
||||
};
|
||||
|
||||
const defaults = schemaDefaultsByCollection[it.collection] || { edition: it.collection, status: "draft", level: 1 };
|
||||
const defaults = schemaDefaultsByCollection[outCollection] || { edition: outCollection, status: "draft", level: 1 };
|
||||
|
||||
const fm = [
|
||||
"---",
|
||||
@@ -282,4 +305,4 @@ async function main() {
|
||||
main().catch((e) => {
|
||||
console.error("\nERROR:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
@@ -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++;
|
||||
|
||||
31
scripts/purge-dist-dev-whoami.mjs
Normal 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);
|
||||
});
|
||||
101
scripts/seed-gitea-labels.mjs
Normal 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
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
26
scripts/write-dev-whoami.mjs
Normal 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})`);
|
||||
@@ -1,2 +1,5 @@
|
||||
{}
|
||||
|
||||
{
|
||||
"/archicrat-ia/chapitre-3/": {
|
||||
"p-1-60c7ea48": "p-1-a21087b0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { getCollection } from "astro:content";
|
||||
|
||||
const { currentSlug } = Astro.props;
|
||||
|
||||
const entries = (await getCollection("archicratie"))
|
||||
.filter((e) => e.slug.startsWith("archicrat-ia/"))
|
||||
// ✅ Après migration : TOC = collection "archicrat-ia"
|
||||
const entries = (await getCollection("archicrat-ia"))
|
||||
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
|
||||
|
||||
const href = (slug) => `/archicratie/${slug}/`;
|
||||
const href = (slug) => `/archicrat-ia/${slug}/`;
|
||||
---
|
||||
|
||||
<nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA">
|
||||
@@ -66,7 +66,6 @@ const href = (slug) => `/archicratie/${slug}/`;
|
||||
opacity: .88;
|
||||
}
|
||||
|
||||
/* On garde <ol> mais on neutralise tout marker/numéro */
|
||||
.toc-global__list{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@@ -148,7 +147,6 @@ const href = (slug) => `/archicratie/${slug}/`;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.toc-global{ background: rgba(255,255,255,0.04); }
|
||||
.toc-link:hover{ background: rgba(255,255,255,0.06); }
|
||||
@@ -162,4 +160,4 @@ const href = (slug) => `/archicratie/${slug}/`;
|
||||
const active = document.querySelector(".toc-global .toc-item.is-active");
|
||||
if (active) active.scrollIntoView({ block: "nearest" });
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,35 +1,128 @@
|
||||
---
|
||||
// src/components/LevelToggle.astro
|
||||
const { initialLevel = 1 } = Astro.props;
|
||||
---
|
||||
<div class="level-toggle" role="group" aria-label="Niveau de lecture">
|
||||
<button type="button" class="lvl-btn" data-level="1" aria-pressed="true">Niveau 1</button>
|
||||
<button type="button" class="lvl-btn" data-level="2" aria-pressed="false">Niveau 2</button>
|
||||
<button type="button" class="lvl-btn" data-level="3" aria-pressed="false">Niveau 3</button>
|
||||
|
||||
<div class="level-toggle" role="group" aria-label="Mode d’édition">
|
||||
<button type="button" class="level-btn" data-level="1">Propos</button>
|
||||
<button type="button" class="level-btn" data-level="2">Références</button>
|
||||
<button type="button" class="level-btn" data-level="3">Illustrations</button>
|
||||
<button type="button" class="level-btn" data-level="4">Commentaires</button>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline define:vars={{ initialLevel }}>
|
||||
(() => {
|
||||
const KEY = "archicratie.readingLevel";
|
||||
const buttons = Array.from(document.querySelectorAll(".lvl-btn"));
|
||||
const BODY = document.body;
|
||||
|
||||
function apply(level) {
|
||||
document.body.setAttribute("data-reading-level", String(level));
|
||||
buttons.forEach((b) => b.setAttribute("aria-pressed", b.dataset.level === String(level) ? "true" : "false"));
|
||||
const wrap = document.querySelector(".level-toggle");
|
||||
if (!wrap) return;
|
||||
|
||||
const buttons = Array.from(wrap.querySelectorAll("button[data-level]"));
|
||||
if (!buttons.length) return;
|
||||
|
||||
const KEY = "archicratie:readingLevel";
|
||||
|
||||
function clampLevel(n) {
|
||||
const x = Number.parseInt(String(n), 10);
|
||||
if (!Number.isFinite(x)) return 1;
|
||||
return Math.min(4, Math.max(1, x));
|
||||
}
|
||||
|
||||
// Valeur par défaut : si rien n'est stocké, on met 1 (citoyen).
|
||||
// Si JS est absent/casse, le site reste lisible (tout s'affiche).
|
||||
const stored = Number(localStorage.getItem(KEY));
|
||||
const level = (stored === 1 || stored === 2 || stored === 3) ? stored : 1;
|
||||
function setActiveUI(lvl) {
|
||||
for (const b of buttons) {
|
||||
const on = String(b.dataset.level) === String(lvl);
|
||||
b.classList.toggle("is-active", on);
|
||||
b.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
apply(level);
|
||||
function captureBeforeLevelSwitch() {
|
||||
const paraId =
|
||||
window.__archiCurrentParaId ||
|
||||
window.__archiLastParaId ||
|
||||
String(location.hash || "").replace(/^#/, "") ||
|
||||
"";
|
||||
|
||||
buttons.forEach((b) => {
|
||||
b.addEventListener("click", () => {
|
||||
const lvl = Number(b.dataset.level);
|
||||
localStorage.setItem(KEY, String(lvl));
|
||||
apply(lvl);
|
||||
});
|
||||
window.__archiLevelSwitchCtx = {
|
||||
paraId,
|
||||
hash: location.hash || "",
|
||||
scrollY: window.scrollY || 0,
|
||||
t: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function applyLevel(lvl, { persist = true } = {}) {
|
||||
const v = clampLevel(lvl);
|
||||
|
||||
if (BODY) BODY.dataset.readingLevel = String(v);
|
||||
setActiveUI(v);
|
||||
|
||||
if (persist) {
|
||||
try { localStorage.setItem(KEY, String(v)); } catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("archicratie:readingLevel", { detail: { level: v } })
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// init : storage > initialLevel
|
||||
let start = clampLevel(initialLevel);
|
||||
try {
|
||||
const stored = localStorage.getItem(KEY);
|
||||
if (stored) start = clampLevel(stored);
|
||||
} catch {}
|
||||
|
||||
applyLevel(start, { persist: false });
|
||||
|
||||
// clicks
|
||||
wrap.addEventListener("click", (ev) => {
|
||||
const btn = ev.target?.closest?.("button[data-level]");
|
||||
if (!btn) return;
|
||||
ev.preventDefault();
|
||||
|
||||
// ✅ crucial : on capture la position AVANT le reflow lié au changement de niveau
|
||||
captureBeforeLevelSwitch();
|
||||
applyLevel(btn.dataset.level);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.level-toggle{
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.level-btn{
|
||||
border: 1px solid rgba(127,127,127,0.40);
|
||||
background: rgba(127,127,127,0.08);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: filter .12s ease, transform .12s ease, background .12s ease, border-color .12s ease;
|
||||
}
|
||||
|
||||
.level-btn:hover{
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.level-btn.is-active{
|
||||
border-color: rgba(160,160,255,0.95);
|
||||
background: rgba(140,140,255,0.18);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.level-btn.is-active:hover{
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
.level-btn:active{
|
||||
transform: translateY(1px);
|
||||
}
|
||||
</style>
|
||||
|
||||
1074
src/components/SidePanel.astro
Normal file
@@ -3,7 +3,7 @@
|
||||
<a href="/editions/">Carte des œuvres</a><span aria-hidden="true"> · </span>
|
||||
<a href="/methode/">Méthode</a><span aria-hidden="true"> · </span>
|
||||
<a href="/recherche/">Recherche</a><span aria-hidden="true"> · </span>
|
||||
<a href="/archicratie/">Essai-thèse</a><span aria-hidden="true"> · </span>
|
||||
<a href="/archicrat-ia/">Essai-thèse</a><span aria-hidden="true"> · </span>
|
||||
<a href="/traite/">Traité</a><span aria-hidden="true"> · </span>
|
||||
<a href="/ia/">Cas IA</a><span aria-hidden="true"> · </span>
|
||||
<a href="/glossaire/">Glossaire</a><span aria-hidden="true"> · </span>
|
||||
|
||||
@@ -14,7 +14,7 @@ source:
|
||||
---
|
||||
Ce chapitre se tient à un point nodal de notre essai-thèse : il ouvre un espace d’exploration systématique des formes conceptuelles et philosophiques à travers lesquelles le pouvoir se configure comme régime de régulation. Il ne s’agit pas ici de revenir une nouvelle fois sur les fondements de l’autorité, ni d’interroger la légitimité politique au sens classique du terme, ni même d’enquêter sur la genèse des institutions. L’ambition est autre, structurelle, transversale, morphologique, elle tentera d’arpenter, à même les dispositifs, les pensées, les théorisations et les expériences, les modalités différentiées par lesquelles s’instaurent, s’éprouvent et se disputent les formes de régulation du vivre-ensemble.
|
||||
|
||||
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point d’origine, ne prétend restituer aucune ontologie stable du politique. Ce qu’il donne à lire, c’est une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières.
|
||||
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point d’origine, ne prétend restituer aucune ontologie stable du politique. Ce qu’il donne à lire, c’est une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières, et souvent complémentaires.
|
||||
|
||||
Ainsi, loin d’être une galerie illustrative de théories politiques juxtaposées, le chapitre s’agence comme une topologie critique, une plongée stratigraphique dans les scènes où s’articule la régulation — entendue ici non comme stabilisation externe ou ajustement technico-fonctionnel, mais comme dispositif instituant, tension structurante, scène traversée de conflictualité et d’exigence normative. Car à nos yeux, la régulation n’est pas ce qui vient après le pouvoir, elle en est la forme même constitutive — son architecture, son rythme, son épaisseur. Elle est ce par quoi le pouvoir ne se contente pas d’être exercé, mais s’institue, se justifie, se dispute, se recompose.
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineCollection, z } from "astro:content";
|
||||
|
||||
const linkSchema = z.object({
|
||||
type: z.enum(["definition", "appui", "transposition"]),
|
||||
target: z.string().min(1), // URL interne (ex: /glossaire/archicratie/) ou slug
|
||||
target: z.string().min(1),
|
||||
note: z.string().optional()
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ const baseTextSchema = z.object({
|
||||
version: z.string().min(1),
|
||||
concepts: z.array(z.string().min(1)).default([]),
|
||||
links: z.array(linkSchema).default([]),
|
||||
// optionnels mais utiles dès maintenant
|
||||
order: z.number().int().nonnegative().optional(),
|
||||
summary: z.string().optional()
|
||||
});
|
||||
@@ -50,20 +49,31 @@ const atlas = defineCollection({
|
||||
})
|
||||
});
|
||||
|
||||
// ✅ NOUVELLE collection : archicrat-ia (Essai-thèse)
|
||||
// NOTE : on accepte temporairement edition/status "archicratie/modele_sociopolitique"
|
||||
// si tes MDX n’ont pas encore été normalisés.
|
||||
// Quand tu voudras "strict", on passera à edition="archicrat-ia" status="essai_these"
|
||||
// + update frontmatter des 7 fichiers.
|
||||
const archicratIa = defineCollection({
|
||||
type: "content",
|
||||
schema: baseTextSchema.extend({
|
||||
edition: z.union([z.literal("archicrat-ia"), z.literal("archicratie")]),
|
||||
status: z.union([z.literal("essai_these"), z.literal("modele_sociopolitique")])
|
||||
})
|
||||
});
|
||||
|
||||
// Glossaire (référentiel terminologique)
|
||||
const glossaire = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string().min(1), // Titre public (souvent identique au terme)
|
||||
term: z.string().min(1), // Terme canonique
|
||||
title: z.string().min(1),
|
||||
term: z.string().min(1),
|
||||
aliases: z.array(z.string().min(1)).default([]),
|
||||
edition: z.literal("glossaire"),
|
||||
status: z.literal("referentiel"),
|
||||
version: z.string().min(1),
|
||||
// Micro-définition affichable en popover (courte, stable)
|
||||
definitionShort: z.string().min(1),
|
||||
concepts: z.array(z.string().min(1)).default([]),
|
||||
// Liens typés (vers ouvrages ou autres termes)
|
||||
links: z.array(linkSchema).default([])
|
||||
})
|
||||
});
|
||||
@@ -73,5 +83,8 @@ export const collections = {
|
||||
archicratie,
|
||||
ia,
|
||||
glossaire,
|
||||
atlas
|
||||
};
|
||||
atlas,
|
||||
|
||||
// ⚠️ clé avec tiret => doit être quotée
|
||||
"archicrat-ia": archicratIa
|
||||
};
|
||||