From 84623b7c9a825d7560b4ee7fec6dc3ac734c10fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 15:54:46 +0200 Subject: [PATCH] ops/dossiers-ngi: cascade bar dates, center/mobile, soft refresh; serve /cascade --- scripts/dossiers-ngi/README.md | 8 +- .../dossiers-ngi/caddy-dossiers-ngi.snippet | 9 + .../_cascade/bar.css | 37 +++- .../_cascade/bar.js | 205 +++++++++++++----- .../cascade/_cascade/bar.css | 37 +++- .../cascade/_cascade/bar.js | 205 +++++++++++++----- 6 files changed, 391 insertions(+), 110 deletions(-) diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 2620b26..3e1f39f 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -87,18 +87,22 @@ rsync -avz --exclude .DS_Store "$SITE/branch-status.json" "$WWW/branch-status.js `bar.js` fetches `/branch-status.json` and draws: ``` -main @ d2fbd79 -├── feat/sm-content @ … +main @ d2fbd79 · tip 2026-09-01 16:52 · build 8.4s · at 2026-09-30 15:23 +├── feat/sm-content @ … · tip YYYY-MM-DD HH:MM · … │ └── … └── project_template @ … ``` +`tip` / `at` always include **date + HH:MM** (ISO already in JSON; clock-only was misleading across days). The bar is **viewport-centered** (max-width 45em, mobile-friendly). Poll every 30s **updates tip/build/at text in place** when the branch structure is unchanged (no full tree remount). + Each name links to `/_cascade/use/` (cookie + 302 `/`). Caddy map: ``` ~^[A-Za-z0-9_-]+$ {http.request.cookie.tdpd_edition} ``` +Also serve `/cascade/*` from the site www root (path stubs). Live Caddy must not use a trailing `~.* flask` catch-all. + so new branches need no Caddy edit — only an `editions//` tree. ```bash diff --git a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet index c04676a..026fae3 100644 --- a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet +++ b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet @@ -21,6 +21,13 @@ root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch file_server } + # Path-root cascade stubs (feat-website-views, …) live under sites/…/cascade/ + # and must not fall through to editions/{cookie}/ (that yields 404). + handle /cascade/* { + root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch + file_server + try_files {path} {path}/ {path}/index.html + } handle /branch-status.json { root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch file_server @@ -28,6 +35,8 @@ # Dynamic editions: cookie value is the dir name under editions/ # (hourly builder stages every origin/* tip as slug with / → -). + # Do not append a catch-all `~.* flask` — that forces unknown cookies + # (and can mask missing edition dirs) onto the default freeze. map {http.request.cookie.tdpd_edition} {edition_dir} { default flask flask flask diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css index 2a5e185..df4d2d9 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css @@ -2,8 +2,13 @@ position: sticky; top: 0; z-index: 10; - margin: -1.5rem -1.5rem 1rem; - padding: 0.55rem 1.5rem 0.65rem; + display: flex; + justify-content: center; + box-sizing: border-box; + width: 100vw; + max-width: 100vw; + margin: -1.25rem calc(50% - 50vw) 1rem; + padding: 0.55rem 0.75rem 0.65rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; @@ -11,9 +16,16 @@ font-size: 12px; line-height: 1.45; } +.cascade-tree { + width: 100%; + max-width: 45em; + margin: 0 auto; + box-sizing: border-box; +} .cascade-tree-lines { white-space: pre; overflow-x: auto; + -webkit-overflow-scrolling: touch; } a.cascade-tree-row { display: block; @@ -34,9 +46,13 @@ a.cascade-tree-row.is-active { a.cascade-tree-row.is-active:hover { text-decoration: none; } -.cascade-bar code { color: inherit; font-size: inherit; } +.cascade-bar code { + color: inherit; + font-size: inherit; +} .cascade-bar .tip { color: #8a96a3; + white-space: pre-wrap; } .cascade-bar .cascade-default { color: #6a9080; @@ -46,6 +62,17 @@ a.cascade-tree-row.is-active:hover { color: #6a7888; font-size: 11px; } -.cascade-bar .tip { - white-space: pre-wrap; + +@media (max-width: 40em) { + .cascade-bar { + margin-top: -1rem; + padding: 0.45rem 0.5rem 0.55rem; + font-size: 11px; + } + .cascade-tree { + max-width: 100%; + } + .cascade-timing { + font-size: 10px; + } } diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js index 766fd12..9ad8924 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js @@ -1,4 +1,6 @@ (function () { + var POLL_MS = 30000; + function cookieEdition() { var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/); if (!m) return "flask"; @@ -17,12 +19,14 @@ .replace(/"/g, """); } + /** ISO → "YYYY-MM-DD HH:MM" (date required; clock-only was misleading across days). */ function shortTime(iso) { if (!iso) return ""; - // keep local-looking wall clock from ISO when possible - var m = String(iso).match(/T(\d{2}:\d{2})/); - if (m) return m[1]; - return String(iso).slice(0, 16); + var m = String(iso).match( + /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::\d{2})?/ + ); + if (m) return m[1] + " " + m[2]; + return String(iso).slice(0, 16).replace("T", " "); } function fmtBuildSec(sec) { @@ -33,6 +37,51 @@ return Math.round(n) + "s"; } + function metaBits(row) { + var tip = row.tip || ""; + var tipTime = row.tip_time || ""; + var builtAt = row.built_at || ""; + var buildSec = row.build_seconds; + var bits = []; + if (tip) bits.push("@" + tip); + if (tipTime) bits.push("tip " + shortTime(tipTime)); + if (buildSec != null && buildSec !== "") + bits.push("build " + fmtBuildSec(buildSec)); + if (builtAt) bits.push("at " + shortTime(builtAt)); + return bits; + } + + function rowTitle(row, buildSec) { + var tip = row.tip || ""; + var tipTime = row.tip_time || ""; + return ( + "Switch build to " + + (row.branch || "") + + (tip ? " @ " + tip : "") + + (tipTime ? " tip=" + tipTime : "") + + (buildSec != null && buildSec !== "" + ? " build=" + fmtBuildSec(buildSec) + : "") + ); + } + + function structureKey(list) { + var parts = []; + for (var i = 0; i < list.length; i++) { + var row = list[i]; + parts.push( + (row.edition || "") + + "\t" + + (row.branch || "") + + "\t" + + (row.prefix || "") + + "\t" + + (row.parent || "") + ); + } + return parts.join("\n"); + } + function markActive(root, edition) { var links = root.querySelectorAll("a[data-edition]"); for (var i = 0; i < links.length; i++) { @@ -51,7 +100,71 @@ } } - function renderTree(data) { + function timingText(timing, listLen) { + if (!timing || timing.wall_seconds == null) return ""; + return ( + "last full pass ~" + + String(timing.wall_seconds) + + "s · built " + + String(timing.built || 0) + + " / skipped " + + String(timing.skipped || 0) + + " / " + + String(timing.branches || listLen) + ); + } + + function updateValues(mount, data) { + var list = (data && data.branches) || []; + var defaultBranch = (data && data.default_branch) || "feat/website-views"; + var timing = (data && data.rebuild_timing) || null; + var rows = mount.querySelectorAll("a.cascade-tree-row[data-edition]"); + if (rows.length !== list.length) return false; + for (var i = 0; i < list.length; i++) { + var row = list[i]; + var a = rows[i]; + if (a.getAttribute("data-edition") !== (row.edition || "")) return false; + if (a.getAttribute("data-branch") !== (row.branch || "")) return false; + var bits = metaBits(row); + var tipEl = a.querySelector(".tip"); + var wantTip = bits.length ? " " + bits.join(" · ") : ""; + if (tipEl) { + if (tipEl.textContent !== wantTip) tipEl.textContent = wantTip; + } else if (wantTip) { + tipEl = document.createElement("span"); + tipEl.className = "tip"; + tipEl.textContent = wantTip; + a.appendChild(tipEl); + } + var defEl = a.querySelector(".cascade-default"); + var isDefault = (row.branch || "") === defaultBranch; + if (isDefault && !defEl) { + defEl = document.createElement("span"); + defEl.className = "cascade-default"; + defEl.textContent = " · default"; + a.appendChild(defEl); + } else if (!isDefault && defEl) { + defEl.parentNode.removeChild(defEl); + } + a.setAttribute("title", rowTitle(row, row.build_seconds)); + } + var timingEl = mount.querySelector(".cascade-timing"); + var t = timingText(timing, list.length); + if (t) { + if (!timingEl) { + timingEl = document.createElement("div"); + timingEl.className = "cascade-timing"; + mount.appendChild(timingEl); + } + if (timingEl.textContent !== t) timingEl.textContent = t; + } else if (timingEl) { + timingEl.parentNode.removeChild(timingEl); + } + markActive(mount, cookieEdition()); + return true; + } + + function renderTree(data, soft) { var mount = document.getElementById("cascade-tree"); if (!mount) return; var list = (data && data.branches) || []; @@ -59,6 +172,9 @@ mount.textContent = "No branches in branch-status.json"; return; } + if (soft && mount.getAttribute("data-structure") === structureKey(list)) { + if (updateValues(mount, data)) return; + } var defaultBranch = (data && data.default_branch) || "feat/website-views"; var timing = (data && data.rebuild_timing) || null; var html = []; @@ -71,22 +187,16 @@ var prefix = row.prefix || ""; var parent = row.parent || ""; var tipTime = row.tip_time || ""; - var builtAt = row.built_at || ""; var buildSec = row.build_seconds; var isDefault = branch === defaultBranch; - var metaBits = []; - if (tip) metaBits.push("@" + tip); - if (tipTime) metaBits.push("tip " + shortTime(tipTime)); - if (buildSec != null && buildSec !== "") - metaBits.push("build " + fmtBuildSec(buildSec)); - if (builtAt) metaBits.push("at " + shortTime(builtAt)); + var bits = metaBits(row); var label = esc(prefix) + "" + esc(branch) + "" + - (metaBits.length - ? ' ' + esc(metaBits.join(" · ")) + "" + (bits.length + ? ' ' + esc(bits.join(" · ")) + "" : "") + (isDefault ? ' · default' : ""); html.push( @@ -94,53 +204,50 @@ esc(edition) + '" data-edition="' + esc(edition) + + '" data-branch="' + + esc(branch) + '"' + (parent ? ' data-parent="' + esc(parent) + '"' : "") + (isDefault ? ' data-default="1"' : "") + - ' title="Switch build to ' + - esc(branch) + - (tip ? " @ " + esc(tip) : "") + - (tipTime ? " tip=" + esc(tipTime) : "") + - (buildSec != null ? " build=" + esc(fmtBuildSec(buildSec)) : "") + + ' title="' + + esc(rowTitle(row, buildSec)) + '">' + label + "" ); } html.push(""); - if (timing && timing.wall_seconds != null) { - html.push( - '
last full pass ~' + - esc(String(timing.wall_seconds)) + - "s · built " + - esc(String(timing.built || 0)) + - " / skipped " + - esc(String(timing.skipped || 0)) + - " / " + - esc(String(timing.branches || list.length)) + - "
" - ); + var t = timingText(timing, list.length); + if (t) { + html.push('
' + esc(t) + "
"); } mount.innerHTML = html.join(""); + mount.setAttribute("data-structure", structureKey(list)); markActive(mount, cookieEdition()); } - var edition = cookieEdition(); - var mount = document.getElementById("cascade-tree"); - fetch("/branch-status.json", { cache: "no-store" }) - .then(function (res) { - if (!res.ok) throw new Error("status"); - return res.json(); - }) - .then(function (data) { - renderTree(data); - }) - .catch(function () { - if (mount) { - mount.innerHTML = - 'flask' + - ' (branch-status unavailable)'; - markActive(mount, edition); - } - }); + function loadStatus(soft) { + var mount = document.getElementById("cascade-tree"); + return fetch("/branch-status.json", { cache: "no-store" }) + .then(function (res) { + if (!res.ok) throw new Error("status"); + return res.json(); + }) + .then(function (data) { + renderTree(data, !!soft); + }) + .catch(function () { + if (!soft && mount && !mount.querySelector("a[data-edition]")) { + mount.innerHTML = + 'flask' + + ' (branch-status unavailable)'; + markActive(mount, cookieEdition()); + } + }); + } + + loadStatus(false); + setInterval(function () { + loadStatus(true); + }, POLL_MS); })(); diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css index 2a5e185..df4d2d9 100644 --- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css @@ -2,8 +2,13 @@ position: sticky; top: 0; z-index: 10; - margin: -1.5rem -1.5rem 1rem; - padding: 0.55rem 1.5rem 0.65rem; + display: flex; + justify-content: center; + box-sizing: border-box; + width: 100vw; + max-width: 100vw; + margin: -1.25rem calc(50% - 50vw) 1rem; + padding: 0.55rem 0.75rem 0.65rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; @@ -11,9 +16,16 @@ font-size: 12px; line-height: 1.45; } +.cascade-tree { + width: 100%; + max-width: 45em; + margin: 0 auto; + box-sizing: border-box; +} .cascade-tree-lines { white-space: pre; overflow-x: auto; + -webkit-overflow-scrolling: touch; } a.cascade-tree-row { display: block; @@ -34,9 +46,13 @@ a.cascade-tree-row.is-active { a.cascade-tree-row.is-active:hover { text-decoration: none; } -.cascade-bar code { color: inherit; font-size: inherit; } +.cascade-bar code { + color: inherit; + font-size: inherit; +} .cascade-bar .tip { color: #8a96a3; + white-space: pre-wrap; } .cascade-bar .cascade-default { color: #6a9080; @@ -46,6 +62,17 @@ a.cascade-tree-row.is-active:hover { color: #6a7888; font-size: 11px; } -.cascade-bar .tip { - white-space: pre-wrap; + +@media (max-width: 40em) { + .cascade-bar { + margin-top: -1rem; + padding: 0.45rem 0.5rem 0.55rem; + font-size: 11px; + } + .cascade-tree { + max-width: 100%; + } + .cascade-timing { + font-size: 10px; + } } diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js index 766fd12..9ad8924 100644 --- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js @@ -1,4 +1,6 @@ (function () { + var POLL_MS = 30000; + function cookieEdition() { var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/); if (!m) return "flask"; @@ -17,12 +19,14 @@ .replace(/"/g, """); } + /** ISO → "YYYY-MM-DD HH:MM" (date required; clock-only was misleading across days). */ function shortTime(iso) { if (!iso) return ""; - // keep local-looking wall clock from ISO when possible - var m = String(iso).match(/T(\d{2}:\d{2})/); - if (m) return m[1]; - return String(iso).slice(0, 16); + var m = String(iso).match( + /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::\d{2})?/ + ); + if (m) return m[1] + " " + m[2]; + return String(iso).slice(0, 16).replace("T", " "); } function fmtBuildSec(sec) { @@ -33,6 +37,51 @@ return Math.round(n) + "s"; } + function metaBits(row) { + var tip = row.tip || ""; + var tipTime = row.tip_time || ""; + var builtAt = row.built_at || ""; + var buildSec = row.build_seconds; + var bits = []; + if (tip) bits.push("@" + tip); + if (tipTime) bits.push("tip " + shortTime(tipTime)); + if (buildSec != null && buildSec !== "") + bits.push("build " + fmtBuildSec(buildSec)); + if (builtAt) bits.push("at " + shortTime(builtAt)); + return bits; + } + + function rowTitle(row, buildSec) { + var tip = row.tip || ""; + var tipTime = row.tip_time || ""; + return ( + "Switch build to " + + (row.branch || "") + + (tip ? " @ " + tip : "") + + (tipTime ? " tip=" + tipTime : "") + + (buildSec != null && buildSec !== "" + ? " build=" + fmtBuildSec(buildSec) + : "") + ); + } + + function structureKey(list) { + var parts = []; + for (var i = 0; i < list.length; i++) { + var row = list[i]; + parts.push( + (row.edition || "") + + "\t" + + (row.branch || "") + + "\t" + + (row.prefix || "") + + "\t" + + (row.parent || "") + ); + } + return parts.join("\n"); + } + function markActive(root, edition) { var links = root.querySelectorAll("a[data-edition]"); for (var i = 0; i < links.length; i++) { @@ -51,7 +100,71 @@ } } - function renderTree(data) { + function timingText(timing, listLen) { + if (!timing || timing.wall_seconds == null) return ""; + return ( + "last full pass ~" + + String(timing.wall_seconds) + + "s · built " + + String(timing.built || 0) + + " / skipped " + + String(timing.skipped || 0) + + " / " + + String(timing.branches || listLen) + ); + } + + function updateValues(mount, data) { + var list = (data && data.branches) || []; + var defaultBranch = (data && data.default_branch) || "feat/website-views"; + var timing = (data && data.rebuild_timing) || null; + var rows = mount.querySelectorAll("a.cascade-tree-row[data-edition]"); + if (rows.length !== list.length) return false; + for (var i = 0; i < list.length; i++) { + var row = list[i]; + var a = rows[i]; + if (a.getAttribute("data-edition") !== (row.edition || "")) return false; + if (a.getAttribute("data-branch") !== (row.branch || "")) return false; + var bits = metaBits(row); + var tipEl = a.querySelector(".tip"); + var wantTip = bits.length ? " " + bits.join(" · ") : ""; + if (tipEl) { + if (tipEl.textContent !== wantTip) tipEl.textContent = wantTip; + } else if (wantTip) { + tipEl = document.createElement("span"); + tipEl.className = "tip"; + tipEl.textContent = wantTip; + a.appendChild(tipEl); + } + var defEl = a.querySelector(".cascade-default"); + var isDefault = (row.branch || "") === defaultBranch; + if (isDefault && !defEl) { + defEl = document.createElement("span"); + defEl.className = "cascade-default"; + defEl.textContent = " · default"; + a.appendChild(defEl); + } else if (!isDefault && defEl) { + defEl.parentNode.removeChild(defEl); + } + a.setAttribute("title", rowTitle(row, row.build_seconds)); + } + var timingEl = mount.querySelector(".cascade-timing"); + var t = timingText(timing, list.length); + if (t) { + if (!timingEl) { + timingEl = document.createElement("div"); + timingEl.className = "cascade-timing"; + mount.appendChild(timingEl); + } + if (timingEl.textContent !== t) timingEl.textContent = t; + } else if (timingEl) { + timingEl.parentNode.removeChild(timingEl); + } + markActive(mount, cookieEdition()); + return true; + } + + function renderTree(data, soft) { var mount = document.getElementById("cascade-tree"); if (!mount) return; var list = (data && data.branches) || []; @@ -59,6 +172,9 @@ mount.textContent = "No branches in branch-status.json"; return; } + if (soft && mount.getAttribute("data-structure") === structureKey(list)) { + if (updateValues(mount, data)) return; + } var defaultBranch = (data && data.default_branch) || "feat/website-views"; var timing = (data && data.rebuild_timing) || null; var html = []; @@ -71,22 +187,16 @@ var prefix = row.prefix || ""; var parent = row.parent || ""; var tipTime = row.tip_time || ""; - var builtAt = row.built_at || ""; var buildSec = row.build_seconds; var isDefault = branch === defaultBranch; - var metaBits = []; - if (tip) metaBits.push("@" + tip); - if (tipTime) metaBits.push("tip " + shortTime(tipTime)); - if (buildSec != null && buildSec !== "") - metaBits.push("build " + fmtBuildSec(buildSec)); - if (builtAt) metaBits.push("at " + shortTime(builtAt)); + var bits = metaBits(row); var label = esc(prefix) + "" + esc(branch) + "" + - (metaBits.length - ? ' ' + esc(metaBits.join(" · ")) + "" + (bits.length + ? ' ' + esc(bits.join(" · ")) + "" : "") + (isDefault ? ' · default' : ""); html.push( @@ -94,53 +204,50 @@ esc(edition) + '" data-edition="' + esc(edition) + + '" data-branch="' + + esc(branch) + '"' + (parent ? ' data-parent="' + esc(parent) + '"' : "") + (isDefault ? ' data-default="1"' : "") + - ' title="Switch build to ' + - esc(branch) + - (tip ? " @ " + esc(tip) : "") + - (tipTime ? " tip=" + esc(tipTime) : "") + - (buildSec != null ? " build=" + esc(fmtBuildSec(buildSec)) : "") + + ' title="' + + esc(rowTitle(row, buildSec)) + '">' + label + "" ); } html.push(""); - if (timing && timing.wall_seconds != null) { - html.push( - '
last full pass ~' + - esc(String(timing.wall_seconds)) + - "s · built " + - esc(String(timing.built || 0)) + - " / skipped " + - esc(String(timing.skipped || 0)) + - " / " + - esc(String(timing.branches || list.length)) + - "
" - ); + var t = timingText(timing, list.length); + if (t) { + html.push('
' + esc(t) + "
"); } mount.innerHTML = html.join(""); + mount.setAttribute("data-structure", structureKey(list)); markActive(mount, cookieEdition()); } - var edition = cookieEdition(); - var mount = document.getElementById("cascade-tree"); - fetch("/branch-status.json", { cache: "no-store" }) - .then(function (res) { - if (!res.ok) throw new Error("status"); - return res.json(); - }) - .then(function (data) { - renderTree(data); - }) - .catch(function () { - if (mount) { - mount.innerHTML = - 'flask' + - ' (branch-status unavailable)'; - markActive(mount, edition); - } - }); + function loadStatus(soft) { + var mount = document.getElementById("cascade-tree"); + return fetch("/branch-status.json", { cache: "no-store" }) + .then(function (res) { + if (!res.ok) throw new Error("status"); + return res.json(); + }) + .then(function (data) { + renderTree(data, !!soft); + }) + .catch(function () { + if (!soft && mount && !mount.querySelector("a[data-edition]")) { + mount.innerHTML = + 'flask' + + ' (branch-status unavailable)'; + markActive(mount, cookieEdition()); + } + }); + } + + loadStatus(false); + setInterval(function () { + loadStatus(true); + }, POLL_MS); })();