From c2bf7a4263a12aa9135eaf877d26a7afc7793f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 16:21:28 +0200 Subject: [PATCH] =?UTF-8?q?feat/dossiers-ngi:=20sticky=20bar=20vs-main=20+?= =?UTF-8?q?green/=E2=88=92red=20diffstat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/dossiers-ngi/README.md | 1 + scripts/dossiers-ngi/branch-status-hourly.py | 33 +++++++++++ .../_cascade/bar.css | 9 +++ .../_cascade/bar.js | 57 +++++++++++++------ .../cascade/_cascade/bar.css | 9 +++ .../cascade/_cascade/bar.js | 57 +++++++++++++------ 6 files changed, 134 insertions(+), 32 deletions(-) diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 718e1ca..064b830 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -58,6 +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_main_plus` / `vs_main_minus` | `git diff --numstat` tip vs `main` tip (sticky bar +grün/−rot; omitted on `main`) | Rebuild runs **only when** `.edition-tip` ≠ current tip (timer never forces). Each run also: `git fetch --prune`, `worktree prune`, drop stale diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index 16b0a3f..9bed2c3 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -234,6 +234,30 @@ def tip_recorded(editions: Path, slug: str) -> str: return p.read_text(encoding="utf-8").strip() + +def diff_vs_main(repo: Path, main_tip: str, tip: str) -> tuple[int, int]: + """Insertions/deletions of tip vs main tip (git diff --numstat).""" + if not main_tip or not tip or main_tip == tip: + return 0, 0 + cp = git(repo, "diff", "--numstat", main_tip, tip, check=False) + if cp.returncode != 0: + return 0, 0 + plus = minus = 0 + for line in (cp.stdout or "").splitlines(): + parts = line.split(" ") + if len(parts) < 3: + continue + a, d = parts[0], parts[1] + if a == "-" or d == "-": + continue + try: + plus += int(a) + minus += int(d) + except ValueError: + continue + return plus, minus + + def tip_commit_time(repo: Path, tip: str) -> str: """Committer ISO time of tip (empty on failure).""" cp = git(repo, "log", "-1", "--format=%cI", tip, check=False) @@ -531,6 +555,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_vs_main(repo, main_tip, tip) for name, tip in tips.items() + } branches_out = [] for row in rows: name = row["branch"] @@ -556,6 +584,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 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 df4d2d9..ae291e2 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css @@ -76,3 +76,12 @@ a.cascade-tree-row.is-active:hover { font-size: 10px; } } + +.cascade-bar .diff-plus { + color: #3fb950; + font-weight: 600; +} +.cascade-bar .diff-minus { + color: #f85149; + font-weight: 600; +} diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js index 9ad8924..a4f4ea4 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js @@ -37,23 +37,49 @@ return Math.round(n) + "s"; } - function metaBits(row) { + function metaHtml(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)); + var parts = []; + if (tip) parts.push(esc("@" + tip)); + if (tipTime) parts.push(esc("tip " + shortTime(tipTime))); if (buildSec != null && buildSec !== "") - bits.push("build " + fmtBuildSec(buildSec)); - if (builtAt) bits.push("at " + shortTime(builtAt)); - return bits; + parts.push(esc("build " + fmtBuildSec(buildSec))); + if (builtAt) parts.push(esc("at " + shortTime(builtAt))); + // vs main: +grün / −rot (omit on main / when both zero absent) + var plus = row.vs_main_plus; + var minus = row.vs_main_minus; + if (plus != null || minus != null) { + var p = plus == null ? 0 : Number(plus); + var m = minus == null ? 0 : Number(minus); + if (isFinite(p) && isFinite(m) && (p > 0 || m > 0 || row.branch !== "main")) { + parts.push( + '+' + + esc(String(p)) + + ' −' + + esc(String(m)) + + "" + ); + } + } + return parts.join(" · "); } function rowTitle(row, buildSec) { var tip = row.tip || ""; var tipTime = row.tip_time || ""; + var plus = row.vs_main_plus; + var minus = row.vs_main_minus; + var vs = ""; + if (plus != null || minus != null) { + vs = + " vs main +" + + String(plus == null ? 0 : plus) + + "/−" + + String(minus == null ? 0 : minus); + } return ( "Switch build to " + (row.branch || "") + @@ -61,7 +87,8 @@ (tipTime ? " tip=" + tipTime : "") + (buildSec != null && buildSec !== "" ? " build=" + fmtBuildSec(buildSec) - : "") + : "") + + vs ); } @@ -125,15 +152,15 @@ 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 tipHtml = metaHtml(row); var tipEl = a.querySelector(".tip"); - var wantTip = bits.length ? " " + bits.join(" · ") : ""; + var wantTip = tipHtml ? " " + tipHtml : ""; if (tipEl) { - if (tipEl.textContent !== wantTip) tipEl.textContent = wantTip; + if (tipEl.innerHTML !== wantTip) tipEl.innerHTML = wantTip; } else if (wantTip) { tipEl = document.createElement("span"); tipEl.className = "tip"; - tipEl.textContent = wantTip; + tipEl.innerHTML = wantTip; a.appendChild(tipEl); } var defEl = a.querySelector(".cascade-default"); @@ -189,15 +216,13 @@ var tipTime = row.tip_time || ""; var buildSec = row.build_seconds; var isDefault = branch === defaultBranch; - var bits = metaBits(row); + var tipHtml = metaHtml(row); var label = esc(prefix) + "" + esc(branch) + "" + - (bits.length - ? ' ' + esc(bits.join(" · ")) + "" - : "") + + (tipHtml ? ' ' + tipHtml + "" : "") + (isDefault ? ' · default' : ""); html.push( ' 0 || m > 0 || row.branch !== "main")) { + parts.push( + '+' + + esc(String(p)) + + ' −' + + esc(String(m)) + + "" + ); + } + } + return parts.join(" · "); } function rowTitle(row, buildSec) { var tip = row.tip || ""; var tipTime = row.tip_time || ""; + var plus = row.vs_main_plus; + var minus = row.vs_main_minus; + var vs = ""; + if (plus != null || minus != null) { + vs = + " vs main +" + + String(plus == null ? 0 : plus) + + "/−" + + String(minus == null ? 0 : minus); + } return ( "Switch build to " + (row.branch || "") + @@ -61,7 +87,8 @@ (tipTime ? " tip=" + tipTime : "") + (buildSec != null && buildSec !== "" ? " build=" + fmtBuildSec(buildSec) - : "") + : "") + + vs ); } @@ -125,15 +152,15 @@ 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 tipHtml = metaHtml(row); var tipEl = a.querySelector(".tip"); - var wantTip = bits.length ? " " + bits.join(" · ") : ""; + var wantTip = tipHtml ? " " + tipHtml : ""; if (tipEl) { - if (tipEl.textContent !== wantTip) tipEl.textContent = wantTip; + if (tipEl.innerHTML !== wantTip) tipEl.innerHTML = wantTip; } else if (wantTip) { tipEl = document.createElement("span"); tipEl.className = "tip"; - tipEl.textContent = wantTip; + tipEl.innerHTML = wantTip; a.appendChild(tipEl); } var defEl = a.querySelector(".cascade-default"); @@ -189,15 +216,13 @@ var tipTime = row.tip_time || ""; var buildSec = row.build_seconds; var isDefault = branch === defaultBranch; - var bits = metaBits(row); + var tipHtml = metaHtml(row); var label = esc(prefix) + "" + esc(branch) + "" + - (bits.length - ? ' ' + esc(bits.join(" · ")) + "" - : "") + + (tipHtml ? ' ' + tipHtml + "" : "") + (isDefault ? ' · default' : ""); html.push( '