From 52e5c1d3286da8443dc2254f512a037dc8266dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 22:25:38 +0200 Subject: [PATCH] feat(dossiers-ngi): inject-editions-bar.sh for freeze HTML missing include --- scripts/dossiers-ngi/README.md | 17 ++++ scripts/dossiers-ngi/branch-status-hourly.py | 13 ++- scripts/dossiers-ngi/inject-editions-bar.sh | 80 +++++++++++++++++++ .../_cascade/bar.css | 37 +++++---- .../_cascade/bar.js | 24 +++++- .../_cascade/index.html | 16 ++++ .../cascade/_cascade/bar.css | 37 +++++---- .../cascade/_cascade/bar.js | 24 +++++- .../cascade/_cascade/index.html | 16 ++++ 9 files changed, 218 insertions(+), 46 deletions(-) create mode 100755 scripts/dossiers-ngi/inject-editions-bar.sh create mode 100644 sites/dossiers.ngi.hacktivism.ch/_cascade/index.html create mode 100644 sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/index.html diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 6479398..fbfa0b0 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -71,6 +71,23 @@ bash scripts/dossiers-ngi/smoke-branch-tree.sh # → SMOKE_OK (temp origin + smoke/temp-sibling deleted) ``` +## Edition freeze missing the bar (cookie trap) + +`feat/website-views` freeze can omit `{% include "_cascade/bar.html" %}`. +Cookie `tdpd_edition` then has no switcher. **No zypper / Caddy plugin.** +Way back: `https://dossiers.ngi.hacktivism.ch/_cascade/use/flask` +or `/_cascade/` (`index.html` switcher). + +On koopa (hernani, no root): + +```bash +bash scripts/dossiers-ngi/inject-editions-bar.sh +``` + +Hourly rebuild injects on skip **and** freeze-error (keep existing HTML + include). + +The bar is `position: fixed` (not sticky); `100vw` overflow is avoided. + ## Deploy landing (Freigabe) Do **not** `--delete` the whole www root. Sync: diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index f9caaec..d4b3a6c 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -239,7 +239,7 @@ 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: return 0, 0 - cp = git(repo, "diff", "--numstat", main_tip, tip, check=False) + cp = git(repo, "diff", "--numstat", parent_tip, tip, check=False) if cp.returncode != 0: return 0, 0 plus = minus = 0 @@ -392,13 +392,19 @@ def run_freeze(work: Path, branch: str, slug: str) -> None: def inject_bar(edition_dir: Path) -> None: if not INJECT.is_file(): + print(f"WARN: inject script missing: {INJECT}", file=sys.stderr) return - subprocess.run( + cp = subprocess.run( [sys.executable, str(INJECT), str(edition_dir)], check=False, capture_output=True, text=True, ) + if cp.stdout: + print(cp.stdout, end="" if str(cp.stdout).endswith("\n") else "\n", file=sys.stderr) + if cp.returncode != 0: + err = (cp.stderr or "").strip()[:300] + print(f"WARN: inject_bar {edition_dir}: {err}", file=sys.stderr) def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict: @@ -425,6 +431,7 @@ def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict: cp = git(repo, "worktree", "add", "--detach", str(wt), tip, check=False) if cp.returncode != 0: ensure_placeholder(editions, slug, tip) + inject_bar(dest) return { "status": f"error:worktree:{cp.stderr.strip()[:200]}", "seconds": round(time.perf_counter() - t0, 3), @@ -434,6 +441,7 @@ def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict: build = wt / "website" / "build" if not build.is_dir(): ensure_placeholder(editions, slug, tip) + inject_bar(dest) return {"status": "error:no-build", "seconds": round(time.perf_counter() - t0, 3)} dest.mkdir(parents=True, exist_ok=True) @@ -462,6 +470,7 @@ def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict: return {"status": "built", "seconds": secs} except Exception as exc: # noqa: BLE001 — hourly must continue ensure_placeholder(editions, slug, tip) + inject_bar(dest) return { "status": f"error:{type(exc).__name__}:{exc}", "seconds": round(time.perf_counter() - t0, 3), diff --git a/scripts/dossiers-ngi/inject-editions-bar.sh b/scripts/dossiers-ngi/inject-editions-bar.sh new file mode 100755 index 0000000..f30790c --- /dev/null +++ b/scripts/dossiers-ngi/inject-editions-bar.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Inject cascade-bar include into every editions/ freeze on koopa. +# No zypper, no Caddy plugin, no root — hernani must own $WWW. +# +# bash scripts/dossiers-ngi/inject-editions-bar.sh +# WWW=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch bash … +# +# Way back without the bar: https://dossiers.ngi.hacktivism.ch/_cascade/use/flask +set -euo pipefail + +WWW="${WWW:-/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch}" +ED="${ED:-$WWW/editions}" + +if [[ ! -d "$ED" ]]; then + echo "FAIL: not a dir: $ED" >&2 + exit 2 +fi + +python3 - "$ED" <<'PY' +from __future__ import annotations + +import re +import sys +from pathlib import Path + +INCLUDE = '{% include "_cascade/bar.html" . %}' +BODY_RE = re.compile(r"(]*>)", re.IGNORECASE) + + +def inject(text: str) -> str | None: + if INCLUDE in text or "_cascade/bar" in text: + return None + m = BODY_RE.search(text) + if not m: + return None + i = m.end() + return text[:i] + "\n" + INCLUDE + "\n" + text[i:] + + +root = Path(sys.argv[1]).resolve() +scanned = changed = skipped = nobody = 0 +print(f"ED={root}") +for slug_dir in sorted(p for p in root.iterdir() if p.is_dir()): + for path in slug_dir.rglob("*.html"): + posix = path.as_posix() + if "/_cascade/" in posix + "/": + continue + scanned += 1 + raw = path.read_text(encoding="utf-8", errors="ignore") + if INCLUDE in raw or "_cascade/bar" in raw: + skipped += 1 + continue + out = inject(raw) + if out is None: + nobody += 1 + print(f"NOBODY {path.relative_to(root)}") + continue + path.write_text(out, encoding="utf-8") + changed += 1 + print(f"INJECT {path.relative_to(root)}") + +print(f"OK scanned={scanned} injected={changed} already={skipped} nobody={nobody}") +if nobody: + sys.exit(1) +PY + +echo "----- index.html bar? -----" +python3 - "$ED" <<'PY' +from pathlib import Path +import sys +root = Path(sys.argv[1]) +for d in sorted(p for p in root.iterdir() if p.is_dir()): + idx = d / "index.html" + if not idx.is_file(): + print(f"MISSING {d.name}/index.html") + continue + t = idx.read_text(encoding="utf-8", errors="ignore") + flag = "BAR" if ("cascade-bar" in t or "_cascade/bar" in t) else "NOBAR" + print(f"{flag} {d.name}") +PY diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css index ae291e2..db35ae9 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css @@ -1,20 +1,25 @@ +html { + padding-top: var(--cascade-pad, 0px); +} .cascade-bar { - position: sticky; + position: fixed; top: 0; - z-index: 10; + left: 0; + right: 0; + z-index: 1000; 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; + width: 100%; + max-width: none; + margin: 0; + padding: 0.2rem 0.5rem 0.25rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 12px; - line-height: 1.45; + font-size: 11px; + line-height: 1.25; } .cascade-tree { width: 100%; @@ -31,7 +36,7 @@ a.cascade-tree-row { display: block; color: #7ec8ff; text-decoration: none; - padding: 0.05rem 0.15rem; + padding: 0; border-radius: 2px; } a.cascade-tree-row:hover { @@ -52,29 +57,23 @@ a.cascade-tree-row.is-active:hover { } .cascade-bar .tip { color: #8a96a3; - white-space: pre-wrap; + white-space: nowrap; } .cascade-bar .cascade-default { color: #6a9080; } .cascade-timing { - margin-top: 0.35rem; - color: #6a7888; - font-size: 11px; + display: none; } @media (max-width: 40em) { .cascade-bar { - margin-top: -1rem; - padding: 0.45rem 0.5rem 0.55rem; - font-size: 11px; + padding: 0.15rem 0.35rem 0.2rem; + font-size: 10px; } .cascade-tree { max-width: 100%; } - .cascade-timing { - font-size: 10px; - } } .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 00fbc83..23351e7 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js @@ -44,10 +44,10 @@ var buildSec = row.build_seconds; var parts = []; if (tip) parts.push(esc("@" + tip)); - if (tipTime) parts.push(esc("tip " + shortTime(tipTime))); + if (tipTime) parts.push(esc(shortTime(tipTime))); if (buildSec != null && buildSec !== "") - parts.push(esc("build " + fmtBuildSec(buildSec))); - if (builtAt) parts.push(esc("at " + shortTime(builtAt))); + 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; @@ -111,6 +111,17 @@ return parts.join("\n"); } + function pinBar(timingMsg) { + var bar = document.getElementById("cascade-bar"); + if (!bar) return; + if (timingMsg) { + bar.setAttribute("data-timing", timingMsg); + bar.setAttribute("title", timingMsg); + } + var h = bar.offsetHeight || 0; + document.documentElement.style.setProperty("--cascade-pad", h + "px"); + } + function markActive(root, edition) { var links = root.querySelectorAll("a[data-edition]"); for (var i = 0; i < links.length; i++) { @@ -190,6 +201,7 @@ timingEl.parentNode.removeChild(timingEl); } markActive(mount, cookieEdition()); + pinBar(t); return true; } @@ -199,6 +211,7 @@ var list = (data && data.branches) || []; if (!list.length) { mount.textContent = "No branches in branch-status.json"; + pinBar(); return; } if (soft && mount.getAttribute("data-structure") === structureKey(list)) { @@ -251,6 +264,7 @@ mount.innerHTML = html.join(""); mount.setAttribute("data-structure", structureKey(list)); markActive(mount, cookieEdition()); + pinBar(t); } function loadStatus(soft) { @@ -269,6 +283,7 @@ 'flask' + ' (branch-status unavailable)'; markActive(mount, cookieEdition()); + pinBar(); } }); } @@ -277,4 +292,7 @@ setInterval(function () { loadStatus(true); }, POLL_MS); + window.addEventListener("resize", function () { + pinBar(); + }); })(); diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/index.html b/sites/dossiers.ngi.hacktivism.ch/_cascade/index.html new file mode 100644 index 0000000..036ead3 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/index.html @@ -0,0 +1,16 @@ + + + + + +cascade editions + + + + +

