Compare commits
91 Commits
docs/norma
...
chore/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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 -->
|
||||
/...
|
||||
|
||||
|
||||
439
.gitea/workflows/anno-apply-pr.yml
Normal file
@@ -0,0 +1,439 @@
|
||||
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 || inputs.issue || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
apply-approved:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
- name: Tools sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Derive context (event.json / workflow_dispatch)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||||
|
||||
node --input-type=module - <<'NODE' > /tmp/anno.env
|
||||
import fs from "node:fs";
|
||||
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const repoObj = ev?.repository || {};
|
||||
|
||||
const cloneUrl =
|
||||
repoObj?.clone_url ||
|
||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
|
||||
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
||||
|
||||
let owner =
|
||||
repoObj?.owner?.login ||
|
||||
repoObj?.owner?.username ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
|
||||
|
||||
let repo =
|
||||
repoObj?.name ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
|
||||
|
||||
if (!owner || !repo) {
|
||||
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
||||
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
||||
}
|
||||
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
|
||||
|
||||
const defaultBranch = repoObj?.default_branch || "main";
|
||||
|
||||
const issueNumber =
|
||||
ev?.issue?.number ||
|
||||
ev?.issue?.index ||
|
||||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
|
||||
|
||||
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
|
||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||
}
|
||||
|
||||
const labelName =
|
||||
ev?.label?.name ||
|
||||
ev?.label ||
|
||||
"workflow_dispatch";
|
||||
|
||||
const u = new URL(cloneUrl);
|
||||
const origin = u.origin;
|
||||
|
||||
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
|
||||
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
|
||||
: origin;
|
||||
|
||||
function sh(s){ return JSON.stringify(String(s)); }
|
||||
|
||||
process.stdout.write([
|
||||
`CLONE_URL=${sh(cloneUrl)}`,
|
||||
`OWNER=${sh(owner)}`,
|
||||
`REPO=${sh(repo)}`,
|
||||
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
|
||||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||||
`LABEL_NAME=${sh(labelName)}`,
|
||||
`API_BASE=${sh(apiBase)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ context:"
|
||||
sed -n '1,120p' /tmp/anno.env
|
||||
|
||||
- name: Gate on label state/approved
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
|
||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||
echo "SKIP=1" >> /tmp/anno.env
|
||||
exit 0
|
||||
fi
|
||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Fetch issue + gate on Type (skip Proposer)
|
||||
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; }
|
||||
|
||||
ISSUE_JSON="$(curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")"
|
||||
|
||||
node --input-type=module - <<'NODE' "$ISSUE_JSON" >> /tmp/anno.env
|
||||
const issue = JSON.parse(process.argv[1] || "{}");
|
||||
const title = String(issue.title || "");
|
||||
const body = String(issue.body || "").replace(/\r\n/g, "\n");
|
||||
|
||||
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)}`);
|
||||
|
||||
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 "✅ issue type gating:"
|
||||
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
|
||||
[[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0
|
||||
|
||||
# message différent si Proposer
|
||||
REASON="${SKIP_REASON:-}"
|
||||
TYPE="${ISSUE_TYPE:-}"
|
||||
TITLE="${ISSUE_TITLE:-}"
|
||||
|
||||
if [[ "$REASON" == proposer_type:* ]]; then
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors** (correction/fact-check + cat/*).\n- Le bot n'applique **jamais** Proposer et n'ajoute **jamais** state/approved automatiquement.\n\n✅ Action : traitement éditorial manuel."
|
||||
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.\n✅ Action : traitement manuel si nécessaire."
|
||||
else
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\n✅ Action : corriger le ticket (ajouter 'Type: type/media|type/reference|type/comment') ou traiter manuellement."
|
||||
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 -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
|
||||
|
||||
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: Comment issue if no-op (already applied)
|
||||
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" ]] || exit 0
|
||||
[[ "${NOOP:-0}" == "1" ]] || exit 0
|
||||
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} : rien à appliquer (déjà présent / dédupliqué)."
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$PAYLOAD"
|
||||
|
||||
- name: Push bot branch
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env || 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; }
|
||||
|
||||
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"
|
||||
98
.gitea/workflows/anno-reject.yml
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Anno Reject
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
reject:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
- name: Derive context
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||||
|
||||
node --input-type=module - <<'NODE' > /tmp/reject.env
|
||||
import fs from "node:fs";
|
||||
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const repoObj = ev?.repository || {};
|
||||
const cloneUrl =
|
||||
repoObj?.clone_url ||
|
||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
if (!cloneUrl) throw new Error("No repository url");
|
||||
|
||||
let owner =
|
||||
repoObj?.owner?.login ||
|
||||
repoObj?.owner?.username ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
|
||||
let repo =
|
||||
repoObj?.name ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
|
||||
|
||||
if (!owner || !repo) {
|
||||
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
||||
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
||||
}
|
||||
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
|
||||
|
||||
const issueNumber = ev?.issue?.number || ev?.issue?.index;
|
||||
if (!issueNumber) throw new Error("No issue number");
|
||||
|
||||
const labelName = ev?.label?.name || ev?.label || "";
|
||||
const u = new URL(cloneUrl);
|
||||
|
||||
function sh(s){ return JSON.stringify(String(s)); }
|
||||
process.stdout.write([
|
||||
`OWNER=${sh(owner)}`,
|
||||
`REPO=${sh(repo)}`,
|
||||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||||
`LABEL_NAME=${sh(labelName)}`,
|
||||
`API_BASE=${sh(u.origin)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
- name: Gate on label state/rejected
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/reject.env
|
||||
if [[ "$LABEL_NAME" != "state/rejected" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||
exit 0
|
||||
fi
|
||||
echo "✅ reject issue=$ISSUE_NUMBER"
|
||||
|
||||
- name: Comment + close issue
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/reject.env
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
|
||||
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$PAYLOAD"
|
||||
|
||||
curl -fsS -X PATCH \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
--data-binary '{"state":"closed"}'
|
||||
@@ -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
|
||||
503
.gitea/workflows/deploy-staging-live.yml
Normal file
@@ -0,0 +1,503 @@
|
||||
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: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
- name: Tools sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Checkout (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";
|
||||
const sha =
|
||||
(process.env.GITHUB_SHA && String(process.env.GITHUB_SHA).trim()) ||
|
||||
ev?.after ||
|
||||
ev?.sha ||
|
||||
ev?.head_commit?.id ||
|
||||
ev?.pull_request?.head?.sha ||
|
||||
"";
|
||||
|
||||
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
||||
fs.writeFileSync("/tmp/deploy.env", [
|
||||
`REPO_URL=${shq(cloneUrl)}`,
|
||||
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
|
||||
`SHA=${shq(sha)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
source /tmp/deploy.env
|
||||
echo "Repo URL: $REPO_URL"
|
||||
echo "Default branch: $DEFAULT_BRANCH"
|
||||
echo "SHA: ${SHA:-<empty>}"
|
||||
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git remote add origin "$REPO_URL"
|
||||
|
||||
if [[ -n "${SHA:-}" ]]; then
|
||||
git fetch --depth 1 origin "$SHA"
|
||||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||||
else
|
||||
git fetch --depth 1 origin "$DEFAULT_BRANCH"
|
||||
git -c advice.detachedHead=false checkout -q "origin/$DEFAULT_BRANCH"
|
||||
SHA="$(git rev-parse HEAD)"
|
||||
echo "SHA='$SHA'" >> /tmp/deploy.env
|
||||
echo "Resolved SHA: $SHA"
|
||||
fi
|
||||
|
||||
git log -1 --oneline
|
||||
|
||||
- name: Gate — decide HOTPATCH vs FULL rebuild
|
||||
env:
|
||||
INPUT_FORCE: ${{ inputs.force }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
|
||||
FORCE="${INPUT_FORCE:-0}"
|
||||
|
||||
# liste fichiers touchés (utile pour copier les médias)
|
||||
CHANGED="$(git show --name-only --pretty="" "$SHA" | sed '/^$/d' || true)"
|
||||
printf "%s\n" "$CHANGED" > /tmp/changed.txt
|
||||
|
||||
echo "== changed files =="
|
||||
echo "$CHANGED" | sed -n '1,260p'
|
||||
|
||||
if [[ "$FORCE" == "1" ]]; then
|
||||
echo "GO=1" >> /tmp/deploy.env
|
||||
echo "MODE='full'" >> /tmp/deploy.env
|
||||
echo "✅ force=1 -> MODE=full (rebuild+restart)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Auto mode: uniquement annotations/media => hotpatch only
|
||||
if echo "$CHANGED" | grep -qE '^(src/annotations/|public/media/)'; then
|
||||
echo "GO=1" >> /tmp/deploy.env
|
||||
echo "MODE='hotpatch'" >> /tmp/deploy.env
|
||||
echo "✅ annotations/media change -> MODE=hotpatch"
|
||||
else
|
||||
echo "GO=0" >> /tmp/deploy.env
|
||||
echo "MODE='skip'" >> /tmp/deploy.env
|
||||
echo "ℹ️ no annotations/media change -> skip deploy"
|
||||
fi
|
||||
|
||||
- name: Install docker client + docker compose plugin (v2) + python yaml
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update
|
||||
apt-get install -y --no-install-recommends ca-certificates curl docker.io python3 python3-yaml
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
mkdir -p /usr/local/lib/docker/cli-plugins
|
||||
curl -fsSL \
|
||||
"https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64" \
|
||||
-o /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
|
||||
docker version
|
||||
docker compose version
|
||||
python3 --version
|
||||
|
||||
# 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
|
||||
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`
|
||||
|
||||
@@ -83,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
|
||||
@@ -43,61 +43,15 @@ Le flow ne doit jamais ouvrir deux onglets.
|
||||
- un seul `a.target="_blank"` (ou équivalent) déclenché
|
||||
- sur click : handler doit neutraliser les propagations parasites
|
||||
|
||||
### Vérification (NAS)
|
||||
en sh :
|
||||
curl -fsS http://127.0.0.1:8082/archicratie/archicrat-ia/chapitre-4/ > /tmp/page.html
|
||||
grep -n "window.open" /tmp/page.html | head
|
||||
## Diagnostic (canonique)
|
||||
|
||||
Doit retourner 0 ligne.
|
||||
Le diagnostic détaillé est centralisé dans `docs/TROUBLESHOOTING.md` pour éviter les doublons.
|
||||
|
||||
## 3) Diagnostic “trace ouverture onglet” (navigateur)
|
||||
- 404 / non autorisé / redirect login :
|
||||
- voir : `TROUBLESHOOTING.md#proposer-404`
|
||||
- cause la plus fréquente : `PUBLIC_GITEA_OWNER/REPO` faux (souvent casse)
|
||||
|
||||
Dans la console, tu peux surcharger temporairement les mécanismes d’ouverture pour tracer :
|
||||
- Double onglet :
|
||||
- voir : `TROUBLESHOOTING.md#proposer-double-onglet`
|
||||
- cause la plus fréquente : double handler (bubbling) ou `window.open` + `a.click()`
|
||||
|
||||
si un window.open survient,
|
||||
|
||||
ou si un a.click target _blank est appelé.
|
||||
|
||||
But : prouver qu’il n’y a qu’un seul événement d’ouverture.
|
||||
|
||||
## 4) URL attendue (forme)
|
||||
|
||||
L’onglet doit ressembler à :
|
||||
|
||||
{PUBLIC_GITEA_BASE}/{OWNER}/{REPO}/issues/new?title=...&body=...
|
||||
|
||||
Important : owner et repo doivent être exactement ceux du repo canonique.
|
||||
|
||||
## 5) Pré-requis d’accès
|
||||
|
||||
L’utilisateur doit être loggé sur Gitea pour accéder à /issues/new
|
||||
|
||||
Si non loggé : redirect vers /user/login (comportement normal)
|
||||
|
||||
## 6) Tests fonctionnels (checklist)
|
||||
|
||||
Ouvrir une page chapitre (ex chapitre 4)
|
||||
|
||||
Clic Proposer (sur un paragraphe)
|
||||
|
||||
Choix 1 puis choix 2
|
||||
|
||||
Vérifier :
|
||||
|
||||
1 seul onglet
|
||||
|
||||
URL du repo correct
|
||||
|
||||
formulaire new issue visible
|
||||
|
||||
title/body pré-remplis (chemin + ancre + texte actuel)
|
||||
|
||||
Créer l’issue → vérifier le traitement CI/runner
|
||||
|
||||
## 7) Pannes typiques + causes
|
||||
|
||||
404 sur issue/new : PUBLIC_GITEA_OWNER/REPO faux (souvent casse)
|
||||
|
||||
2 onglets : double handler (bubbling + ouverture multiple)
|
||||
|
||||
pas de favicon : cache ou absence dans public/ → rebuild
|
||||
|
||||
@@ -11,6 +11,36 @@ Objectif : déployer une nouvelle version du site sur le NAS (DS220+) sans jamai
|
||||
|
||||
> 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).
|
||||
|
||||
## 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
|
||||
@@ -393,3 +423,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).
|
||||
|
||||
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)
|
||||
@@ -15,6 +15,7 @@ Toujours isoler : **Local**, **Gitea**, **NAS**, **Navigateur**.
|
||||
|
||||
---
|
||||
|
||||
<a id="proposer-404"></a>
|
||||
## 1) “Proposer” ouvre Gitea mais retourne 404 / non autorisé
|
||||
|
||||
### Symptôme
|
||||
@@ -38,6 +39,7 @@ Puis rebuild + restart du container + smoke.
|
||||
|
||||
---
|
||||
|
||||
<a id="proposer-double-onglet"></a>
|
||||
## 2) Double onglet à la validation du flow “Proposer”
|
||||
|
||||
### Symptôme
|
||||
@@ -64,7 +66,9 @@ garder un seul mécanisme 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
|
||||
|
||||
@@ -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
|
||||
202
docs/runbooks/DEPLOY-BLUE-GREEN.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# RUNBOOK — Déploiement Blue/Green (NAS DS220+)
|
||||
> Objectif : déployer une release **sans casser**, avec rollback immédiat.
|
||||
|
||||
## 0) Portée
|
||||
Ce runbook décrit le déploiement de l’édition web Archicratie sur NAS (Synology), en mode blue/green :
|
||||
- `web_blue` : upstream staging → `127.0.0.1:8081`
|
||||
- `web_green` : upstream live → `127.0.0.1:8082`
|
||||
- Edge Traefik publie :
|
||||
- `staging.archicratie.trans-hands.synology.me` → 8081
|
||||
- `archicratie.trans-hands.synology.me` → 8082
|
||||
|
||||
## 1) Pré-requis
|
||||
- Accès shell NAS (user `archicratia`) + `sudo`
|
||||
- Docker Compose Synology nécessite souvent :
|
||||
- `sudo env DOCKER_API_VERSION=1.43 docker compose ...`
|
||||
- Les fichiers edge Traefik sont dans :
|
||||
- `/volume2/docker/edge/config/dynamic/`
|
||||
|
||||
## 2) Répertoires canon (NAS)
|
||||
On considère ces chemins (adapter si besoin, mais rester cohérent) :
|
||||
- Base : `/volume2/docker/archicratie-web`
|
||||
- Releases : `/volume2/docker/archicratie-web/releases/YYYYMMDD-HHMMSS/app`
|
||||
- Symlink actif : `/volume2/docker/archicratie-web/current` → pointe vers le `.../app` actif
|
||||
|
||||
## 3) Garde-fous (AVANT toute action)
|
||||
### 3.1 Snapshot de l’état actuel
|
||||
en bash :
|
||||
|
||||
cd /volume2/docker/archicratie-web
|
||||
ls -la current || true
|
||||
readlink current || true
|
||||
|
||||
### 3.2 Vérifier l’état live/staging upstream direct
|
||||
|
||||
curl -sSI http://127.0.0.1:8081/ | head -n 12
|
||||
curl -sSI http://127.0.0.1:8082/ | head -n 12
|
||||
|
||||
### 3.3 Vérifier l’état edge (host routing)
|
||||
|
||||
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
|
||||
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
|
||||
|
||||
curl -sSI -H 'Host: archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
|
||||
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
|
||||
|
||||
Si tu 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).
|
||||
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.
|
||||
473
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"astro": "^5.16.11"
|
||||
"astro": "^5.17.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
@@ -1905,9 +1905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/astro": {
|
||||
"version": "5.16.11",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.16.11.tgz",
|
||||
"integrity": "sha512-Z7kvkTTT5n6Hn5lCm6T3WU6pkxx84Hn25dtQ6dR7ATrBGq9eVa8EuB/h1S8xvaoVyCMZnIESu99Z9RJfdLRLDA==",
|
||||
"version": "5.17.3",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
|
||||
"integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.13.0",
|
||||
@@ -1933,7 +1933,7 @@
|
||||
"dlv": "^1.1.3",
|
||||
"dset": "^3.1.4",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"estree-walker": "^3.0.3",
|
||||
"flattie": "^1.1.1",
|
||||
"fontace": "~0.4.0",
|
||||
@@ -1954,16 +1954,16 @@
|
||||
"prompts": "^2.4.2",
|
||||
"rehype": "^13.0.2",
|
||||
"semver": "^7.7.3",
|
||||
"shiki": "^3.20.0",
|
||||
"shiki": "^3.21.0",
|
||||
"smol-toml": "^1.6.0",
|
||||
"svgo": "^4.0.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tsconfck": "^3.1.6",
|
||||
"ultrahtml": "^1.6.0",
|
||||
"unifont": "~0.7.1",
|
||||
"unifont": "~0.7.3",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"unstorage": "^1.17.3",
|
||||
"unstorage": "^1.17.4",
|
||||
"vfile": "^6.0.3",
|
||||
"vite": "^6.4.1",
|
||||
"vitefu": "^1.1.1",
|
||||
@@ -1990,6 +1990,463 @@
|
||||
"sharp": "^0.34.0"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
|
||||
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.17.3"
|
||||
},
|
||||
"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 |
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);
|
||||
});
|
||||
@@ -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})`);
|
||||
10
src/annotations/archicrat-ia/chapitre-1/p-0-8d27a7f5.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
schema: 1
|
||||
page: archicrat-ia/chapitre-1
|
||||
paras:
|
||||
p-0-8d27a7f5:
|
||||
refs:
|
||||
- url: https://auth.archicratie.trans-hands.synology.me/authenticated
|
||||
label: Lien web
|
||||
kind: (livre / article / vidéo / site / autre) Site
|
||||
ts: 2026-02-27T12:34:31.704Z
|
||||
fromIssue: 142
|
||||
9
src/annotations/archicrat-ia/chapitre-1/p-1-8a6c18bf.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
schema: 1
|
||||
page: archicrat-ia/chapitre-1
|
||||
paras:
|
||||
p-1-8a6c18bf:
|
||||
comments_editorial:
|
||||
- text: Yeaha
|
||||
status: new
|
||||
ts: 2026-02-27T12:40:39.462Z
|
||||
fromIssue: 143
|
||||
12
src/annotations/archicrat-ia/chapitre-3/p-0-ace27175.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
schema: 1
|
||||
page: archicrat-ia/chapitre-3
|
||||
paras:
|
||||
p-0-ace27175:
|
||||
media:
|
||||
- type: image
|
||||
src: /media/archicrat-ia/chapitre-3/p-0-ace27175/Capture_d_e_cran_2025-05-05_a_19.20.40.png
|
||||
caption: "[Media] p-0-ace27175 — Chapitre 3 — Philosophies du pouvoir et
|
||||
archicration"
|
||||
credit: ""
|
||||
ts: 2026-02-27T12:43:14.259Z
|
||||
fromIssue: 144
|
||||
30
src/annotations/archicrat-ia/chapitre-4.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
schema: 1
|
||||
page: archicrat-ia/chapitre-4
|
||||
paras:
|
||||
p-2-31b12529:
|
||||
media:
|
||||
- type: image
|
||||
src: /media/archicrat-ia/chapitre-4/p-2-31b12529/Capture_d_e_cran_2026-02-16_a_13.05.58.png
|
||||
caption: "[Media] p-2-31b12529 — Chapitre 4 — Histoire archicratique des
|
||||
révolutions industrielles"
|
||||
credit: ""
|
||||
ts: 2026-02-25T18:58:32.359Z
|
||||
fromIssue: 115
|
||||
p-7-1da4a458:
|
||||
media:
|
||||
- type: image
|
||||
src: /media/archicrat-ia/chapitre-4/p-7-1da4a458/Capture_d_e_cran_2026-02-16_a_13.05.58.png
|
||||
caption: "[Media] p-7-1da4a458 — Chapitre 4 — Histoire archicratique des
|
||||
révolutions industrielles"
|
||||
credit: ""
|
||||
ts: 2026-02-25T19:11:32.634Z
|
||||
fromIssue: 121
|
||||
p-11-67c14c09:
|
||||
media:
|
||||
- type: image
|
||||
src: /media/archicrat-ia/chapitre-4/p-11-67c14c09/Capture_d_e_cran_2026-02-16_a_13.07.35.png
|
||||
caption: "[Media] p-11-67c14c09 — Chapitre 4 — Histoire archicratique des
|
||||
révolutions industrielles"
|
||||
credit: ""
|
||||
ts: 2026-02-26T13:17:41.286Z
|
||||
fromIssue: 129
|
||||
19
src/annotations/archicrat-ia/chapitre-4/p-11-67c14c09.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
schema: 1
|
||||
page: archicrat-ia/chapitre-4
|
||||
paras:
|
||||
p-11-67c14c09:
|
||||
media:
|
||||
- type: image
|
||||
src: /media/archicrat-ia/chapitre-4/p-11-67c14c09/Capture_d_e_cran_2026-02-16_a_13.07.35.png
|
||||
caption: "[Media] p-11-67c14c09 — Chapitre 4 — Histoire archicratique des
|
||||
révolutions industrielles"
|
||||
credit: ""
|
||||
ts: 2026-02-26T13:17:41.286Z
|
||||
fromIssue: 129
|
||||
- type: image
|
||||
src: /media/archicrat-ia/chapitre-4/p-11-67c14c09/Capture_d_e_cran_2025-05-05_a_19.20.40.png
|
||||
caption: "[Media] p-11-67c14c09 — Chapitre 4 — Histoire archicratique des
|
||||
révolutions industrielles"
|
||||
credit: ""
|
||||
ts: 2026-02-27T09:17:04.386Z
|
||||
fromIssue: 127
|
||||
50
src/annotations/archicrat-ia/prologue.yml
Normal file
@@ -0,0 +1,50 @@
|
||||
schema: 1
|
||||
|
||||
paras:
|
||||
p-0-d7974f88:
|
||||
refs:
|
||||
- label: "Happycratie — (Cabanas & Illouz) via Cairn"
|
||||
url: "https://shs.cairn.info/revue-ethnologie-francaise-2019-4-page-813?lang=fr"
|
||||
kind: "article"
|
||||
- label: "Techno-féodalisme — Variations (OpenEdition)"
|
||||
url: "https://journals.openedition.org/variations/2290"
|
||||
kind: "article"
|
||||
|
||||
authors:
|
||||
- "Eva Illouz"
|
||||
- "Yanis Varoufakis"
|
||||
|
||||
quotes:
|
||||
- text: "Dans Happycratie, Edgar Cabanas et Eva Illouz..."
|
||||
source: "Happycratie, p.1"
|
||||
- text: "En eux-mêmes, les actifs ne sont ni féodaux ni capitalistes..."
|
||||
source: "Entretien Morozov/Varoufakis — techno-féodalisme"
|
||||
|
||||
media:
|
||||
- type: "image"
|
||||
src: "/public/media/archicratie/archicrat-ia/prologue/p-0-d7974f88/schema-1.svg"
|
||||
caption: "Tableau explicatif"
|
||||
credit: "ChatGPT"
|
||||
- type: "image"
|
||||
src: "/public/media/archicratie/archicrat-ia/prologue/p-0-d7974f88/schema-2.svg"
|
||||
caption: "Diagramme d’évolution"
|
||||
credit: "Yanis Varoufakis"
|
||||
|
||||
comments_editorial:
|
||||
- text: "TODO: nuancer / préciser — commentaire éditorial versionné (pas public)."
|
||||
status: "draft"
|
||||
|
||||
p-1-2ef25f29:
|
||||
refs:
|
||||
- label: "Kafka et le pouvoir — Bernard Lahire (Cairn)"
|
||||
url: "https://shs.cairn.info/franz-kafka--9782707159410-page-475?lang=fr"
|
||||
kind: "book"
|
||||
|
||||
authors:
|
||||
- "Bernard Lahire"
|
||||
|
||||
quotes:
|
||||
- text: "Si l’on voulait chercher quelque chose comme une vision du monde chez Kafka..."
|
||||
source: "Bernard Lahire, Franz Kafka, p.475+"
|
||||
|
||||
comments_editorial: []
|
||||
@@ -7,7 +7,10 @@ const entries = (await getCollection("archicratie"))
|
||||
.filter((e) => e.slug.startsWith("archicrat-ia/"))
|
||||
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
|
||||
|
||||
const href = (slug) => `/archicratie/${slug}/`;
|
||||
// ✅ On route l’Essai-thèse sur /archicrat-ia/<slug-sans-prefix>/
|
||||
// (Astro trailingSlash = always → on garde le "/" final)
|
||||
const strip = (s) => String(s || "").replace(/^archicrat-ia\//, "");
|
||||
const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
|
||||
---
|
||||
|
||||
<nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA">
|
||||
@@ -66,7 +69,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 +150,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); }
|
||||
|
||||
@@ -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>
|
||||
|
||||
1076
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>
|
||||
|
||||
199
src/pages/annotations-index.json.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
// src/pages/annotations-index.json.ts
|
||||
import type { APIRoute } from "astro";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import YAML from "yaml";
|
||||
|
||||
const CWD = process.cwd();
|
||||
const ANNO_ROOT = path.join(CWD, "src", "annotations");
|
||||
|
||||
const isObj = (x: any) => !!x && typeof x === "object" && !Array.isArray(x);
|
||||
const isArr = (x: any) => Array.isArray(x);
|
||||
|
||||
function normPath(s: string) {
|
||||
return String(s || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
function paraNum(pid: string) {
|
||||
const m = String(pid).match(/^p-(\d+)-/i);
|
||||
return m ? Number(m[1]) : Number.POSITIVE_INFINITY;
|
||||
}
|
||||
function toIso(v: any) {
|
||||
if (v instanceof Date) return v.toISOString();
|
||||
return typeof v === "string" ? v : "";
|
||||
}
|
||||
function stableSortByTs(arr: any[]) {
|
||||
if (!Array.isArray(arr)) return;
|
||||
arr.sort((a, b) => {
|
||||
const ta = Date.parse(toIso(a?.ts)) || 0;
|
||||
const tb = Date.parse(toIso(b?.ts)) || 0;
|
||||
if (ta !== tb) return ta - tb;
|
||||
return JSON.stringify(a).localeCompare(JSON.stringify(b));
|
||||
});
|
||||
}
|
||||
|
||||
function keyMedia(x: any) { return String(x?.src || ""); }
|
||||
function keyRef(x: any) {
|
||||
return `${x?.url || ""}||${x?.label || ""}||${x?.kind || ""}||${x?.citation || ""}`;
|
||||
}
|
||||
function keyComment(x: any) { return String(x?.text || "").trim(); }
|
||||
|
||||
function uniqUnion(dst: any[], src: any[], keyFn: (x:any)=>string) {
|
||||
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: any, src: any) {
|
||||
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 as any)[k])) (dst as any)[k] = {};
|
||||
deepMergeEntry((dst as any)[k], v);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isArr(v)) {
|
||||
const cur = isArr((dst as any)[k]) ? (dst as any)[k] : [];
|
||||
const seen = new Set(cur.map((x:any) => 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 as any)[k] = out;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(k in (dst as any)) || (dst as any)[k] == null || (dst as any)[k] === "") (dst as any)[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
async function walk(dir: string): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
const ents = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const e of ents) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...await walk(p));
|
||||
else if (e.isFile() && /\.ya?ml$/i.test(e.name)) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function inferExpected(relNoExt: string) {
|
||||
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 };
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const pages: Record<string, { paras: Record<string, any> }> = {};
|
||||
const errors: Array<{ file: string; error: string }> = [];
|
||||
|
||||
let files: string[] = [];
|
||||
try {
|
||||
files = await walk(ANNO_ROOT);
|
||||
} catch (e: any) {
|
||||
throw new Error(`Missing annotations root: ${ANNO_ROOT} (${e?.message || e})`);
|
||||
}
|
||||
|
||||
for (const fp of files) {
|
||||
const rel = normPath(path.relative(ANNO_ROOT, fp));
|
||||
const relNoExt = rel.replace(/\.ya?ml$/i, "");
|
||||
const { isShard, pageKey, paraId } = inferExpected(relNoExt);
|
||||
|
||||
try {
|
||||
const raw = await fs.readFile(fp, "utf8");
|
||||
const doc = YAML.parse(raw) || {};
|
||||
|
||||
if (!isObj(doc) || doc.schema !== 1) continue;
|
||||
|
||||
const docPage = normPath(doc.page || "");
|
||||
if (docPage && docPage !== pageKey) {
|
||||
throw new Error(`page mismatch (page="${doc.page}" vs path="${pageKey}")`);
|
||||
}
|
||||
if (!doc.page) doc.page = pageKey;
|
||||
|
||||
if (!isObj(doc.paras)) throw new Error(`missing object key "paras"`);
|
||||
|
||||
const pg = pages[pageKey] ??= { paras: {} };
|
||||
|
||||
if (isShard) {
|
||||
if (!paraId) throw new Error("internal: missing paraId");
|
||||
if (!(paraId in doc.paras)) {
|
||||
throw new Error(`shard mismatch: file must contain paras["${paraId}"]`);
|
||||
}
|
||||
// ✅ invariant aligné avec build-annotations-index
|
||||
const keys = Object.keys(doc.paras).map(String);
|
||||
if (!(keys.length === 1 && keys[0] === paraId)) {
|
||||
throw new Error(`shard invariant violated: shard must contain ONLY paras["${paraId}"] (got: ${keys.join(", ")})`);
|
||||
}
|
||||
|
||||
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: any) {
|
||||
errors.push({ file: `src/annotations/${rel}`, error: String(e?.message || e) });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [pk, 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: Record<string, any> = {};
|
||||
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}`);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(out), {
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
};
|
||||
38
src/pages/archicrat-ia/[...slug].astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import EditionLayout from "../../layouts/EditionLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import EditionToc from "../../components/EditionToc.astro";
|
||||
import LocalToc from "../../components/LocalToc.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = (await getCollection("archicratie"))
|
||||
.filter((e) => e.slug.startsWith("archicrat-ia/"));
|
||||
|
||||
return entries.map((entry) => ({
|
||||
// ✅ inline : jamais de helper externe (évite "stripPrefix is not defined")
|
||||
params: { slug: entry.slug.replace(/^archicrat-ia\//, "") },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content, headings } = await entry.render();
|
||||
---
|
||||
|
||||
<EditionLayout
|
||||
title={entry.data.title}
|
||||
editionLabel="Essai-thèse"
|
||||
editionKey="archicrat-ia"
|
||||
statusLabel="essai-thèse"
|
||||
statusKey="essai_these"
|
||||
level={entry.data.level}
|
||||
version={entry.data.version}
|
||||
>
|
||||
<Fragment slot="aside">
|
||||
<EditionToc currentSlug={entry.slug} />
|
||||
<LocalToc headings={headings} />
|
||||
</Fragment>
|
||||
|
||||
<h1>{entry.data.title}</h1>
|
||||
<Content />
|
||||
</EditionLayout>
|
||||
22
src/pages/archicrat-ia/index.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import SiteLayout from "../../layouts/SiteLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const entries = (await getCollection("archicratie"))
|
||||
.filter((e) => e.slug.startsWith("archicrat-ia/"));
|
||||
|
||||
entries.sort((a, b) => (a.data.order ?? 9999) - (b.data.order ?? 9999));
|
||||
|
||||
const strip = (slug) => slug.replace(/^archicrat-ia\//, "");
|
||||
const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
|
||||
---
|
||||
|
||||
<SiteLayout title="Essai-thèse — ArchiCraT-IA">
|
||||
<h1>Essai-thèse — ArchiCraT-IA</h1>
|
||||
|
||||
<ul>
|
||||
{entries.map((e) => (
|
||||
<li><a href={href(e.slug)}>{e.data.title}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</SiteLayout>
|
||||
@@ -5,7 +5,8 @@ import EditionToc from "../../components/EditionToc.astro";
|
||||
import LocalToc from "../../components/LocalToc.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getCollection("archicratie");
|
||||
const entries = (await getCollection("archicratie"))
|
||||
.filter((e) => !e.slug.startsWith("archicrat-ia/"));
|
||||
return entries.map((entry) => ({
|
||||
params: { slug: entry.slug },
|
||||
props: { entry },
|
||||
|
||||
@@ -4,13 +4,13 @@ import SiteLayout from "../layouts/SiteLayout.astro";
|
||||
<SiteLayout title="Accueil">
|
||||
<h1>Archicratie — Édition web</h1>
|
||||
<p>
|
||||
Portail d’accès aux éditions : Traité (Ontodynamique générative), Essai-thèse (Archicratie),
|
||||
Portail d’accès aux éditions : Traité (Ontodynamique générative), Essai-thèse (ArchiCraT-IA),
|
||||
Cas pratique (IA), Glossaire, Atlas.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="/editions/">Carte des œuvres</a></li>
|
||||
<li><a href="/archicratie/">Essai-thèse — Archicratie</a></li>
|
||||
<li><a href="/archicrat-ia/">Essai-thèse — ArchiCraT-IA</a></li>
|
||||
<li><a href="/traite/">Traité — Ontodynamique générative</a></li>
|
||||
<li><a href="/ia/">Cas pratique — Gouvernance des systèmes IA</a></li>
|
||||
<li><a href="/glossaire/">Glossaire archicratique</a></li>
|
||||
|
||||
42
src/pages/para-index.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
async function exists(p: string) {
|
||||
try { await fs.access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const distFile = path.join(process.cwd(), "dist", "para-index.json");
|
||||
|
||||
// Si dist existe (ex: après un build), on renvoie le vrai fichier.
|
||||
if (await exists(distFile)) {
|
||||
const raw = await fs.readFile(distFile, "utf8");
|
||||
return new Response(raw, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"cache-control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Sinon stub (dev sans build) : pas d’erreur, pas de crash, pas de 404.
|
||||
const stub = {
|
||||
schema: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
items: [],
|
||||
byId: {},
|
||||
note: "para-index not built yet (run: npm run build to generate dist/para-index.json)",
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(stub), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"cache-control": "no-store",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -80,6 +80,12 @@ main { padding: 0; }
|
||||
border-top: 1px dashed rgba(127,127,127,0.35);
|
||||
font-size: 14px;
|
||||
}
|
||||
/* Edition-bar: cacher des badges (non destructif) */
|
||||
.edition-bar [data-badge="edition"],
|
||||
.edition-bar [data-badge="status"],
|
||||
.edition-bar [data-badge="version"]{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2px 8px;
|
||||
@@ -95,7 +101,34 @@ main { padding: 0; }
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
/* Toggle niveaux */
|
||||
/* Jump by paragraph id */
|
||||
.jump-form{
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.jump-input{
|
||||
border: 1px solid rgba(127,127,127,0.55);
|
||||
background: transparent;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
width: 320px;
|
||||
}
|
||||
.jump-input.is-error{
|
||||
outline: 2px solid rgba(127,127,127,0.55);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.jump-btn{
|
||||
border: 1px solid rgba(127,127,127,0.55);
|
||||
background: transparent;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Toggle niveaux (legacy, non bloquant) */
|
||||
.level-toggle { display: inline-flex; gap: 6px; }
|
||||
.lvl-btn {
|
||||
border: 1px solid rgba(127,127,127,0.55);
|
||||
@@ -112,14 +145,22 @@ main { padding: 0; }
|
||||
|
||||
/* Règles niveaux */
|
||||
body[data-reading-level="1"] .level-2,
|
||||
body[data-reading-level="1"] .level-3 { display: none; }
|
||||
body[data-reading-level="2"] .level-3 { display: none; }
|
||||
body[data-reading-level="1"] .level-3,
|
||||
body[data-reading-level="1"] .level-4 { display: none; }
|
||||
|
||||
body[data-reading-level="2"] .level-3,
|
||||
body[data-reading-level="2"] .level-4 { display: none; }
|
||||
|
||||
body[data-reading-level="3"] .level-2,
|
||||
body[data-reading-level="3"] .level-4 { display: none; }
|
||||
|
||||
body[data-reading-level="4"] .level-2,
|
||||
body[data-reading-level="4"] .level-3 { display: none; }
|
||||
|
||||
/* ==========================
|
||||
Scroll offset (anchors / headings / paras)
|
||||
========================== */
|
||||
|
||||
/* Paragraph tools + bookmark */
|
||||
.reading p[id]{
|
||||
position: relative;
|
||||
padding-right: 14rem;
|
||||
@@ -183,6 +224,14 @@ body[data-reading-level="2"] .level-3 { display: none; }
|
||||
}
|
||||
.para-bookmark:hover{ text-decoration: underline; }
|
||||
|
||||
/* Highlight (jump / resume / arrivée hash) */
|
||||
.para-highlight{
|
||||
background: rgba(127,127,127,0.10);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 0 2px rgba(127,127,127,0.35);
|
||||
transition: box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.build-stamp {
|
||||
margin-top: 28px;
|
||||
padding-top: 14px;
|
||||
@@ -196,15 +245,51 @@ body[data-reading-level="2"] .level-3 { display: none; }
|
||||
border-radius: 16px;
|
||||
padding: 10px 12px;
|
||||
margin: 14px 0;
|
||||
position: relative;
|
||||
}
|
||||
.details-summary {
|
||||
cursor: pointer;
|
||||
font-weight: 650;
|
||||
|
||||
/* ✅ Handle minimal pour sections fermées : pas de titre visible, mais ouvrable */
|
||||
.details-summary{
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
border: 1px dashed rgba(127,127,127,.25);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
margin: 10px 0;
|
||||
|
||||
background: rgba(127,127,127,0.06);
|
||||
position: relative;
|
||||
|
||||
/* cache le texte réel (souvent le titre), sans casser l’accessibilité */
|
||||
color: transparent;
|
||||
}
|
||||
.details-summary::-webkit-details-marker { display: none; }
|
||||
.details-summary a { text-decoration: none; }
|
||||
.details-summary a:hover { text-decoration: underline; }
|
||||
|
||||
.details-summary::before{
|
||||
content: "▸ Ouvrir la section";
|
||||
color: rgba(127,127,127,0.85);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
.details-summary::before{ color: rgba(220,220,220,0.82); }
|
||||
}
|
||||
|
||||
details[open] > .details-summary{
|
||||
/* une fois ouvert, on le rend “SR-only” pour éviter le doublon visuel */
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.details-body { margin-top: 10px; }
|
||||
|
||||
/* Smooth scroll */
|
||||
@@ -224,7 +309,6 @@ html{ scroll-behavior: smooth; }
|
||||
width: var(--reading-width);
|
||||
right: auto;
|
||||
|
||||
/* colle au header */
|
||||
top: var(--sticky-header-h);
|
||||
|
||||
z-index: 60;
|
||||
@@ -247,7 +331,7 @@ html{ scroll-behavior: smooth; }
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 8px 12px;
|
||||
padding-right: 84px; /* réserve pour les boutons */
|
||||
padding-right: 84px;
|
||||
|
||||
border: 1px solid rgba(127,127,127,.20);
|
||||
border-top: 0;
|
||||
@@ -259,7 +343,7 @@ html{ scroll-behavior: smooth; }
|
||||
|
||||
box-shadow: 0 10px 22px rgba(0,0,0,.06);
|
||||
|
||||
position: relative; /* pour rf-actions */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
@@ -278,7 +362,6 @@ html{ scroll-behavior: smooth; }
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rf-line[hidden]{ display: none !important; }
|
||||
|
||||
.rf-h1{
|
||||
@@ -298,7 +381,6 @@ html{ scroll-behavior: smooth; }
|
||||
font-weight: var(--rf-h3-fw);
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.rf-line:hover{ text-decoration: underline; }
|
||||
|
||||
/* Actions */
|
||||
@@ -327,3 +409,14 @@ html{ scroll-behavior: smooth; }
|
||||
background: rgba(127,127,127,0.16);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ==========================
|
||||
PATCH CRUCIAL : éviter les “rectangles vides”
|
||||
(details fermés + summary handle minimal)
|
||||
========================== */
|
||||
|
||||
.reading details.details-section:not([open]){
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"archicratie/00-demarrage/index.html": [
|
||||
"p-0-d64c1c39",
|
||||
"p-1-3f750540"
|
||||
],
|
||||
"archicratie/archicrat-ia/chapitre-1/index.html": [
|
||||
"archicrat-ia/chapitre-1/index.html": [
|
||||
"p-0-8d27a7f5",
|
||||
"p-1-8a6c18bf",
|
||||
"p-2-39c6e4f4",
|
||||
@@ -664,7 +660,7 @@
|
||||
"p-657-7c465f0f",
|
||||
"p-658-3fc26620"
|
||||
],
|
||||
"archicratie/archicrat-ia/chapitre-2/index.html": [
|
||||
"archicrat-ia/chapitre-2/index.html": [
|
||||
"p-0-32820f76",
|
||||
"p-1-63506bae",
|
||||
"p-2-206c653a",
|
||||
@@ -1981,7 +1977,7 @@
|
||||
"p-1313-c83e97d4",
|
||||
"p-1314-4c2ed9ba"
|
||||
],
|
||||
"archicratie/archicrat-ia/chapitre-3/index.html": [
|
||||
"archicrat-ia/chapitre-3/index.html": [
|
||||
"p-0-ace27175",
|
||||
"p-1-60c7ea48",
|
||||
"p-2-1167ed0e",
|
||||
@@ -2973,7 +2969,7 @@
|
||||
"p-988-d8a0cce7",
|
||||
"p-989-fdbb8595"
|
||||
],
|
||||
"archicratie/archicrat-ia/chapitre-4/index.html": [
|
||||
"archicrat-ia/chapitre-4/index.html": [
|
||||
"p-0-ba984c9d",
|
||||
"p-1-ea84724d",
|
||||
"p-2-31b12529",
|
||||
@@ -3730,7 +3726,7 @@
|
||||
"p-753-f778aef4",
|
||||
"p-754-d99a24c1"
|
||||
],
|
||||
"archicratie/archicrat-ia/chapitre-5/index.html": [
|
||||
"archicrat-ia/chapitre-5/index.html": [
|
||||
"p-0-96edff22",
|
||||
"p-1-a51a4ee1",
|
||||
"p-2-dff32cbc",
|
||||
@@ -4771,7 +4767,7 @@
|
||||
"p-1037-4825033b",
|
||||
"p-1038-54aa72be"
|
||||
],
|
||||
"archicratie/archicrat-ia/conclusion/index.html": [
|
||||
"archicrat-ia/conclusion/index.html": [
|
||||
"p-0-5ec4522a",
|
||||
"p-1-e481f7e6",
|
||||
"p-2-7a56c59b",
|
||||
@@ -4892,7 +4888,7 @@
|
||||
"p-117-3a086369",
|
||||
"p-118-67afae83"
|
||||
],
|
||||
"archicratie/archicrat-ia/prologue/index.html": [
|
||||
"archicrat-ia/prologue/index.html": [
|
||||
"p-0-d7974f88",
|
||||
"p-1-2ef25f29",
|
||||
"p-2-edb49e0a",
|
||||
|
||||