From da6f38439922d4fad652bd7214f569b777dc56d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 23:23:14 +0200 Subject: [PATCH] feat(dossiers-ngi): sticky bar vs-main compact dates tabular wider tree --- scripts/dossiers-ngi/README.md | 12 +- scripts/dossiers-ngi/branch-status-hourly.py | 23 +-- .../_cascade/bar.css | 36 ++-- .../_cascade/bar.js | 158 +++++++++--------- .../cascade/_cascade/bar.css | 36 ++-- .../cascade/_cascade/bar.js | 158 +++++++++--------- 6 files changed, 231 insertions(+), 192 deletions(-) diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index fbfa0b0..4abcd67 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -58,7 +58,7 @@ Per branch in JSON / sticky bar: | `tip_time` | committer time of tip (`%cI`) | | `built_at` | when this edition freeze was written | | `build_seconds` | wall time of that freeze | -| `vs_parent_plus` / `vs_parent_minus` | `git diff --numstat` tip vs parent tip (sticky bar +grün/−rot; omitted when no parent) | +| `vs_main_plus` / `vs_main_minus` | `git diff --numstat` tip vs **main** tip (sticky bar +grün/−rot; omitted on `main` itself) | Rebuild runs **only when** `.edition-tip` ≠ current tip (timer never forces). Each run also: `git fetch --prune`, `worktree prune`, drop stale @@ -105,13 +105,13 @@ rsync -avz --exclude .DS_Store "$SITE/branch-status.json" "$WWW/branch-status.js `bar.js` fetches `/branch-status.json` and draws: ``` -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 @ … +main sep01 16:52 8.4s sep30 15:23 +├── feat/sm-content sep30 16:11 13s sep30 15:23 +N −M +│ └── feat/website-features … +└── project_template sep28 15:03 2.2s sep30 15:23 +N −M ``` -`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). +Visible cells: name (tree prefix + branch) · tip `monDD HH:MM` · `Ns monDD HH:MM` (build seconds + freeze time) · `+N −M` vs main. No `@sha`, no `default` label (`data-default` stays for flask/cookie). The bar is **viewport-centered**, inner tree `max-width: min(80rem, 100vw − 1.5rem)` (was 45em at 11px ≈ 495px, which clipped long names). Rear cells are right-aligned `tabular-nums`. Poll every 30s **updates when/built/diff in place** when the branch structure is unchanged (no full tree remount). Each name links to `/_cascade/use/` (cookie + 302 `/`). Caddy map: diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index d4b3a6c..5230f9c 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -235,11 +235,11 @@ def tip_recorded(editions: Path, slug: str) -> str: -def diff_numstat(repo: Path, parent_tip: str, tip: str) -> tuple[int, int]: - """Insertions/deletions of tip vs parent tip (git diff --numstat).""" - if not parent_tip or not tip or parent_tip == tip: +def diff_numstat(repo: Path, a: str, b: str) -> tuple[int, int]: + """Insertions/deletions of b vs a (git diff --numstat).""" + if not a or not b or a == b: return 0, 0 - cp = git(repo, "diff", "--numstat", parent_tip, tip, check=False) + cp = git(repo, "diff", "--numstat", a, b, check=False) if cp.returncode != 0: return 0, 0 plus = minus = 0 @@ -564,6 +564,10 @@ def build_payload(repo: Path, tips: dict[str, str], rebuild: bool) -> dict: sync_flask_alias(ed_root, tips) tip_times = {name: tip_commit_time(repo, tip) for name, tip in tips.items()} + main_tip = tips.get("main", "") + vs_main = { + name: diff_numstat(repo, main_tip, tip) for name, tip in tips.items() + } branches_out = [] for row in rows: name = row["branch"] @@ -589,12 +593,11 @@ def build_payload(repo: Path, tips: dict[str, str], rebuild: bool) -> dict: entry["built_at"] = meta["built_at"] if "build_seconds" in meta: entry["build_seconds"] = meta["build_seconds"] - # sticky bar: +grün / −rot vs parent tip; omit when no parent - parent = row["parent"] - if parent and parent in tips: - plus, minus = diff_numstat(repo, tips[parent], row["tip"]) - entry["vs_parent_plus"] = plus - entry["vs_parent_minus"] = minus + # sticky bar: +grün / −rot vs main (omit on main itself) + if name != "main" and main_tip: + plus, minus = vs_main.get(name, (0, 0)) + entry["vs_main_plus"] = plus + entry["vs_main_minus"] = minus branches_out.append(entry) payload = { diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css index db35ae9..fb485be 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css @@ -13,7 +13,7 @@ html { width: 100%; max-width: none; margin: 0; - padding: 0.2rem 0.5rem 0.25rem; + padding: 0.2rem 0.75rem 0.25rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; @@ -23,20 +23,24 @@ html { } .cascade-tree { width: 100%; - max-width: 45em; + max-width: min(80rem, calc(100vw - 1.5rem)); margin: 0 auto; box-sizing: border-box; } .cascade-tree-lines { - white-space: pre; + display: flex; + flex-direction: column; overflow-x: auto; -webkit-overflow-scrolling: touch; } a.cascade-tree-row { - display: block; + display: grid; + grid-template-columns: minmax(12rem, 1fr) 7.5rem 11.5rem 7rem; + column-gap: 0.85rem; + align-items: baseline; color: #7ec8ff; text-decoration: none; - padding: 0; + padding: 0 0.15rem; border-radius: 2px; } a.cascade-tree-row:hover { @@ -55,12 +59,20 @@ a.cascade-tree-row.is-active:hover { color: inherit; font-size: inherit; } -.cascade-bar .tip { - color: #8a96a3; - white-space: nowrap; +.cascade-name { + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + color: inherit; } -.cascade-bar .cascade-default { - color: #6a9080; +.cascade-when, +.cascade-built, +.cascade-diff { + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; + color: #8a96a3; } .cascade-timing { display: none; @@ -74,6 +86,10 @@ a.cascade-tree-row.is-active:hover { .cascade-tree { max-width: 100%; } + a.cascade-tree-row { + grid-template-columns: minmax(8rem, 1fr) 6.5rem 10rem 6rem; + column-gap: 0.45rem; + } } .cascade-bar .diff-plus { diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js index 23351e7..019adf9 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js @@ -1,5 +1,19 @@ (function () { var POLL_MS = 30000; + var MONTHS = [ + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec", + ]; function cookieEdition() { var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/); @@ -19,13 +33,14 @@ .replace(/"/g, """); } - /** ISO → "YYYY-MM-DD HH:MM" (date required; clock-only was misleading across days). */ + /** ISO → "sep30 16:12" (3-letter English month + DD + HH:MM). */ function shortTime(iso) { if (!iso) return ""; - 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]; + var m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/); + if (m) { + var mon = MONTHS[Number(m[2]) - 1] || m[2]; + return mon + m[3] + " " + m[4]; + } return String(iso).slice(0, 16).replace("T", " "); } @@ -37,59 +52,55 @@ return Math.round(n) + "s"; } - function metaHtml(row) { - var tip = row.tip || ""; - var tipTime = row.tip_time || ""; - var builtAt = row.built_at || ""; - var buildSec = row.build_seconds; - var parts = []; - if (tip) parts.push(esc("@" + tip)); - if (tipTime) parts.push(esc(shortTime(tipTime))); - if (buildSec != null && buildSec !== "") - parts.push(esc(fmtBuildSec(buildSec))); - if (builtAt) parts.push(esc(shortTime(builtAt))); - // vs parent: +grün / −rot (omit when no parent) - var plus = row.vs_parent_plus; - var minus = row.vs_parent_minus; - if (row.parent && (plus != null || minus != null)) { - var p = plus == null ? 0 : Number(plus); - var m = minus == null ? 0 : Number(minus); - if (isFinite(p) && isFinite(m)) { - parts.push( - '+' + - esc(String(p)) + - ' −' + - esc(String(m)) + - "" - ); - } - } - return parts.join(" · "); + function builtText(row) { + var sec = fmtBuildSec(row.build_seconds); + var at = shortTime(row.built_at || ""); + if (sec && at) return sec + " " + at; + return sec || at; } - function rowTitle(row, buildSec) { + function diffHtml(row) { + var plus = row.vs_main_plus; + var minus = row.vs_main_minus; + if (plus == null && minus == null) return ""; + var p = plus == null ? 0 : Number(plus); + var m = minus == null ? 0 : Number(minus); + if (!isFinite(p) || !isFinite(m)) return ""; + return ( + '+' + + esc(String(p)) + + ' −' + + esc(String(m)) + + "" + ); + } + + function setText(el, text) { + if (!el) return; + var t = text || ""; + if (el.textContent !== t) el.textContent = t; + } + + function rowTitle(row) { var tip = row.tip || ""; var tipTime = row.tip_time || ""; - var plus = row.vs_parent_plus; - var minus = row.vs_parent_minus; + var plus = row.vs_main_plus; + var minus = row.vs_main_minus; var vs = ""; - if (row.parent && (plus != null || minus != null)) { + if (plus != null || minus != null) { vs = - " vs " + - row.parent + - " +" + + " vs main +" + String(plus == null ? 0 : plus) + "/−" + String(minus == null ? 0 : minus); } + var built = builtText(row); return ( "Switch build to " + (row.branch || "") + (tip ? " @ " + tip : "") + - (tipTime ? " tip=" + tipTime : "") + - (buildSec != null && buildSec !== "" - ? " build=" + fmtBuildSec(buildSec) - : "") + + (tipTime ? " tip=" + shortTime(tipTime) : "") + + (built ? " build=" + built : "") + vs ); } @@ -165,28 +176,17 @@ var a = rows[i]; if (a.getAttribute("data-edition") !== (row.edition || "")) return false; if (a.getAttribute("data-branch") !== (row.branch || "")) return false; - var tipHtml = metaHtml(row); - var tipEl = a.querySelector(".tip"); - var wantTip = tipHtml ? " " + tipHtml : ""; - if (tipEl) { - if (tipEl.innerHTML !== wantTip) tipEl.innerHTML = wantTip; - } else if (wantTip) { - tipEl = document.createElement("span"); - tipEl.className = "tip"; - tipEl.innerHTML = 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); + if (isDefault) a.setAttribute("data-default", "1"); + else a.removeAttribute("data-default"); + setText(a.querySelector(".cascade-when"), shortTime(row.tip_time || "")); + setText(a.querySelector(".cascade-built"), builtText(row)); + var diffEl = a.querySelector(".cascade-diff"); + if (diffEl) { + var dh = diffHtml(row); + if (diffEl.innerHTML !== dh) diffEl.innerHTML = dh; } - a.setAttribute("title", rowTitle(row, row.build_seconds)); + a.setAttribute("title", rowTitle(row)); } var timingEl = mount.querySelector(".cascade-timing"); var t = timingText(timing, list.length); @@ -225,20 +225,9 @@ var row = list[i]; var edition = row.edition || ""; var branch = row.branch || ""; - var tip = row.tip || ""; var prefix = row.prefix || ""; var parent = row.parent || ""; - var tipTime = row.tip_time || ""; - var buildSec = row.build_seconds; var isDefault = branch === defaultBranch; - var tipHtml = metaHtml(row); - var label = - esc(prefix) + - "" + - esc(branch) + - "" + - (tipHtml ? ' ' + tipHtml + "" : "") + - (isDefault ? ' · default' : ""); html.push( '' + - label + + '' + + esc(prefix) + + "" + + esc(branch) + + "" + + '' + + esc(shortTime(row.tip_time || "")) + + "" + + '' + + esc(builtText(row)) + + "" + + '' + + diffHtml(row) + + "" + "" ); } @@ -280,7 +282,7 @@ .catch(function () { if (!soft && mount && !mount.querySelector("a[data-edition]")) { mount.innerHTML = - 'flask' + + 'flask' + ' (branch-status unavailable)'; markActive(mount, cookieEdition()); pinBar(); diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css index db35ae9..fb485be 100644 --- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css @@ -13,7 +13,7 @@ html { width: 100%; max-width: none; margin: 0; - padding: 0.2rem 0.5rem 0.25rem; + padding: 0.2rem 0.75rem 0.25rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; @@ -23,20 +23,24 @@ html { } .cascade-tree { width: 100%; - max-width: 45em; + max-width: min(80rem, calc(100vw - 1.5rem)); margin: 0 auto; box-sizing: border-box; } .cascade-tree-lines { - white-space: pre; + display: flex; + flex-direction: column; overflow-x: auto; -webkit-overflow-scrolling: touch; } a.cascade-tree-row { - display: block; + display: grid; + grid-template-columns: minmax(12rem, 1fr) 7.5rem 11.5rem 7rem; + column-gap: 0.85rem; + align-items: baseline; color: #7ec8ff; text-decoration: none; - padding: 0; + padding: 0 0.15rem; border-radius: 2px; } a.cascade-tree-row:hover { @@ -55,12 +59,20 @@ a.cascade-tree-row.is-active:hover { color: inherit; font-size: inherit; } -.cascade-bar .tip { - color: #8a96a3; - white-space: nowrap; +.cascade-name { + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + color: inherit; } -.cascade-bar .cascade-default { - color: #6a9080; +.cascade-when, +.cascade-built, +.cascade-diff { + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; + color: #8a96a3; } .cascade-timing { display: none; @@ -74,6 +86,10 @@ a.cascade-tree-row.is-active:hover { .cascade-tree { max-width: 100%; } + a.cascade-tree-row { + grid-template-columns: minmax(8rem, 1fr) 6.5rem 10rem 6rem; + column-gap: 0.45rem; + } } .cascade-bar .diff-plus { diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js index 23351e7..019adf9 100644 --- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js @@ -1,5 +1,19 @@ (function () { var POLL_MS = 30000; + var MONTHS = [ + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec", + ]; function cookieEdition() { var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/); @@ -19,13 +33,14 @@ .replace(/"/g, """); } - /** ISO → "YYYY-MM-DD HH:MM" (date required; clock-only was misleading across days). */ + /** ISO → "sep30 16:12" (3-letter English month + DD + HH:MM). */ function shortTime(iso) { if (!iso) return ""; - 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]; + var m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/); + if (m) { + var mon = MONTHS[Number(m[2]) - 1] || m[2]; + return mon + m[3] + " " + m[4]; + } return String(iso).slice(0, 16).replace("T", " "); } @@ -37,59 +52,55 @@ return Math.round(n) + "s"; } - function metaHtml(row) { - var tip = row.tip || ""; - var tipTime = row.tip_time || ""; - var builtAt = row.built_at || ""; - var buildSec = row.build_seconds; - var parts = []; - if (tip) parts.push(esc("@" + tip)); - if (tipTime) parts.push(esc(shortTime(tipTime))); - if (buildSec != null && buildSec !== "") - parts.push(esc(fmtBuildSec(buildSec))); - if (builtAt) parts.push(esc(shortTime(builtAt))); - // vs parent: +grün / −rot (omit when no parent) - var plus = row.vs_parent_plus; - var minus = row.vs_parent_minus; - if (row.parent && (plus != null || minus != null)) { - var p = plus == null ? 0 : Number(plus); - var m = minus == null ? 0 : Number(minus); - if (isFinite(p) && isFinite(m)) { - parts.push( - '+' + - esc(String(p)) + - ' −' + - esc(String(m)) + - "" - ); - } - } - return parts.join(" · "); + function builtText(row) { + var sec = fmtBuildSec(row.build_seconds); + var at = shortTime(row.built_at || ""); + if (sec && at) return sec + " " + at; + return sec || at; } - function rowTitle(row, buildSec) { + function diffHtml(row) { + var plus = row.vs_main_plus; + var minus = row.vs_main_minus; + if (plus == null && minus == null) return ""; + var p = plus == null ? 0 : Number(plus); + var m = minus == null ? 0 : Number(minus); + if (!isFinite(p) || !isFinite(m)) return ""; + return ( + '+' + + esc(String(p)) + + ' −' + + esc(String(m)) + + "" + ); + } + + function setText(el, text) { + if (!el) return; + var t = text || ""; + if (el.textContent !== t) el.textContent = t; + } + + function rowTitle(row) { var tip = row.tip || ""; var tipTime = row.tip_time || ""; - var plus = row.vs_parent_plus; - var minus = row.vs_parent_minus; + var plus = row.vs_main_plus; + var minus = row.vs_main_minus; var vs = ""; - if (row.parent && (plus != null || minus != null)) { + if (plus != null || minus != null) { vs = - " vs " + - row.parent + - " +" + + " vs main +" + String(plus == null ? 0 : plus) + "/−" + String(minus == null ? 0 : minus); } + var built = builtText(row); return ( "Switch build to " + (row.branch || "") + (tip ? " @ " + tip : "") + - (tipTime ? " tip=" + tipTime : "") + - (buildSec != null && buildSec !== "" - ? " build=" + fmtBuildSec(buildSec) - : "") + + (tipTime ? " tip=" + shortTime(tipTime) : "") + + (built ? " build=" + built : "") + vs ); } @@ -165,28 +176,17 @@ var a = rows[i]; if (a.getAttribute("data-edition") !== (row.edition || "")) return false; if (a.getAttribute("data-branch") !== (row.branch || "")) return false; - var tipHtml = metaHtml(row); - var tipEl = a.querySelector(".tip"); - var wantTip = tipHtml ? " " + tipHtml : ""; - if (tipEl) { - if (tipEl.innerHTML !== wantTip) tipEl.innerHTML = wantTip; - } else if (wantTip) { - tipEl = document.createElement("span"); - tipEl.className = "tip"; - tipEl.innerHTML = 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); + if (isDefault) a.setAttribute("data-default", "1"); + else a.removeAttribute("data-default"); + setText(a.querySelector(".cascade-when"), shortTime(row.tip_time || "")); + setText(a.querySelector(".cascade-built"), builtText(row)); + var diffEl = a.querySelector(".cascade-diff"); + if (diffEl) { + var dh = diffHtml(row); + if (diffEl.innerHTML !== dh) diffEl.innerHTML = dh; } - a.setAttribute("title", rowTitle(row, row.build_seconds)); + a.setAttribute("title", rowTitle(row)); } var timingEl = mount.querySelector(".cascade-timing"); var t = timingText(timing, list.length); @@ -225,20 +225,9 @@ var row = list[i]; var edition = row.edition || ""; var branch = row.branch || ""; - var tip = row.tip || ""; var prefix = row.prefix || ""; var parent = row.parent || ""; - var tipTime = row.tip_time || ""; - var buildSec = row.build_seconds; var isDefault = branch === defaultBranch; - var tipHtml = metaHtml(row); - var label = - esc(prefix) + - "" + - esc(branch) + - "" + - (tipHtml ? ' ' + tipHtml + "" : "") + - (isDefault ? ' · default' : ""); html.push( '' + - label + + '' + + esc(prefix) + + "" + + esc(branch) + + "" + + '' + + esc(shortTime(row.tip_time || "")) + + "" + + '' + + esc(builtText(row)) + + "" + + '' + + diffHtml(row) + + "" + "" ); } @@ -280,7 +282,7 @@ .catch(function () { if (!soft && mount && !mount.querySelector("a[data-edition]")) { mount.innerHTML = - 'flask' + + 'flask' + ' (branch-status unavailable)'; markActive(mount, cookieEdition()); pinBar();