Stuck on an edition without a bar? Pick a build above, or reset to flask.

+ + + diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css index ae291e2..db35ae9 100644 --- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css @@ -1,20 +1,25 @@ +html { + padding-top: var(--cascade-pad, 0px); +} .cascade-bar { - position: sticky; + position: fixed; top: 0; - z-index: 10; + left: 0; + right: 0; + z-index: 1000; 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; + width: 100%; + max-width: none; + margin: 0; + padding: 0.2rem 0.5rem 0.25rem; background: #10141a; border-bottom: 1px solid #243040; color: #c8d0d8; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 12px; - line-height: 1.45; + font-size: 11px; + line-height: 1.25; } .cascade-tree { width: 100%; @@ -31,7 +36,7 @@ a.cascade-tree-row { display: block; color: #7ec8ff; text-decoration: none; - padding: 0.05rem 0.15rem; + padding: 0; border-radius: 2px; } a.cascade-tree-row:hover { @@ -52,29 +57,23 @@ a.cascade-tree-row.is-active:hover { } .cascade-bar .tip { color: #8a96a3; - white-space: pre-wrap; + white-space: nowrap; } .cascade-bar .cascade-default { color: #6a9080; } .cascade-timing { - margin-top: 0.35rem; - color: #6a7888; - font-size: 11px; + display: none; } @media (max-width: 40em) { .cascade-bar { - margin-top: -1rem; - padding: 0.45rem 0.5rem 0.55rem; - font-size: 11px; + padding: 0.15rem 0.35rem 0.2rem; + font-size: 10px; } .cascade-tree { max-width: 100%; } - .cascade-timing { - font-size: 10px; - } } .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 00fbc83..23351e7 100644 --- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js @@ -44,10 +44,10 @@ var buildSec = row.build_seconds; var parts = []; if (tip) parts.push(esc("@" + tip)); - if (tipTime) parts.push(esc("tip " + shortTime(tipTime))); + if (tipTime) parts.push(esc(shortTime(tipTime))); if (buildSec != null && buildSec !== "") - parts.push(esc("build " + fmtBuildSec(buildSec))); - if (builtAt) parts.push(esc("at " + shortTime(builtAt))); + 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; @@ -111,6 +111,17 @@ return parts.join("\n"); } + function pinBar(timingMsg) { + var bar = document.getElementById("cascade-bar"); + if (!bar) return; + if (timingMsg) { + bar.setAttribute("data-timing", timingMsg); + bar.setAttribute("title", timingMsg); + } + var h = bar.offsetHeight || 0; + document.documentElement.style.setProperty("--cascade-pad", h + "px"); + } + function markActive(root, edition) { var links = root.querySelectorAll("a[data-edition]"); for (var i = 0; i < links.length; i++) { @@ -190,6 +201,7 @@ timingEl.parentNode.removeChild(timingEl); } markActive(mount, cookieEdition()); + pinBar(t); return true; } @@ -199,6 +211,7 @@ var list = (data && data.branches) || []; if (!list.length) { mount.textContent = "No branches in branch-status.json"; + pinBar(); return; } if (soft && mount.getAttribute("data-structure") === structureKey(list)) { @@ -251,6 +264,7 @@ mount.innerHTML = html.join(""); mount.setAttribute("data-structure", structureKey(list)); markActive(mount, cookieEdition()); + pinBar(t); } function loadStatus(soft) { @@ -269,6 +283,7 @@ 'flask' + ' (branch-status unavailable)'; markActive(mount, cookieEdition()); + pinBar(); } }); } @@ -277,4 +292,7 @@ setInterval(function () { loadStatus(true); }, POLL_MS); + window.addEventListener("resize", function () { + pinBar(); + }); })(); diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/index.html new file mode 100644 index 0000000..036ead3 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/index.html @@ -0,0 +1,16 @@ + + + + + +cascade editions + + + + +

Stuck on an edition without a bar? Pick a build above, or reset to flask.

+ + +