From e722224315d476ef4dd587fad3f0d3af75134ae7 Mon Sep 17 00:00:00 2001 From: Archicratia Date: Thu, 12 Feb 2026 16:48:10 +0100 Subject: [PATCH] Proposer: robust gitea fallback + safe gate + debug --- src/layouts/EditionLayout.astro | 171 ++++++++++++++++++++++---------- 1 file changed, 117 insertions(+), 54 deletions(-) diff --git a/src/layouts/EditionLayout.astro b/src/layouts/EditionLayout.astro index 773ba3b..8605cec 100644 --- a/src/layouts/EditionLayout.astro +++ b/src/layouts/EditionLayout.astro @@ -373,11 +373,37 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; updateResumeButton(); // ========================== - // Gitea propose (optional) + // Gitea propose (optional, antifragile) // ========================== - const giteaReady = Boolean(GITEA_BASE && GITEA_OWNER && GITEA_REPO); - const FULL_TEXT_SOFT_LIMIT = 1600; - const URL_HARD_LIMIT = 6500; + function debugOn() { + try { + const u = new URL(window.location.href); + return u.searchParams.get("debug") === "1" || localStorage.getItem("archicratie:debug") === "1"; + } catch { return false; } + } + const DEBUG = debugOn(); + + // Fallbacks (si build args non injectés, on évite "giteaReady=false -> plus de Proposer") + const FALLBACK = (() => { + const host = (window.location.hostname || "").toLowerCase(); + // prod/staging : on déduit gitea du domaine + if (host.endsWith("archicratie.trans-hands.synology.me")) { + return { + base: "https://gitea.archicratie.trans-hands.synology.me", + owner: "Archicratia", + repo: "archicratie-edition", + }; + } + return { base: "", owner: "", repo: "" }; + })(); + + const GITEA = { + base: String(GITEA_BASE || FALLBACK.base || "").trim(), + owner: String(GITEA_OWNER || FALLBACK.owner || "").trim(), + repo: String(GITEA_REPO || FALLBACK.repo || "").trim(), + }; + + const giteaReady = Boolean(GITEA.base && GITEA.owner && GITEA.repo); // ✅ Gate par groupe (Traefik→Authelia→LLDAP via /_auth/whoami) const WHOAMI_PATH = "/_auth/whoami"; @@ -391,16 +417,26 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; return (m?.[1] ?? "").trim(); } + async function fetchWithTimeout(url, ms = 4500) { + const ac = new AbortController(); + const t = setTimeout(() => ac.abort(), ms); + try { + return await fetch(url, { + credentials: "include", + cache: "no-store", + redirect: "follow", + signal: ac.signal, + }); + } finally { + clearTimeout(t); + } + } + async function getAuthInfo() { if (_authInfoPromise) return _authInfoPromise; _authInfoPromise = (async () => { - const res = await fetch(`${WHOAMI_PATH}?_=${Date.now()}`, { - credentials: "include", - cache: "no-store", - redirect: "follow", - }); - + const res = await fetchWithTimeout(`${WHOAMI_PATH}?_=${Date.now()}`, 4500); const text = await res.text().catch(() => ""); const groups = parseWhoamiLine(text, "Remote-Groups") @@ -408,7 +444,7 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; .map((s) => s.trim()) .filter(Boolean); - return { + const info = { ok: Boolean(res && res.ok), user: parseWhoamiLine(text, "Remote-User"), name: parseWhoamiLine(text, "Remote-Name"), @@ -416,22 +452,44 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; groups, raw: text, }; - })().catch(() => ({ - ok: false, - user: "", - name: "", - email: "", - groups: [], - raw: "", - })); + + // Debug safe + try { + window.__archicratie = window.__archicratie || {}; + window.__archicratie.auth = { ok: info.ok, user: info.user, groups: info.groups }; + } catch {} + + return info; + })().catch((err) => { + if (DEBUG) console.warn("[proposer] whoami failed", err); + try { + window.__archicratie = window.__archicratie || {}; + window.__archicratie.auth = { ok: false, user: "", groups: [] }; + } catch {} + return { ok: false, user: "", name: "", email: "", groups: [], raw: "" }; + }); return _authInfoPromise; } - // Promise unique : est-on editor ? + // true/false/null (null = indéterminé => on laisse caché) const isEditorP = giteaReady - ? getAuthInfo().then((info) => info.groups.includes(PROPOSE_REQUIRED_GROUP)).catch(() => false) - : Promise.resolve(false); + ? getAuthInfo().then((info) => { + if (!info || !info.ok) return null; + return info.groups.includes(PROPOSE_REQUIRED_GROUP); + }).catch(() => null) + : Promise.resolve(null); + + if (DEBUG) { + try { + window.__archicratie = window.__archicratie || {}; + window.__archicratie.gitea = { ...GITEA, ready: giteaReady }; + } catch {} + console.log("[proposer] giteaReady=", giteaReady, GITEA); + } + + const FULL_TEXT_SOFT_LIMIT = 1600; + const URL_HARD_LIMIT = 6500; const quoteBlock = (s) => String(s || "") @@ -440,8 +498,8 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; .join("\n"); function buildIssueURL(anchorId, fullText, excerpt) { - const base = String(GITEA_BASE).replace(/\/+$/, ""); - const issue = new URL(`${base}/${GITEA_OWNER}/${GITEA_REPO}/issues/new`); + const base = String(GITEA.base).replace(/\/+$/, ""); + const issue = new URL(`${base}/${GITEA.owner}/${GITEA.repo}/issues/new`); const local = new URL(window.location.href); local.search = ""; @@ -501,6 +559,23 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; // ========================== const paras = Array.from(document.querySelectorAll('.reading p[id^="p-"]')); + function hidePropose(el) { + try { + el.hidden = true; + el.style.display = "none"; + el.setAttribute("aria-hidden", "true"); + el.setAttribute("tabindex", "-1"); + } catch {} + } + function showPropose(el) { + try { + el.hidden = false; + el.style.display = ""; + el.removeAttribute("aria-hidden"); + el.removeAttribute("tabindex"); + } catch {} + } + for (const p of paras) { if (p.querySelector(".para-tools")) continue; @@ -547,24 +622,28 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; propose.textContent = "Proposer"; propose.setAttribute("aria-label", "Proposer une correction sur Gitea"); - // ✅ fail-closed DUR : inline-style (ne peut pas être overridé par ton CSS) - propose.style.display = "none"; propose.dataset.requiresGroup = PROPOSE_REQUIRED_GROUP; + hidePropose(propose); const raw = (p.textContent || "").trim().replace(/\s+/g, " "); const excerpt = raw.length > 420 ? (raw.slice(0, 420) + "…") : raw; const issueUrl = buildIssueURL(p.id, raw, excerpt); - // Lien fallback (si JS casse totalement) + // Lien fallback (si la modale casse) propose.href = issueUrl; - // ✅ Marqueurs pour ProposeModal (interception 2 étapes) + // Marqueurs pour ProposeModal (interception 2 étapes) propose.dataset.propose = "1"; propose.dataset.url = issueUrl; propose.dataset.full = raw; row.appendChild(propose); + } else if (DEBUG) { + // debug : on marque pourquoi il n'y a pas de proposer + // (aucun impact prod) + // eslint-disable-next-line no-console + console.warn("[proposer] disabled: giteaReady=false", GITEA_BASE, GITEA_OWNER, GITEA_REPO); } tools.appendChild(row); @@ -599,17 +678,20 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; p.appendChild(tools); } - // ✅ Après insertion : on autorise Proposer seulement si groupe editors + // ✅ Gate : on montre uniquement si ok===true. Si ok===false on supprime. Si null => on laisse caché (non destructif). if (giteaReady) { isEditorP.then((ok) => { + if (DEBUG) console.log("[proposer] isEditor=", ok); const els = document.querySelectorAll(".para-propose"); + for (const el of els) { - if (ok) el.style.display = ""; - else el.remove(); + if (ok === true) showPropose(el); + else if (ok === false) el.remove(); + else hidePropose(el); // indéterminé => on ne casse rien } - }).catch(() => { - // fail-closed - document.querySelectorAll(".para-propose").forEach((el) => el.remove()); + }).catch((err) => { + if (DEBUG) console.warn("[proposer] gate failed", err); + document.querySelectorAll(".para-propose").forEach((el) => hidePropose(el)); }); } @@ -654,7 +736,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; // ========================== const h1 = reading ? reading.querySelector("h1") : null; - // H2 : soit via details-anchor (pour citabilité stable), soit via h2[id] const h2Anchors = Array.from(document.querySelectorAll("span.details-anchor[id]")) .map((s) => { const d = s.closest("details"); @@ -673,7 +754,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; h2 })); - // On préfère le mode "anchors" s'il existe const H2 = (h2Anchors.length ? h2Anchors : h2Plain) .slice() .sort((a,b) => absTop(a.marker) - absTop(b.marker)); @@ -686,7 +766,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; let lastY = window.scrollY || 0; function computeLineY(followH) { - // “ligne” = bas du bandeau (header + page-gap + bandeau) return headerH() + PAGE_GAP + (followH || 0) + HYST; } @@ -712,24 +791,21 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; return Boolean(start.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING); } function isBefore(end, el) { - if (!end || !el) return true; // pas de borne haute => OK + if (!end || !el) return true; return Boolean(end.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING); } function pickH3(lineY, activeH2) { if (!activeH2) return null; - // H3 visibles (pas dans un details fermé) const visible = H3.filter((t) => { const d = t.el.closest?.("details"); return !(d && !d.open); }); - // Scope : même
si applicable (plus robuste) const d2 = activeH2.h2?.closest?.("details") || activeH2.anchor?.closest?.("details") || null; let scoped = d2 ? visible.filter((t) => t.el.closest?.("details") === d2) : visible; - // Scope DOM : entre H2 courant et H2 suivant (gère aussi les pages "plates") const i2 = H2.findIndex((t) => t.id === activeH2.id); const nextH2 = (i2 >= 0 && i2 + 1 < H2.length) ? H2[i2 + 1] : null; @@ -738,7 +814,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; scoped = scoped.filter((t) => isAfter(startNode, t.el) && isBefore(endNode, t.el)); - // Choisir le dernier H3 du scope qui a passé la ligne let cand = null; for (const t of scoped) { const top = t.el.getBoundingClientRect().top; @@ -751,7 +826,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; function updateFollow() { if (!followEl) return; - // PASS 1 : H1 visible quand le H1 du reading passe sous la barre const baseY = headerH() + PAGE_GAP; const showH1 = !!(h1 && (h1.getBoundingClientRect().bottom <= baseY + HYST)); @@ -762,7 +836,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; rfH1.hidden = true; } - // PASS 2 : estimation H2/H3 sans bandeau (pour mesurer hauteur réelle) const lineY0 = computeLineY(0); curH2 = pickH2(lineY0); curH3 = pickH3(lineY0, curH2); @@ -780,19 +853,16 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; const inner = followEl.querySelector(".reading-follow__inner"); const followH = (any0 && inner) ? inner.getBoundingClientRect().height : 0; - // PASS 3 : recalc H2/H3 avec la vraie ligne (bas du bandeau) const lineY = computeLineY(followH); curH2 = pickH2(lineY); curH3 = pickH3(lineY, curH2); - // H2 n’apparaît que s’il est “actif” (titre passé sous la ligne) if (rfH2 && curH2) { const on = curH2.marker.getBoundingClientRect().top <= lineY; rfH2.hidden = !on; if (on) rfH2.textContent = curH2.title; } else if (rfH2) rfH2.hidden = true; - // H3 : actif seulement si un H3 du H2 actif a passé la ligne if (rfH3 && curH3) { const on = curH3.el.getBoundingClientRect().top <= lineY; rfH3.hidden = !on; @@ -809,17 +879,13 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; setRootVar("--followbar-h", px(followH2)); setRootVar("--sticky-offset-px", String(Math.round(headerH() + PAGE_GAP + followH2))); - // Boutons actions if (btnTopChapter) btnTopChapter.hidden = !any; - if (btnTopSection) { const ok = Boolean(curH2 && rfH2 && !rfH2.hidden); btnTopSection.hidden = !ok; } lastY = window.scrollY || 0; - - // recalage horizontal syncReadingRect(); } @@ -836,7 +902,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); - // clics bandeau if (rfH1) rfH1.addEventListener("click", () => { if (!h1) return; scrollToElWithOffset(h1, 12, "smooth"); @@ -856,7 +921,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; history.replaceState(null, "", `${window.location.pathname}#${curH3.id}`); }); - // clics actions if (btnTopChapter) btnTopChapter.addEventListener("click", () => { if (!h1) return; scrollToElWithOffset(h1, 12, "smooth"); @@ -871,7 +935,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? ""; updateFollow(); - // arrivée avec hash => ouvrir + offset const initialHash = (location.hash || "").slice(1); if (initialHash) { const el = document.getElementById(initialHash); -- 2.49.1