Proposer: robust gitea fallback + safe gate + debug #76

Closed
Archicratia wants to merge 1 commits from fix/proposer-robust into main

View File

@@ -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 <details> 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 napparaît que sil 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);