Proposer: robust gitea fallback + safe gate + debug #76
@@ -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 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);
|
||||
|
||||
Reference in New Issue
Block a user