feat/dossiers-ngi: inject-editions-bar.sh for freeze HTML missing include

This commit is contained in:
Hernâni Marques 2026-09-30 22:15:38 +02:00
parent b24dd17ee9
commit 3feb694084
No known key found for this signature in database
9 changed files with 218 additions and 46 deletions

View file

@ -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:

View file

@ -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),

View file

@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Inject cascade-bar include into every editions/<slug> 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"(<body[^>]*>)", 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

View file

@ -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 {

View file

@ -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 @@
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><code>flask</code></a>' +
'<span class="tip"> (branch-status unavailable)</span>';
markActive(mount, cookieEdition());
pinBar();
}
});
}
@ -277,4 +292,7 @@
setInterval(function () {
loadStatus(true);
}, POLL_MS);
window.addEventListener("resize", function () {
pinBar();
});
})();

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cascade editions</title>
<link rel="stylesheet" href="/_cascade/bar.css">
</head>
<body>
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
</nav>
<p>Stuck on an edition without a bar? Pick a build above, or <a href="/_cascade/use/flask">reset to flask</a>.</p>
<script src="/_cascade/bar.js" defer></script>
</body>
</html>

View file

@ -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 {

View file

@ -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 @@
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><code>flask</code></a>' +
'<span class="tip"> (branch-status unavailable)</span>';
markActive(mount, cookieEdition());
pinBar();
}
});
}
@ -277,4 +292,7 @@
setInterval(function () {
loadStatus(true);
}, POLL_MS);
window.addEventListener("resize", function () {
pinBar();
});
})();

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>cascade editions</title>
<link rel="stylesheet" href="/_cascade/bar.css">
</head>
<body>
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
</nav>
<p>Stuck on an edition without a bar? Pick a build above, or <a href="/_cascade/use/flask">reset to flask</a>.</p>
<script src="/_cascade/bar.js" defer></script>
</body>
</html>