Compare commits
67 Commits
chore/rout
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
| 435e41ed4d | |||
| b55decbea4 | |||
| cbd4f3a57f | |||
| 5afa5cbfda | |||
| a1b1df38ba | |||
| d3f7d74da7 | |||
| 6919190107 | |||
| 021ef5abd7 | |||
| 76cdc85f9c | |||
| f2f6df2127 | |||
| dfe13757f7 | |||
| 148ac997df | |||
| 84492d2741 | |||
| 81baadd57f | |||
| 63d0ffc5fc | |||
| 24143fc2c4 | |||
| 55370b704f | |||
| b8a3ce1337 | |||
| 7f9baedf41 | |||
| 1adbe1c7a3 | |||
| 107a26352f | |||
| 1c2b9ddbb6 | |||
| be99460d4d | |||
| 9e1b704aa6 | |||
| 941fbf5845 | |||
| 0b4a31a432 | |||
| c617dc3979 | |||
| 1b95161de0 | |||
| ebd976bd46 | |||
| f8d57d8fe0 | |||
| 09a4d2c472 | |||
| 1f6dc874d0 | |||
| 4dd63945ee | |||
| ba64b0694b | |||
| 58e5ceda59 | |||
| 08f826ee01 | |||
| 3358d280ec | |||
| 9cb0d5e416 | |||
| a46f058917 | |||
| 604b2199da | |||
| d153f71be6 | |||
| 8f64e4b098 | |||
| 459bf195d8 | |||
| 0c46b0d19b | |||
| bfbdc7b688 | |||
| 8fd53dd4d2 | |||
|
|
c8bbee4f74 | ||
| 04cdf54eb7 | |||
|
|
d6bf645ae9 | ||
| 1ca6bcbd81 | |||
| dec5f8eba7 | |||
| 716c887045 | |||
| 9b1789a164 | |||
| 17fa39c7ff | |||
| 8132e315f4 | |||
| 8d993915d7 | |||
| 497bddd05d | |||
| 7c8e49c1a9 | |||
| 901d28b89b | |||
| 43e2862c89 | |||
| 73fb38c4d1 | |||
| a81d206aba | |||
| 9801ea3cea | |||
| c11189fe11 | |||
| b47edb24cf | |||
| be191b09a0 | |||
| e06587478d |
@@ -17,12 +17,13 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
concurrency:
|
||||
group: anno-apply-${{ github.event.issue.number || inputs.issue || 'manual' }}
|
||||
group: anno-apply-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
apply-approved:
|
||||
runs-on: mac-ci container:
|
||||
runs-on: mac-ci
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
- name: Derive context (event.json / workflow_dispatch)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
@@ -80,10 +81,12 @@ jobs:
|
||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||
}
|
||||
|
||||
const labelName =
|
||||
ev?.label?.name ||
|
||||
ev?.label ||
|
||||
"workflow_dispatch";
|
||||
// label name: best-effort (non-bloquant)
|
||||
let labelName = "workflow_dispatch";
|
||||
const lab = ev?.label;
|
||||
if (typeof lab === "string") labelName = lab;
|
||||
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
|
||||
else if (ev?.label?.name) labelName = ev.label.name;
|
||||
|
||||
const u = new URL(cloneUrl);
|
||||
const origin = u.origin;
|
||||
@@ -108,19 +111,25 @@ jobs:
|
||||
echo "✅ context:"
|
||||
sed -n '1,120p' /tmp/anno.env
|
||||
|
||||
- name: Gate on label state/approved
|
||||
- name: Early gate (label event fast-skip, but tolerant)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
|
||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||
echo "ℹ️ event label = $LABEL_NAME"
|
||||
|
||||
# Fast skip on obvious non-approved label events (avoid noise),
|
||||
# BUT do NOT skip if label payload is weird/unknown.
|
||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip early"
|
||||
echo "SKIP=1" >> /tmp/anno.env
|
||||
echo "SKIP_REASON=\"label_not_approved_event\"" >> /tmp/anno.env
|
||||
exit 0
|
||||
fi
|
||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Fetch issue + gate on Type (skip Proposer)
|
||||
echo "✅ continue to API gating (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Fetch issue + hard gate on labels + Type
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
@@ -130,16 +139,22 @@ jobs:
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
|
||||
ISSUE_JSON="$(curl -fsS \
|
||||
curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")"
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
-o /tmp/issue.json
|
||||
|
||||
node --input-type=module - <<'NODE' "$ISSUE_JSON" >> /tmp/anno.env
|
||||
const issue = JSON.parse(process.argv[1] || "{}");
|
||||
node --input-type=module - <<'NODE' >> /tmp/anno.env
|
||||
import fs from "node:fs";
|
||||
|
||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
||||
const title = String(issue.title || "");
|
||||
const body = String(issue.body || "").replace(/\r\n/g, "\n");
|
||||
|
||||
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
|
||||
const hasApproved = labels.includes("state/approved");
|
||||
|
||||
function pickLine(key) {
|
||||
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
|
||||
const m = body.match(re);
|
||||
@@ -156,6 +171,14 @@ jobs:
|
||||
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
||||
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
|
||||
|
||||
// HARD gate: must currently have state/approved (avoids depending on event payload)
|
||||
if (!hasApproved) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("not_approved_label_present")}`);
|
||||
process.stdout.write(out.join("\n") + "\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
|
||||
@@ -172,7 +195,7 @@ jobs:
|
||||
process.stdout.write(out.join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ issue type gating:"
|
||||
echo "✅ gating result:"
|
||||
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
|
||||
|
||||
- name: Comment issue if skipped (Proposer / unsupported / missing Type)
|
||||
@@ -184,19 +207,24 @@ jobs:
|
||||
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
|
||||
# IMPORTANT: do NOT comment for "not_approved_label_present" (avoid spam on other label events)
|
||||
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
|
||||
echo "ℹ️ skip reason=${SKIP_REASON} -> no comment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||
|
||||
REASON="${SKIP_REASON:-}"
|
||||
TYPE="${ISSUE_TYPE:-}"
|
||||
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."
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors**.\n✅ Aucun traitement automatique."
|
||||
elif [[ "$REASON" == unsupported_type:* ]]; then
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment.\n✅ Action : traitement manuel si nécessaire."
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment."
|
||||
else
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\n✅ Action : corriger le ticket (ajouter 'Type: type/media|type/reference|type/comment') ou traiter manuellement."
|
||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\nAjoute : Type: type/media|type/reference|type/comment"
|
||||
fi
|
||||
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
@@ -263,6 +291,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
source /tmp/anno.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
test -d .git || { echo "❌ not a git repo (checkout failed)"; echo "APPLY_RC=90" >> /tmp/anno.env; exit 0; }
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
|
||||
@@ -319,6 +348,8 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||
|
||||
if [[ -f /tmp/apply.log ]]; then
|
||||
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
|
||||
else
|
||||
@@ -334,27 +365,6 @@ jobs:
|
||||
"$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:
|
||||
@@ -366,6 +376,7 @@ jobs:
|
||||
|
||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip push"; exit 0; }
|
||||
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; }
|
||||
test -d .git || { echo "ℹ️ no git repo -> skip push"; exit 0; }
|
||||
|
||||
AUTH_URL="$(node --input-type=module -e '
|
||||
const [clone, tok] = process.argv.slice(1);
|
||||
|
||||
@@ -17,12 +17,13 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
concurrency:
|
||||
group: anno-reject-${{ github.event.issue.number || inputs.issue || 'manual' }}
|
||||
group: anno-reject-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
reject:
|
||||
runs-on: mac-ci container:
|
||||
runs-on: mac-ci
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
- name: Derive context (event.json / workflow_dispatch)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
@@ -74,10 +75,11 @@ jobs:
|
||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||
}
|
||||
|
||||
const labelName =
|
||||
ev?.label?.name ||
|
||||
ev?.label ||
|
||||
"workflow_dispatch";
|
||||
// label name: best-effort (non-bloquant)
|
||||
let labelName = "workflow_dispatch";
|
||||
const lab = ev?.label;
|
||||
if (typeof lab === "string") labelName = lab;
|
||||
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
|
||||
|
||||
let apiBase = "";
|
||||
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
|
||||
@@ -102,19 +104,20 @@ jobs:
|
||||
echo "✅ context:"
|
||||
sed -n '1,120p' /tmp/reject.env
|
||||
|
||||
- name: Gate on label state/rejected only
|
||||
- name: Early gate (fast-skip, tolerant)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/reject.env
|
||||
echo "ℹ️ event label = $LABEL_NAME"
|
||||
|
||||
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip early"
|
||||
echo "SKIP=1" >> /tmp/reject.env
|
||||
echo "SKIP_REASON=\"label_not_rejected_event\"" >> /tmp/reject.env
|
||||
exit 0
|
||||
fi
|
||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Comment + close (only if not conflicting with state/approved)
|
||||
- name: Comment + close (only if label state/rejected is PRESENT now, and no conflict)
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
@@ -125,14 +128,15 @@ jobs:
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; }
|
||||
|
||||
ISSUE_JSON="$(curl -fsS \
|
||||
curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")"
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
-o /tmp/reject.issue.json
|
||||
|
||||
# conflict guard: approved + rejected => do nothing, comment warning
|
||||
node --input-type=module - <<'NODE' "$ISSUE_JSON" > /tmp/reject.flags
|
||||
const issue = JSON.parse(process.argv[1] || "{}");
|
||||
node --input-type=module - <<'NODE' > /tmp/reject.flags
|
||||
import fs from "node:fs";
|
||||
const issue = JSON.parse(fs.readFileSync("/tmp/reject.issue.json","utf8"));
|
||||
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
|
||||
const hasApproved = labels.includes("state/approved");
|
||||
const hasRejected = labels.includes("state/rejected");
|
||||
@@ -141,6 +145,12 @@ jobs:
|
||||
|
||||
source /tmp/reject.flags
|
||||
|
||||
# Do nothing unless state/rejected is truly present now (anti payload weird)
|
||||
if [[ "${HAS_REJECTED:-0}" != "1" ]]; then
|
||||
echo "ℹ️ state/rejected not present -> skip"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${HAS_APPROVED:-0}" == "1" && "${HAS_REJECTED:-0}" == "1" ]]; then
|
||||
MSG="⚠️ Conflit d'état sur le ticket #${ISSUE_NUMBER} : labels **state/approved** et **state/rejected** présents.\n\n➡️ Action manuelle requise : retirer l'un des deux labels avant relance."
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
@@ -153,7 +163,6 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# comment reject
|
||||
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
@@ -163,7 +172,6 @@ jobs:
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$PAYLOAD"
|
||||
|
||||
# close issue
|
||||
curl -fsS -X PATCH \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
@@ -4,21 +4,37 @@ on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
concurrency:
|
||||
group: auto-label-${{ github.event.issue.number || github.event.issue.index || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: mac-ci steps:
|
||||
runs-on: mac-ci
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
- name: Apply labels from Type/State/Category
|
||||
env:
|
||||
FORGE_BASE: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
# IMPORTANT: préfère FORGE_BASE (LAN) si défini, sinon FORGE_API
|
||||
FORGE_BASE: ${{ vars.FORGE_BASE || vars.FORGE_API || vars.FORGE_API_BASE }}
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
REPO_FULL: ${{ gitea.repository }}
|
||||
EVENT_PATH: ${{ github.event_path }}
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os, re, urllib.request, urllib.error
|
||||
import json, os, re, time, urllib.request, urllib.error, socket
|
||||
|
||||
forge = (os.environ.get("FORGE_BASE") or "").rstrip("/")
|
||||
if not forge:
|
||||
raise SystemExit("Missing FORGE_BASE/FORGE_API repo variable (e.g. http://192.168.1.20:3000)")
|
||||
|
||||
token = os.environ.get("FORGE_TOKEN") or ""
|
||||
if not token:
|
||||
raise SystemExit("Missing secret FORGE_TOKEN")
|
||||
|
||||
forge = os.environ["FORGE_BASE"].rstrip("/")
|
||||
token = os.environ["FORGE_TOKEN"]
|
||||
owner, repo = os.environ["REPO_FULL"].split("/", 1)
|
||||
event_path = os.environ["EVENT_PATH"]
|
||||
|
||||
@@ -45,12 +61,9 @@ jobs:
|
||||
print("PARSED:", {"Type": t, "State": s, "Category": c})
|
||||
|
||||
# 1) explicite depuis le body
|
||||
if t:
|
||||
desired.add(t)
|
||||
if s:
|
||||
desired.add(s)
|
||||
if c:
|
||||
desired.add(c)
|
||||
if t: desired.add(t)
|
||||
if s: desired.add(s)
|
||||
if c: desired.add(c)
|
||||
|
||||
# 2) fallback depuis le titre si Type absent
|
||||
if not t:
|
||||
@@ -75,42 +88,56 @@ jobs:
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "archicratie-auto-label/1.0",
|
||||
"User-Agent": "archicratie-auto-label/1.1",
|
||||
}
|
||||
|
||||
def jreq(method, url, payload=None):
|
||||
def jreq(method, url, payload=None, timeout=60, retries=4, backoff=2.0):
|
||||
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
b = r.read()
|
||||
return json.loads(b.decode("utf-8")) if b else None
|
||||
except urllib.error.HTTPError as e:
|
||||
b = e.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
|
||||
last_err = None
|
||||
for i in range(retries):
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
b = r.read()
|
||||
return json.loads(b.decode("utf-8")) if b else None
|
||||
except urllib.error.HTTPError as e:
|
||||
b = e.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
|
||||
except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
|
||||
last_err = e
|
||||
# retry only on network/timeout
|
||||
time.sleep(backoff * (i + 1))
|
||||
raise RuntimeError(f"Network/timeout after retries: {method} {url}\n{last_err}")
|
||||
|
||||
# labels repo
|
||||
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000") or []
|
||||
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000", timeout=60) or []
|
||||
name_to_id = {x.get("name"): x.get("id") for x in labels}
|
||||
|
||||
missing = [x for x in desired if x not in name_to_id]
|
||||
if missing:
|
||||
raise SystemExit("Missing labels in repo: " + ", ".join(sorted(missing)))
|
||||
|
||||
wanted_ids = [name_to_id[x] for x in desired]
|
||||
wanted_ids = sorted({int(name_to_id[x]) for x in desired})
|
||||
|
||||
# labels actuels de l'issue
|
||||
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels") or []
|
||||
current_ids = {x.get("id") for x in current if x.get("id") is not None}
|
||||
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
|
||||
current_ids = {int(x.get("id")) for x in current if x.get("id") is not None}
|
||||
|
||||
final_ids = sorted(current_ids.union(wanted_ids))
|
||||
|
||||
# set labels = union (n'enlève rien)
|
||||
# Replace labels = union (n'enlève rien)
|
||||
url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels"
|
||||
try:
|
||||
jreq("PUT", url, {"labels": final_ids})
|
||||
except Exception:
|
||||
jreq("PUT", url, final_ids)
|
||||
|
||||
# IMPORTANT: on n'envoie JAMAIS une liste brute ici (ça a causé le 422)
|
||||
jreq("PUT", url, {"labels": final_ids}, timeout=90, retries=4)
|
||||
|
||||
# vérif post-apply (anti "timeout mais appliqué")
|
||||
post = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
|
||||
post_ids = {int(x.get("id")) for x in post if x.get("id") is not None}
|
||||
|
||||
missing_ids = [i for i in wanted_ids if i not in post_ids]
|
||||
if missing_ids:
|
||||
raise RuntimeError(f"Labels not applied after PUT (missing ids): {missing_ids}")
|
||||
|
||||
print(f"OK labels #{number}: {sorted(desired)}")
|
||||
PY
|
||||
@@ -15,7 +15,8 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build-and-anchors:
|
||||
runs-on: mac-ci container:
|
||||
runs-on: mac-ci
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
|
||||
@@ -26,9 +26,9 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: nas-deploy
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
image: localhost:5000/archicratie/nas-deploy-node22@sha256:fefa8bb307005cebec07796661ab25528dc319c33a8f1e480e1d66f90cd5cff6
|
||||
|
||||
steps:
|
||||
- name: Tools sanity
|
||||
@@ -47,105 +47,179 @@ jobs:
|
||||
|
||||
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 =
|
||||
|
||||
// Push-range (most reliable for change detection)
|
||||
const before = String(ev?.before || "").trim();
|
||||
const after =
|
||||
(process.env.GITHUB_SHA && String(process.env.GITHUB_SHA).trim()) ||
|
||||
ev?.after ||
|
||||
ev?.sha ||
|
||||
ev?.head_commit?.id ||
|
||||
ev?.pull_request?.head?.sha ||
|
||||
"";
|
||||
String(ev?.after || ev?.sha || ev?.head_commit?.id || ev?.pull_request?.head?.sha || "").trim();
|
||||
|
||||
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
||||
|
||||
fs.writeFileSync("/tmp/deploy.env", [
|
||||
`REPO_URL=${shq(cloneUrl)}`,
|
||||
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
|
||||
`SHA=${shq(sha)}`
|
||||
`BEFORE=${shq(before)}`,
|
||||
`AFTER=${shq(after)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
source /tmp/deploy.env
|
||||
echo "Repo URL: $REPO_URL"
|
||||
echo "Default branch: $DEFAULT_BRANCH"
|
||||
echo "SHA: ${SHA:-<empty>}"
|
||||
echo "BEFORE: ${BEFORE:-<empty>}"
|
||||
echo "AFTER: ${AFTER:-<empty>}"
|
||||
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git remote add origin "$REPO_URL"
|
||||
|
||||
if [[ -n "${SHA:-}" ]]; then
|
||||
git fetch --depth 1 origin "$SHA"
|
||||
# Checkout AFTER (or default branch if missing)
|
||||
if [[ -n "${AFTER:-}" ]]; then
|
||||
git fetch --depth 50 origin "$AFTER"
|
||||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||||
else
|
||||
git fetch --depth 1 origin "$DEFAULT_BRANCH"
|
||||
git fetch --depth 50 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"
|
||||
AFTER="$(git rev-parse HEAD)"
|
||||
echo "AFTER='$AFTER'" >> /tmp/deploy.env
|
||||
echo "Resolved AFTER: $AFTER"
|
||||
fi
|
||||
|
||||
git log -1 --oneline
|
||||
|
||||
- name: Gate — decide HOTPATCH vs FULL rebuild
|
||||
- name: Gate — decide SKIP vs HOTPATCH vs FULL rebuild
|
||||
env:
|
||||
INPUT_FORCE: ${{ inputs.force }}
|
||||
EVENT_JSON: /var/run/act/workflow/event.json
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
|
||||
FORCE="${INPUT_FORCE:-0}"
|
||||
|
||||
# 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
|
||||
# Lire before/after du push depuis event.json (merge-proof)
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const before = ev?.before || "";
|
||||
const after = ev?.after || ev?.sha || "";
|
||||
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
||||
fs.writeFileSync("/tmp/gate.env", [
|
||||
`EV_BEFORE=${shq(before)}`,
|
||||
`EV_AFTER=${shq(after)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "== changed files =="
|
||||
echo "$CHANGED" | sed -n '1,260p'
|
||||
source /tmp/gate.env
|
||||
|
||||
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
|
||||
BEFORE="${EV_BEFORE:-}"
|
||||
AFTER="${EV_AFTER:-}"
|
||||
if [[ -z "${AFTER:-}" ]]; then
|
||||
AFTER="${SHA:-}"
|
||||
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 "Gate ctx: BEFORE=${BEFORE:-<empty>} AFTER=${AFTER:-<empty>} FORCE=${FORCE}"
|
||||
|
||||
# Produire une liste CHANGED fiable :
|
||||
# - si BEFORE/AFTER valides -> git diff before..after
|
||||
# - sinon fallback -> diff parent1..after ou show after
|
||||
CHANGED=""
|
||||
Z40="0000000000000000000000000000000000000000"
|
||||
|
||||
if [[ -n "${BEFORE:-}" && "${BEFORE}" != "${Z40}" ]] \
|
||||
&& git cat-file -e "${BEFORE}^{commit}" 2>/dev/null \
|
||||
&& git cat-file -e "${AFTER}^{commit}" 2>/dev/null; then
|
||||
CHANGED="$(git diff --name-only "${BEFORE}" "${AFTER}" || true)"
|
||||
else
|
||||
P1="$(git rev-parse "${AFTER}^" 2>/dev/null || true)"
|
||||
if [[ -n "${P1:-}" ]] && git cat-file -e "${P1}^{commit}" 2>/dev/null; then
|
||||
CHANGED="$(git diff --name-only "${P1}" "${AFTER}" || true)"
|
||||
else
|
||||
CHANGED="$(git show --name-only --pretty="" "${AFTER}" | sed '/^$/d' || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%s\n" "${CHANGED}" > /tmp/changed.txt
|
||||
|
||||
echo "== changed files (first 200) =="
|
||||
sed -n '1,200p' /tmp/changed.txt || true
|
||||
|
||||
# Flags
|
||||
HAS_FULL=0
|
||||
HAS_HOTPATCH=0
|
||||
|
||||
# HOTPATCH si annotations/media touchés
|
||||
if grep -qE '^(src/annotations/|public/media/)' /tmp/changed.txt; then
|
||||
HAS_HOTPATCH=1
|
||||
fi
|
||||
|
||||
# FULL si build-impacting (robuste)
|
||||
# 1) Tout src/ SAUF src/annotations/
|
||||
if grep -qE '^src/' /tmp/changed.txt && grep -qEv '^src/annotations/' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
# 2) scripts/
|
||||
if grep -qE '^scripts/' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
# 3) Tout public/ SAUF public/media/
|
||||
if grep -qE '^public/' /tmp/changed.txt && grep -qEv '^public/media/' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
# 4) fichiers racine qui changent le build / l’image
|
||||
if grep -qE '^(package\.json|package-lock\.json|astro\.config\.mjs|tsconfig\.json|\.npmrc|\.nvmrc|Dockerfile|docker-compose\.yml|nginx\.conf)$' /tmp/changed.txt; then
|
||||
HAS_FULL=1
|
||||
fi
|
||||
|
||||
echo "Gate flags: HAS_FULL=${HAS_FULL} HAS_HOTPATCH=${HAS_HOTPATCH}"
|
||||
|
||||
# Décision
|
||||
if [[ "${FORCE}" == "1" ]]; then
|
||||
GO=1
|
||||
MODE="full"
|
||||
echo "✅ force=1 -> MODE=full (rebuild+restart)"
|
||||
elif [[ "${HAS_FULL}" == "1" ]]; then
|
||||
GO=1
|
||||
MODE="full"
|
||||
echo "✅ build-impacting change -> MODE=full (rebuild+restart)"
|
||||
elif [[ "${HAS_HOTPATCH}" == "1" ]]; then
|
||||
GO=1
|
||||
MODE="hotpatch"
|
||||
echo "✅ annotations/media change -> MODE=hotpatch"
|
||||
else
|
||||
echo "GO=0" >> /tmp/deploy.env
|
||||
echo "MODE='skip'" >> /tmp/deploy.env
|
||||
echo "ℹ️ no annotations/media change -> skip deploy"
|
||||
GO=0
|
||||
MODE="skip"
|
||||
echo "ℹ️ no relevant change -> skip deploy"
|
||||
fi
|
||||
|
||||
- name: Install docker client + docker compose plugin (v2) + python yaml
|
||||
echo "GO=${GO}" >> /tmp/deploy.env
|
||||
echo "MODE='${MODE}'" >> /tmp/deploy.env
|
||||
|
||||
- name: Toolchain sanity + resolve COMPOSE_PROJECT_NAME
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
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
|
||||
|
||||
# tools are prebaked in the image
|
||||
git --version
|
||||
docker version
|
||||
docker compose version
|
||||
python3 --version
|
||||
python3 -c 'import yaml; print("PyYAML OK")'
|
||||
|
||||
# Reuse existing compose project name if containers already exist
|
||||
PROJ="$(docker inspect archicratie-web-blue --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
|
||||
|
||||
395
.gitea/workflows/proposer-apply-pr.yml
Normal file
395
.gitea/workflows/proposer-apply-pr.yml
Normal file
@@ -0,0 +1,395 @@
|
||||
name: Proposer Apply (PR)
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue:
|
||||
description: "Issue number to apply (Proposer: correction/fact-check)"
|
||||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
concurrency:
|
||||
group: proposer-apply-${{ github.event.issue.number || inputs.issue || 'manual' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
apply-proposer:
|
||||
runs-on: mac-ci
|
||||
container:
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||
|
||||
steps:
|
||||
- name: Tools sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git --version
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Derive context (event.json / workflow_dispatch)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||||
|
||||
node --input-type=module - <<'NODE' > /tmp/proposer.env
|
||||
import fs from "node:fs";
|
||||
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const repoObj = ev?.repository || {};
|
||||
|
||||
const cloneUrl =
|
||||
repoObj?.clone_url ||
|
||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
|
||||
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
||||
|
||||
let owner =
|
||||
repoObj?.owner?.login ||
|
||||
repoObj?.owner?.username ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
|
||||
|
||||
let repo =
|
||||
repoObj?.name ||
|
||||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
|
||||
|
||||
if (!owner || !repo) {
|
||||
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
||||
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
||||
}
|
||||
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
|
||||
|
||||
const defaultBranch = repoObj?.default_branch || "main";
|
||||
|
||||
const issueNumber =
|
||||
ev?.issue?.number ||
|
||||
ev?.issue?.index ||
|
||||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
|
||||
|
||||
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
|
||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||
}
|
||||
|
||||
const labelName =
|
||||
ev?.label?.name ||
|
||||
ev?.label ||
|
||||
"workflow_dispatch";
|
||||
|
||||
const u = new URL(cloneUrl);
|
||||
const origin = u.origin;
|
||||
|
||||
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
|
||||
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
|
||||
: origin;
|
||||
|
||||
function sh(s){ return JSON.stringify(String(s)); }
|
||||
process.stdout.write([
|
||||
`CLONE_URL=${sh(cloneUrl)}`,
|
||||
`OWNER=${sh(owner)}`,
|
||||
`REPO=${sh(repo)}`,
|
||||
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
|
||||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||||
`LABEL_NAME=${sh(labelName)}`,
|
||||
`API_BASE=${sh(apiBase)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ context:"
|
||||
sed -n '1,120p' /tmp/proposer.env
|
||||
|
||||
- name: Gate on label state/approved
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
|
||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
||||
echo "SKIP=1" >> /tmp/proposer.env
|
||||
exit 0
|
||||
fi
|
||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
||||
|
||||
- name: Fetch issue + API-hard gate on (state/approved present + proposer type)
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||
|
||||
curl -fsS \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
-o /tmp/issue.json
|
||||
|
||||
node --input-type=module - <<'NODE' >> /tmp/proposer.env
|
||||
import fs from "node:fs";
|
||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
||||
const title = String(issue.title || "");
|
||||
const body = String(issue.body || "").replace(/\r\n/g, "\n");
|
||||
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name||"")).filter(Boolean) : [];
|
||||
|
||||
function pickLine(key) {
|
||||
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
|
||||
const m = body.match(re);
|
||||
return m ? m[1].trim() : "";
|
||||
}
|
||||
|
||||
const typeRaw = pickLine("Type");
|
||||
const type = String(typeRaw || "").trim().toLowerCase();
|
||||
|
||||
const hasApproved = labels.includes("state/approved");
|
||||
const proposer = new Set(["type/correction","type/fact-check"]);
|
||||
|
||||
const out = [];
|
||||
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
||||
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
|
||||
out.push(`HAS_APPROVED=${hasApproved ? "1":"0"}`);
|
||||
|
||||
if (!hasApproved) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("approved_not_present")}`);
|
||||
} else if (!type) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
|
||||
} else if (!proposer.has(type)) {
|
||||
out.push(`SKIP=1`);
|
||||
out.push(`SKIP_REASON=${JSON.stringify("not_proposer:"+type)}`);
|
||||
}
|
||||
process.stdout.write(out.join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ proposer gating:"
|
||||
grep -E '^(ISSUE_TYPE|HAS_APPROVED|SKIP|SKIP_REASON)=' /tmp/proposer.env || true
|
||||
|
||||
- name: Comment issue if skipped
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
|
||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||
[[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0
|
||||
|
||||
REASON="${SKIP_REASON:-}"
|
||||
TYPE="${ISSUE_TYPE:-}"
|
||||
|
||||
if [[ "$REASON" == "approved_not_present" ]]; then
|
||||
MSG="ℹ️ Proposer Apply: skip — le label **state/approved** n'est pas présent sur le ticket au moment du run (gate API-hard)."
|
||||
elif [[ "$REASON" == "missing_type" ]]; then
|
||||
MSG="ℹ️ Proposer Apply: skip — champ **Type:** manquant/illisible. Attendu: type/correction ou type/fact-check."
|
||||
else
|
||||
MSG="ℹ️ Proposer Apply: skip — Type non-Proposer (${TYPE}). (Ce workflow ne traite que correction/fact-check.)"
|
||||
fi
|
||||
|
||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$PAYLOAD" || true
|
||||
|
||||
- name: Checkout default branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git remote add origin "$CLONE_URL"
|
||||
git fetch --depth 1 origin "$DEFAULT_BRANCH"
|
||||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||||
git log -1 --oneline
|
||||
echo "✅ workspace:"
|
||||
ls -la | sed -n '1,120p'
|
||||
|
||||
- name: Detect app dir (repo-root vs ./site)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
APP_DIR="."
|
||||
if [[ -d "site" && -f "site/package.json" ]]; then
|
||||
APP_DIR="site"
|
||||
fi
|
||||
|
||||
echo "APP_DIR=$APP_DIR" >> /tmp/proposer.env
|
||||
echo "✅ APP_DIR=$APP_DIR"
|
||||
ls -la "$APP_DIR" | sed -n '1,120p'
|
||||
test -f "$APP_DIR/package.json" || { echo "❌ package.json missing in APP_DIR=$APP_DIR"; exit 1; }
|
||||
test -d "$APP_DIR/scripts" || { echo "❌ scripts/ missing in APP_DIR=$APP_DIR"; exit 1; }
|
||||
|
||||
- name: NPM harden (reduce flakiness)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
cd "$APP_DIR"
|
||||
npm config set fetch-retries 5
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
npm config set fetch-retry-maxtimeout 120000
|
||||
npm config set registry https://registry.npmjs.org
|
||||
|
||||
- name: Install deps (APP_DIR)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
cd "$APP_DIR"
|
||||
npm ci --no-audit --no-fund
|
||||
|
||||
- name: Build dist baseline (APP_DIR)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
cd "$APP_DIR"
|
||||
npm run build
|
||||
|
||||
- name: Apply ticket (alias + commit) on bot branch
|
||||
continue-on-error: true
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
|
||||
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
|
||||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env
|
||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
|
||||
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
|
||||
|
||||
START_SHA="$(git rev-parse HEAD)"
|
||||
TS="$(date -u +%Y%m%d-%H%M%S)"
|
||||
BR="bot/proposer-${ISSUE_NUMBER}-${TS}"
|
||||
echo "BRANCH=$BR" >> /tmp/proposer.env
|
||||
git checkout -b "$BR"
|
||||
|
||||
export GITEA_OWNER="$OWNER"
|
||||
export GITEA_REPO="$REPO"
|
||||
export FORGE_BASE="$API_BASE"
|
||||
|
||||
LOG="/tmp/proposer-apply.log"
|
||||
set +e
|
||||
(cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE_NUMBER" --alias --commit) >"$LOG" 2>&1
|
||||
RC=$?
|
||||
set -e
|
||||
|
||||
echo "APPLY_RC=$RC" >> /tmp/proposer.env
|
||||
|
||||
echo "== apply log (tail) =="
|
||||
tail -n 200 "$LOG" || true
|
||||
|
||||
END_SHA="$(git rev-parse HEAD)"
|
||||
if [[ "$RC" -ne 0 ]]; then
|
||||
echo "NOOP=0" >> /tmp/proposer.env
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$START_SHA" == "$END_SHA" ]]; then
|
||||
echo "NOOP=1" >> /tmp/proposer.env
|
||||
else
|
||||
echo "NOOP=0" >> /tmp/proposer.env
|
||||
echo "END_SHA=$END_SHA" >> /tmp/proposer.env
|
||||
fi
|
||||
|
||||
- name: Push bot branch
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip push"; exit 0; }
|
||||
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; }
|
||||
[[ -n "${BRANCH:-}" ]] || { echo "ℹ️ BRANCH unset -> skip push"; exit 0; }
|
||||
|
||||
AUTH_URL="$(node --input-type=module -e '
|
||||
const [clone, tok] = process.argv.slice(1);
|
||||
const u = new URL(clone);
|
||||
u.username = "oauth2";
|
||||
u.password = tok;
|
||||
console.log(u.toString());
|
||||
' "$CLONE_URL" "$FORGE_TOKEN")"
|
||||
|
||||
git remote set-url origin "$AUTH_URL"
|
||||
git push -u origin "$BRANCH"
|
||||
|
||||
- name: Create PR + comment issue
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
|
||||
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
||||
[[ -n "${BRANCH:-}" ]] || { echo "ℹ️ BRANCH unset -> skip PR"; exit 0; }
|
||||
|
||||
PR_TITLE="proposer: apply ticket #${ISSUE_NUMBER}"
|
||||
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA:-unknown}\n\nMerge si CI OK."
|
||||
|
||||
PR_PAYLOAD="$(node --input-type=module -e '
|
||||
const [title, body, base, head] = process.argv.slice(1);
|
||||
console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
|
||||
' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"
|
||||
|
||||
PR_JSON="$(curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
|
||||
--data-binary "$PR_PAYLOAD")"
|
||||
|
||||
PR_URL="$(node --input-type=module -e '
|
||||
const pr = JSON.parse(process.argv[1] || "{}");
|
||||
console.log(pr.html_url || pr.url || "");
|
||||
' "$PR_JSON")"
|
||||
|
||||
test -n "$PR_URL" || { echo "❌ PR URL missing. Raw: $PR_JSON"; exit 1; }
|
||||
|
||||
MSG="✅ PR Proposer créée pour ticket #${ISSUE_NUMBER} : ${PR_URL}"
|
||||
C_PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
--data-binary "$C_PAYLOAD"
|
||||
|
||||
- name: Finalize (fail job if apply failed)
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/proposer.env || true
|
||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||
|
||||
RC="${APPLY_RC:-0}"
|
||||
if [[ "$RC" != "0" ]]; then
|
||||
echo "❌ apply failed (rc=$RC)"
|
||||
exit "$RC"
|
||||
fi
|
||||
echo "✅ apply ok"
|
||||
@@ -3,6 +3,7 @@ on: [push, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: mac-ci steps:
|
||||
runs-on: mac-ci
|
||||
steps:
|
||||
- run: node -v && npm -v
|
||||
- run: echo "runner OK"
|
||||
|
||||
@@ -25,6 +25,19 @@ Objectif : déployer une nouvelle version du site sur le NAS (DS220+) sans jamai
|
||||
|
||||
➡️ Déploiement = `docs/DEPLOY_PROD_SYNOLOGY_DS220.md` (procédure détaillée, à jour).
|
||||
|
||||
## Mise à jour (2026-03-03) — Gate CI de déploiement (SKIP / HOTPATCH / FULL) + preuves A/B
|
||||
|
||||
La procédure de déploiement “vivante” est désormais pilotée par **Gitea Actions** via le workflow :
|
||||
- `.gitea/workflows/deploy-staging-live.yml`
|
||||
|
||||
Ce workflow décide automatiquement :
|
||||
- **FULL** (rebuild + restart blue + green) dès qu’un changement impacte le build (ex: `src/content/`, `src/pages/`, `scripts/`, `src/anchors/`, etc.)
|
||||
- **HOTPATCH** (patch JSON + copie media) quand le changement ne concerne que `src/annotations/` et/ou `public/media/`
|
||||
- **SKIP** sinon
|
||||
|
||||
Les preuves et la procédure de test reproductible A/B sont documentées dans :
|
||||
➡️ `docs/runbooks/DEPLOY-BLUE-GREEN.md` → section “CI Deploy gate (merge-proof) + Tests A/B + preuve alias injection”.
|
||||
|
||||
## Schéma (résumé, sans commandes)
|
||||
|
||||
- Ne jamais toucher au slot live.
|
||||
|
||||
@@ -202,4 +202,33 @@ docker compose logs --tail=200 web_blue
|
||||
docker compose logs --tail=200 web_green
|
||||
|
||||
# Si tu veux suivre en live :
|
||||
docker compose logs -f web_green
|
||||
docker compose logs -f web_green
|
||||
|
||||
|
||||
## Historique synthétique (2026-03-03) — Stabilisation CI/CD “zéro surprise”
|
||||
|
||||
### Problème initial observé
|
||||
- Déploiement parfois lancé en “hotpatch” alors qu’un rebuild était nécessaire.
|
||||
- Sur merge commits, la détection de fichiers modifiés pouvait être ambiguë.
|
||||
- Résultat : besoin de `force=1` manuel pour éviter des incohérences.
|
||||
|
||||
### Correctif appliqué
|
||||
- Gate CI rendu **merge-proof** :
|
||||
- lecture de `BEFORE` et `AFTER` depuis `event.json`
|
||||
- calcul des fichiers modifiés via `git diff --name-only BEFORE AFTER`
|
||||
|
||||
- Politique de décision stabilisée :
|
||||
- FULL auto dès qu’un changement impacte build/runtime (content/pages/scripts/anchors/etc.)
|
||||
- HOTPATCH auto uniquement pour annotations/media
|
||||
|
||||
### Preuves
|
||||
- Test A (touch src/content) :
|
||||
- Gate flags: HAS_FULL=1 HAS_HOTPATCH=0 → MODE=full
|
||||
- Test B (touch src/annotations) :
|
||||
- Gate flags: HAS_FULL=0 HAS_HOTPATCH=1 → MODE=hotpatch
|
||||
|
||||
### Audit post-déploiement (preuves côté NAS)
|
||||
- 8081 + 8082 répondent HTTP 200
|
||||
- `/para-index.json` + `/annotations-index.json` OK
|
||||
- Aliases injectés visibles dans HTML via `.para-alias` quand alias présent
|
||||
|
||||
|
||||
@@ -199,4 +199,348 @@ 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).
|
||||
Le canonical dépend du build : PUBLIC_SITE doit être injecté (voir runbook ENV-PUBLIC_SITE).
|
||||
|
||||
## 10) CI Deploy (Gitea Actions) — Gate SKIP / HOTPATCH / FULL (merge-proof) + preuves
|
||||
|
||||
Cette section documente le comportement **canonique** du workflow :
|
||||
- `.gitea/workflows/deploy-staging-live.yml`
|
||||
|
||||
Objectif : **zéro surprise**.
|
||||
On ne veut plus “penser à force=1”.
|
||||
Le gate doit décider automatiquement, y compris sur des **merge commits**.
|
||||
|
||||
### 10.1 — Principe (ce que fait réellement le gate)
|
||||
|
||||
Le job `deploy` calcule les fichiers modifiés entre :
|
||||
- `BEFORE` = commit précédent (avant le push sur main)
|
||||
- `AFTER` = commit actuel (après le push / merge sur main)
|
||||
|
||||
Puis il classe le déploiement dans un mode :
|
||||
|
||||
- **MODE=full**
|
||||
- rebuild image + restart `archicratie-web-blue` (8081) + `archicratie-web-green` (8082)
|
||||
- warmup endpoints (para-index, annotations-index, pagefind.js)
|
||||
- vérification canonical staging + live
|
||||
|
||||
- **MODE=hotpatch**
|
||||
- rebuild d’un `annotations-index.json` consolidé depuis `src/annotations/**`
|
||||
- patch direct dans les conteneurs en cours d’exécution (blue+green)
|
||||
- copie des médias modifiés `public/media/**` vers `/usr/share/nginx/html/media/**`
|
||||
- smoke sur `/annotations-index.json` des deux ports
|
||||
|
||||
- **MODE=skip**
|
||||
- pas de déploiement (on évite le bruit)
|
||||
|
||||
⚠️ Important : le mode “hotpatch” **ne rebuild pas** Astro.
|
||||
Donc toute modification de contenu, routes, scripts, anchors, etc. doit déclencher **full**.
|
||||
|
||||
### 10.2 — Matrice de décision (règles officielles)
|
||||
|
||||
Le gate définit deux flags :
|
||||
- `HAS_FULL=1` si changement “build-impacting”
|
||||
- `HAS_HOTPATCH=1` si changement “annotations/media only”
|
||||
|
||||
Règle de priorité :
|
||||
1) Si `HAS_FULL=1` → **MODE=full**
|
||||
2) Sinon si `HAS_HOTPATCH=1` → **MODE=hotpatch**
|
||||
3) Sinon → **MODE=skip**
|
||||
|
||||
#### 10.2.1 — Changements qui déclenchent FULL (build-impacting)
|
||||
|
||||
Exemples typiques (non exhaustif, mais on couvre le cœur) :
|
||||
- `src/content/**` (contenu MD/MDX)
|
||||
- `src/pages/**` (routes Astro)
|
||||
- `src/anchors/**` (aliases d’ancres)
|
||||
- `scripts/**` (tooling postbuild : injection, index, tests)
|
||||
- `src/layouts/**`, `src/components/**`, `src/styles/**` (rendu et scripts inline)
|
||||
- `astro.config.mjs`, `package.json`, `package-lock.json`
|
||||
- `Dockerfile`, `docker-compose.yml`, `nginx.conf`
|
||||
- `.gitea/workflows/**` (changement infra CI/CD)
|
||||
|
||||
=> On veut **full** pour garantir cohérence et éviter “site partiellement mis à jour”.
|
||||
|
||||
#### 10.2.2 — Changements qui déclenchent HOTPATCH (sans rebuild)
|
||||
|
||||
Uniquement :
|
||||
- `src/annotations/**` (shards YAML)
|
||||
- `public/media/**` (assets média)
|
||||
|
||||
=> On veut hotpatch pour vitesse et éviter rebuild NAS.
|
||||
|
||||
### 10.3 — “Merge-proof” : pourquoi on ne lit PAS seulement `git show $SHA`
|
||||
|
||||
Sur un merge commit, `git show --name-only $SHA` peut être trompeur selon le contexte.
|
||||
La méthode robuste est :
|
||||
- utiliser `event.json` (Gitea Actions) pour récupérer `before` et `after`
|
||||
- calculer `git diff --name-only BEFORE AFTER`
|
||||
|
||||
C’est ce qui rend le gate **merge-proof**.
|
||||
|
||||
### 10.4 — Tests de preuve A/B (reproductibles)
|
||||
|
||||
Ces tests valident le gate sans ambiguïté.
|
||||
But : vérifier que le mode choisi est EXACTEMENT celui attendu.
|
||||
|
||||
#### Test A — toucher `src/content/...` (FULL auto)
|
||||
|
||||
1) Créer une branche test
|
||||
2) Modifier 1 fichier dans `src/content/` (ex : ajouter une ligne de commentaire non destructive)
|
||||
3) PR → merge dans `main`
|
||||
4) Vérifier dans `deploy-staging-live.yml` :
|
||||
|
||||
Attendus :
|
||||
- `Gate flags: HAS_FULL=1 HAS_HOTPATCH=0`
|
||||
- `✅ build-impacting change -> MODE=full (rebuild+restart)`
|
||||
- Les étapes FULL (blue puis green) s’exécutent réellement
|
||||
|
||||
#### Test B — toucher `src/annotations/...` uniquement (HOTPATCH auto)
|
||||
|
||||
1) Créer une branche test
|
||||
2) Modifier 1 fichier sous `src/annotations/**` (ex: un champ comment, ts, etc.)
|
||||
3) PR → merge dans `main`
|
||||
4) Vérifier dans `deploy-staging-live.yml` :
|
||||
|
||||
Attendus :
|
||||
- `Gate flags: HAS_FULL=0 HAS_HOTPATCH=1`
|
||||
- `✅ annotations/media change -> MODE=hotpatch`
|
||||
- Les étapes FULL sont “skip” (durée 0s)
|
||||
- L’étape HOTPATCH s’exécute réellement
|
||||
|
||||
### 10.5 — Preuve opérationnelle côté NAS (2 URLs + 2 commandes)
|
||||
|
||||
But : prouver que staging+live servent bien les endpoints essentiels (et que le déploiement n’a pas “fait semblant”).
|
||||
|
||||
#### 10.5.1 — Deux URLs à vérifier (staging et live)
|
||||
|
||||
- Staging (blue) : `http://127.0.0.1:8081/`
|
||||
- Live (green) : `http://127.0.0.1:8082/`
|
||||
|
||||
#### 10.5.2 — Deux commandes minimales (zéro débat)
|
||||
|
||||
```bash
|
||||
curl -fsSI http://127.0.0.1:8081/ | head -n 1
|
||||
curl -fsSI http://127.0.0.1:8082/ | head -n 1
|
||||
|
||||
---
|
||||
|
||||
## 10) CI Deploy (Gitea Actions) — Gate SKIP / HOTPATCH / FULL (merge-proof) + preuves
|
||||
|
||||
Cette section documente le comportement **canonique** du workflow :
|
||||
- `.gitea/workflows/deploy-staging-live.yml`
|
||||
|
||||
Objectif : **zéro surprise**.
|
||||
On ne veut plus “penser à force=1”.
|
||||
Le gate doit décider automatiquement, y compris sur des **merge commits**.
|
||||
|
||||
### 10.1 — Principe (ce que fait réellement le gate)
|
||||
|
||||
Le job `deploy` calcule les fichiers modifiés entre :
|
||||
- `BEFORE` = commit précédent (avant le push sur main)
|
||||
- `AFTER` = commit actuel (après le push / merge sur main)
|
||||
|
||||
Puis il classe le déploiement dans un mode :
|
||||
|
||||
- **MODE=full**
|
||||
- rebuild image + restart `archicratie-web-blue` (8081) + `archicratie-web-green` (8082)
|
||||
- warmup endpoints (para-index, annotations-index, pagefind.js)
|
||||
- vérification canonical staging + live
|
||||
|
||||
- **MODE=hotpatch**
|
||||
- rebuild d’un `annotations-index.json` consolidé depuis `src/annotations/**`
|
||||
- patch direct dans les conteneurs en cours d’exécution (blue+green)
|
||||
- copie des médias modifiés `public/media/**` vers `/usr/share/nginx/html/media/**`
|
||||
- smoke sur `/annotations-index.json` des deux ports
|
||||
|
||||
- **MODE=skip**
|
||||
- pas de déploiement (on évite le bruit)
|
||||
|
||||
⚠️ Important : le mode “hotpatch” **ne rebuild pas** Astro.
|
||||
Donc toute modification de contenu, routes, scripts, anchors, etc. doit déclencher **full**.
|
||||
|
||||
### 10.2 — Matrice de décision (règles officielles)
|
||||
|
||||
Le gate définit deux flags :
|
||||
- `HAS_FULL=1` si changement “build-impacting”
|
||||
- `HAS_HOTPATCH=1` si changement “annotations/media only”
|
||||
|
||||
Règle de priorité :
|
||||
1) Si `HAS_FULL=1` → **MODE=full**
|
||||
2) Sinon si `HAS_HOTPATCH=1` → **MODE=hotpatch**
|
||||
3) Sinon → **MODE=skip**
|
||||
|
||||
#### 10.2.1 — Changements qui déclenchent FULL (build-impacting)
|
||||
|
||||
Exemples typiques (non exhaustif, mais on couvre le cœur) :
|
||||
- `src/content/**` (contenu MD/MDX)
|
||||
- `src/pages/**` (routes Astro)
|
||||
- `src/anchors/**` (aliases d’ancres)
|
||||
- `scripts/**` (tooling postbuild : injection, index, tests)
|
||||
- `src/layouts/**`, `src/components/**`, `src/styles/**` (rendu et scripts inline)
|
||||
- `astro.config.mjs`, `package.json`, `package-lock.json`
|
||||
- `Dockerfile`, `docker-compose.yml`, `nginx.conf`
|
||||
- `.gitea/workflows/**` (changement infra CI/CD)
|
||||
|
||||
=> On veut **full** pour garantir cohérence et éviter “site partiellement mis à jour”.
|
||||
|
||||
#### 10.2.2 — Changements qui déclenchent HOTPATCH (sans rebuild)
|
||||
|
||||
Uniquement :
|
||||
- `src/annotations/**` (shards YAML)
|
||||
- `public/media/**` (assets média)
|
||||
|
||||
=> On veut hotpatch pour vitesse et éviter rebuild NAS.
|
||||
|
||||
### 10.3 — “Merge-proof” : pourquoi on ne lit PAS seulement `git show $SHA`
|
||||
|
||||
Sur un merge commit, `git show --name-only $SHA` peut être trompeur selon le contexte.
|
||||
La méthode robuste est :
|
||||
- utiliser `event.json` (Gitea Actions) pour récupérer `before` et `after`
|
||||
- calculer `git diff --name-only BEFORE AFTER`
|
||||
|
||||
C’est ce qui rend le gate **merge-proof**.
|
||||
|
||||
### 10.4 — Tests de preuve A/B (reproductibles)
|
||||
|
||||
Ces tests valident le gate sans ambiguïté.
|
||||
But : vérifier que le mode choisi est EXACTEMENT celui attendu.
|
||||
|
||||
#### Test A — toucher `src/content/...` (FULL auto)
|
||||
|
||||
1) Créer une branche test
|
||||
2) Modifier 1 fichier dans `src/content/` (ex : ajouter une ligne de commentaire non destructive)
|
||||
3) PR → merge dans `main`
|
||||
4) Vérifier dans `deploy-staging-live.yml` :
|
||||
|
||||
Attendus :
|
||||
- `Gate flags: HAS_FULL=1 HAS_HOTPATCH=0`
|
||||
- `✅ build-impacting change -> MODE=full (rebuild+restart)`
|
||||
- Les étapes FULL (blue puis green) s’exécutent réellement
|
||||
|
||||
#### Test B — toucher `src/annotations/...` uniquement (HOTPATCH auto)
|
||||
|
||||
1) Créer une branche test
|
||||
2) Modifier 1 fichier sous `src/annotations/**` (ex: un champ comment, ts, etc.)
|
||||
3) PR → merge dans `main`
|
||||
4) Vérifier dans `deploy-staging-live.yml` :
|
||||
|
||||
Attendus :
|
||||
- `Gate flags: HAS_FULL=0 HAS_HOTPATCH=1`
|
||||
- `✅ annotations/media change -> MODE=hotpatch`
|
||||
- Les étapes FULL sont “skip” (durée 0s)
|
||||
- L’étape HOTPATCH s’exécute réellement
|
||||
|
||||
### 10.5 — Preuve opérationnelle côté NAS (2 URLs + 2 commandes)
|
||||
|
||||
But : prouver que staging+live servent bien les endpoints essentiels (et que le déploiement n’a pas “fait semblant”).
|
||||
|
||||
#### 10.5.1 — Deux URLs à vérifier (staging et live)
|
||||
|
||||
- Staging (blue) : `http://127.0.0.1:8081/`
|
||||
- Live (green) : `http://127.0.0.1:8082/`
|
||||
|
||||
#### 10.5.2 — Deux commandes minimales (zéro débat)
|
||||
|
||||
en bash :
|
||||
curl -fsSI http://127.0.0.1:8081/ | head -n 1
|
||||
curl -fsSI http://127.0.0.1:8082/ | head -n 1
|
||||
|
||||
Attendu : HTTP/1.1 200 OK des deux côtés.
|
||||
|
||||
10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page
|
||||
|
||||
Contexte : lorsqu’un paragraphe change (ex: ticket “Proposer” appliqué),
|
||||
l’ID de paragraphe peut changer, mais on doit préserver les liens anciens via :
|
||||
|
||||
src/anchors/anchor-aliases.json
|
||||
|
||||
injection build-time dans dist (span .para-alias)
|
||||
|
||||
10.6.1 — Check rapide (staging + live)
|
||||
|
||||
Remplacer OLD/NEW par tes ids réels :
|
||||
|
||||
Attendu : HTTP/1.1 200 OK des deux côtés.
|
||||
|
||||
10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page
|
||||
|
||||
Contexte : lorsqu’un paragraphe change (ex: ticket “Proposer” appliqué),
|
||||
l’ID de paragraphe peut changer, mais on doit préserver les liens anciens via :
|
||||
|
||||
src/anchors/anchor-aliases.json
|
||||
|
||||
injection build-time dans dist (span .para-alias)
|
||||
|
||||
10.6.1 — Check rapide (staging + live)
|
||||
|
||||
Remplacer OLD/NEW par tes ids réels :
|
||||
|
||||
OLD="p-1-60c7ea48"
|
||||
NEW="p-1-a21087b0"
|
||||
|
||||
for P in 8081 8082; do
|
||||
echo "=== $P ==="
|
||||
HTML="$(curl -fsS "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/" | tr -d '\r')"
|
||||
echo "OLD count: $(printf '%s' "$HTML" | grep -o "$OLD" | wc -l | tr -d ' ')"
|
||||
echo "NEW count: $(printf '%s' "$HTML" | grep -o "$NEW" | wc -l | tr -d ' ')"
|
||||
printf '%s\n' "$HTML" | grep -nE "$OLD|$NEW|class=\"para-alias\"" | head -n 40 || true
|
||||
done
|
||||
|
||||
Attendu :
|
||||
|
||||
présence d’un alias : <span id="$OLD" class="para-alias"...>
|
||||
|
||||
présence du nouveau paragraphe : <p id="$NEW">...
|
||||
|
||||
10.6.2 — Check “lien ancien ne casse pas” (HTTP 200)
|
||||
|
||||
for P in 8081 8082; do
|
||||
curl -fsSI "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/#${OLD}" | head -n 1
|
||||
done
|
||||
|
||||
Attendu : HTTP/1.1 200 OK et navigation fonctionnelle côté navigateur.
|
||||
|
||||
10.7 — Troubleshooting gate (symptômes typiques)
|
||||
Symptom 1 : job bloqué “Set up job” très longtemps
|
||||
|
||||
Causes fréquentes :
|
||||
|
||||
runner indisponible / capacity saturée
|
||||
|
||||
runner ne récupère pas les tâches (fetch_timeout trop court + réseau instable)
|
||||
|
||||
erreur dans “Gate — decide …” qui casse bash (et donne l’impression d’un hang)
|
||||
|
||||
Commandes NAS (diagnostic rapide) :
|
||||
|
||||
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | grep -E 'gitea-act-runner|registry|archicratie-web'
|
||||
docker logs --since 30m --tail 400 gitea-act-runner | tail -n 200
|
||||
Symptom 2 : conditional binary operator expected
|
||||
|
||||
Cause :
|
||||
|
||||
test bash du type [[ "$X" == "1" && "$Y" == "2" ]] mal formé
|
||||
|
||||
variable vide non quotée
|
||||
|
||||
usage d’un opérateur non supporté dans la shell effective
|
||||
|
||||
Fix :
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
toujours quoter : [[ "${VAR:-}" == "..." ]]
|
||||
|
||||
logguer BEFORE/AFTER/FORCE et s’assurer qu’ils ne sont pas vides
|
||||
|
||||
Symptom 3 : le gate liste “trop de fichiers” alors qu’on a changé 1 seul fichier
|
||||
|
||||
Cause :
|
||||
|
||||
comparaison faite sur le mauvais range (ex: git show sur merge, ou mauvais parent)
|
||||
Fix :
|
||||
|
||||
toujours utiliser git diff --name-only "$BEFORE" "$AFTER" (merge-proof)
|
||||
|
||||
confirmer dans le log : Gate ctx: BEFORE=... AFTER=...
|
||||
|
||||
|
||||
226
package-lock.json
generated
226
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"astro": "^5.17.3"
|
||||
"astro": "^5.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
@@ -1247,9 +1247,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
|
||||
"integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1260,9 +1260,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1273,9 +1273,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1286,9 +1286,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1299,9 +1299,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1312,9 +1312,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1325,9 +1325,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
|
||||
"integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1338,9 +1338,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
|
||||
"integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1351,9 +1351,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1364,9 +1364,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1377,9 +1377,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1390,9 +1390,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1403,9 +1403,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1416,9 +1416,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1429,9 +1429,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1442,9 +1442,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1455,9 +1455,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1468,9 +1468,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1481,9 +1481,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
|
||||
"integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1494,9 +1494,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
|
||||
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1507,9 +1507,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1520,9 +1520,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1533,9 +1533,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1546,9 +1546,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1559,9 +1559,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1905,9 +1905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/astro": {
|
||||
"version": "5.17.3",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.17.3.tgz",
|
||||
"integrity": "sha512-69dcfPe8LsHzklwj+hl+vunWUbpMB6pmg35mACjetxbJeUNNys90JaBM8ZiwsPK689SAj/4Zqb1ayaANls9/MA==",
|
||||
"version": "5.18.0",
|
||||
"resolved": "https://registry.npmjs.org/astro/-/astro-5.18.0.tgz",
|
||||
"integrity": "sha512-CHiohwJIS4L0G6/IzE1Fx3dgWqXBCXus/od0eGUfxrZJD2um2pE7ehclMmgL/fXqbU7NfE1Ze2pq34h2QaA6iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^2.13.0",
|
||||
@@ -2883,9 +2883,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
@@ -5690,9 +5690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -5705,31 +5705,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.55.1",
|
||||
"@rollup/rollup-android-arm64": "4.55.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.55.1",
|
||||
"@rollup/rollup-darwin-x64": "4.55.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.55.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.55.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.55.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.55.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.55.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.55.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.55.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.55.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.55.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.55.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.55.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -6138,9 +6138,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"astro": "^5.17.3"
|
||||
"astro": "^5.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
|
||||
0
public/media/.gitkeep
Normal file
0
public/media/.gitkeep
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 816 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 822 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 822 KiB |
@@ -1 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone","orientation":"any"}
|
||||
@@ -114,7 +114,6 @@ async function runMammoth(docxPath, assetsOutDirWebRoot) {
|
||||
);
|
||||
|
||||
let html = result.value || "";
|
||||
|
||||
// Mammoth gives relative src="image-xx.png" ; we will prefix later
|
||||
return html;
|
||||
}
|
||||
@@ -182,6 +181,25 @@ async function exists(p) {
|
||||
try { await fs.access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ compat:
|
||||
* - ancien : collection="archicratie" + slug="archicrat-ia/chapitre-3"
|
||||
* - nouveau : collection="archicrat-ia" + slug="chapitre-3"
|
||||
*
|
||||
* But : toujours écrire dans src/content/archicrat-ia/<slugSansPrefix>.mdx
|
||||
*/
|
||||
function normalizeDest(collection, slug) {
|
||||
let outCollection = String(collection || "").trim();
|
||||
let outSlug = String(slug || "").trim().replace(/^\/+|\/+$/g, "");
|
||||
|
||||
if (outCollection === "archicratie" && outSlug.startsWith("archicrat-ia/")) {
|
||||
outCollection = "archicrat-ia";
|
||||
outSlug = outSlug.replace(/^archicrat-ia\//, "");
|
||||
}
|
||||
|
||||
return { outCollection, outSlug };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const manifestPath = path.resolve(args.manifest);
|
||||
@@ -203,11 +221,14 @@ async function main() {
|
||||
|
||||
for (const it of selected) {
|
||||
const docxPath = path.resolve(it.source);
|
||||
const outFile = path.resolve("src/content", it.collection, `${it.slug}.mdx`);
|
||||
|
||||
const { outCollection, outSlug } = normalizeDest(it.collection, it.slug);
|
||||
|
||||
const outFile = path.resolve("src/content", outCollection, `${outSlug}.mdx`);
|
||||
const outDir = path.dirname(outFile);
|
||||
|
||||
const assetsPublicDir = path.posix.join("/imported", it.collection, it.slug);
|
||||
const assetsDiskDir = path.resolve("public", "imported", it.collection, it.slug);
|
||||
const assetsPublicDir = path.posix.join("/imported", outCollection, outSlug);
|
||||
const assetsDiskDir = path.resolve("public", "imported", outCollection, outSlug);
|
||||
|
||||
if (!(await exists(docxPath))) {
|
||||
throw new Error(`Missing source docx: ${docxPath}`);
|
||||
@@ -241,18 +262,20 @@ async function main() {
|
||||
html = rewriteLocalImageLinks(html, assetsPublicDir);
|
||||
body = html.trim() ? html : "<p>(Import vide)</p>";
|
||||
}
|
||||
|
||||
|
||||
const defaultVersion = process.env.PUBLIC_RELEASE || "0.1.0";
|
||||
|
||||
// ✅ IMPORTANT: archicrat-ia partage edition/status avec archicratie (pas de migration frontmatter)
|
||||
const schemaDefaultsByCollection = {
|
||||
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
|
||||
ia: { edition: "ia", status: "cas_pratique", level: 1 },
|
||||
traite: { edition: "traite", status: "ontodynamique", level: 1 },
|
||||
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
|
||||
atlas: { edition: "atlas", status: "atlas", level: 1 },
|
||||
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
|
||||
"archicrat-ia": { edition: "archicrat-ia", status: "essai_these", level: 1 },
|
||||
ia: { edition: "ia", status: "cas_pratique", level: 1 },
|
||||
traite: { edition: "traite", status: "ontodynamique", level: 1 },
|
||||
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
|
||||
atlas: { edition: "atlas", status: "atlas", level: 1 },
|
||||
};
|
||||
|
||||
const defaults = schemaDefaultsByCollection[it.collection] || { edition: it.collection, status: "draft", level: 1 };
|
||||
const defaults = schemaDefaultsByCollection[outCollection] || { edition: outCollection, status: "draft", level: 1 };
|
||||
|
||||
const fm = [
|
||||
"---",
|
||||
@@ -282,4 +305,4 @@ async function main() {
|
||||
main().catch((e) => {
|
||||
console.error("\nERROR:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1,5 @@
|
||||
{}
|
||||
|
||||
{
|
||||
"/archicrat-ia/chapitre-3/": {
|
||||
"p-1-60c7ea48": "p-1-a21087b0"
|
||||
}
|
||||
}
|
||||
|
||||
0
src/annotations/.gitkeep
Normal file
0
src/annotations/.gitkeep
Normal file
@@ -1,10 +0,0 @@
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
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
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -1,50 +0,0 @@
|
||||
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: []
|
||||
@@ -3,14 +3,11 @@ import { getCollection } from "astro:content";
|
||||
|
||||
const { currentSlug } = Astro.props;
|
||||
|
||||
const entries = (await getCollection("archicratie"))
|
||||
.filter((e) => e.slug.startsWith("archicrat-ia/"))
|
||||
// ✅ Après migration : TOC = collection "archicrat-ia"
|
||||
const entries = (await getCollection("archicrat-ia"))
|
||||
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
|
||||
|
||||
// ✅ 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)}/`;
|
||||
const href = (slug) => `/archicrat-ia/${slug}/`;
|
||||
---
|
||||
|
||||
<nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA">
|
||||
@@ -163,4 +160,4 @@ const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
|
||||
const active = document.querySelector(".toc-global .toc-item.is-active");
|
||||
if (active) active.scrollIntoView({ block: "nearest" });
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
@@ -25,12 +25,12 @@
|
||||
|
||||
{/* ✅ actions références en haut (niveau 2 uniquement) */}
|
||||
<div class="panel-top-actions level-2" aria-label="Actions références">
|
||||
<div class="panel-actions">
|
||||
<div class="panel-actions">
|
||||
<button class="panel-btn panel-btn--primary" id="panel-ref-submit" type="button">
|
||||
Soumettre une référence (Gitea)
|
||||
Soumettre une référence (Gitea)
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-msg" id="panel-ref-msg" hidden></div>
|
||||
</div>
|
||||
<div class="panel-msg" id="panel-ref-msg" hidden></div>
|
||||
</div>
|
||||
|
||||
<section class="panel-block level-2" aria-label="Références et auteurs">
|
||||
@@ -60,7 +60,7 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* ✅ Lightbox media (pop-up au-dessus du panel) */}
|
||||
{/* ✅ Lightbox media (plein écran) */}
|
||||
<div class="panel-lightbox" id="panel-lightbox" hidden aria-hidden="true">
|
||||
<div class="panel-lightbox__overlay" data-close="1"></div>
|
||||
<div class="panel-lightbox__dialog" role="dialog" aria-modal="true" aria-label="Aperçu du média">
|
||||
@@ -93,6 +93,9 @@
|
||||
const btnMediaSubmit = root.querySelector("#panel-media-submit");
|
||||
const msgMedia = root.querySelector("#panel-media-msg");
|
||||
|
||||
const btnRefSubmit = root.querySelector("#panel-ref-submit");
|
||||
const msgRef = root.querySelector("#panel-ref-msg");
|
||||
|
||||
const taComment = root.querySelector("#panel-comment-text");
|
||||
const btnSend = root.querySelector("#panel-comment-send");
|
||||
const msgComment = root.querySelector("#panel-comment-msg");
|
||||
@@ -101,9 +104,6 @@
|
||||
const lbContent = root.querySelector("#panel-lightbox-content");
|
||||
const lbCaption = root.querySelector("#panel-lightbox-caption");
|
||||
|
||||
const btnRefSubmit = root.querySelector("#panel-ref-submit");
|
||||
const msgRef = root.querySelector("#panel-ref-msg");
|
||||
|
||||
const docTitle = document.body?.dataset?.docTitle || document.title || "Archicratie";
|
||||
const docVersion = document.body?.dataset?.docVersion || "";
|
||||
|
||||
@@ -114,6 +114,16 @@
|
||||
let currentParaId = "";
|
||||
let mediaShowAll = (localStorage.getItem("archicratie:panel:mediaAll") === "1");
|
||||
|
||||
// ===== cosmetics: micro flash “update” =====
|
||||
let _flashT = 0;
|
||||
function flashUpdate(){
|
||||
try {
|
||||
root.classList.add("is-updating");
|
||||
if (_flashT) clearTimeout(_flashT);
|
||||
_flashT = setTimeout(() => root.classList.remove("is-updating"), 180);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ===== globals =====
|
||||
function getG() {
|
||||
return window.__archiGitea || { ready: false, base: "", owner: "", repo: "" };
|
||||
@@ -121,9 +131,6 @@
|
||||
function getAuthInfoP() {
|
||||
return window.__archiAuthInfoP || Promise.resolve({ ok: false, groups: [] });
|
||||
}
|
||||
function isDev() {
|
||||
return Boolean((window.__archiFlags && window.__archiFlags.dev) || /^(localhost|127\.0\.0\.1|\[::1\])$/i.test(location.hostname));
|
||||
}
|
||||
|
||||
const access = { ready: false, canUsers: false };
|
||||
|
||||
@@ -137,8 +144,7 @@
|
||||
return Array.isArray(groups) && groups.some((x) => String(x).toLowerCase() === gg);
|
||||
}
|
||||
|
||||
// ✅ règle mission : readers + editors peuvent soumettre médias + commentaires
|
||||
// ✅ dev fallback : si /_auth/whoami n’existe pas, on autorise pour tester
|
||||
// ✅ readers + editors peuvent soumettre médias + commentaires + refs
|
||||
getAuthInfoP().then((info) => {
|
||||
const groups = Array.isArray(info?.groups) ? info.groups : [];
|
||||
const canReaders = inGroup(groups, "readers");
|
||||
@@ -152,7 +158,6 @@
|
||||
if (btnSend) btnSend.disabled = !access.canUsers;
|
||||
if (btnRefSubmit) btnRefSubmit.disabled = !access.canUsers;
|
||||
|
||||
// si pas d'accès, on informe (soft)
|
||||
if (!access.canUsers) {
|
||||
if (msgHead) {
|
||||
msgHead.hidden = false;
|
||||
@@ -161,7 +166,6 @@
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// fallback dev (cohérent: media + ref + comment)
|
||||
access.ready = true;
|
||||
if (Boolean(window.__archiFlags && window.__archiFlags.whoamiSkipped)) {
|
||||
access.canUsers = true;
|
||||
@@ -213,7 +217,6 @@
|
||||
const res = await fetch("/annotations-index.json?_=" + Date.now(), { cache: "no-store" });
|
||||
if (res && res.ok) return await res.json();
|
||||
} catch {}
|
||||
// ✅ antifragile: ne pas “cacher” un échec pour toujours (dev/HMR/boot race)
|
||||
_idxP = null;
|
||||
return null;
|
||||
})();
|
||||
@@ -255,24 +258,22 @@
|
||||
return issue.toString();
|
||||
}
|
||||
|
||||
// Ouvre un nouvel onglet UNE SEULE FOIS (évite le double-open Safari/Firefox + noopener).
|
||||
function openNewTab(url) {
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
return true; // on ne peut pas détecter proprement un blocage sans retomber dans le double-open
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
function openNewTab(url) {
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ====== GARDES ANTI-DOUBLONS ======
|
||||
const _openStamp = new Map();
|
||||
function openOnce(key, fn) {
|
||||
const now = Date.now();
|
||||
@@ -301,13 +302,21 @@
|
||||
}
|
||||
|
||||
// ===== Lightbox =====
|
||||
function lockScroll(on) {
|
||||
try {
|
||||
document.documentElement.classList.toggle("archi-lb-open", !!on);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
if (!lb) return;
|
||||
lb.hidden = true;
|
||||
lb.setAttribute("aria-hidden", "true");
|
||||
if (lbContent) clear(lbContent);
|
||||
if (lbCaption) { lbCaption.hidden = true; lbCaption.textContent = ""; }
|
||||
lockScroll(false);
|
||||
}
|
||||
|
||||
function openLightbox({ type, src, caption }) {
|
||||
if (!lb || !lbContent) return;
|
||||
clear(lbContent);
|
||||
@@ -346,6 +355,7 @@
|
||||
else { lbCaption.hidden = true; lbCaption.textContent = ""; }
|
||||
}
|
||||
|
||||
lockScroll(true);
|
||||
lb.hidden = false;
|
||||
lb.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
@@ -363,7 +373,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Renders =====
|
||||
function renderLevel2(data) {
|
||||
clear(elL2);
|
||||
if (!elL2) return;
|
||||
@@ -563,13 +572,16 @@
|
||||
async function updatePanel(paraId) {
|
||||
currentParaId = paraId || currentParaId || "";
|
||||
if (elId) elId.textContent = currentParaId || "—";
|
||||
|
||||
flashUpdate();
|
||||
|
||||
hideMsg(msgHead);
|
||||
hideMsg(msgMedia);
|
||||
hideMsg(msgComment);
|
||||
hideMsg(msgRef);
|
||||
|
||||
const idx = await loadIndex();
|
||||
|
||||
// ✅ message soft si l’index est indisponible (sans écraser le message d’auth)
|
||||
if (!idx && msgHead && msgHead.hidden) {
|
||||
msgHead.hidden = false;
|
||||
msgHead.textContent = "Index annotations indisponible (annotations-index.json).";
|
||||
@@ -583,7 +595,6 @@
|
||||
renderLevel4(data);
|
||||
}
|
||||
|
||||
// ===== media "voir tous" =====
|
||||
if (btnMediaAll) {
|
||||
bindClickOnce(btnMediaAll, (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -595,7 +606,6 @@
|
||||
btnMediaAll.textContent = mediaShowAll ? "Réduire la liste" : "Voir tous les éléments";
|
||||
}
|
||||
|
||||
// ===== media submit (readers + editors) =====
|
||||
if (btnMediaSubmit) {
|
||||
bindClickOnce(btnMediaSubmit, (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -638,27 +648,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== référence submit (readers + editors) =====
|
||||
if (btnRefSubmit) {
|
||||
if (btnRefSubmit) {
|
||||
bindClickOnce(btnRefSubmit, (ev) => {
|
||||
ev.preventDefault();
|
||||
hideMsg(msgRef);
|
||||
ev.preventDefault();
|
||||
hideMsg(msgRef);
|
||||
|
||||
if (guardEventOnce(ev, "gitea_open_ref")) return;
|
||||
if (guardEventOnce(ev, "gitea_open_ref")) return;
|
||||
|
||||
if (!currentParaId) return showMsg(msgRef, "Choisis d’abord un paragraphe (scroll / survol).", "warn");
|
||||
if (!getG().ready) return showMsg(msgRef, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
|
||||
if (btnRefSubmit.disabled) return showMsg(msgRef, "Connexion requise (readers/editors).", "error");
|
||||
if (!currentParaId) return showMsg(msgRef, "Choisis d’abord un paragraphe (scroll / survol).", "warn");
|
||||
if (!getG().ready) return showMsg(msgRef, "Gitea non configuré (PUBLIC_GITEA_*).", "error");
|
||||
if (btnRefSubmit.disabled) return showMsg(msgRef, "Connexion requise (readers/editors).", "error");
|
||||
|
||||
const pageUrl = new URL(location.href);
|
||||
pageUrl.search = "";
|
||||
pageUrl.hash = currentParaId;
|
||||
const pageUrl = new URL(location.href);
|
||||
pageUrl.search = "";
|
||||
pageUrl.hash = currentParaId;
|
||||
|
||||
const paraTxt = getParaText(currentParaId);
|
||||
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
|
||||
const paraTxt = getParaText(currentParaId);
|
||||
const excerpt = paraTxt.length > FULL_TEXT_SOFT_LIMIT ? (paraTxt.slice(0, FULL_TEXT_SOFT_LIMIT) + "…") : paraTxt;
|
||||
|
||||
const title = `[Reference] ${currentParaId} — ${docTitle}`;
|
||||
const body = [
|
||||
const title = `[Reference] ${currentParaId} — ${docTitle}`;
|
||||
const body = [
|
||||
`Chemin: ${location.pathname}`,
|
||||
`URL: ${pageUrl.toString()}`,
|
||||
`Ancre: #${currentParaId}`,
|
||||
@@ -676,18 +685,16 @@
|
||||
``,
|
||||
`---`,
|
||||
`Note: issue générée depuis le site (pré-remplissage).`,
|
||||
].join("\n");
|
||||
].join("\n");
|
||||
|
||||
const url = buildIssueURL({ title, body });
|
||||
if (!url) return showMsg(msgRef, "Impossible de générer l’issue.", "error");
|
||||
const url = buildIssueURL({ title, body });
|
||||
if (!url) return showMsg(msgRef, "Impossible de générer l’issue.", "error");
|
||||
|
||||
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
|
||||
if (!ok) showMsg(msgRef, "Si rien ne s’ouvre : autorise les popups pour ce site.", "error");
|
||||
const ok = openOnce(`ref:${currentParaId}`, () => openNewTab(url));
|
||||
if (!ok) showMsg(msgRef, "Si rien ne s’ouvre : autorise les popups pour ce site.", "error");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== commentaire (readers + editors) =====
|
||||
if (btnSend) {
|
||||
bindClickOnce(btnSend, (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -739,60 +746,31 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ===== wiring: para courant (aligné sur le paragraphe sous le reading-follow) =====
|
||||
function isPara(el) {
|
||||
return Boolean(el && el.nodeType === 1 && el.matches && el.matches('.reading p[id^="p-"]'));
|
||||
// ===== wiring: para courant (SOURCE OF TRUTH = EditionLayout) =====
|
||||
function onCurrentPara(ev) {
|
||||
try {
|
||||
const id = ev?.detail?.id ? String(ev.detail.id) : "";
|
||||
if (!id || !/^p-\d+-/i.test(id)) return;
|
||||
if (id === currentParaId) return;
|
||||
updatePanel(id);
|
||||
} catch {}
|
||||
}
|
||||
window.addEventListener("archicratie:currentPara", onCurrentPara);
|
||||
|
||||
function pickParaAtY(y) {
|
||||
const x = Math.max(0, Math.round(window.innerWidth * 0.5));
|
||||
const candidates = [
|
||||
document.elementFromPoint(x, y),
|
||||
document.elementFromPoint(Math.min(window.innerWidth - 1, x + 60), y),
|
||||
document.elementFromPoint(Math.max(0, x - 60), y),
|
||||
].filter(Boolean);
|
||||
const initial = String(location.hash || "").replace(/^#/, "").trim();
|
||||
|
||||
for (const c of candidates) {
|
||||
if (isPara(c)) return c;
|
||||
const p = c.closest ? c.closest('.reading p[id^="p-"]') : null;
|
||||
if (isPara(p)) return p;
|
||||
}
|
||||
return null;
|
||||
if (/^p-\d+-/i.test(initial)) {
|
||||
updatePanel(initial);
|
||||
} else if (window.__archiCurrentParaId && /^p-\d+-/i.test(String(window.__archiCurrentParaId))) {
|
||||
updatePanel(String(window.__archiCurrentParaId));
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const id = String(window.__archiCurrentParaId || "").trim();
|
||||
if (/^p-\d+-/i.test(id)) updatePanel(id);
|
||||
} catch {}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
let _lastPicked = "";
|
||||
function syncFromFollowLine() {
|
||||
const off = Number(document.documentElement.style.getPropertyValue("--sticky-offset-px")) || 0;
|
||||
const y = Math.round(off + 8);
|
||||
const p = pickParaAtY(y);
|
||||
if (!p || !p.id) return;
|
||||
if (p.id === _lastPicked) return;
|
||||
_lastPicked = p.id;
|
||||
|
||||
// met à jour l'app global (EditionLayout écoute déjà currentPara)
|
||||
try { window.dispatchEvent(new CustomEvent("archicratie:currentPara", { detail: { id: p.id } })); } catch {}
|
||||
|
||||
// et met à jour le panel immédiatement (sans attendre)
|
||||
updatePanel(p.id);
|
||||
}
|
||||
|
||||
let ticking = false;
|
||||
function onScroll() {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => {
|
||||
ticking = false;
|
||||
syncFromFollowLine();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.addEventListener("resize", onScroll);
|
||||
|
||||
// Initial: hash > sinon calc
|
||||
const initial = String(location.hash || "").replace(/^#/, "");
|
||||
if (/^p-\d+-/i.test(initial)) updatePanel(initial);
|
||||
else setTimeout(() => { try { syncFromFollowLine(); } catch {} }, 0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -805,6 +783,8 @@
|
||||
position: sticky;
|
||||
top: calc(var(--sticky-header-h) + var(--page-gap));
|
||||
align-self: start;
|
||||
|
||||
--thumb: 92px; /* ✅ taille des vignettes (80–110 selon goût) */
|
||||
}
|
||||
|
||||
:global(body[data-reading-level="3"]) .page-panel{
|
||||
@@ -922,28 +902,33 @@
|
||||
/* actions médias en haut */
|
||||
.panel-top-actions{ margin-top: 8px; }
|
||||
|
||||
/* ===== media thumbnails (150x150) ===== */
|
||||
/* ===== media thumbnails (plus petits + plus denses) ===== */
|
||||
.panel-media-grid{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--thumb), 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel-media-tile{
|
||||
width: 150px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(127,127,127,.20);
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
background: rgba(127,127,127,0.04);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.panel-media-tile:hover{
|
||||
transform: translateY(-1px);
|
||||
background: rgba(127,127,127,0.07);
|
||||
border-color: rgba(127,127,127,.32);
|
||||
}
|
||||
|
||||
.panel-media-tile img{
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: var(--thumb);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
@@ -951,8 +936,8 @@
|
||||
}
|
||||
|
||||
.panel-media-ph{
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
height: var(--thumb);
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -995,7 +980,11 @@
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ===== Lightbox ===== */
|
||||
/* ===== Lightbox (plein écran “cinéma”) ===== */
|
||||
:global(html.archi-lb-open){
|
||||
overflow: hidden; /* ✅ empêche le scroll derrière */
|
||||
}
|
||||
|
||||
.panel-lightbox{
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -1005,58 +994,66 @@
|
||||
.panel-lightbox__overlay{
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.80);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
background: rgba(0,0,0,0.84);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.panel-lightbox__dialog{
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: calc(var(--sticky-header-h) + 16px);
|
||||
width: min(520px, calc(100vw - 48px));
|
||||
max-height: calc(100vh - (var(--sticky-header-h) + 32px));
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
width: min(1100px, 92vw);
|
||||
max-height: 92vh;
|
||||
overflow: auto;
|
||||
|
||||
border: 1px solid rgba(127,127,127,0.22);
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.10);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 12px;
|
||||
}
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
border-radius: 18px;
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-lightbox__dialog{
|
||||
background: rgba(0,0,0,0.28);
|
||||
}
|
||||
background: rgba(20,20,20,0.55);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
|
||||
padding: 16px;
|
||||
box-shadow: 0 24px 70px rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
.panel-lightbox__close{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin-left: auto;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(127,127,127,0.35);
|
||||
background: rgba(127,127,127,0.10);
|
||||
|
||||
width: 44px;
|
||||
height: 40px;
|
||||
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.22);
|
||||
background: rgba(255,255,255,0.10);
|
||||
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.panel-lightbox__content{
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.panel-lightbox__content img,
|
||||
.panel-lightbox__content video{
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
max-height: calc(92vh - 160px);
|
||||
object-fit: contain;
|
||||
|
||||
background: rgba(0,0,0,0.22);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.panel-lightbox__content audio{
|
||||
@@ -1064,13 +1061,14 @@
|
||||
}
|
||||
|
||||
.panel-lightbox__caption{
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
opacity: .92;
|
||||
color: rgba(255,255,255,0.92);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px){
|
||||
.page-panel{ display: none; }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -14,7 +14,7 @@ source:
|
||||
---
|
||||
Ce chapitre se tient à un point nodal de notre essai-thèse : il ouvre un espace d’exploration systématique des formes conceptuelles et philosophiques à travers lesquelles le pouvoir se configure comme régime de régulation. Il ne s’agit pas ici de revenir une nouvelle fois sur les fondements de l’autorité, ni d’interroger la légitimité politique au sens classique du terme, ni même d’enquêter sur la genèse des institutions. L’ambition est autre, structurelle, transversale, morphologique, elle tentera d’arpenter, à même les dispositifs, les pensées, les théorisations et les expériences, les modalités différentiées par lesquelles s’instaurent, s’éprouvent et se disputent les formes de régulation du vivre-ensemble.
|
||||
|
||||
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point d’origine, ne prétend restituer aucune ontologie stable du politique. Ce qu’il donne à lire, c’est une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières.
|
||||
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point d’origine, ne prétend restituer aucune ontologie stable du politique. Ce qu’il donne à lire, c’est une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières, et souvent complémentaires.
|
||||
|
||||
Ainsi, loin d’être une galerie illustrative de théories politiques juxtaposées, le chapitre s’agence comme une topologie critique, une plongée stratigraphique dans les scènes où s’articule la régulation — entendue ici non comme stabilisation externe ou ajustement technico-fonctionnel, mais comme dispositif instituant, tension structurante, scène traversée de conflictualité et d’exigence normative. Car à nos yeux, la régulation n’est pas ce qui vient après le pouvoir, elle en est la forme même constitutive — son architecture, son rythme, son épaisseur. Elle est ce par quoi le pouvoir ne se contente pas d’être exercé, mais s’institue, se justifie, se dispute, se recompose.
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineCollection, z } from "astro:content";
|
||||
|
||||
const linkSchema = z.object({
|
||||
type: z.enum(["definition", "appui", "transposition"]),
|
||||
target: z.string().min(1), // URL interne (ex: /glossaire/archicratie/) ou slug
|
||||
target: z.string().min(1),
|
||||
note: z.string().optional()
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ const baseTextSchema = z.object({
|
||||
version: z.string().min(1),
|
||||
concepts: z.array(z.string().min(1)).default([]),
|
||||
links: z.array(linkSchema).default([]),
|
||||
// optionnels mais utiles dès maintenant
|
||||
order: z.number().int().nonnegative().optional(),
|
||||
summary: z.string().optional()
|
||||
});
|
||||
@@ -50,20 +49,31 @@ const atlas = defineCollection({
|
||||
})
|
||||
});
|
||||
|
||||
// ✅ NOUVELLE collection : archicrat-ia (Essai-thèse)
|
||||
// NOTE : on accepte temporairement edition/status "archicratie/modele_sociopolitique"
|
||||
// si tes MDX n’ont pas encore été normalisés.
|
||||
// Quand tu voudras "strict", on passera à edition="archicrat-ia" status="essai_these"
|
||||
// + update frontmatter des 7 fichiers.
|
||||
const archicratIa = defineCollection({
|
||||
type: "content",
|
||||
schema: baseTextSchema.extend({
|
||||
edition: z.union([z.literal("archicrat-ia"), z.literal("archicratie")]),
|
||||
status: z.union([z.literal("essai_these"), z.literal("modele_sociopolitique")])
|
||||
})
|
||||
});
|
||||
|
||||
// Glossaire (référentiel terminologique)
|
||||
const glossaire = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string().min(1), // Titre public (souvent identique au terme)
|
||||
term: z.string().min(1), // Terme canonique
|
||||
title: z.string().min(1),
|
||||
term: z.string().min(1),
|
||||
aliases: z.array(z.string().min(1)).default([]),
|
||||
edition: z.literal("glossaire"),
|
||||
status: z.literal("referentiel"),
|
||||
version: z.string().min(1),
|
||||
// Micro-définition affichable en popover (courte, stable)
|
||||
definitionShort: z.string().min(1),
|
||||
concepts: z.array(z.string().min(1)).default([]),
|
||||
// Liens typés (vers ouvrages ou autres termes)
|
||||
links: z.array(linkSchema).default([])
|
||||
})
|
||||
});
|
||||
@@ -73,5 +83,8 @@ export const collections = {
|
||||
archicratie,
|
||||
ia,
|
||||
glossaire,
|
||||
atlas
|
||||
};
|
||||
atlas,
|
||||
|
||||
// ⚠️ clé avec tiret => doit être quotée
|
||||
"archicrat-ia": archicratIa
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import LevelToggle from "../components/LevelToggle.astro";
|
||||
import BuildStamp from "../components/BuildStamp.astro";
|
||||
import SidePanel from "../components/SidePanel.astro";
|
||||
import "../styles/global.css";
|
||||
import "../styles/panel.css";
|
||||
|
||||
const {
|
||||
title,
|
||||
@@ -261,17 +262,24 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
|
||||
padding: var(--page-gap) 16px 48px;
|
||||
}
|
||||
|
||||
/* ✅ Grille robustifiée + panel plus confortable (base) */
|
||||
.page-shell{
|
||||
--aside-w: 320px;
|
||||
--reading-w: 78ch;
|
||||
--panel-w: 380px;
|
||||
--panel-w: 420px;
|
||||
--gap: 18px;
|
||||
|
||||
max-width: calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2));
|
||||
max-width: min(
|
||||
calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2)),
|
||||
calc(100vw - 32px)
|
||||
);
|
||||
margin: 0 auto;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: var(--aside-w) minmax(0, var(--reading-w)) var(--panel-w);
|
||||
grid-template-columns:
|
||||
var(--aside-w)
|
||||
minmax(0, var(--reading-w))
|
||||
minmax(0, var(--panel-w));
|
||||
column-gap: var(--gap);
|
||||
align-items: start;
|
||||
}
|
||||
@@ -346,23 +354,38 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
|
||||
}
|
||||
:global(body[data-reading-level="1"]) .page-panel{ display: none !important; }
|
||||
|
||||
/* Niveau 3 : lecture plus étroite + panel large ; TOC masqué */
|
||||
/* Niveau 3 : lecture plus étroite + panel LARGE ; TOC masqué */
|
||||
:global(body[data-reading-level="3"]) .page-shell{
|
||||
--reading-w: 62ch;
|
||||
--panel-w: 560px;
|
||||
max-width: calc(var(--reading-w) + var(--panel-w) + (var(--gap) * 1));
|
||||
grid-template-columns: minmax(0, var(--reading-w)) var(--panel-w);
|
||||
--reading-w: 60ch;
|
||||
--panel-w: clamp(720px, 48vw, 940px);
|
||||
|
||||
max-width: min(
|
||||
calc(var(--reading-w) + var(--panel-w) + var(--gap)),
|
||||
calc(100vw - 32px)
|
||||
);
|
||||
|
||||
grid-template-columns:
|
||||
minmax(0, var(--reading-w))
|
||||
minmax(0, var(--panel-w));
|
||||
}
|
||||
:global(body[data-reading-level="3"]) .page-aside{ display: none; }
|
||||
:global(body[data-reading-level="3"]) .reading{ grid-column: 1; }
|
||||
|
||||
/* Niveau 4 : TOC à gauche + panel */
|
||||
/* Niveau 4 : TOC à gauche + panel confortable */
|
||||
:global(body[data-reading-level="4"]) .page-shell{
|
||||
--aside-w: 320px;
|
||||
--reading-w: 64ch;
|
||||
--panel-w: 520px;
|
||||
max-width: calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2));
|
||||
grid-template-columns: var(--aside-w) minmax(0, var(--reading-w)) var(--panel-w);
|
||||
--panel-w: clamp(520px, 36vw, 720px);
|
||||
|
||||
max-width: min(
|
||||
calc(var(--aside-w) + var(--reading-w) + var(--panel-w) + (var(--gap) * 2)),
|
||||
calc(100vw - 32px)
|
||||
);
|
||||
|
||||
grid-template-columns:
|
||||
var(--aside-w)
|
||||
minmax(0, var(--reading-w))
|
||||
minmax(0, var(--panel-w));
|
||||
}
|
||||
:global(body[data-reading-level="4"]) .page-aside{ display: block; }
|
||||
:global(body[data-reading-level="4"]) .reading{ grid-column: 2; }
|
||||
@@ -1158,6 +1181,45 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
|
||||
});
|
||||
});
|
||||
|
||||
/* ✅ NOUVEAU : clic paragraphe => snap sous reading-follow + SidePanel aligné */
|
||||
safe("click-para-align", () => {
|
||||
if (!reading) return;
|
||||
|
||||
reading.addEventListener("click", (ev) => {
|
||||
try {
|
||||
const t = ev.target;
|
||||
|
||||
// ne pas casser les boutons / liens / para-tools
|
||||
if (t && t.closest && t.closest(".para-tools")) return;
|
||||
const a = t && t.closest ? t.closest("a") : null;
|
||||
if (a) return;
|
||||
|
||||
const p = t && t.closest ? t.closest('.reading p[id^="p-"]') : null;
|
||||
if (!p || !p.id) return;
|
||||
|
||||
// si sélection texte en cours => ne pas snap
|
||||
const sel = window.getSelection ? window.getSelection() : null;
|
||||
if (sel && !sel.isCollapsed) return;
|
||||
|
||||
openDetailsIfNeeded(p);
|
||||
|
||||
// snap immédiat sous followbar
|
||||
scrollToElWithOffset(p, 12, "auto");
|
||||
|
||||
// URL stable
|
||||
history.replaceState(null, "", `${location.pathname}#${p.id}`);
|
||||
|
||||
// force la source de vérité (SidePanel suit via event archicratie:currentPara)
|
||||
__hoverParaId = "";
|
||||
__activeParaId = p.id;
|
||||
setCurrentPara(p.id, "click");
|
||||
|
||||
writeLastSeen(p.id);
|
||||
updateResumeButton();
|
||||
} catch {}
|
||||
}, { passive: true });
|
||||
});
|
||||
|
||||
// ===== Hash handling (robuste) =====
|
||||
function resolveParaIdFromEl(el) {
|
||||
if (!el) return "";
|
||||
@@ -1281,8 +1343,6 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
|
||||
|
||||
const h1 = reading.querySelector("h1");
|
||||
|
||||
// ✅ FIX: ne pas dépendre du tag <span>; accepter .details-anchor quel que soit le tag,
|
||||
// et utiliser <details> comme marker (top fiable) pour que rf-h2 s'affiche correctement.
|
||||
const h2Anchors = Array.from(reading.querySelectorAll(".details-anchor[id]"))
|
||||
.map((s) => {
|
||||
const d = (s.nextElementSibling && s.nextElementSibling.tagName === "DETAILS")
|
||||
@@ -1572,4 +1632,4 @@ const WHOAMI_FORCE_LOCALHOST = (import.meta.env.PUBLIC_WHOAMI_FORCE_LOCALHOST ??
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -103,10 +103,16 @@ export const GET: APIRoute = async () => {
|
||||
const errors: Array<{ file: string; error: string }> = [];
|
||||
|
||||
let files: string[] = [];
|
||||
let missingRoot = false;
|
||||
|
||||
try {
|
||||
files = await walk(ANNO_ROOT);
|
||||
} catch (e: any) {
|
||||
throw new Error(`Missing annotations root: ${ANNO_ROOT} (${e?.message || e})`);
|
||||
// ✅ FAIL-OPEN : pas d’annotations => index vide (ne casse pas la build)
|
||||
missingRoot = true;
|
||||
console.warn(`[annotations-index] Missing annotations root: ${ANNO_ROOT} (${e?.message || e})`);
|
||||
files = [];
|
||||
errors.push({ file: "src/annotations", error: `Missing annotations root: ${e?.message || e}` });
|
||||
}
|
||||
|
||||
for (const fp of files) {
|
||||
@@ -189,7 +195,9 @@ export const GET: APIRoute = async () => {
|
||||
errors,
|
||||
};
|
||||
|
||||
if (errors.length) {
|
||||
// ✅ FAIL-OPEN uniquement si le dossier manque.
|
||||
// Si le dossier existe mais qu’un YAML est cassé -> fail-closed.
|
||||
if (errors.length && !missingRoot) {
|
||||
throw new Error(`${errors[0].file}: ${errors[0].error}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ 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/"));
|
||||
// ✅ Après migration : plus de filtre par prefix, on prend toute la collection
|
||||
const entries = await getCollection("archicrat-ia");
|
||||
|
||||
return entries.map((entry) => ({
|
||||
// ✅ inline : jamais de helper externe (évite "stripPrefix is not defined")
|
||||
params: { slug: entry.slug.replace(/^archicrat-ia\//, "") },
|
||||
params: { slug: entry.slug },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
@@ -35,4 +34,4 @@ const { Content, headings } = await entry.render();
|
||||
|
||||
<h1>{entry.data.title}</h1>
|
||||
<Content />
|
||||
</EditionLayout>
|
||||
</EditionLayout>
|
||||
@@ -2,13 +2,12 @@
|
||||
import SiteLayout from "../../layouts/SiteLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const entries = (await getCollection("archicratie"))
|
||||
.filter((e) => e.slug.startsWith("archicrat-ia/"));
|
||||
// ✅ Après migration physique : collection = "archicrat-ia", slug = "chapitre-3" (sans prefix)
|
||||
const entries = await getCollection("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)}/`;
|
||||
const href = (slug) => `/archicrat-ia/${slug}/`;
|
||||
---
|
||||
|
||||
<SiteLayout title="Essai-thèse — ArchiCraT-IA">
|
||||
@@ -19,4 +18,4 @@ const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
|
||||
<li><a href={href(e.slug)}>{e.data.title}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</SiteLayout>
|
||||
</SiteLayout>
|
||||
@@ -27,17 +27,40 @@
|
||||
--rf-h3-size: 1.25rem;
|
||||
--rf-h3-lh: 1.25;
|
||||
--rf-h3-fw: 650;
|
||||
|
||||
/* ===== Polish (non destructif) ===== */
|
||||
--ease-out: cubic-bezier(.2,.9,.2,1);
|
||||
--ease-soft: cubic-bezier(.2,.8,.2,1);
|
||||
|
||||
--focus-ring: 2px solid rgba(140,140,255,0.60);
|
||||
--focus-ring-offset: 2px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
|
||||
/* polish */
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* ===== Focus (accessibilité + feel premium) ===== */
|
||||
:focus { outline: none; }
|
||||
:focus-visible{
|
||||
outline: var(--focus-ring);
|
||||
outline-offset: var(--focus-ring-offset);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
*{ transition: none !important; animation: none !important; }
|
||||
}
|
||||
|
||||
/* Header sticky */
|
||||
header{
|
||||
position: sticky;
|
||||
@@ -61,15 +84,26 @@ main { padding: 0; }
|
||||
.reading {
|
||||
max-width: 78ch;
|
||||
margin: 0 auto;
|
||||
|
||||
/* polish */
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.005em;
|
||||
}
|
||||
|
||||
.reading h1 {
|
||||
line-height: 1.2;
|
||||
margin: 0 0 10px;
|
||||
|
||||
/* polish */
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.reading p { margin: 0 0 12px; }
|
||||
|
||||
/* petits ajustements de respiration */
|
||||
.reading h2 { margin-top: 18px; }
|
||||
.reading h3 { margin-top: 14px; }
|
||||
|
||||
.edition-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -99,6 +133,11 @@ main { padding: 0; }
|
||||
border: 1px solid rgba(127,127,127,0.55);
|
||||
border-radius: 999px;
|
||||
padding: 5px 12px;
|
||||
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
|
||||
}
|
||||
.resume-btn:hover{
|
||||
background: rgba(127,127,127,0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Jump by paragraph id */
|
||||
@@ -114,6 +153,11 @@ main { padding: 0; }
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
width: 320px;
|
||||
transition: border-color 120ms var(--ease-out), box-shadow 120ms var(--ease-out);
|
||||
}
|
||||
.jump-input:focus-visible{
|
||||
border-color: rgba(140,140,255,0.65);
|
||||
box-shadow: 0 0 0 3px rgba(140,140,255,0.18);
|
||||
}
|
||||
.jump-input.is-error{
|
||||
outline: 2px solid rgba(127,127,127,0.55);
|
||||
@@ -126,6 +170,11 @@ main { padding: 0; }
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
|
||||
}
|
||||
.jump-btn:hover{
|
||||
background: rgba(127,127,127,0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Toggle niveaux (legacy, non bloquant) */
|
||||
@@ -137,6 +186,11 @@ main { padding: 0; }
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out), border-color 120ms var(--ease-out);
|
||||
}
|
||||
.lvl-btn:hover{
|
||||
background: rgba(127,127,127,0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.lvl-btn[aria-pressed="true"] {
|
||||
border-color: rgba(127,127,127,0.9);
|
||||
@@ -211,6 +265,13 @@ body[data-reading-level="4"] .level-3 { display: none; }
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
|
||||
}
|
||||
.para-anchor:hover,
|
||||
.para-propose:hover,
|
||||
.para-cite:hover{
|
||||
background: rgba(127,127,127,0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.para-cite{ cursor: pointer; }
|
||||
|
||||
@@ -221,8 +282,13 @@ body[data-reading-level="4"] .level-3 { display: none; }
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 120ms var(--ease-out), background 120ms var(--ease-out);
|
||||
}
|
||||
.para-bookmark:hover{
|
||||
background: rgba(127,127,127,0.10);
|
||||
transform: translateY(-1px);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.para-bookmark:hover{ text-decoration: underline; }
|
||||
|
||||
/* Highlight (jump / resume / arrivée hash) */
|
||||
.para-highlight{
|
||||
@@ -420,3 +486,57 @@ html{ scroll-behavior: smooth; }
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ==========================
|
||||
Landscape boost (mobile/tablet)
|
||||
CSS-only, non destructif
|
||||
- iOS + Android
|
||||
- élargit la lecture en paysage sur petits écrans
|
||||
- ne touche pas au desktop
|
||||
========================== */
|
||||
|
||||
/* 1) Quand on est en paysage sur “petits” devices,
|
||||
on évite une colonne unique trop “étroite” et on retire le max-width 78ch. */
|
||||
@media (orientation: landscape) and (max-width: 1024px){
|
||||
/* La grille (EditionLayout) utilise .page-shell ; on la rend plus “fluide” */
|
||||
.page-shell{
|
||||
max-width: calc(100vw - 16px) !important; /* respire mais exploite la largeur */
|
||||
}
|
||||
|
||||
/* La lecture ne doit pas rester “bridée” à 78ch en paysage */
|
||||
.reading{
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2) Cas extrême : petits téléphones en paysage (hauteur faible).
|
||||
On réduit légèrement les paddings pour gagner de la place utile. */
|
||||
@media (orientation: landscape) and (max-width: 820px) and (max-height: 520px){
|
||||
header{
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* allège un peu la barre d’édition (sans la casser) */
|
||||
.edition-bar{
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
/* champ jump un peu moins large, pour éviter de “manger” la nav */
|
||||
.jump-input{
|
||||
width: min(260px, 48vw);
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) iOS Safari : le viewport peut être “bizarre” en paysage (barres).
|
||||
Cette règle aide à éviter des layouts coincés quand l’UI Safari bouge. */
|
||||
@supports (height: 100dvh){
|
||||
@media (orientation: landscape) and (max-width: 1024px){
|
||||
.page-shell{
|
||||
min-height: 100dvh;
|
||||
}
|
||||
}
|
||||
}
|
||||
568
src/styles/panel.css
Normal file
568
src/styles/panel.css
Normal file
@@ -0,0 +1,568 @@
|
||||
/* src/styles/panel.css — v3.1 premium FIX
|
||||
Objectifs :
|
||||
- Lightbox AU-DESSUS du header (pas masquée)
|
||||
- bouton close toujours visible
|
||||
- le haut du média jamais coupé
|
||||
- CSS-only, aucune logique JS modifiée
|
||||
*/
|
||||
|
||||
/* ===== Tokens ===== */
|
||||
:root{
|
||||
--panel-radius: 18px;
|
||||
--panel-radius-sm: 14px;
|
||||
|
||||
--panel-border: rgba(127,127,127,0.16);
|
||||
--panel-border-strong: rgba(127,127,127,0.26);
|
||||
|
||||
--panel-glass: rgba(255,255,255,0.055);
|
||||
--panel-glass-2: rgba(255,255,255,0.075);
|
||||
|
||||
--panel-shadow: 0 14px 34px rgba(0,0,0,0.07);
|
||||
--panel-shadow-2: 0 22px 60px rgba(0,0,0,0.10);
|
||||
|
||||
--panel-title: rgba(20,20,20,0.90);
|
||||
--panel-text: rgba(20,20,20,0.84);
|
||||
--panel-muted: rgba(20,20,20,0.62);
|
||||
|
||||
--chip-bg: rgba(127,127,127,0.10);
|
||||
--chip-bd: rgba(127,127,127,0.22);
|
||||
|
||||
--btn-bg: rgba(127,127,127,0.10);
|
||||
--btn-bd: rgba(127,127,127,0.30);
|
||||
|
||||
/* thumbs */
|
||||
--thumb: 84px;
|
||||
|
||||
/* Motion */
|
||||
--ease: cubic-bezier(.2,.85,.2,1);
|
||||
--ease2: cubic-bezier(.18,.9,.18,1);
|
||||
|
||||
/* Lightbox sizing */
|
||||
--lb-maxw: 1480px;
|
||||
|
||||
/* Inset (viewport safe) : header + notch + marges */
|
||||
--lb-inset-x: 14px;
|
||||
--lb-inset-top: max(calc(env(safe-area-inset-top, 0px) + 12px), 12px);
|
||||
--lb-inset-bot: max(calc(env(safe-area-inset-bottom, 0px) + 12px), 12px);
|
||||
|
||||
/* Space reserved for close button strip inside dialog */
|
||||
--lb-topbar-h: 56px;
|
||||
|
||||
/* padding viewer */
|
||||
--lb-pad: 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 900px){
|
||||
:root{ --lb-inset-x: 24px; --lb-pad: 18px; }
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
:root{
|
||||
--panel-border: rgba(255,255,255,0.12);
|
||||
--panel-border-strong: rgba(255,255,255,0.18);
|
||||
|
||||
--panel-glass: rgba(255,255,255,0.045);
|
||||
--panel-glass-2: rgba(255,255,255,0.070);
|
||||
|
||||
--panel-shadow: 0 18px 48px rgba(0,0,0,0.26);
|
||||
--panel-shadow-2: 0 30px 80px rgba(0,0,0,0.38);
|
||||
|
||||
--panel-title: rgba(255,255,255,0.90);
|
||||
--panel-text: rgba(255,255,255,0.86);
|
||||
--panel-muted: rgba(255,255,255,0.62);
|
||||
|
||||
--chip-bg: rgba(255,255,255,0.08);
|
||||
--chip-bd: rgba(255,255,255,0.14);
|
||||
|
||||
--btn-bg: rgba(255,255,255,0.07);
|
||||
--btn-bd: rgba(255,255,255,0.16);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Panel container ===== */
|
||||
body[data-reading-level="1"] .page-panel{ display: none !important; }
|
||||
|
||||
/* IMPORTANT :
|
||||
Pour que la Lightbox (enfant du panel) puisse passer AU-DESSUS du header,
|
||||
on met le panel dans une couche au-dessus du header (z=50).
|
||||
Comme le panel n’empiète pas sur le header (top calculé), c’est safe.
|
||||
*/
|
||||
.page-panel{
|
||||
position: sticky;
|
||||
top: calc(var(--sticky-header-h) + var(--page-gap));
|
||||
align-self: start;
|
||||
color: var(--panel-text);
|
||||
|
||||
z-index: 80; /* ✅ au-dessus du header (50) => Lightbox non masquée */
|
||||
}
|
||||
|
||||
.page-panel__inner{
|
||||
max-height: calc(100vh - (var(--sticky-header-h) + var(--page-gap) + 12px));
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: var(--panel-radius);
|
||||
|
||||
padding: 14px;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 20% 0%, rgba(140,140,255,0.10), transparent 55%),
|
||||
radial-gradient(900px 520px at 80% 20%, rgba(127,255,210,0.06), transparent 55%),
|
||||
linear-gradient(180deg, var(--panel-glass), transparent 68%);
|
||||
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
|
||||
box-shadow: var(--panel-shadow);
|
||||
|
||||
transition: border-color 140ms var(--ease), box-shadow 140ms var(--ease), transform 140ms var(--ease);
|
||||
}
|
||||
|
||||
.page-panel.is-updating .page-panel__inner{
|
||||
border-color: var(--panel-border-strong);
|
||||
box-shadow: var(--panel-shadow-2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ===== Head ===== */
|
||||
.panel-head{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
border-bottom: 1px solid rgba(127,127,127,0.18);
|
||||
}
|
||||
|
||||
.panel-head__left{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-head__label{ font-weight: 900; opacity: .88; }
|
||||
|
||||
.panel-head__id{
|
||||
font-weight: 850;
|
||||
letter-spacing: .01em;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 16rem;
|
||||
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--chip-bd);
|
||||
background: var(--chip-bg);
|
||||
}
|
||||
|
||||
/* ===== Messages ===== */
|
||||
.panel-msg{ margin-top: 8px; font-size: 12px; opacity: .92; }
|
||||
.panel-msg--head{ margin-top: 0; margin-bottom: 10px; }
|
||||
|
||||
/* ===== Toolbar actions : compacte + sticky ===== */
|
||||
.panel-top-actions{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 6;
|
||||
|
||||
margin: 10px 0 10px;
|
||||
padding: 10px;
|
||||
|
||||
border: 1px solid rgba(127,127,127,0.14);
|
||||
border-radius: var(--panel-radius-sm);
|
||||
|
||||
background: rgba(255,255,255,0.55);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-top-actions{
|
||||
background: rgba(0,0,0,0.28);
|
||||
box-shadow: 0 12px 28px rgba(0,0,0,0.22);
|
||||
}
|
||||
}
|
||||
|
||||
.panel-top-actions::after{
|
||||
content: "";
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
height: 1px;
|
||||
background: rgba(127,127,127,0.16);
|
||||
}
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
.panel-actions{
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panel-btn{
|
||||
border: 1px solid var(--btn-bd);
|
||||
background: var(--btn-bg);
|
||||
border-radius: 999px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
|
||||
transition: transform 120ms var(--ease2), background 120ms var(--ease2), border-color 120ms var(--ease2);
|
||||
}
|
||||
|
||||
.panel-btn:hover{
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--panel-border-strong);
|
||||
background: rgba(127,127,127,0.14);
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-btn:hover{ background: rgba(255,255,255,0.10); }
|
||||
}
|
||||
|
||||
.panel-btn:disabled{ opacity: .55; cursor: default; transform: none; }
|
||||
.panel-btn--primary{ font-weight: 900; border-color: var(--panel-border-strong); }
|
||||
|
||||
/* ===== Sections ===== */
|
||||
.panel-block{
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(127,127,127,0.16);
|
||||
}
|
||||
.panel-block:first-of-type{ margin-top: 10px; border-top: 0; padding-top: 0; }
|
||||
|
||||
.panel-title{
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 950;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--panel-title);
|
||||
opacity: .96;
|
||||
}
|
||||
|
||||
.panel-subtitle{ font-size: 12px; font-weight: 900; margin: 12px 0 6px; opacity: .90; }
|
||||
.panel-body p{ margin: 8px 0; opacity: .92; }
|
||||
|
||||
.panel-list{ margin: 0; padding-left: 18px; }
|
||||
.panel-list li{ margin: 7px 0; }
|
||||
|
||||
.panel-chip{
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--chip-bd);
|
||||
background: var(--chip-bg);
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.panel-quote{
|
||||
margin: 8px 0;
|
||||
padding: 10px 10px;
|
||||
border-left: 3px solid rgba(140,140,255,0.35);
|
||||
background: rgba(127,127,127,0.06);
|
||||
border-radius: 12px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-quote{ background: rgba(255,255,255,0.05); }
|
||||
}
|
||||
.panel-quote__src{ margin-top: 6px; font-size: 12px; opacity: .72; }
|
||||
|
||||
/* ===== Media grid ===== */
|
||||
.panel-media-grid{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--thumb), 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel-media-tile{
|
||||
width: 100%;
|
||||
border: 1px solid rgba(127,127,127,0.16);
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
background: rgba(127,127,127,0.04);
|
||||
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
transition: transform 120ms var(--ease2), background 120ms var(--ease2), border-color 120ms var(--ease2);
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-media-tile{ background: rgba(255,255,255,0.035); }
|
||||
}
|
||||
|
||||
.panel-media-tile:hover{
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--panel-border-strong);
|
||||
background: rgba(127,127,127,0.08);
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-media-tile:hover{ background: rgba(255,255,255,0.055); }
|
||||
}
|
||||
|
||||
.panel-media-tile img{
|
||||
width: 100%;
|
||||
height: var(--thumb);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.panel-media-ph{
|
||||
width: 100%;
|
||||
height: var(--thumb);
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--chip-bg);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 950;
|
||||
font-size: 12px;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.panel-media-cap{
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
opacity: .92;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.panel-media-credit{
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
opacity: .74;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Featured (1 média) */
|
||||
.panel-media-grid > .panel-media-tile:only-child{
|
||||
grid-column: 1 / -1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.panel-media-grid > .panel-media-tile:only-child img,
|
||||
.panel-media-grid > .panel-media-tile:only-child .panel-media-ph{
|
||||
height: min(52vh, 460px);
|
||||
object-fit: contain;
|
||||
background: rgba(0,0,0,0.08);
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
.panel-media-grid > .panel-media-tile:only-child img,
|
||||
.panel-media-grid > .panel-media-tile:only-child .panel-media-ph{
|
||||
background: rgba(0,0,0,0.22);
|
||||
}
|
||||
}
|
||||
.panel-media-grid > .panel-media-tile:only-child .panel-media-cap{
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* ===== Compose ===== */
|
||||
.panel-compose{
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed rgba(127,127,127,0.22);
|
||||
}
|
||||
.panel-label{
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
opacity: .90;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.panel-textarea{
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(127,127,127,0.24);
|
||||
border-radius: 12px;
|
||||
padding: 9px 10px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
LIGHTBOX FIX (CRITIQUE)
|
||||
- overlay couvre le header
|
||||
- close toujours visible
|
||||
- le haut du média n’est jamais “mangé”
|
||||
- overrides forts (important) car styles inline du composant existent
|
||||
======================================================================= */
|
||||
|
||||
/* Quand hidden => pas d’interaction */
|
||||
.panel-lightbox[hidden]{ display: none !important; }
|
||||
|
||||
/* Conteneur full-viewport AU DESSUS de tout */
|
||||
.panel-lightbox{
|
||||
position: fixed !important;
|
||||
inset: 0 !important;
|
||||
z-index: 2147483647 !important; /* max practical */
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Overlay : couvre TOUT y compris header */
|
||||
.panel-lightbox__overlay{
|
||||
position: absolute !important;
|
||||
inset: 0 !important;
|
||||
background: rgba(0,0,0,0.86) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
-webkit-backdrop-filter: blur(10px) !important;
|
||||
}
|
||||
|
||||
/* Dialog : on bannit transform/centering qui peut “couper” le haut.
|
||||
On utilise un inset fixe => le haut est toujours visible. */
|
||||
.panel-lightbox__dialog{
|
||||
position: fixed !important;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
|
||||
inset: var(--lb-inset-top) var(--lb-inset-x) var(--lb-inset-bot) var(--lb-inset-x) !important;
|
||||
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
|
||||
border-radius: 18px !important;
|
||||
border: 1px solid rgba(255,255,255,0.14) !important;
|
||||
|
||||
background: rgba(18,18,18,0.58) !important;
|
||||
backdrop-filter: blur(14px) !important;
|
||||
-webkit-backdrop-filter: blur(14px) !important;
|
||||
|
||||
box-shadow: 0 28px 90px rgba(0,0,0,0.60) !important;
|
||||
overflow: hidden !important;
|
||||
|
||||
/* layout interne */
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
|
||||
/* petite anim (discrète) */
|
||||
animation: archiLbIn 160ms var(--ease) !important;
|
||||
}
|
||||
|
||||
@keyframes archiLbIn{
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Close : FIXED dans le viewport => ne peut plus être masqué par le header */
|
||||
.panel-lightbox__close{
|
||||
position: fixed !important;
|
||||
top: calc(var(--lb-inset-top) + 8px) !important;
|
||||
right: calc(var(--lb-inset-x) + 8px) !important;
|
||||
z-index: 2147483647 !important;
|
||||
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
|
||||
width: 46px !important;
|
||||
height: 42px !important;
|
||||
|
||||
border-radius: 14px !important;
|
||||
border: 1px solid rgba(255,255,255,0.30) !important;
|
||||
background: rgba(0,0,0,0.50) !important;
|
||||
color: rgba(255,255,255,0.92) !important;
|
||||
|
||||
cursor: pointer !important;
|
||||
font-size: 22px !important;
|
||||
font-weight: 950 !important;
|
||||
|
||||
transition: transform 120ms var(--ease2), background 120ms var(--ease2) !important;
|
||||
}
|
||||
.panel-lightbox__close:hover{
|
||||
transform: translateY(-1px) !important;
|
||||
background: rgba(255,255,255,0.14) !important;
|
||||
}
|
||||
|
||||
/* Contenu : prend l’espace, jamais sous le close (close est fixed viewport) */
|
||||
.panel-lightbox__content{
|
||||
flex: 1 1 auto !important;
|
||||
min-height: 0 !important;
|
||||
|
||||
padding: var(--lb-pad) !important;
|
||||
padding-top: 12px !important;
|
||||
|
||||
display: grid !important;
|
||||
place-items: center !important;
|
||||
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* Média : contain, et jamais coupé en haut (dialog inset fixe + scroll possible) */
|
||||
.panel-lightbox__content img,
|
||||
.panel-lightbox__content video{
|
||||
display: block !important;
|
||||
max-width: min(var(--lb-maxw), calc(100vw - (var(--lb-inset-x) * 2) - (var(--lb-pad) * 2))) !important;
|
||||
max-height: calc(100vh - var(--lb-inset-top) - var(--lb-inset-bot) - 64px) !important;
|
||||
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
|
||||
object-fit: contain !important;
|
||||
|
||||
background: rgba(0,0,0,0.22) !important;
|
||||
border-radius: 14px !important;
|
||||
}
|
||||
|
||||
/* audio */
|
||||
.panel-lightbox__content audio{
|
||||
width: min(860px, calc(100vw - (var(--lb-inset-x) * 2) - 28px)) !important;
|
||||
}
|
||||
|
||||
/* Caption : en bas, sans masquer le média (on reste simple) */
|
||||
.panel-lightbox__caption{
|
||||
position: relative !important;
|
||||
z-index: 2 !important;
|
||||
|
||||
padding: 10px 14px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 950 !important;
|
||||
color: rgba(255,255,255,0.92) !important;
|
||||
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.72), rgba(0,0,0,0.00)) !important;
|
||||
}
|
||||
|
||||
/* Bonus : si le navigateur supporte :has(), on fige le scroll arrière-plan (premium) */
|
||||
@supports selector(html:has(*)){
|
||||
html:has(.panel-lightbox:not([hidden])){
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Responsive : comme avant, panel caché sous 1100px ===== */
|
||||
@media (max-width: 1100px){
|
||||
.page-panel{ display: none; }
|
||||
}
|
||||
|
||||
/* ===== Reduce motion ===== */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
.panel-lightbox__dialog{ animation: none !important; }
|
||||
.panel-btn,
|
||||
.panel-media-tile,
|
||||
.page-panel__inner{ transition: none !important; }
|
||||
}
|
||||
Reference in New Issue
Block a user