From c14e215ad8b4a1cfe54eecac057edb40d400d8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 23:56:51 +0200 Subject: [PATCH] feat/dossiers-ngi: sticky bar vs-parent loc-on-roots split plus/minus 45rem --- scripts/dossiers-ngi/README.md | 14 ++-- scripts/dossiers-ngi/branch-status-hourly.py | 29 +++++--- .../_cascade/bar.css | 25 ++++--- .../_cascade/bar.js | 70 ++++++++++++------- .../cascade/_cascade/bar.css | 25 ++++--- .../cascade/_cascade/bar.js | 70 ++++++++++++------- 6 files changed, 148 insertions(+), 85 deletions(-) diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 4abcd67..dd42487 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -58,7 +58,8 @@ 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` itself) | +| `vs_parent_plus` / `vs_parent_minus` | `git diff --numstat` tip vs **parent** tip (sticky bar +grün/−rot; omitted when `parent` is empty or not in the tree, e.g. `main` and `feature/nl-net-injestion-injestor-update`) | +| `loc` | text-file line count of the tip vs the empty tree (gray `N LOC` in the plus/minus columns; only when there is no parent) | Rebuild runs **only when** `.edition-tip` ≠ current tip (timer never forces). Each run also: `git fetch --prune`, `worktree prune`, drop stale @@ -105,13 +106,14 @@ rsync -avz --exclude .DS_Store "$SITE/branch-status.json" "$WWW/branch-status.js `bar.js` fetches `/branch-status.json` and draws: ``` -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 +feature/nl-net-injestion-injestor-update … N LOC +main … N LOC +├── feat/sm-content … +N −M # vs main +│ └── feat/website-features … +N −M # vs sm-content +└── project_template … +N −M # vs main ``` -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). +Visible cells: name (tree prefix + branch) · tip `monDD HH:MM` · `Ns monDD HH:MM` (build seconds + freeze time) · plus column · minus column. `+N −M` vs the **parent** tip, in two right-aligned columns so `+898` and `+1200` share an edge. Roots with no parent show gray `N` in the plus column and `LOC` in the minus column. No `@sha`, no `default` label (`data-default` stays for flask/cookie). Inner tree `max-width: 45rem` (same as site `#navigation` / `#content` 45em at root 16px), so names line up with About and the minus column with the page edge; below 45rem the tree is full width with a small pad. Rear cells are right-aligned `tabular-nums`. Poll every 30s **updates when/built/plus/minus 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 5230f9c..910eb82 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -258,6 +258,17 @@ def diff_numstat(repo: Path, a: str, b: str) -> tuple[int, int]: return plus, minus +def empty_tree_oid(repo: Path) -> str: + cp = git(repo, "hash-object", "-t", "tree", "/dev/null") + return (cp.stdout or "").strip() + + +def loc_at_tip(repo: Path, tip: str, empty: str) -> int: + """Text-file line count of tip vs the empty tree.""" + plus, _ = diff_numstat(repo, empty, tip) + return plus + + 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) @@ -564,10 +575,7 @@ 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() - } + empty = empty_tree_oid(repo) branches_out = [] for row in rows: name = row["branch"] @@ -593,11 +601,14 @@ 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 + # sticky bar: +grün/−rot vs parent tip; gray loc when no parent in tree + 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 + else: + entry["loc"] = loc_at_tip(repo, row["tip"], empty) 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 fb485be..94d3b58 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.75rem 0.25rem; + padding: 0.2rem 0 0.25rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; @@ -22,8 +22,9 @@ html { line-height: 1.25; } .cascade-tree { + /* 45rem = site #navigation/#content 45em at root 16px (bar font is 11px) */ width: 100%; - max-width: min(80rem, calc(100vw - 1.5rem)); + max-width: 45rem; margin: 0 auto; box-sizing: border-box; } @@ -35,8 +36,8 @@ html { } a.cascade-tree-row { display: grid; - grid-template-columns: minmax(12rem, 1fr) 7.5rem 11.5rem 7rem; - column-gap: 0.85rem; + grid-template-columns: minmax(6rem, 1fr) 7.2rem 10.5rem 4.4rem 3.6rem; + column-gap: 0.55rem; align-items: baseline; color: #7ec8ff; text-decoration: none; @@ -68,7 +69,8 @@ a.cascade-tree-row.is-active:hover { } .cascade-when, .cascade-built, -.cascade-diff { +.cascade-plus, +.cascade-minus { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; @@ -78,17 +80,17 @@ a.cascade-tree-row.is-active:hover { display: none; } -@media (max-width: 40em) { +@media (max-width: 45rem) { .cascade-bar { - padding: 0.15rem 0.35rem 0.2rem; + padding: 0.15rem 0.4rem 0.2rem; font-size: 10px; } .cascade-tree { max-width: 100%; } a.cascade-tree-row { - grid-template-columns: minmax(8rem, 1fr) 6.5rem 10rem 6rem; - column-gap: 0.45rem; + grid-template-columns: minmax(5rem, 1fr) 5.6rem 8.4rem 3.6rem 3rem; + column-gap: 0.35rem; } } @@ -100,3 +102,8 @@ a.cascade-tree-row.is-active:hover { color: #f85149; font-weight: 600; } +.cascade-bar .diff-loc, +.cascade-bar .diff-loc-unit { + color: #8a96a3; + font-weight: 600; +} diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js index 019adf9..720e070 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js @@ -59,20 +59,30 @@ return sec || at; } - 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 plusHtml(row) { + if (row.vs_parent_plus != null) { + var p = Number(row.vs_parent_plus); + if (!isFinite(p)) return ""; + return '+' + esc(String(p)) + ""; + } + if (row.loc != null) { + var n = Number(row.loc); + if (!isFinite(n)) return ""; + return '' + esc(String(n)) + ""; + } + return ""; + } + + function minusHtml(row) { + if (row.vs_parent_minus != null) { + var m = Number(row.vs_parent_minus); + if (!isFinite(m)) return ""; + return '−' + esc(String(m)) + ""; + } + if (row.loc != null) { + return 'LOC'; + } + return ""; } function setText(el, text) { @@ -84,15 +94,15 @@ function rowTitle(row) { 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) { + if (row.vs_parent_plus != null || row.vs_parent_minus != null) { vs = - " vs main +" + - String(plus == null ? 0 : plus) + + " vs parent +" + + String(row.vs_parent_plus == null ? 0 : row.vs_parent_plus) + "/−" + - String(minus == null ? 0 : minus); + String(row.vs_parent_minus == null ? 0 : row.vs_parent_minus); + } else if (row.loc != null) { + vs = " loc=" + String(row.loc); } var built = builtText(row); return ( @@ -181,10 +191,15 @@ 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; + var plusEl = a.querySelector(".cascade-plus"); + if (plusEl) { + var ph = plusHtml(row); + if (plusEl.innerHTML !== ph) plusEl.innerHTML = ph; + } + var minusEl = a.querySelector(".cascade-minus"); + if (minusEl) { + var mh = minusHtml(row); + if (minusEl.innerHTML !== mh) minusEl.innerHTML = mh; } a.setAttribute("title", rowTitle(row)); } @@ -252,8 +267,11 @@ '' + esc(builtText(row)) + "" + - '' + - diffHtml(row) + + '' + + plusHtml(row) + + "" + + '' + + minusHtml(row) + "" + "" ); diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css index fb485be..94d3b58 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.75rem 0.25rem; + padding: 0.2rem 0 0.25rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; @@ -22,8 +22,9 @@ html { line-height: 1.25; } .cascade-tree { + /* 45rem = site #navigation/#content 45em at root 16px (bar font is 11px) */ width: 100%; - max-width: min(80rem, calc(100vw - 1.5rem)); + max-width: 45rem; margin: 0 auto; box-sizing: border-box; } @@ -35,8 +36,8 @@ html { } a.cascade-tree-row { display: grid; - grid-template-columns: minmax(12rem, 1fr) 7.5rem 11.5rem 7rem; - column-gap: 0.85rem; + grid-template-columns: minmax(6rem, 1fr) 7.2rem 10.5rem 4.4rem 3.6rem; + column-gap: 0.55rem; align-items: baseline; color: #7ec8ff; text-decoration: none; @@ -68,7 +69,8 @@ a.cascade-tree-row.is-active:hover { } .cascade-when, .cascade-built, -.cascade-diff { +.cascade-plus, +.cascade-minus { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; @@ -78,17 +80,17 @@ a.cascade-tree-row.is-active:hover { display: none; } -@media (max-width: 40em) { +@media (max-width: 45rem) { .cascade-bar { - padding: 0.15rem 0.35rem 0.2rem; + padding: 0.15rem 0.4rem 0.2rem; font-size: 10px; } .cascade-tree { max-width: 100%; } a.cascade-tree-row { - grid-template-columns: minmax(8rem, 1fr) 6.5rem 10rem 6rem; - column-gap: 0.45rem; + grid-template-columns: minmax(5rem, 1fr) 5.6rem 8.4rem 3.6rem 3rem; + column-gap: 0.35rem; } } @@ -100,3 +102,8 @@ a.cascade-tree-row.is-active:hover { color: #f85149; font-weight: 600; } +.cascade-bar .diff-loc, +.cascade-bar .diff-loc-unit { + color: #8a96a3; + font-weight: 600; +} diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js index 019adf9..720e070 100644 --- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js @@ -59,20 +59,30 @@ return sec || at; } - 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 plusHtml(row) { + if (row.vs_parent_plus != null) { + var p = Number(row.vs_parent_plus); + if (!isFinite(p)) return ""; + return '+' + esc(String(p)) + ""; + } + if (row.loc != null) { + var n = Number(row.loc); + if (!isFinite(n)) return ""; + return '' + esc(String(n)) + ""; + } + return ""; + } + + function minusHtml(row) { + if (row.vs_parent_minus != null) { + var m = Number(row.vs_parent_minus); + if (!isFinite(m)) return ""; + return '−' + esc(String(m)) + ""; + } + if (row.loc != null) { + return 'LOC'; + } + return ""; } function setText(el, text) { @@ -84,15 +94,15 @@ function rowTitle(row) { 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) { + if (row.vs_parent_plus != null || row.vs_parent_minus != null) { vs = - " vs main +" + - String(plus == null ? 0 : plus) + + " vs parent +" + + String(row.vs_parent_plus == null ? 0 : row.vs_parent_plus) + "/−" + - String(minus == null ? 0 : minus); + String(row.vs_parent_minus == null ? 0 : row.vs_parent_minus); + } else if (row.loc != null) { + vs = " loc=" + String(row.loc); } var built = builtText(row); return ( @@ -181,10 +191,15 @@ 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; + var plusEl = a.querySelector(".cascade-plus"); + if (plusEl) { + var ph = plusHtml(row); + if (plusEl.innerHTML !== ph) plusEl.innerHTML = ph; + } + var minusEl = a.querySelector(".cascade-minus"); + if (minusEl) { + var mh = minusHtml(row); + if (minusEl.innerHTML !== mh) minusEl.innerHTML = mh; } a.setAttribute("title", rowTitle(row)); } @@ -252,8 +267,11 @@ '' + esc(builtText(row)) + "" + - '' + - diffHtml(row) + + '' + + plusHtml(row) + + "" + + '' + + minusHtml(row) + "" + "" );