From 6e57fceb190c878c357ac688f474e112d7296f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Tue, 29 Sep 2026 23:26:13 +0200 Subject: [PATCH 01/43] ops/dossiers-ngi: hourly branch-status builder + systemd timer --- .../user/dossiers-ngi-branch-status.service | 19 +++++ .../user/dossiers-ngi-branch-status.timer | 13 +++ scripts/dossiers-ngi/README.md | 42 ++++++++++ scripts/dossiers-ngi/branch-status-hourly.py | 84 +++++++++++++++++++ .../branch-status.json | 37 ++++++++ 5 files changed, 195 insertions(+) create mode 100644 configs/systemd/user/dossiers-ngi-branch-status.service create mode 100644 configs/systemd/user/dossiers-ngi-branch-status.timer create mode 100644 scripts/dossiers-ngi/README.md create mode 100755 scripts/dossiers-ngi/branch-status-hourly.py create mode 100644 sites/dossiers.ngi.hacktivism.ch/branch-status.json diff --git a/configs/systemd/user/dossiers-ngi-branch-status.service b/configs/systemd/user/dossiers-ngi-branch-status.service new file mode 100644 index 0000000..a6ad491 --- /dev/null +++ b/configs/systemd/user/dossiers-ngi-branch-status.service @@ -0,0 +1,19 @@ +[Unit] +Description=Write dossiers.ngi branch-status.json from tdpd tips +Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md +After=default.target + +[Service] +Type=oneshot +Nice=10 +RemainAfterExit=no +Environment=TZ=Europe/Zurich +Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log +# Override on koopa if the tdpd checkout path differs +Environment=TDPD_ROOT=%h/git/src/ngi/tech-dossier-project-data +EnvironmentFile=-%h/.config/dossiers-ngi/branch-status.env +ExecStart=/usr/bin/python3 %h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/branch-status-hourly.py +SuccessExitStatus=0 1 + +[Install] +WantedBy=default.target diff --git a/configs/systemd/user/dossiers-ngi-branch-status.timer b/configs/systemd/user/dossiers-ngi-branch-status.timer new file mode 100644 index 0000000..e6c701b --- /dev/null +++ b/configs/systemd/user/dossiers-ngi-branch-status.timer @@ -0,0 +1,13 @@ +[Unit] +Description=Timer · dossiers.ngi branch-status JSON (hourly) +Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md + +[Timer] +OnBootSec=3min +OnUnitActiveSec=1h +AccuracySec=2min +Persistent=true +Unit=dossiers-ngi-branch-status.service + +[Install] +WantedBy=timers.target diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md new file mode 100644 index 0000000..11da294 --- /dev/null +++ b/scripts/dossiers-ngi/README.md @@ -0,0 +1,42 @@ +# dossiers-ngi — cascade landing + hourly branch-status + +SoT for the blank cascade landing on **dossiers.ngi.hacktivism.ch** +(path roots only; no dossiers.2 / dossiers.3 ownership here). + +| Path | Role | +|------|------| +| `sites/dossiers.ngi.hacktivism.ch/` | Landing HTML (EN, ASCII ladder) | +| `scripts/dossiers-ngi/branch-status-hourly.py` | Read tips from tdpd checkout → `branch-status.json` | +| `configs/systemd/user/dossiers-ngi-branch-status.{timer,service}` | Hourly oneshot on koopa | + +## Env + +| Var | Default | Meaning | +|-----|---------|---------| +| `TDPD_ROOT` | `$HOME/git/src/ngi/tech-dossier-project-data` | tech-dossier-project-data checkout for tip SHAs | +| `ADMIN_LOG` | (script derives from `__file__`) | koopa-admin-log root | + +On koopa the clone often lives at `%h/src/koopa/koopa-admin-log`; set +`TDPD_ROOT` in the service `Environment=` if the tdpd path differs. + +## Manual run + +```bash +python3 scripts/dossiers-ngi/branch-status-hourly.py +# → sites/dossiers.ngi.hacktivism.ch/branch-status.json +``` + +## Deploy landing (Freigabe) + +```text +hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/ +``` + +rsync from `sites/dossiers.ngi.hacktivism.ch/` — Freigabe this session required. +Hourly JSON can sit next to the HTML once the timer is installed on koopa +(Freigabe for host mutation). + +## Ownership + +Hourly builder belongs in **kal** (this tree). Do **not** put +`scripts/branch-status-hourly.py` back into tdpd. diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py new file mode 100755 index 0000000..feae12a --- /dev/null +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Emit branch-status.json for the dossiers.ngi path-root ladder. + +Reads tip SHAs from the tech-dossier-project-data git checkout +(TDPD_ROOT / default). Writes under the kal landing SoT: + + sites/dossiers.ngi.hacktivism.ch/branch-status.json + +Path roots only under https://dossiers.ngi.hacktivism.ch/ +(/main/, /feat-sm-content/, /feat-website-features/, + /feat-website-design/, /feat-website-views/). +Single public host only. Hourly builder lives in kal, not tdpd. +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +ADMIN_LOG = Path(__file__).resolve().parents[2] +OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json" +HOST = "https://dossiers.ngi.hacktivism.ch" + +# Default local clone; override with TDPD_ROOT (koopa may use a different path). +DEFAULT_TDPD = Path.home() / "git" / "src" / "ngi" / "tech-dossier-project-data" + +LADDER = [ + ("main", "", f"{HOST}/main/"), + ("feat/sm-content", "main", f"{HOST}/feat-sm-content/"), + ("feat/website-features", "feat/sm-content", f"{HOST}/feat-website-features/"), + ("feat/website-design", "feat/website-features", f"{HOST}/feat-website-design/"), + ("feat/website-views", "feat/website-design", f"{HOST}/feat-website-views/"), +] + + +def tdpd_root() -> Path: + raw = os.environ.get("TDPD_ROOT", "").strip() + return Path(raw).expanduser() if raw else DEFAULT_TDPD + + +def tip_of(repo: Path, branch: str) -> str: + try: + out = subprocess.check_output( + ["git", "-C", str(repo), "rev-parse", "--short", branch], + stderr=subprocess.DEVNULL, + text=True, + ).strip() + return out + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + return "" + + +def main() -> int: + repo = tdpd_root() + if not (repo / ".git").exists() and not repo.is_dir(): + print(f"TDPD_ROOT missing or not a dir: {repo}", file=sys.stderr) + return 1 + branches = [] + for name, parent, url in LADDER: + branches.append( + { + "branch": name, + "tip": tip_of(repo, name), + "parent": parent, + "url": url, + } + ) + payload = { + "generated": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"), + "host": HOST, + "tdpd_root": str(repo), + "branches": branches, + } + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + print(f"wrote {OUT}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sites/dossiers.ngi.hacktivism.ch/branch-status.json b/sites/dossiers.ngi.hacktivism.ch/branch-status.json new file mode 100644 index 0000000..74c3df4 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/branch-status.json @@ -0,0 +1,37 @@ +{ + "generated": "2026-09-30T02:30:50+02:00", + "host": "https://dossiers.ngi.hacktivism.ch", + "tdpd_root": "/Users/newkamek/git/src/ngi/tech-dossier-project-data", + "branches": [ + { + "branch": "main", + "tip": "d2fbd79", + "parent": "", + "url": "https://dossiers.ngi.hacktivism.ch/main/" + }, + { + "branch": "feat/sm-content", + "tip": "089ef1f", + "parent": "main", + "url": "https://dossiers.ngi.hacktivism.ch/feat-sm-content/" + }, + { + "branch": "feat/website-features", + "tip": "53a3176", + "parent": "feat/sm-content", + "url": "https://dossiers.ngi.hacktivism.ch/feat-website-features/" + }, + { + "branch": "feat/website-design", + "tip": "2e26c80", + "parent": "feat/website-features", + "url": "https://dossiers.ngi.hacktivism.ch/feat-website-design/" + }, + { + "branch": "feat/website-views", + "tip": "bbafc38", + "parent": "feat/website-design", + "url": "https://dossiers.ngi.hacktivism.ch/feat-website-views/" + } + ] +} From 9035b9600bd77d7f681c324fe78226dab2361704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 01:16:45 +0200 Subject: [PATCH 02/43] docs: daylog tech-dossier Freeze cascade tip correction --- 2026/2026-09-28--tech-dossier-freeze-cascade.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/2026/2026-09-28--tech-dossier-freeze-cascade.md b/2026/2026-09-28--tech-dossier-freeze-cascade.md index 776a34e..b24c6cd 100644 --- a/2026/2026-09-28--tech-dossier-freeze-cascade.md +++ b/2026/2026-09-28--tech-dossier-freeze-cascade.md @@ -9,7 +9,7 @@ Ladder (each tip autonomously `make html`; each depends on the previous): | 2 | `feat/sm-content` | `089ef1f` | http://127.0.0.1:8102/ | `dossiers.3.ngi.hacktivism.ch` (optional) | | 3 | `feat/website-features` | `53a3176` | http://127.0.0.1:8103/ | `dossiers.2.ngi.hacktivism.ch` (optional) | | 4 | `feat/website-design` | `2e26c80` | http://127.0.0.1:8104/ | — (no public host after views deploy) | -| 5 | `feat/website-views` | `7aca0e8` (Semigrafik `1b2fcf7`; unpushed, ahead origin `bbafc38` by 2) | http://127.0.0.1:8105/ | path roots under blank landing (see Live) | +| 5 | `feat/website-views` | `bbafc38` (origin tip; local soft-reset dropped bad `1b2fcf7`/`7aca0e8`) | http://127.0.0.1:8105/ | path roots under blank landing (see Live) | Caddy roots (SoT `scripts/caddy/Caddyfile.koopa-live-wdGet`): @@ -29,9 +29,10 @@ basic_auth: `scripts/caddy/apply-dossiers-ngi-basicauth.sh` ## Live -- [dossiers.ngi](https://dossiers.ngi.hacktivism.ch/) ← **blank cascade landing** (kal tip `56ee97a`, sites/dossiers.ngi path roots; HTTP 200 SIZE≈3505) -- Path Unterseiten: `/main/`, `/feat-sm-content/`, `/feat-website-features/`, `/feat-website-design/`, `/feat-website-views/` -- Local freeze Semigrafik + hourly branch-status: tdpd `feat/website-views` @ `7aca0e8` ← `1b2fcf7` (unpushed) +- [dossiers.ngi](https://dossiers.ngi.hacktivism.ch/) ← **blank cascade landing** (kal `sites/dossiers.ngi.hacktivism.ch/`; EN + ASCII ladder; views tip cite `bbafc38`) +- Path editions: `/main/`, `/feat-sm-content/`, `/feat-website-features/`, `/feat-website-design/`, `/feat-website-views/` +- Hourly branch-status builder: **kal** `scripts/dossiers-ngi/branch-status-hourly.py` → `sites/…/branch-status.json` (+ user timer); **not** inside tdpd +- tdpd `feat/website-views` local HEAD = origin = `bbafc38` (NO_PUSH; bad tips discarded locally) - [dossiers.2.ngi](https://dossiers.2.ngi.hacktivism.ch/) ← website-features (optional) - [dossiers.3.ngi](https://dossiers.3.ngi.hacktivism.ch/) ← sm-content (optional) @@ -43,6 +44,8 @@ Earlier primary: rsync `website/build/` from tip `feat/website-views` → **Result 2026-09-28 ~22:25+02 (design @ `2e26c80`):** `RSYNC_OK` — ~2698 files / ~27M; live `dossier/sm/index.html` 41693 bytes. -**Result 2026-09-29 ~04:30+02 (views Semigrafik era):** `RSYNC_OK` — live `/` HTTP 200; Semigrafik ladder on dossiers.ngi; later remade as tdpd tips `1b2fcf7` → `7aca0e8` (hourly builder). Push offen (eigene Freigabe). +**Result 2026-09-29 ~04:30+02 (views-era freeze):** `RSYNC_OK` — live `/` HTTP 200. Local tdpd later briefly carried wrong tips `1b2fcf7` → `7aca0e8` (ladder rewrite + hourly in tdpd); those were **dropped** locally back to `bbafc38` (TODO=430). Never push those tips. -**Result 2026-09-29 (blank landing kal @ `56ee97a`):** `RSYNC_OK` — live `/` HTTP 200 SIZE=3505 title `cascade landing`; path-root Unterseiten owned by koopa-admin-log `sites/dossiers.ngi.hacktivism.ch/`; NO_PUSH. +**Result 2026-09-29 (blank landing kal @ `56ee97a`):** `RSYNC_OK` — live `/` HTTP 200 SIZE=3505 title `cascade landing`; path-root editions owned by koopa-admin-log `sites/dossiers.ngi.hacktivism.ch/`; NO_PUSH. + +**TODO=430 follow-up (local, NO_PUSH):** kal landing EN/ASCII + tip cite `bbafc38`; hourly builder + systemd under kal; daylog tip table corrected. Deploy landing / enable timer on koopa needs Freigabe. From 2b9f6c6b8305c11a756cbefa456237d7fed8a23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Tue, 29 Sep 2026 23:27:19 +0200 Subject: [PATCH 03/43] ops: dossiers-ngi hourly branch-status builder + systemd timer --- .../user/dossiers-ngi-branch-status.service | 19 +++++ .../user/dossiers-ngi-branch-status.timer | 13 +++ scripts/dossiers-ngi/README.md | 42 ++++++++++ scripts/dossiers-ngi/branch-status-hourly.py | 84 +++++++++++++++++++ .../branch-status.json | 37 ++++++++ 5 files changed, 195 insertions(+) create mode 100644 configs/systemd/user/dossiers-ngi-branch-status.service create mode 100644 configs/systemd/user/dossiers-ngi-branch-status.timer create mode 100644 scripts/dossiers-ngi/README.md create mode 100755 scripts/dossiers-ngi/branch-status-hourly.py create mode 100644 sites/dossiers.ngi.hacktivism.ch/branch-status.json diff --git a/configs/systemd/user/dossiers-ngi-branch-status.service b/configs/systemd/user/dossiers-ngi-branch-status.service new file mode 100644 index 0000000..a6ad491 --- /dev/null +++ b/configs/systemd/user/dossiers-ngi-branch-status.service @@ -0,0 +1,19 @@ +[Unit] +Description=Write dossiers.ngi branch-status.json from tdpd tips +Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md +After=default.target + +[Service] +Type=oneshot +Nice=10 +RemainAfterExit=no +Environment=TZ=Europe/Zurich +Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log +# Override on koopa if the tdpd checkout path differs +Environment=TDPD_ROOT=%h/git/src/ngi/tech-dossier-project-data +EnvironmentFile=-%h/.config/dossiers-ngi/branch-status.env +ExecStart=/usr/bin/python3 %h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/branch-status-hourly.py +SuccessExitStatus=0 1 + +[Install] +WantedBy=default.target diff --git a/configs/systemd/user/dossiers-ngi-branch-status.timer b/configs/systemd/user/dossiers-ngi-branch-status.timer new file mode 100644 index 0000000..e6c701b --- /dev/null +++ b/configs/systemd/user/dossiers-ngi-branch-status.timer @@ -0,0 +1,13 @@ +[Unit] +Description=Timer · dossiers.ngi branch-status JSON (hourly) +Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md + +[Timer] +OnBootSec=3min +OnUnitActiveSec=1h +AccuracySec=2min +Persistent=true +Unit=dossiers-ngi-branch-status.service + +[Install] +WantedBy=timers.target diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md new file mode 100644 index 0000000..11da294 --- /dev/null +++ b/scripts/dossiers-ngi/README.md @@ -0,0 +1,42 @@ +# dossiers-ngi — cascade landing + hourly branch-status + +SoT for the blank cascade landing on **dossiers.ngi.hacktivism.ch** +(path roots only; no dossiers.2 / dossiers.3 ownership here). + +| Path | Role | +|------|------| +| `sites/dossiers.ngi.hacktivism.ch/` | Landing HTML (EN, ASCII ladder) | +| `scripts/dossiers-ngi/branch-status-hourly.py` | Read tips from tdpd checkout → `branch-status.json` | +| `configs/systemd/user/dossiers-ngi-branch-status.{timer,service}` | Hourly oneshot on koopa | + +## Env + +| Var | Default | Meaning | +|-----|---------|---------| +| `TDPD_ROOT` | `$HOME/git/src/ngi/tech-dossier-project-data` | tech-dossier-project-data checkout for tip SHAs | +| `ADMIN_LOG` | (script derives from `__file__`) | koopa-admin-log root | + +On koopa the clone often lives at `%h/src/koopa/koopa-admin-log`; set +`TDPD_ROOT` in the service `Environment=` if the tdpd path differs. + +## Manual run + +```bash +python3 scripts/dossiers-ngi/branch-status-hourly.py +# → sites/dossiers.ngi.hacktivism.ch/branch-status.json +``` + +## Deploy landing (Freigabe) + +```text +hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/ +``` + +rsync from `sites/dossiers.ngi.hacktivism.ch/` — Freigabe this session required. +Hourly JSON can sit next to the HTML once the timer is installed on koopa +(Freigabe for host mutation). + +## Ownership + +Hourly builder belongs in **kal** (this tree). Do **not** put +`scripts/branch-status-hourly.py` back into tdpd. diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py new file mode 100755 index 0000000..feae12a --- /dev/null +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Emit branch-status.json for the dossiers.ngi path-root ladder. + +Reads tip SHAs from the tech-dossier-project-data git checkout +(TDPD_ROOT / default). Writes under the kal landing SoT: + + sites/dossiers.ngi.hacktivism.ch/branch-status.json + +Path roots only under https://dossiers.ngi.hacktivism.ch/ +(/main/, /feat-sm-content/, /feat-website-features/, + /feat-website-design/, /feat-website-views/). +Single public host only. Hourly builder lives in kal, not tdpd. +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +ADMIN_LOG = Path(__file__).resolve().parents[2] +OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json" +HOST = "https://dossiers.ngi.hacktivism.ch" + +# Default local clone; override with TDPD_ROOT (koopa may use a different path). +DEFAULT_TDPD = Path.home() / "git" / "src" / "ngi" / "tech-dossier-project-data" + +LADDER = [ + ("main", "", f"{HOST}/main/"), + ("feat/sm-content", "main", f"{HOST}/feat-sm-content/"), + ("feat/website-features", "feat/sm-content", f"{HOST}/feat-website-features/"), + ("feat/website-design", "feat/website-features", f"{HOST}/feat-website-design/"), + ("feat/website-views", "feat/website-design", f"{HOST}/feat-website-views/"), +] + + +def tdpd_root() -> Path: + raw = os.environ.get("TDPD_ROOT", "").strip() + return Path(raw).expanduser() if raw else DEFAULT_TDPD + + +def tip_of(repo: Path, branch: str) -> str: + try: + out = subprocess.check_output( + ["git", "-C", str(repo), "rev-parse", "--short", branch], + stderr=subprocess.DEVNULL, + text=True, + ).strip() + return out + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + return "" + + +def main() -> int: + repo = tdpd_root() + if not (repo / ".git").exists() and not repo.is_dir(): + print(f"TDPD_ROOT missing or not a dir: {repo}", file=sys.stderr) + return 1 + branches = [] + for name, parent, url in LADDER: + branches.append( + { + "branch": name, + "tip": tip_of(repo, name), + "parent": parent, + "url": url, + } + ) + payload = { + "generated": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"), + "host": HOST, + "tdpd_root": str(repo), + "branches": branches, + } + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + print(f"wrote {OUT}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sites/dossiers.ngi.hacktivism.ch/branch-status.json b/sites/dossiers.ngi.hacktivism.ch/branch-status.json new file mode 100644 index 0000000..74c3df4 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/branch-status.json @@ -0,0 +1,37 @@ +{ + "generated": "2026-09-30T02:30:50+02:00", + "host": "https://dossiers.ngi.hacktivism.ch", + "tdpd_root": "/Users/newkamek/git/src/ngi/tech-dossier-project-data", + "branches": [ + { + "branch": "main", + "tip": "d2fbd79", + "parent": "", + "url": "https://dossiers.ngi.hacktivism.ch/main/" + }, + { + "branch": "feat/sm-content", + "tip": "089ef1f", + "parent": "main", + "url": "https://dossiers.ngi.hacktivism.ch/feat-sm-content/" + }, + { + "branch": "feat/website-features", + "tip": "53a3176", + "parent": "feat/sm-content", + "url": "https://dossiers.ngi.hacktivism.ch/feat-website-features/" + }, + { + "branch": "feat/website-design", + "tip": "2e26c80", + "parent": "feat/website-features", + "url": "https://dossiers.ngi.hacktivism.ch/feat-website-design/" + }, + { + "branch": "feat/website-views", + "tip": "bbafc38", + "parent": "feat/website-design", + "url": "https://dossiers.ngi.hacktivism.ch/feat-website-views/" + } + ] +} From ef0ff7f60d47f80004d7a1d18d694085ddba5ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 01:08:52 +0200 Subject: [PATCH 04/43] docs: daylog tech-dossier Freeze cascade tip correction --- 2026/2026-09-28--tech-dossier-freeze-cascade.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/2026/2026-09-28--tech-dossier-freeze-cascade.md b/2026/2026-09-28--tech-dossier-freeze-cascade.md index 776a34e..b24c6cd 100644 --- a/2026/2026-09-28--tech-dossier-freeze-cascade.md +++ b/2026/2026-09-28--tech-dossier-freeze-cascade.md @@ -9,7 +9,7 @@ Ladder (each tip autonomously `make html`; each depends on the previous): | 2 | `feat/sm-content` | `089ef1f` | http://127.0.0.1:8102/ | `dossiers.3.ngi.hacktivism.ch` (optional) | | 3 | `feat/website-features` | `53a3176` | http://127.0.0.1:8103/ | `dossiers.2.ngi.hacktivism.ch` (optional) | | 4 | `feat/website-design` | `2e26c80` | http://127.0.0.1:8104/ | — (no public host after views deploy) | -| 5 | `feat/website-views` | `7aca0e8` (Semigrafik `1b2fcf7`; unpushed, ahead origin `bbafc38` by 2) | http://127.0.0.1:8105/ | path roots under blank landing (see Live) | +| 5 | `feat/website-views` | `bbafc38` (origin tip; local soft-reset dropped bad `1b2fcf7`/`7aca0e8`) | http://127.0.0.1:8105/ | path roots under blank landing (see Live) | Caddy roots (SoT `scripts/caddy/Caddyfile.koopa-live-wdGet`): @@ -29,9 +29,10 @@ basic_auth: `scripts/caddy/apply-dossiers-ngi-basicauth.sh` ## Live -- [dossiers.ngi](https://dossiers.ngi.hacktivism.ch/) ← **blank cascade landing** (kal tip `56ee97a`, sites/dossiers.ngi path roots; HTTP 200 SIZE≈3505) -- Path Unterseiten: `/main/`, `/feat-sm-content/`, `/feat-website-features/`, `/feat-website-design/`, `/feat-website-views/` -- Local freeze Semigrafik + hourly branch-status: tdpd `feat/website-views` @ `7aca0e8` ← `1b2fcf7` (unpushed) +- [dossiers.ngi](https://dossiers.ngi.hacktivism.ch/) ← **blank cascade landing** (kal `sites/dossiers.ngi.hacktivism.ch/`; EN + ASCII ladder; views tip cite `bbafc38`) +- Path editions: `/main/`, `/feat-sm-content/`, `/feat-website-features/`, `/feat-website-design/`, `/feat-website-views/` +- Hourly branch-status builder: **kal** `scripts/dossiers-ngi/branch-status-hourly.py` → `sites/…/branch-status.json` (+ user timer); **not** inside tdpd +- tdpd `feat/website-views` local HEAD = origin = `bbafc38` (NO_PUSH; bad tips discarded locally) - [dossiers.2.ngi](https://dossiers.2.ngi.hacktivism.ch/) ← website-features (optional) - [dossiers.3.ngi](https://dossiers.3.ngi.hacktivism.ch/) ← sm-content (optional) @@ -43,6 +44,8 @@ Earlier primary: rsync `website/build/` from tip `feat/website-views` → **Result 2026-09-28 ~22:25+02 (design @ `2e26c80`):** `RSYNC_OK` — ~2698 files / ~27M; live `dossier/sm/index.html` 41693 bytes. -**Result 2026-09-29 ~04:30+02 (views Semigrafik era):** `RSYNC_OK` — live `/` HTTP 200; Semigrafik ladder on dossiers.ngi; later remade as tdpd tips `1b2fcf7` → `7aca0e8` (hourly builder). Push offen (eigene Freigabe). +**Result 2026-09-29 ~04:30+02 (views-era freeze):** `RSYNC_OK` — live `/` HTTP 200. Local tdpd later briefly carried wrong tips `1b2fcf7` → `7aca0e8` (ladder rewrite + hourly in tdpd); those were **dropped** locally back to `bbafc38` (TODO=430). Never push those tips. -**Result 2026-09-29 (blank landing kal @ `56ee97a`):** `RSYNC_OK` — live `/` HTTP 200 SIZE=3505 title `cascade landing`; path-root Unterseiten owned by koopa-admin-log `sites/dossiers.ngi.hacktivism.ch/`; NO_PUSH. +**Result 2026-09-29 (blank landing kal @ `56ee97a`):** `RSYNC_OK` — live `/` HTTP 200 SIZE=3505 title `cascade landing`; path-root editions owned by koopa-admin-log `sites/dossiers.ngi.hacktivism.ch/`; NO_PUSH. + +**TODO=430 follow-up (local, NO_PUSH):** kal landing EN/ASCII + tip cite `bbafc38`; hourly builder + systemd under kal; daylog tip table corrected. Deploy landing / enable timer on koopa needs Freigabe. From c406352e1adae82faf4007322647d8637120b24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 03:41:39 +0200 Subject: [PATCH 05/43] sites/dossiers.ngi: strip meta keep links and last-built --- .../branch-status.json | 6 --- .../feat-sm-content/index.html | 42 ++++------------- .../feat-website-design/index.html | 42 ++++------------- .../feat-website-features/index.html | 42 ++++------------- .../feat-website-views/index.html | 46 ++++--------------- sites/dossiers.ngi.hacktivism.ch/index.html | 46 +++++-------------- .../main/index.html | 42 ++++------------- 7 files changed, 57 insertions(+), 209 deletions(-) diff --git a/sites/dossiers.ngi.hacktivism.ch/branch-status.json b/sites/dossiers.ngi.hacktivism.ch/branch-status.json index 74c3df4..1d049ef 100644 --- a/sites/dossiers.ngi.hacktivism.ch/branch-status.json +++ b/sites/dossiers.ngi.hacktivism.ch/branch-status.json @@ -1,36 +1,30 @@ { "generated": "2026-09-30T02:30:50+02:00", "host": "https://dossiers.ngi.hacktivism.ch", - "tdpd_root": "/Users/newkamek/git/src/ngi/tech-dossier-project-data", "branches": [ { "branch": "main", "tip": "d2fbd79", - "parent": "", "url": "https://dossiers.ngi.hacktivism.ch/main/" }, { "branch": "feat/sm-content", "tip": "089ef1f", - "parent": "main", "url": "https://dossiers.ngi.hacktivism.ch/feat-sm-content/" }, { "branch": "feat/website-features", "tip": "53a3176", - "parent": "feat/sm-content", "url": "https://dossiers.ngi.hacktivism.ch/feat-website-features/" }, { "branch": "feat/website-design", "tip": "2e26c80", - "parent": "feat/website-features", "url": "https://dossiers.ngi.hacktivism.ch/feat-website-design/" }, { "branch": "feat/website-views", "tip": "bbafc38", - "parent": "feat/website-design", "url": "https://dossiers.ngi.hacktivism.ch/feat-website-views/" } ] diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html index a3100e1..bd25fd1 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html @@ -3,7 +3,7 @@ -feat/sm-content · cascade +feat/sm-content - -

edition: feat/sm-content

-

path /feat-sm-content/ · tip 089ef1f · depends on main

+

/feat-sm-content/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/sm-content @ 089ef1f

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | -* +-- feat/sm-content @ 089ef1f <-- this edition - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

SM dossier prose content on top of main.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html index 12c451c..d4a7fae 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html @@ -3,7 +3,7 @@ -feat/website-design · cascade +feat/website-design - -

edition: feat/website-design

-

path /feat-website-design/ · tip 2e26c80 · depends on feat/website-features

+

/feat-website-design/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/website-design @ 2e26c80

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | -* +-- feat/website-design @ 2e26c80 <-- this edition - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

Design/CSS layer on top of features.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html index 257847a..ed90716 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html @@ -3,7 +3,7 @@ -feat/website-features · cascade +feat/website-features - -

edition: feat/website-features

-

path /feat-website-features/ · tip 53a3176 · depends on feat/sm-content

+

/feat-website-features/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/website-features @ 53a3176

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | -* +-- feat/website-features @ 53a3176 <-- this edition - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

Website feature routes on top of sm-content.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html index 1b622fd..5bc14c7 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html @@ -3,7 +3,7 @@ -feat/website-views · cascade +feat/website-views - -

edition: feat/website-views

-

path /feat-website-views/ · tip bbafc38 · depends on feat/website-design

+

/feat-website-views/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/website-views @ bbafc38

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | -* +-- feat/website-views @ bbafc38 <-- this edition
-

what this looks like

-

Views/list layer on top of design (origin tip).

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- -

views gap vs dossiers.ngi-0.eu: -Frozen Flask on this tip exposes project_list / project_detail / dossier_list / blank. -It does not yet mirror the Next.js category-style views on dossiers.ngi-0.eu. -This path documents that gap; category-parity views remain open work.

- diff --git a/sites/dossiers.ngi.hacktivism.ch/index.html b/sites/dossiers.ngi.hacktivism.ch/index.html index a7b111e..7126152 100644 --- a/sites/dossiers.ngi.hacktivism.ch/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/index.html @@ -15,26 +15,19 @@ body { } a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } -h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.75rem; color: #e8eef4; } -h2 { font-size: 1rem; font-weight: 600; margin: 1.25rem 0 0.5rem; color: #e8eef4; } -.meta { color: #8a96a3; margin-bottom: 1rem; } -.box { - border: 1px solid #2a3340; background: #12161c; - padding: 0.85rem 1rem; margin: 0.75rem 0; white-space: pre; overflow-x: auto; -} +h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } +.meta { color: #8a96a3; margin: 0 0 1rem; } .nav { margin: 0.5rem 0 1rem; } .nav a { margin-right: 0.85rem; } -.note { color: #b8a46a; margin-top: 1rem; max-width: 72ch; white-space: normal; } -.ok { color: #7dcea0; } -.dim { color: #6a7682; } -ul.plain { padding-left: 1.2rem; } +ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } +.dim { color: #6a7682; } + - -

tech-dossier cascade landing

-

host: dossiers.ngi.hacktivism.ch · blank relational index · owned by koopa-admin-log

+

cascade

+

last-built: 2026-09-30T02:30:50+02:00

-

relation to main

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

editions

-

Each path is its own root for that branch edition. Single public host dossiers.ngi only.

- diff --git a/sites/dossiers.ngi.hacktivism.ch/main/index.html b/sites/dossiers.ngi.hacktivism.ch/main/index.html index 9f9490c..c4ca691 100644 --- a/sites/dossiers.ngi.hacktivism.ch/main/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/main/index.html @@ -3,7 +3,7 @@ -main · cascade +main - -

edition: main

-

path /main/ · tip d2fbd79 · depends on none (base)

+

/main/

+

last-built: 2026-09-30T02:30:50+02:00 · main @ d2fbd79

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - -* main @ d2fbd79 <-- this edition - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

Base freeze tip.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - From c1fefcc766388420975fd0f4ea3cf77736df74e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 03:41:41 +0200 Subject: [PATCH 06/43] ops: dossiers-ngi hourly branch-status emit tip fields only --- scripts/dossiers-ngi/branch-status-hourly.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index feae12a..e121273 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -71,8 +71,10 @@ def main() -> int: payload = { "generated": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"), "host": HOST, - "tdpd_root": str(repo), - "branches": branches, + "branches": [ + {"branch": b["branch"], "tip": b["tip"], "url": b["url"]} + for b in branches + ], } OUT.parent.mkdir(parents=True, exist_ok=True) OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") From f2a0d533b80e7e6dea98f8279df793afde42d2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 03:44:42 +0200 Subject: [PATCH 07/43] sites/dossiers.ngi: strip meta keep links and last-built --- .../branch-status.json | 6 --- .../feat-sm-content/index.html | 42 ++++------------- .../feat-website-design/index.html | 42 ++++------------- .../feat-website-features/index.html | 42 ++++------------- .../feat-website-views/index.html | 46 ++++--------------- sites/dossiers.ngi.hacktivism.ch/index.html | 46 +++++-------------- .../main/index.html | 42 ++++------------- 7 files changed, 57 insertions(+), 209 deletions(-) diff --git a/sites/dossiers.ngi.hacktivism.ch/branch-status.json b/sites/dossiers.ngi.hacktivism.ch/branch-status.json index 74c3df4..1d049ef 100644 --- a/sites/dossiers.ngi.hacktivism.ch/branch-status.json +++ b/sites/dossiers.ngi.hacktivism.ch/branch-status.json @@ -1,36 +1,30 @@ { "generated": "2026-09-30T02:30:50+02:00", "host": "https://dossiers.ngi.hacktivism.ch", - "tdpd_root": "/Users/newkamek/git/src/ngi/tech-dossier-project-data", "branches": [ { "branch": "main", "tip": "d2fbd79", - "parent": "", "url": "https://dossiers.ngi.hacktivism.ch/main/" }, { "branch": "feat/sm-content", "tip": "089ef1f", - "parent": "main", "url": "https://dossiers.ngi.hacktivism.ch/feat-sm-content/" }, { "branch": "feat/website-features", "tip": "53a3176", - "parent": "feat/sm-content", "url": "https://dossiers.ngi.hacktivism.ch/feat-website-features/" }, { "branch": "feat/website-design", "tip": "2e26c80", - "parent": "feat/website-features", "url": "https://dossiers.ngi.hacktivism.ch/feat-website-design/" }, { "branch": "feat/website-views", "tip": "bbafc38", - "parent": "feat/website-design", "url": "https://dossiers.ngi.hacktivism.ch/feat-website-views/" } ] diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html index a3100e1..bd25fd1 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html @@ -3,7 +3,7 @@ -feat/sm-content · cascade +feat/sm-content - -

edition: feat/sm-content

-

path /feat-sm-content/ · tip 089ef1f · depends on main

+

/feat-sm-content/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/sm-content @ 089ef1f

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | -* +-- feat/sm-content @ 089ef1f <-- this edition - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

SM dossier prose content on top of main.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html index 12c451c..d4a7fae 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html @@ -3,7 +3,7 @@ -feat/website-design · cascade +feat/website-design - -

edition: feat/website-design

-

path /feat-website-design/ · tip 2e26c80 · depends on feat/website-features

+

/feat-website-design/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/website-design @ 2e26c80

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | -* +-- feat/website-design @ 2e26c80 <-- this edition - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

Design/CSS layer on top of features.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html index 257847a..ed90716 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html @@ -3,7 +3,7 @@ -feat/website-features · cascade +feat/website-features - -

edition: feat/website-features

-

path /feat-website-features/ · tip 53a3176 · depends on feat/sm-content

+

/feat-website-features/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/website-features @ 53a3176

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | -* +-- feat/website-features @ 53a3176 <-- this edition - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

Website feature routes on top of sm-content.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html index 1b622fd..5bc14c7 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html @@ -3,7 +3,7 @@ -feat/website-views · cascade +feat/website-views - -

edition: feat/website-views

-

path /feat-website-views/ · tip bbafc38 · depends on feat/website-design

+

/feat-website-views/

+

last-built: 2026-09-30T02:30:50+02:00 · feat/website-views @ bbafc38

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | -* +-- feat/website-views @ bbafc38 <-- this edition
-

what this looks like

-

Views/list layer on top of design (origin tip).

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- -

views gap vs dossiers.ngi-0.eu: -Frozen Flask on this tip exposes project_list / project_detail / dossier_list / blank. -It does not yet mirror the Next.js category-style views on dossiers.ngi-0.eu. -This path documents that gap; category-parity views remain open work.

- diff --git a/sites/dossiers.ngi.hacktivism.ch/index.html b/sites/dossiers.ngi.hacktivism.ch/index.html index a7b111e..7126152 100644 --- a/sites/dossiers.ngi.hacktivism.ch/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/index.html @@ -15,26 +15,19 @@ body { } a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } -h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.75rem; color: #e8eef4; } -h2 { font-size: 1rem; font-weight: 600; margin: 1.25rem 0 0.5rem; color: #e8eef4; } -.meta { color: #8a96a3; margin-bottom: 1rem; } -.box { - border: 1px solid #2a3340; background: #12161c; - padding: 0.85rem 1rem; margin: 0.75rem 0; white-space: pre; overflow-x: auto; -} +h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } +.meta { color: #8a96a3; margin: 0 0 1rem; } .nav { margin: 0.5rem 0 1rem; } .nav a { margin-right: 0.85rem; } -.note { color: #b8a46a; margin-top: 1rem; max-width: 72ch; white-space: normal; } -.ok { color: #7dcea0; } -.dim { color: #6a7682; } -ul.plain { padding-left: 1.2rem; } +ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } +.dim { color: #6a7682; } + - -

tech-dossier cascade landing

-

host: dossiers.ngi.hacktivism.ch · blank relational index · owned by koopa-admin-log

+

cascade

+

last-built: 2026-09-30T02:30:50+02:00

-

relation to main

-
cascade ladder (path roots on dossiers.ngi only) - - main @ d2fbd79 - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

editions

-

Each path is its own root for that branch edition. Single public host dossiers.ngi only.

- diff --git a/sites/dossiers.ngi.hacktivism.ch/main/index.html b/sites/dossiers.ngi.hacktivism.ch/main/index.html index 9f9490c..c4ca691 100644 --- a/sites/dossiers.ngi.hacktivism.ch/main/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/main/index.html @@ -3,7 +3,7 @@ -main · cascade +main - -

edition: main

-

path /main/ · tip d2fbd79 · depends on none (base)

+

/main/

+

last-built: 2026-09-30T02:30:50+02:00 · main @ d2fbd79

-

ladder position

-
cascade ladder (path roots on dossiers.ngi only) - -* main @ d2fbd79 <-- this edition - | - +-- feat/sm-content @ 089ef1f - | - +-- feat/website-features @ 53a3176 - | - +-- feat/website-design @ 2e26c80 - | - +-- feat/website-views @ bbafc38
-

what this looks like

-

Base freeze tip.

-

Blank relational stub. Frozen Flask build for this tip is separate; this page only shows cascade relation and path root.

- - From ff934ce5e9324c73dcfceaf5305334dee4161f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 03:48:19 +0200 Subject: [PATCH 08/43] ops/dossiers-ngi: hourly branch-status emit tip fields only --- scripts/dossiers-ngi/branch-status-hourly.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index feae12a..e121273 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -71,8 +71,10 @@ def main() -> int: payload = { "generated": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"), "host": HOST, - "tdpd_root": str(repo), - "branches": branches, + "branches": [ + {"branch": b["branch"], "tip": b["tip"], "url": b["url"]} + for b in branches + ], } OUT.parent.mkdir(parents=True, exist_ok=True) OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") From 467092db1fafc8a8c5c131067d35f8e7af0b42ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 12:50:54 +0200 Subject: [PATCH 09/43] docs: update README.md with new content --- scripts/dossiers-ngi/README.md | 44 +++++++++++++++++++ .../dossiers-ngi/caddy-dossiers-ngi.snippet | 19 ++++++++ .../_cascade/bar.css | 29 ++++++++++++ .../_cascade/bar.html | 11 +++++ .../_cascade/bar.js | 24 ++++++++++ .../feat-sm-content/index.html | 11 +---- .../feat-website-design/index.html | 11 +---- .../feat-website-features/index.html | 11 +---- .../feat-website-views/index.html | 11 +---- sites/dossiers.ngi.hacktivism.ch/index.html | 11 +---- .../main/index.html | 11 +---- 11 files changed, 133 insertions(+), 60 deletions(-) create mode 100644 scripts/dossiers-ngi/caddy-dossiers-ngi.snippet create mode 100644 sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css create mode 100644 sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html create mode 100644 sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 11da294..37f674a 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -40,3 +40,47 @@ Hourly JSON can sit next to the HTML once the timer is installed on koopa Hourly builder belongs in **kal** (this tree). Do **not** put `scripts/branch-status-hourly.py` back into tdpd. + +## Sticky cascade bar + +Stock Caddy `templates` renders `{% include "_cascade/bar.html" . %}` on each +HTML page. The include path has no leading slash. The dot passes the request, +so `.Req.URL.Path` marks the active link with `class="is-active"` and +`aria-current="page"`. Delimiters are `{%` `%}`, so a later `{{ }}` in real +dossier HTML stays literal. + +The bar lists cascade `/` plus the five branch paths. Seeded tips match +`branch-status.json`. `/_cascade/bar.js` refreshes `[data-tip]` from that JSON +(`cache: no-store`). CSS is `/_cascade/bar.css` (`position: sticky`). + +Enable templates before rsync. A page without the include does not grow the +bar. `{%` without the templates directive is shown as raw text. + +Templates apply is **host-local on koopa** (`~/bin/apply-dossiers-ngi-templates.sh`), +not tracked in this repo. It edits `~/koopa-caddy/Caddyfile` only (this host; +keeps `header Alt-Svc "clear"` and any `basic_auth`; leaves `.2` / `.3` +untouched). Then reload with the usual home→`/etc` helper: + +```bash +bash ~/bin/apply-dossiers-ngi-templates.sh +sudo ~/bin/caddy-apply +``` + +The stanza body for hand-check / re-seed lives in +`scripts/dossiers-ngi/caddy-dossiers-ngi.snippet`. +`scripts/caddy/caddy-apply.sh --snippet` leaves an existing block unchanged. +Leave `scripts/caddy/apply-goa-wdGet-full-replace.sh` and +`scripts/caddy/apply-dossiers-ngi-basicauth.sh` unused. No response-body +replace plugin. + +After validate and reload succeed, rsync from the laptop (Freigabe): + +```bash +rsync -avz --delete --exclude .DS_Store \ + "$HOME/git/admin-logs/local/koopa-admin-log/sites/dossiers.ngi.hacktivism.ch/" \ + hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/ +``` + +`--delete` removes `_cascade/` on the server when the source tree is an older +copy. The bar does not finish the dossier site. This section does not install +the hourly timer. diff --git a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet new file mode 100644 index 0000000..1418403 --- /dev/null +++ b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet @@ -0,0 +1,19 @@ +# Stanza body for dossiers.ngi.hacktivism.ch only (hand-check / re-seed). +# Templates apply is host-local on koopa (not in this repo): +# bash ~/bin/apply-dossiers-ngi-templates.sh +# sudo ~/bin/caddy-apply +# +# Keep the existing line: header Alt-Svc "clear" +# Keep any existing basic_auth in that same block. +# scripts/caddy/caddy-apply.sh --snippet reports an existing block and +# does not merge these lines. Leave that script unused for this change. +# Leave scripts/caddy/apply-goa-wdGet-full-replace.sh unused. +# Leave scripts/caddy/apply-dossiers-ngi-basicauth.sh unused. +# Stock Caddy templates only. No replace-response plugin. +# +# Tab-indented lines inserted after header Alt-Svc: + + templates { + mime text/html + between "{%" "%}" + } diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css new file mode 100644 index 0000000..0f9e1cc --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css @@ -0,0 +1,29 @@ +.cascade-bar { + position: sticky; + top: 0; + z-index: 10; + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.85rem; + align-items: baseline; + margin: -1.5rem -1.5rem 1rem; + padding: 0.55rem 1.5rem; + background: #10141a; + border-bottom: 1px solid #243040; +} +.cascade-bar a { + color: #7ec8ff; + text-decoration: none; +} +.cascade-bar a:hover { text-decoration: underline; } +.cascade-bar a.is-active { + color: #e8eef4; + text-decoration: none; + box-shadow: inset 0 -2px 0 #e8eef4; +} +.cascade-bar a.is-active:hover { text-decoration: none; } +.cascade-bar code { color: inherit; } +.cascade-bar .tip { + color: #8a96a3; + font-size: 12px; +} diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html new file mode 100644 index 0000000..d6ed145 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html @@ -0,0 +1,11 @@ +{% $p := .Req.URL.Path %} + + + diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js new file mode 100644 index 0000000..9371535 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js @@ -0,0 +1,24 @@ +(function () { + var nodes = document.querySelectorAll(".cascade-bar [data-tip]"); + if (!nodes.length) return; + fetch("/branch-status.json", { cache: "no-store" }) + .then(function (res) { + if (!res.ok) throw new Error("status"); + return res.json(); + }) + .then(function (data) { + var tips = {}; + var list = (data && data.branches) || []; + for (var i = 0; i < list.length; i++) { + var row = list[i]; + if (row && row.branch && row.tip) tips[row.branch] = row.tip; + } + for (var j = 0; j < nodes.length; j++) { + var el = nodes[j]; + var name = el.getAttribute("data-tip"); + var tip = tips[name]; + if (tip) el.textContent = name + " @ " + tip; + } + }) + .catch(function () {}); +})(); diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html index bd25fd1..13f4285 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

/feat-sm-content/

last-built: 2026-09-30T02:30:50+02:00 · feat/sm-content @ 089ef1f

- diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html index d4a7fae..142b80e 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

/feat-website-design/

last-built: 2026-09-30T02:30:50+02:00 · feat/website-design @ 2e26c80

- diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html index ed90716..3271f8f 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

/feat-website-features/

last-built: 2026-09-30T02:30:50+02:00 · feat/website-features @ 53a3176

- diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html index 5bc14c7..5896890 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

/feat-website-views/

last-built: 2026-09-30T02:30:50+02:00 · feat/website-views @ bbafc38

- diff --git a/sites/dossiers.ngi.hacktivism.ch/index.html b/sites/dossiers.ngi.hacktivism.ch/index.html index 7126152..4d40f7f 100644 --- a/sites/dossiers.ngi.hacktivism.ch/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,16 +24,9 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

cascade

last-built: 2026-09-30T02:30:50+02:00

-
  • /main/ · main @ d2fbd79
  • /feat-sm-content/ · feat/sm-content @ 089ef1f
  • diff --git a/sites/dossiers.ngi.hacktivism.ch/main/index.html b/sites/dossiers.ngi.hacktivism.ch/main/index.html index c4ca691..941036c 100644 --- a/sites/dossiers.ngi.hacktivism.ch/main/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/main/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

    /main/

    last-built: 2026-09-30T02:30:50+02:00 · main @ d2fbd79

    - From ebecb26c7c59c9f9879ab02432a582606072ee8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 12:52:38 +0200 Subject: [PATCH 10/43] scripts/dossiers-ngi/README.md: update branch-status-hourly.py reference --- .../user/dossiers-ngi-branch-status.service | 2 ++ scripts/dossiers-ngi/README.md | 4 +++- scripts/dossiers-ngi/branch-status-hourly.py | 14 ++++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/configs/systemd/user/dossiers-ngi-branch-status.service b/configs/systemd/user/dossiers-ngi-branch-status.service index a6ad491..4a65aad 100644 --- a/configs/systemd/user/dossiers-ngi-branch-status.service +++ b/configs/systemd/user/dossiers-ngi-branch-status.service @@ -11,6 +11,8 @@ Environment=TZ=Europe/Zurich Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log # Override on koopa if the tdpd checkout path differs Environment=TDPD_ROOT=%h/git/src/ngi/tech-dossier-project-data +# Live site root (timer must refresh public JSON, not only the kal tree) +Environment=OUT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/branch-status.json EnvironmentFile=-%h/.config/dossiers-ngi/branch-status.env ExecStart=/usr/bin/python3 %h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/branch-status-hourly.py SuccessExitStatus=0 1 diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 37f674a..1b1131d 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -14,10 +14,12 @@ SoT for the blank cascade landing on **dossiers.ngi.hacktivism.ch** | Var | Default | Meaning | |-----|---------|---------| | `TDPD_ROOT` | `$HOME/git/src/ngi/tech-dossier-project-data` | tech-dossier-project-data checkout for tip SHAs | +| `OUT` | `sites/dossiers.ngi.hacktivism.ch/branch-status.json` under kal | write path for JSON (live www on koopa) | | `ADMIN_LOG` | (script derives from `__file__`) | koopa-admin-log root | On koopa the clone often lives at `%h/src/koopa/koopa-admin-log`; set -`TDPD_ROOT` in the service `Environment=` if the tdpd path differs. +`TDPD_ROOT` and `OUT` in `~/.config/dossiers-ngi/branch-status.env` when the +tdpd path or live www path differs from the laptop defaults. ## Manual run diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index e121273..4507313 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -21,9 +21,14 @@ from datetime import datetime, timezone from pathlib import Path ADMIN_LOG = Path(__file__).resolve().parents[2] -OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json" +_DEFAULT_OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json" HOST = "https://dossiers.ngi.hacktivism.ch" + +def out_path() -> Path: + raw = os.environ.get("OUT", "").strip() + return Path(raw).expanduser() if raw else _DEFAULT_OUT + # Default local clone; override with TDPD_ROOT (koopa may use a different path). DEFAULT_TDPD = Path.home() / "git" / "src" / "ngi" / "tech-dossier-project-data" @@ -76,9 +81,10 @@ def main() -> int: for b in branches ], } - OUT.parent.mkdir(parents=True, exist_ok=True) - OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - print(f"wrote {OUT}", file=sys.stderr) + dest = out_path() + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + print(f"wrote {dest}", file=sys.stderr) return 0 From 8a09314eaf2cf613f48d8008876d7ca4b08490f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 13:01:15 +0200 Subject: [PATCH 11/43] docs: update README.md with new content --- scripts/dossiers-ngi/README.md | 44 +++++++++++++++++++ .../dossiers-ngi/caddy-dossiers-ngi.snippet | 19 ++++++++ .../_cascade/bar.css | 29 ++++++++++++ .../_cascade/bar.html | 11 +++++ .../_cascade/bar.js | 24 ++++++++++ .../feat-sm-content/index.html | 11 +---- .../feat-website-design/index.html | 11 +---- .../feat-website-features/index.html | 11 +---- .../feat-website-views/index.html | 11 +---- sites/dossiers.ngi.hacktivism.ch/index.html | 11 +---- .../main/index.html | 11 +---- 11 files changed, 133 insertions(+), 60 deletions(-) create mode 100644 scripts/dossiers-ngi/caddy-dossiers-ngi.snippet create mode 100644 sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css create mode 100644 sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html create mode 100644 sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 11da294..37f674a 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -40,3 +40,47 @@ Hourly JSON can sit next to the HTML once the timer is installed on koopa Hourly builder belongs in **kal** (this tree). Do **not** put `scripts/branch-status-hourly.py` back into tdpd. + +## Sticky cascade bar + +Stock Caddy `templates` renders `{% include "_cascade/bar.html" . %}` on each +HTML page. The include path has no leading slash. The dot passes the request, +so `.Req.URL.Path` marks the active link with `class="is-active"` and +`aria-current="page"`. Delimiters are `{%` `%}`, so a later `{{ }}` in real +dossier HTML stays literal. + +The bar lists cascade `/` plus the five branch paths. Seeded tips match +`branch-status.json`. `/_cascade/bar.js` refreshes `[data-tip]` from that JSON +(`cache: no-store`). CSS is `/_cascade/bar.css` (`position: sticky`). + +Enable templates before rsync. A page without the include does not grow the +bar. `{%` without the templates directive is shown as raw text. + +Templates apply is **host-local on koopa** (`~/bin/apply-dossiers-ngi-templates.sh`), +not tracked in this repo. It edits `~/koopa-caddy/Caddyfile` only (this host; +keeps `header Alt-Svc "clear"` and any `basic_auth`; leaves `.2` / `.3` +untouched). Then reload with the usual home→`/etc` helper: + +```bash +bash ~/bin/apply-dossiers-ngi-templates.sh +sudo ~/bin/caddy-apply +``` + +The stanza body for hand-check / re-seed lives in +`scripts/dossiers-ngi/caddy-dossiers-ngi.snippet`. +`scripts/caddy/caddy-apply.sh --snippet` leaves an existing block unchanged. +Leave `scripts/caddy/apply-goa-wdGet-full-replace.sh` and +`scripts/caddy/apply-dossiers-ngi-basicauth.sh` unused. No response-body +replace plugin. + +After validate and reload succeed, rsync from the laptop (Freigabe): + +```bash +rsync -avz --delete --exclude .DS_Store \ + "$HOME/git/admin-logs/local/koopa-admin-log/sites/dossiers.ngi.hacktivism.ch/" \ + hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/ +``` + +`--delete` removes `_cascade/` on the server when the source tree is an older +copy. The bar does not finish the dossier site. This section does not install +the hourly timer. diff --git a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet new file mode 100644 index 0000000..1418403 --- /dev/null +++ b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet @@ -0,0 +1,19 @@ +# Stanza body for dossiers.ngi.hacktivism.ch only (hand-check / re-seed). +# Templates apply is host-local on koopa (not in this repo): +# bash ~/bin/apply-dossiers-ngi-templates.sh +# sudo ~/bin/caddy-apply +# +# Keep the existing line: header Alt-Svc "clear" +# Keep any existing basic_auth in that same block. +# scripts/caddy/caddy-apply.sh --snippet reports an existing block and +# does not merge these lines. Leave that script unused for this change. +# Leave scripts/caddy/apply-goa-wdGet-full-replace.sh unused. +# Leave scripts/caddy/apply-dossiers-ngi-basicauth.sh unused. +# Stock Caddy templates only. No replace-response plugin. +# +# Tab-indented lines inserted after header Alt-Svc: + + templates { + mime text/html + between "{%" "%}" + } diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css new file mode 100644 index 0000000..0f9e1cc --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css @@ -0,0 +1,29 @@ +.cascade-bar { + position: sticky; + top: 0; + z-index: 10; + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.85rem; + align-items: baseline; + margin: -1.5rem -1.5rem 1rem; + padding: 0.55rem 1.5rem; + background: #10141a; + border-bottom: 1px solid #243040; +} +.cascade-bar a { + color: #7ec8ff; + text-decoration: none; +} +.cascade-bar a:hover { text-decoration: underline; } +.cascade-bar a.is-active { + color: #e8eef4; + text-decoration: none; + box-shadow: inset 0 -2px 0 #e8eef4; +} +.cascade-bar a.is-active:hover { text-decoration: none; } +.cascade-bar code { color: inherit; } +.cascade-bar .tip { + color: #8a96a3; + font-size: 12px; +} diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html new file mode 100644 index 0000000..d6ed145 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html @@ -0,0 +1,11 @@ +{% $p := .Req.URL.Path %} + + + diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js new file mode 100644 index 0000000..9371535 --- /dev/null +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js @@ -0,0 +1,24 @@ +(function () { + var nodes = document.querySelectorAll(".cascade-bar [data-tip]"); + if (!nodes.length) return; + fetch("/branch-status.json", { cache: "no-store" }) + .then(function (res) { + if (!res.ok) throw new Error("status"); + return res.json(); + }) + .then(function (data) { + var tips = {}; + var list = (data && data.branches) || []; + for (var i = 0; i < list.length; i++) { + var row = list[i]; + if (row && row.branch && row.tip) tips[row.branch] = row.tip; + } + for (var j = 0; j < nodes.length; j++) { + var el = nodes[j]; + var name = el.getAttribute("data-tip"); + var tip = tips[name]; + if (tip) el.textContent = name + " @ " + tip; + } + }) + .catch(function () {}); +})(); diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html index bd25fd1..13f4285 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-sm-content/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

    /feat-sm-content/

    last-built: 2026-09-30T02:30:50+02:00 · feat/sm-content @ 089ef1f

    - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html index d4a7fae..142b80e 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-design/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

    /feat-website-design/

    last-built: 2026-09-30T02:30:50+02:00 · feat/website-design @ 2e26c80

    - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html index ed90716..3271f8f 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-features/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

    /feat-website-features/

    last-built: 2026-09-30T02:30:50+02:00 · feat/website-features @ 53a3176

    - diff --git a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html index 5bc14c7..5896890 100644 --- a/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/feat-website-views/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

    /feat-website-views/

    last-built: 2026-09-30T02:30:50+02:00 · feat/website-views @ bbafc38

    - diff --git a/sites/dossiers.ngi.hacktivism.ch/index.html b/sites/dossiers.ngi.hacktivism.ch/index.html index 7126152..4d40f7f 100644 --- a/sites/dossiers.ngi.hacktivism.ch/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,16 +24,9 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

    cascade

    last-built: 2026-09-30T02:30:50+02:00

    -
    • /main/ · main @ d2fbd79
    • /feat-sm-content/ · feat/sm-content @ 089ef1f
    • diff --git a/sites/dossiers.ngi.hacktivism.ch/main/index.html b/sites/dossiers.ngi.hacktivism.ch/main/index.html index c4ca691..941036c 100644 --- a/sites/dossiers.ngi.hacktivism.ch/main/index.html +++ b/sites/dossiers.ngi.hacktivism.ch/main/index.html @@ -17,8 +17,6 @@ a { color: #7ec8ff; text-decoration: none; } a:hover { text-decoration: underline; } h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; color: #e8eef4; } .meta { color: #8a96a3; margin: 0 0 1rem; } -.nav { margin: 0.5rem 0 1rem; } -.nav a { margin-right: 0.85rem; } ul.plain { padding-left: 1.2rem; margin: 0; } li { margin: 0.25rem 0; } .dim { color: #6a7682; } @@ -26,15 +24,8 @@ li { margin: 0.25rem 0; } +{% include "_cascade/bar.html" . %}

      /main/

      last-built: 2026-09-30T02:30:50+02:00 · main @ d2fbd79

      - From e35205b6cd1ad01e0ffc0310e64fcda6c6d66474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 13:08:46 +0200 Subject: [PATCH 12/43] scripts/dossiers-ngi/README.md: update branch-status-hourly.py reference --- .../user/dossiers-ngi-branch-status.service | 2 ++ scripts/dossiers-ngi/README.md | 4 +++- scripts/dossiers-ngi/branch-status-hourly.py | 14 ++++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/configs/systemd/user/dossiers-ngi-branch-status.service b/configs/systemd/user/dossiers-ngi-branch-status.service index a6ad491..4a65aad 100644 --- a/configs/systemd/user/dossiers-ngi-branch-status.service +++ b/configs/systemd/user/dossiers-ngi-branch-status.service @@ -11,6 +11,8 @@ Environment=TZ=Europe/Zurich Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log # Override on koopa if the tdpd checkout path differs Environment=TDPD_ROOT=%h/git/src/ngi/tech-dossier-project-data +# Live site root (timer must refresh public JSON, not only the kal tree) +Environment=OUT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/branch-status.json EnvironmentFile=-%h/.config/dossiers-ngi/branch-status.env ExecStart=/usr/bin/python3 %h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/branch-status-hourly.py SuccessExitStatus=0 1 diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 37f674a..1b1131d 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -14,10 +14,12 @@ SoT for the blank cascade landing on **dossiers.ngi.hacktivism.ch** | Var | Default | Meaning | |-----|---------|---------| | `TDPD_ROOT` | `$HOME/git/src/ngi/tech-dossier-project-data` | tech-dossier-project-data checkout for tip SHAs | +| `OUT` | `sites/dossiers.ngi.hacktivism.ch/branch-status.json` under kal | write path for JSON (live www on koopa) | | `ADMIN_LOG` | (script derives from `__file__`) | koopa-admin-log root | On koopa the clone often lives at `%h/src/koopa/koopa-admin-log`; set -`TDPD_ROOT` in the service `Environment=` if the tdpd path differs. +`TDPD_ROOT` and `OUT` in `~/.config/dossiers-ngi/branch-status.env` when the +tdpd path or live www path differs from the laptop defaults. ## Manual run diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index e121273..4507313 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -21,9 +21,14 @@ from datetime import datetime, timezone from pathlib import Path ADMIN_LOG = Path(__file__).resolve().parents[2] -OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json" +_DEFAULT_OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json" HOST = "https://dossiers.ngi.hacktivism.ch" + +def out_path() -> Path: + raw = os.environ.get("OUT", "").strip() + return Path(raw).expanduser() if raw else _DEFAULT_OUT + # Default local clone; override with TDPD_ROOT (koopa may use a different path). DEFAULT_TDPD = Path.home() / "git" / "src" / "ngi" / "tech-dossier-project-data" @@ -76,9 +81,10 @@ def main() -> int: for b in branches ], } - OUT.parent.mkdir(parents=True, exist_ok=True) - OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - print(f"wrote {OUT}", file=sys.stderr) + dest = out_path() + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + print(f"wrote {dest}", file=sys.stderr) return 0 From ee82ef9b6ed8580f6edb2ad7a18d4df4b7314675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Wed, 30 Sep 2026 15:35:35 +0200 Subject: [PATCH 13/43] ops: dossiers-ngi edition tree bar, 10min tip-skip rebuild, cleanup --- .../user/dossiers-ngi-branch-status.service | 12 +- .../user/dossiers-ngi-branch-status.timer | 7 +- scripts/dossiers-ngi/README.md | 135 ++-- .../apply-dossiers-ngi-edition-switch.sh | 82 +++ scripts/dossiers-ngi/branch-status-hourly.py | 647 ++++++++++++++++-- .../dossiers-ngi/caddy-dossiers-ngi.snippet | 45 +- scripts/dossiers-ngi/inject-cascade-bar.py | 56 ++ scripts/dossiers-ngi/smoke-branch-tree.sh | 98 +++ .../_cascade/bar.css | 48 +- .../_cascade/bar.html | 10 +- .../_cascade/bar.js | 152 +++- .../_edition-placeholder/index.html | 17 + .../branch-status.json | 109 ++- .../cascade/_cascade/bar.css | 51 ++ .../cascade/_cascade/bar.html | 5 + .../cascade/_cascade/bar.js | 146 ++++ .../{ => cascade}/feat-sm-content/index.html | 0 .../feat-website-design/index.html | 0 .../feat-website-features/index.html | 0 .../feat-website-views/index.html | 0 .../{ => cascade}/index.html | 11 +- .../{ => cascade}/main/index.html | 0 22 files changed, 1458 insertions(+), 173 deletions(-) create mode 100755 scripts/dossiers-ngi/apply-dossiers-ngi-edition-switch.sh create mode 100755 scripts/dossiers-ngi/inject-cascade-bar.py create mode 100755 scripts/dossiers-ngi/smoke-branch-tree.sh create mode 100644 sites/dossiers.ngi.hacktivism.ch/_edition-placeholder/index.html create mode 100644 sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css create mode 100644 sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.html create mode 100644 sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js rename sites/dossiers.ngi.hacktivism.ch/{ => cascade}/feat-sm-content/index.html (100%) rename sites/dossiers.ngi.hacktivism.ch/{ => cascade}/feat-website-design/index.html (100%) rename sites/dossiers.ngi.hacktivism.ch/{ => cascade}/feat-website-features/index.html (100%) rename sites/dossiers.ngi.hacktivism.ch/{ => cascade}/feat-website-views/index.html (100%) rename sites/dossiers.ngi.hacktivism.ch/{ => cascade}/index.html (50%) rename sites/dossiers.ngi.hacktivism.ch/{ => cascade}/main/index.html (100%) diff --git a/configs/systemd/user/dossiers-ngi-branch-status.service b/configs/systemd/user/dossiers-ngi-branch-status.service index 4a65aad..76ef361 100644 --- a/configs/systemd/user/dossiers-ngi-branch-status.service +++ b/configs/systemd/user/dossiers-ngi-branch-status.service @@ -1,5 +1,5 @@ [Unit] -Description=Write dossiers.ngi branch-status.json from tdpd tips +Description=dossiers.ngi branch-status JSON + on-demand edition rebuild Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md After=default.target @@ -7,14 +7,18 @@ After=default.target Type=oneshot Nice=10 RemainAfterExit=no +# Full freezes per changed tip can take a while; skip overlap via flock +TimeoutStartSec=1h Environment=TZ=Europe/Zurich Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log -# Override on koopa if the tdpd checkout path differs Environment=TDPD_ROOT=%h/git/src/ngi/tech-dossier-project-data -# Live site root (timer must refresh public JSON, not only the kal tree) Environment=OUT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/branch-status.json +Environment=EDITIONS_ROOT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/editions +# Rebuild only when .edition-tip differs / missing (never force from timer) +Environment=REBUILD=1 EnvironmentFile=-%h/.config/dossiers-ngi/branch-status.env -ExecStart=/usr/bin/python3 %h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/branch-status-hourly.py +# flock -n: second fire while busy exits 0 without stacking garbage +ExecStart=/usr/bin/flock -n %t/dossiers-ngi-branch-status.lock /usr/bin/python3 %h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/branch-status-hourly.py --rebuild SuccessExitStatus=0 1 [Install] diff --git a/configs/systemd/user/dossiers-ngi-branch-status.timer b/configs/systemd/user/dossiers-ngi-branch-status.timer index e6c701b..ba283a6 100644 --- a/configs/systemd/user/dossiers-ngi-branch-status.timer +++ b/configs/systemd/user/dossiers-ngi-branch-status.timer @@ -1,11 +1,12 @@ [Unit] -Description=Timer · dossiers.ngi branch-status JSON (hourly) +Description=Timer · dossiers.ngi branch-status JSON (every 10 min) Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md [Timer] OnBootSec=3min -OnUnitActiveSec=1h -AccuracySec=2min +OnUnitActiveSec=10min +AccuracySec=1min +RandomizedDelaySec=30 Persistent=true Unit=dossiers-ngi-branch-status.service diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md index 1b1131d..2620b26 100644 --- a/scripts/dossiers-ngi/README.md +++ b/scripts/dossiers-ngi/README.md @@ -1,88 +1,109 @@ -# dossiers-ngi — cascade landing + hourly branch-status +# dossiers-ngi — dynamic edition bar + 10‑min rebuild -SoT for the blank cascade landing on **dossiers.ngi.hacktivism.ch** -(path roots only; no dossiers.2 / dossiers.3 ownership here). +SoT for the sticky edition bar on **dossiers.ngi.hacktivism.ch**. Live freezes +sit under `editions//` on koopa; cookie `tdpd_edition` selects the build. +The bar renders a **semi-graphic parent→children tree** from +`/branch-status.json` (all `origin/*` tips, not a hard-coded ladder). | Path | Role | |------|------| -| `sites/dossiers.ngi.hacktivism.ch/` | Landing HTML (EN, ASCII ladder) | -| `scripts/dossiers-ngi/branch-status-hourly.py` | Read tips from tdpd checkout → `branch-status.json` | -| `configs/systemd/user/dossiers-ngi-branch-status.{timer,service}` | Hourly oneshot on koopa | +| `sites/…/_cascade/` | Sticky bar shell + JS tree renderer | +| `editions/` on koopa www | Full freezes switched by the bar | +| `sites/…/cascade/` | Legacy tip stubs (optional) | +| `scripts/dossiers-ngi/branch-status-hourly.py` | Fetch remotes → tree JSON → on-demand rebuild | +| `scripts/dossiers-ngi/smoke-branch-tree.sh` | Temp-branch smoke (creates/deletes local origin only) | +| `configs/systemd/user/dossiers-ngi-branch-status.{timer,service}` | Every **10 min** oneshot on koopa (`flock` against overlap) | ## Env | Var | Default | Meaning | |-----|---------|---------| -| `TDPD_ROOT` | `$HOME/git/src/ngi/tech-dossier-project-data` | tech-dossier-project-data checkout for tip SHAs | -| `OUT` | `sites/dossiers.ngi.hacktivism.ch/branch-status.json` under kal | write path for JSON (live www on koopa) | -| `ADMIN_LOG` | (script derives from `__file__`) | koopa-admin-log root | +| `TDPD_ROOT` | `$HOME/git/src/ngi/tech-dossier-project-data` | tdpd git checkout (may be git-dir only) | +| `TDPD_VENV` | `(TDPD_ROOT)/.venv` | Frozen-Flask venv; symlinked into worktrees | +| `OUT` | kal `sites/…/branch-status.json` | write path (live www on koopa) | +| `EDITIONS_ROOT` | (none) | `…/editions` — required for `--rebuild` | +| `REBUILD` / `--rebuild` | off | rebuild when `.edition-tip` ≠ tip | +| `FORCE_REBUILD` / `--force` | off | rebuild every branch (timing) | +| `FETCH` / `--no-fetch` | fetch on | `git fetch --prune origin` | +| `FREEZE_CMD` | (make/venv freeze) | stub for smoke tests; `{root}` `{branch}` `{slug}` | +| `DEFAULT_BRANCH` | `feat/website-views` | behind cookie `flask` | +| `HOST` | `https://dossiers.ngi.hacktivism.ch` | public host in JSON | -On koopa the clone often lives at `%h/src/koopa/koopa-admin-log`; set -`TDPD_ROOT` and `OUT` in `~/.config/dossiers-ngi/branch-status.env` when the -tdpd path or live www path differs from the laptop defaults. +On koopa: `%h/src/koopa/koopa-admin-log` + +`~/.config/dossiers-ngi/branch-status.env` for path overrides. +NGI clone token lives in **koopa-admin-secrets-enc** (not this tree). ## Manual run ```bash +# JSON only python3 scripts/dossiers-ngi/branch-status-hourly.py -# → sites/dossiers.ngi.hacktivism.ch/branch-status.json + +# rebuild stale freezes + timing block in JSON +EDITIONS_ROOT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/editions \ + python3 scripts/dossiers-ngi/branch-status-hourly.py --rebuild + +# force all branches (wall clock → rebuild_timing) +… --rebuild --force +``` + +`rebuild_timing` fields: `wall_seconds`, `built`, `skipped`, `errors`, +`built_seconds_sum`, `built_seconds_max`. Per-branch `rebuild[name].seconds`. + +Per branch in JSON / sticky bar: + +| Field | Meaning | +|-------|---------| +| `tip` | short SHA | +| `tip_time` | committer time of tip (`%cI`) | +| `built_at` | when this edition freeze was written | +| `build_seconds` | wall time of that freeze | + +Rebuild runs **only when** `.edition-tip` ≠ current tip (timer never forces). +Each run also: `git fetch --prune`, `worktree prune`, drop stale +`editions//` (gone remotes), purge leftover `tdpd-*` tmp dirs. + +## Smoke + +```bash +bash scripts/dossiers-ngi/smoke-branch-tree.sh +# → SMOKE_OK (temp origin + smoke/temp-sibling deleted) ``` ## Deploy landing (Freigabe) -```text -hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/ +Do **not** `--delete` the whole www root. Sync: + +```bash +SITE="$HOME/git/admin-logs/local/koopa-admin-log/sites/dossiers.ngi.hacktivism.ch" +WWW=hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch +rsync -avz --exclude .DS_Store "$SITE/_cascade/" "$WWW/_cascade/" +rsync -avz --exclude .DS_Store "$SITE/branch-status.json" "$WWW/branch-status.json" +# editions/ from a --rebuild run ``` -rsync from `sites/dossiers.ngi.hacktivism.ch/` — Freigabe this session required. -Hourly JSON can sit next to the HTML once the timer is installed on koopa -(Freigabe for host mutation). - -## Ownership - -Hourly builder belongs in **kal** (this tree). Do **not** put -`scripts/branch-status-hourly.py` back into tdpd. - ## Sticky cascade bar -Stock Caddy `templates` renders `{% include "_cascade/bar.html" . %}` on each -HTML page. The include path has no leading slash. The dot passes the request, -so `.Req.URL.Path` marks the active link with `class="is-active"` and -`aria-current="page"`. Delimiters are `{%` `%}`, so a later `{{ }}` in real -dossier HTML stays literal. +`bar.js` fetches `/branch-status.json` and draws: -The bar lists cascade `/` plus the five branch paths. Seeded tips match -`branch-status.json`. `/_cascade/bar.js` refreshes `[data-tip]` from that JSON -(`cache: no-store`). CSS is `/_cascade/bar.css` (`position: sticky`). +``` +main @ d2fbd79 +├── feat/sm-content @ … +│ └── … +└── project_template @ … +``` -Enable templates before rsync. A page without the include does not grow the -bar. `{%` without the templates directive is shown as raw text. +Each name links to `/_cascade/use/` (cookie + 302 `/`). Caddy map: -Templates apply is **host-local on koopa** (`~/bin/apply-dossiers-ngi-templates.sh`), -not tracked in this repo. It edits `~/koopa-caddy/Caddyfile` only (this host; -keeps `header Alt-Svc "clear"` and any `basic_auth`; leaves `.2` / `.3` -untouched). Then reload with the usual home→`/etc` helper: +``` +~^[A-Za-z0-9_-]+$ {http.request.cookie.tdpd_edition} +``` + +so new branches need no Caddy edit — only an `editions//` tree. ```bash -bash ~/bin/apply-dossiers-ngi-templates.sh +bash ~/bin/apply-dossiers-ngi-edition-switch.sh sudo ~/bin/caddy-apply ``` -The stanza body for hand-check / re-seed lives in -`scripts/dossiers-ngi/caddy-dossiers-ngi.snippet`. -`scripts/caddy/caddy-apply.sh --snippet` leaves an existing block unchanged. -Leave `scripts/caddy/apply-goa-wdGet-full-replace.sh` and -`scripts/caddy/apply-dossiers-ngi-basicauth.sh` unused. No response-body -replace plugin. - -After validate and reload succeed, rsync from the laptop (Freigabe): - -```bash -rsync -avz --delete --exclude .DS_Store \ - "$HOME/git/admin-logs/local/koopa-admin-log/sites/dossiers.ngi.hacktivism.ch/" \ - hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/ -``` - -`--delete` removes `_cascade/` on the server when the source tree is an older -copy. The bar does not finish the dossier site. This section does not install -the hourly timer. +Stanza SoT: `scripts/dossiers-ngi/caddy-dossiers-ngi.snippet`. diff --git a/scripts/dossiers-ngi/apply-dossiers-ngi-edition-switch.sh b/scripts/dossiers-ngi/apply-dossiers-ngi-edition-switch.sh new file mode 100755 index 0000000..ea043c3 --- /dev/null +++ b/scripts/dossiers-ngi/apply-dossiers-ngi-edition-switch.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Host-local on koopa. Replaces dossiers.ngi.hacktivism.ch site-block body +# with cookie→editions sticky-bar switch. Leaves .2/.3 alone. +# Next: sudo ~/bin/caddy-apply +set -euo pipefail + +CADDY="${CADDY:-$HOME/koopa-caddy/Caddyfile}" +SITE="dossiers.ngi.hacktivism.ch" +SNIPPET="${SNIPPET:-$HOME/bin/caddy-dossiers-ngi.snippet}" + +if [[ ! -f "$CADDY" ]]; then + echo "FAIL: missing $CADDY" >&2 + exit 1 +fi +if [[ ! -f "$SNIPPET" ]]; then + echo "FAIL: missing snippet $SNIPPET" >&2 + exit 1 +fi + +ts="$(date +%Y%m%d-%H%M%S)" +bak="${CADDY}.bak-dossiers-edition-${ts}" +cp -a "$CADDY" "$bak" +echo "bak: $bak" + +python3 - "$CADDY" "$SITE" "$SNIPPET" <<'PY' +import sys +from pathlib import Path + +caddy_path, site, snippet_path = Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]) +text = caddy_path.read_text() +raw_snip = snippet_path.read_text().splitlines() +body_lines = [] +for line in raw_snip: + if line.startswith("#") or not line.strip(): + continue + body_lines.append(line if line.startswith("\t") else "\t" + line.lstrip()) +body = "\n".join(body_lines).rstrip() + "\n" +if "edition_dir" not in body or "tdpd_edition" not in body: + print("FAIL: snippet body looks empty/wrong", file=sys.stderr) + sys.exit(1) + +needle = site + " {" +start = text.find(needle) +if start < 0: + # allow no space + needle = site + "{" + start = text.find(needle) +if start < 0: + print(f"FAIL: site block not found: {site}", file=sys.stderr) + sys.exit(1) + +brace_open = text.find("{", start) +if brace_open < 0: + print("FAIL: no opening brace", file=sys.stderr) + sys.exit(1) + +depth = 0 +end = None +for i, ch in enumerate(text[brace_open:], start=brace_open): + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + end = i + break +if end is None: + print("FAIL: unclosed site block", file=sys.stderr) + sys.exit(1) + +# keep optional newline after closing brace +close_end = end + 1 +if close_end < len(text) and text[close_end] == "\n": + close_end += 1 + +new_text = text[: brace_open + 1] + "\n" + body + "}" + ("\n" if text[end+1:end+2] == "\n" or True else "") +# normalize: site {\n BODY }\n +new_text = text[: brace_open + 1] + "\n" + body + "}\n" + text[close_end:] +caddy_path.write_text(new_text) +print(f"OK: replaced body of {site} ({len(body_lines)} lines)") +print("Next: sudo ~/bin/caddy-apply") +PY diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py index 4507313..16b0a3f 100755 --- a/scripts/dossiers-ngi/branch-status-hourly.py +++ b/scripts/dossiers-ngi/branch-status-hourly.py @@ -1,90 +1,637 @@ #!/usr/bin/env python3 -"""Emit branch-status.json for the dossiers.ngi path-root ladder. +"""Discover all origin branches, emit branch-status.json, optional edition rebuild. -Reads tip SHAs from the tech-dossier-project-data git checkout -(TDPD_ROOT / default). Writes under the kal landing SoT: +Dynamic cascade for dossiers.ngi.hacktivism.ch: - sites/dossiers.ngi.hacktivism.ch/branch-status.json + • every origin/* tip (not a hard-coded ladder) + • parent = nearest other tip that is a git ancestor + • sticky bar reads this JSON and draws a semi-graphic tree + • optional --rebuild / REBUILD=1: worktree + make html into editions// + only when .edition-tip differs (on demand; timer ~10min) -Path roots only under https://dossiers.ngi.hacktivism.ch/ -(/main/, /feat-sm-content/, /feat-website-features/, - /feat-website-design/, /feat-website-views/). -Single public host only. Hourly builder lives in kal, not tdpd. +Env: + + TDPD_ROOT tech-dossier-project-data checkout (may be git-dir only) + OUT branch-status.json path (live www on koopa) + EDITIONS_ROOT editions/ dir on www (required for rebuild) + REBUILD 1 → rebuild stale/missing freezes + FETCH 0 → skip git fetch (default 1) + FREEZE_CMD override build (default: make html); `{root}` `{branch}` `{slug}` + DEFAULT_BRANCH branch behind cookie flask (default feat/website-views) + HOST public host URL """ from __future__ import annotations +import argparse import json import os +import re +import shutil import subprocess import sys +import tempfile +import time from datetime import datetime, timezone from pathlib import Path ADMIN_LOG = Path(__file__).resolve().parents[2] _DEFAULT_OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json" -HOST = "https://dossiers.ngi.hacktivism.ch" +_PLACEHOLDER = ( + ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "_edition-placeholder" / "index.html" +) +HOST_DEFAULT = "https://dossiers.ngi.hacktivism.ch" +DEFAULT_TDPD = Path.home() / "git" / "src" / "ngi" / "tech-dossier-project-data" +INJECT = Path(__file__).resolve().parent / "inject-cascade-bar.py" +SLUG_RE = re.compile(r"[^A-Za-z0-9_-]+") + + +def env_path(name: str, default: Path | None = None) -> Path | None: + raw = os.environ.get(name, "").strip() + if raw: + return Path(raw).expanduser() + return default def out_path() -> Path: - raw = os.environ.get("OUT", "").strip() - return Path(raw).expanduser() if raw else _DEFAULT_OUT - -# Default local clone; override with TDPD_ROOT (koopa may use a different path). -DEFAULT_TDPD = Path.home() / "git" / "src" / "ngi" / "tech-dossier-project-data" - -LADDER = [ - ("main", "", f"{HOST}/main/"), - ("feat/sm-content", "main", f"{HOST}/feat-sm-content/"), - ("feat/website-features", "feat/sm-content", f"{HOST}/feat-website-features/"), - ("feat/website-design", "feat/website-features", f"{HOST}/feat-website-design/"), - ("feat/website-views", "feat/website-design", f"{HOST}/feat-website-views/"), -] + return env_path("OUT", _DEFAULT_OUT) # type: ignore[return-value] def tdpd_root() -> Path: - raw = os.environ.get("TDPD_ROOT", "").strip() - return Path(raw).expanduser() if raw else DEFAULT_TDPD + return env_path("TDPD_ROOT", DEFAULT_TDPD) # type: ignore[return-value] -def tip_of(repo: Path, branch: str) -> str: +def editions_root() -> Path | None: + return env_path("EDITIONS_ROOT", None) + + +def host() -> str: + return os.environ.get("HOST", "").strip() or HOST_DEFAULT + + +def default_branch() -> str: + return os.environ.get("DEFAULT_BRANCH", "").strip() or "feat/website-views" + + +def edition_slug(branch: str) -> str: + return SLUG_RE.sub("-", branch.replace("/", "-")).strip("-") or "branch" + + +def git(repo: Path, *args: str, check: bool = True, text: bool = True) -> subprocess.CompletedProcess: + return subprocess.run( + ["git", "-C", str(repo), *args], + check=check, + text=text, + capture_output=True, + ) + + +def fetch_origin(repo: Path) -> None: + if os.environ.get("FETCH", "1").strip() == "0": + return + subprocess.run( + ["git", "-C", str(repo), "fetch", "--prune", "origin"], + check=False, + capture_output=True, + text=True, + ) + + +def list_origin_branches(repo: Path) -> dict[str, str]: + """branch name → full SHA for refs/remotes/origin/* (no HEAD).""" + cp = git(repo, "for-each-ref", "--format=%(refname:short)|%(objectname)", "refs/remotes/origin") + out: dict[str, str] = {} + for line in cp.stdout.splitlines(): + if "|" not in line: + continue + short, sha = line.split("|", 1) + if short in ("origin", "origin/HEAD") or "->" in short: + continue + if not short.startswith("origin/"): + continue + name = short[len("origin/") :] + if name == "HEAD": + continue + out[name] = sha + return out + + +def is_ancestor(repo: Path, anc: str, desc: str) -> bool: + cp = git(repo, "merge-base", "--is-ancestor", anc, desc, check=False) + return cp.returncode == 0 + + +def commits_between(repo: Path, a: str, b: str) -> int: + cp = git(repo, "rev-list", "--count", f"{a}..{b}", check=False) + if cp.returncode != 0: + return 10**9 try: - out = subprocess.check_output( - ["git", "-C", str(repo), "rev-parse", "--short", branch], - stderr=subprocess.DEVNULL, - text=True, - ).strip() - return out - except (subprocess.CalledProcessError, FileNotFoundError, OSError): - return "" + return int(cp.stdout.strip() or "0") + except ValueError: + return 10**9 -def main() -> int: - repo = tdpd_root() - if not (repo / ".git").exists() and not repo.is_dir(): - print(f"TDPD_ROOT missing or not a dir: {repo}", file=sys.stderr) - return 1 - branches = [] - for name, parent, url in LADDER: - branches.append( +def short_sha(sha: str) -> str: + return sha[:7] if sha else "" + + +def nearest_parents(repo: Path, tips: dict[str, str]) -> dict[str, str]: + """For each branch, nearest other tip that is a strict ancestor.""" + parents: dict[str, str] = {} + names = list(tips) + for b in names: + tip_b = tips[b] + cands: list[tuple[int, str]] = [] + for a in names: + if a == b: + continue + tip_a = tips[a] + if tip_a == tip_b: + continue + if not is_ancestor(repo, tip_a, tip_b): + continue + cands.append((commits_between(repo, tip_a, tip_b), a)) + parents[b] = min(cands)[1] if cands else "" + return parents + + +def children_map(parents: dict[str, str]) -> dict[str, list[str]]: + kids: dict[str, list[str]] = {b: [] for b in parents} + orphans: list[str] = [] + for b, p in parents.items(): + if p and p in kids: + kids[p].append(b) + else: + orphans.append(b) + for p in kids: + kids[p].sort() + orphans.sort() + # roots = orphans (parent missing or empty) + kids[""] = orphans + return kids + + +def walk_tree( + parents: dict[str, str], + tips: dict[str, str], +) -> list[dict]: + """Flat rows in DFS order with semi-graphic prefix fields.""" + kids = children_map(parents) + rows: list[dict] = [] + + def walk(name: str, prefix: str, is_last: bool, is_root: bool) -> None: + tip = tips[name] + if is_root: + branch_prefix = "" + child_prefix = "" + else: + branch_prefix = prefix + ("└── " if is_last else "├── ") + child_prefix = prefix + (" " if is_last else "│ ") + rows.append( { "branch": name, - "tip": tip_of(repo, name), - "parent": parent, - "url": url, + "edition": edition_slug(name), + "tip": short_sha(tip), + "tip_full": tip, + "parent": parents.get(name, ""), + "prefix": branch_prefix, + "url": f"/_cascade/use/{edition_slug(name)}", } ) + ch = kids.get(name, []) + for i, c in enumerate(ch): + walk(c, child_prefix, i == len(ch) - 1, False) + + roots = kids.get("", []) + for i, r in enumerate(roots): + walk(r, "", i == len(roots) - 1, True) + return rows + + +def ensure_placeholder(editions: Path, slug: str, tip: str) -> None: + dest = editions / slug + dest.mkdir(parents=True, exist_ok=True) + idx = dest / "index.html" + if not idx.exists(): + src = _PLACEHOLDER + if src.is_file(): + idx.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") + else: + idx.write_text( + "" + '{% include "_cascade/bar.html" . %}' + "

      edition pending

      \n", + encoding="utf-8", + ) + tip_path = dest / ".edition-tip" + if not tip_path.exists(): + tip_path.write_text(tip + "\n", encoding="utf-8") + + +def tip_recorded(editions: Path, slug: str) -> str: + p = editions / slug / ".edition-tip" + if not p.is_file(): + return "" + return p.read_text(encoding="utf-8").strip() + + +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) + if cp.returncode != 0: + return "" + return (cp.stdout or "").strip() + + +def read_build_meta(editions: Path, slug: str) -> dict: + dest = editions / slug + out: dict = {} + at = dest / ".edition-built-at" + sec = dest / ".edition-build-seconds" + if at.is_file(): + out["built_at"] = at.read_text(encoding="utf-8").strip() + if sec.is_file(): + raw = sec.read_text(encoding="utf-8").strip() + try: + out["build_seconds"] = float(raw) + except ValueError: + pass + return out + + +def write_build_meta(editions: Path, slug: str, seconds: float) -> None: + dest = editions / slug + dest.mkdir(parents=True, exist_ok=True) + now = datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds") + (dest / ".edition-built-at").write_text(now + "\n", encoding="utf-8") + (dest / ".edition-build-seconds").write_text(f"{seconds:.3f}\n", encoding="utf-8") + + +def cleanup_stale_editions(editions: Path, tips: dict[str, str]) -> list[str]: + """Drop edition dirs whose branch tip is gone (keep flask alias).""" + keep = {edition_slug(b) for b in tips} | {"flask"} + removed: list[str] = [] + if not editions.is_dir(): + return removed + for child in list(editions.iterdir()): + if not child.is_dir(): + continue + if child.name in keep: + continue + shutil.rmtree(child, ignore_errors=True) + removed.append(child.name) + return removed + + +def cleanup_orphan_tmps(max_age_sec: int = 3600) -> int: + """Remove leftover tdpd-* workdirs older than max_age_sec.""" + bases: list[Path] = [] + home_tmp = Path.home() / "tmp" + if home_tmp.is_dir(): + bases.append(home_tmp) + bases.append(Path(tempfile.gettempdir())) + cutoff = time.time() - max_age_sec + n = 0 + for base in bases: + try: + kids = list(base.glob("tdpd-*")) + except OSError: + continue + for path in kids: + if not path.is_dir(): + continue + try: + if path.stat().st_mtime >= cutoff: + continue + except OSError: + continue + shutil.rmtree(path, ignore_errors=True) + n += 1 + return n + + +def cleanup_repo(repo: Path) -> None: + git(repo, "worktree", "prune", check=False) + + +def _attach_venv(work: Path) -> None: + """Worktrees lack .venv; reuse TDPD_VENV or sibling checkout .venv via symlink.""" + if (work / ".venv" / "bin" / "python").exists(): + return + candidates: list[Path] = [] + raw = os.environ.get("TDPD_VENV", "").strip() + if raw: + candidates.append(Path(raw).expanduser()) + root = tdpd_root() + candidates.append(root / ".venv") + # laptop SoT checkout even when TDPD_ROOT is git-dir-only on koopa + candidates.append(DEFAULT_TDPD / ".venv") + for venv in candidates: + py = venv / "bin" / "python" + if py.is_file(): + dest = work / ".venv" + if dest.exists() or dest.is_symlink(): + return + dest.symlink_to(venv.resolve()) + return + + +def run_freeze(work: Path, branch: str, slug: str) -> None: + tmpl = os.environ.get("FREEZE_CMD", "").strip() + env = os.environ.copy() + env["TDPD_BRANCH"] = branch + env["TDPD_SLUG"] = slug + if tmpl: + cmd = tmpl.format(root=str(work), branch=branch, slug=slug) + subprocess.run(cmd, shell=True, check=True, cwd=str(work), env=env) + return + _attach_venv(work) + # Older branches hardcode `python3 -m website.freeze` without .venv; + # prefer the attached venv interpreter directly. + py = work / ".venv" / "bin" / "python" + freeze_mod = work / "website" / "freeze.py" + if py.is_file() and freeze_mod.is_file(): + build = work / "website" / "build" + if build.exists(): + shutil.rmtree(build) + subprocess.run( + [str(py), "-m", "website.freeze"], + check=True, + cwd=str(work), + env=env, + ) + return + makefile = work / "Makefile" + if not makefile.is_file(): + raise FileNotFoundError(f"no Makefile in {work}") + subprocess.run(["make", "html"], check=True, cwd=str(work), env=env) + + +def inject_bar(edition_dir: Path) -> None: + if not INJECT.is_file(): + return + subprocess.run( + [sys.executable, str(INJECT), str(edition_dir)], + check=False, + capture_output=True, + text=True, + ) + + +def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict: + """Return {status, seconds} — status skipped|built|placeholder|error:…""" + slug = edition_slug(branch) + dest = editions / slug + t0 = time.perf_counter() + force = os.environ.get("FORCE_REBUILD", "").strip() in ("1", "true", "yes") + if ( + not force + and tip_recorded(editions, slug) == tip + and (dest / "index.html").is_file() + ): + inject_bar(dest) + return {"status": "skipped", "seconds": round(time.perf_counter() - t0, 3)} + + wt: Path | None = None + try: + tmp_base = Path.home() / "tmp" if (Path.home() / "tmp").is_dir() else Path(tempfile.gettempdir()) + wt = Path(tempfile.mkdtemp(prefix=f"tdpd-{slug}-", dir=str(tmp_base))) + cp = git(repo, "worktree", "add", "--detach", str(wt), tip, check=False) + if cp.returncode != 0: + git(repo, "worktree", "prune", check=False) + cp = git(repo, "worktree", "add", "--detach", str(wt), tip, check=False) + if cp.returncode != 0: + ensure_placeholder(editions, slug, tip) + return { + "status": f"error:worktree:{cp.stderr.strip()[:200]}", + "seconds": round(time.perf_counter() - t0, 3), + } + + run_freeze(wt, branch, slug) + build = wt / "website" / "build" + if not build.is_dir(): + ensure_placeholder(editions, slug, tip) + return {"status": "error:no-build", "seconds": round(time.perf_counter() - t0, 3)} + + dest.mkdir(parents=True, exist_ok=True) + keep_meta = { + ".edition-tip", + ".edition-built-at", + ".edition-build-seconds", + } + for child in list(dest.iterdir()): + if child.name in keep_meta: + continue + if child.is_dir(): + shutil.rmtree(child) + else: + child.unlink() + for child in build.iterdir(): + target = dest / child.name + if child.is_dir(): + shutil.copytree(child, target) + else: + shutil.copy2(child, target) + secs = round(time.perf_counter() - t0, 3) + (dest / ".edition-tip").write_text(tip + "\n", encoding="utf-8") + write_build_meta(editions, slug, secs) + inject_bar(dest) + return {"status": "built", "seconds": secs} + except Exception as exc: # noqa: BLE001 — hourly must continue + ensure_placeholder(editions, slug, tip) + return { + "status": f"error:{type(exc).__name__}:{exc}", + "seconds": round(time.perf_counter() - t0, 3), + } + finally: + if wt is not None: + git(repo, "worktree", "remove", "--force", str(wt), check=False) + shutil.rmtree(wt, ignore_errors=True) + + +def sync_flask_alias(editions: Path, tips: dict[str, str]) -> None: + """Keep editions/flask in sync with DEFAULT_BRANCH freeze when present.""" + src_branch = default_branch() + if src_branch not in tips: + return + src = editions / edition_slug(src_branch) + dst = editions / "flask" + tip = tips[src_branch] + if tip_recorded(editions, "flask") == tip and (dst / "index.html").is_file(): + return + if not (src / "index.html").is_file(): + ensure_placeholder(editions, "flask", tip) + return + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) + (dst / ".edition-tip").write_text(tip + "\n", encoding="utf-8") + # reuse src build meta if present + for name in (".edition-built-at", ".edition-build-seconds"): + s = src / name + if s.is_file(): + shutil.copy2(s, dst / name) + inject_bar(dst) + + +def build_payload(repo: Path, tips: dict[str, str], rebuild: bool) -> dict: + parents = nearest_parents(repo, tips) + rows = walk_tree(parents, tips) + rebuild_status: dict[str, dict] = {} + rebuild_timing: dict | None = None + ed_root = editions_root() + cleanup_repo(repo) + n_tmp = cleanup_orphan_tmps() + if n_tmp: + print(f"cleanup_tmp removed={n_tmp}", file=sys.stderr) + if ed_root is not None: + ed_root.mkdir(parents=True, exist_ok=True) + removed = cleanup_stale_editions(ed_root, tips) + if removed: + print(f"cleanup_editions removed={removed}", file=sys.stderr) + if rebuild and ed_root is not None: + wall0 = time.perf_counter() + for name, tip in sorted(tips.items()): + # rebuild_edition itself skips when .edition-tip matches tip + rebuild_status[name] = rebuild_edition(repo, name, tip, ed_root) + sync_flask_alias(ed_root, tips) + cleanup_repo(repo) + cleanup_orphan_tmps(max_age_sec=600) + wall = round(time.perf_counter() - wall0, 3) + built_secs = [ + v["seconds"] + for v in rebuild_status.values() + if v.get("status") == "built" + ] + rebuild_timing = { + "wall_seconds": wall, + "branches": len(tips), + "built": sum(1 for v in rebuild_status.values() if v.get("status") == "built"), + "skipped": sum(1 for v in rebuild_status.values() if v.get("status") == "skipped"), + "errors": sum( + 1 for v in rebuild_status.values() if str(v.get("status", "")).startswith("error") + ), + "built_seconds_sum": round(sum(built_secs), 3) if built_secs else 0.0, + "built_seconds_max": round(max(built_secs), 3) if built_secs else 0.0, + } + print( + f"REBUILD_TIMING wall_seconds={wall} built={rebuild_timing['built']} " + f"skipped={rebuild_timing['skipped']} errors={rebuild_timing['errors']} " + f"built_seconds_sum={rebuild_timing['built_seconds_sum']} " + f"built_seconds_max={rebuild_timing['built_seconds_max']}", + file=sys.stderr, + ) + elif ed_root is not None: + ed_root.mkdir(parents=True, exist_ok=True) + for name, tip in tips.items(): + slug = edition_slug(name) + if not (ed_root / slug / "index.html").is_file(): + ensure_placeholder(ed_root, slug, tip) + rebuild_status[name] = {"status": "placeholder", "seconds": 0.0} + if not (ed_root / "flask" / "index.html").is_file(): + sync_flask_alias(ed_root, tips) + + tip_times = {name: tip_commit_time(repo, tip) for name, tip in tips.items()} + branches_out = [] + for row in rows: + name = row["branch"] + slug = row["edition"] + meta = read_build_meta(ed_root, slug) if ed_root is not None else {} + # prefer this-run built seconds when present + rb = rebuild_status.get(name) or {} + if rb.get("status") == "built" and "seconds" in rb: + meta = { + **meta, + "build_seconds": rb["seconds"], + } + entry = { + "branch": name, + "edition": slug, + "tip": row["tip"], + "tip_time": tip_times.get(name, ""), + "parent": row["parent"], + "prefix": row["prefix"], + "url": row["url"], + } + if meta.get("built_at"): + entry["built_at"] = meta["built_at"] + if "build_seconds" in meta: + entry["build_seconds"] = meta["build_seconds"] + branches_out.append(entry) + payload = { "generated": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"), - "host": HOST, - "branches": [ - {"branch": b["branch"], "tip": b["tip"], "url": b["url"]} - for b in branches - ], + "host": host(), + "default_branch": default_branch(), + "default_edition": "flask", + "branches": branches_out, } + if rebuild_status: + payload["rebuild"] = rebuild_status + if rebuild_timing is not None: + payload["rebuild_timing"] = rebuild_timing + return payload + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument( + "--rebuild", + action="store_true", + help="rebuild stale editions into EDITIONS_ROOT", + ) + p.add_argument( + "--force", + action="store_true", + help="rebuild every edition even when .edition-tip matches (timing)", + ) + p.add_argument( + "--no-fetch", + action="store_true", + help="skip git fetch --prune origin", + ) + return p.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + repo = tdpd_root() + if not (repo / ".git").exists() and not (repo / "HEAD").exists(): + # allow bare / gitfile + gitfile = repo / ".git" + if not repo.is_dir(): + print(f"TDPD_ROOT missing or not a dir: {repo}", file=sys.stderr) + return 1 + if not gitfile.exists() and not (repo / "objects").exists(): + print(f"TDPD_ROOT has no git metadata: {repo}", file=sys.stderr) + return 1 + + if args.no_fetch: + os.environ["FETCH"] = "0" + if args.force: + os.environ["FORCE_REBUILD"] = "1" + fetch_origin(repo) + + tips = list_origin_branches(repo) + if not tips: + print("no origin/* branches found", file=sys.stderr) + return 1 + + rebuild = ( + args.rebuild + or args.force + or os.environ.get("REBUILD", "").strip() in ("1", "true", "yes") + ) + if rebuild and editions_root() is None: + print("REBUILD set but EDITIONS_ROOT missing", file=sys.stderr) + return 1 + + payload = build_payload(repo, tips, rebuild) dest = out_path() dest.parent.mkdir(parents=True, exist_ok=True) dest.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") - print(f"wrote {dest}", file=sys.stderr) + print(f"wrote {dest} branches={len(payload['branches'])}", file=sys.stderr) + if "rebuild" in payload: + print(f"rebuild={payload['rebuild']}", file=sys.stderr) return 0 diff --git a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet index 1418403..c04676a 100644 --- a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet +++ b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet @@ -1,19 +1,44 @@ # Stanza body for dossiers.ngi.hacktivism.ch only (hand-check / re-seed). -# Templates apply is host-local on koopa (not in this repo): -# bash ~/bin/apply-dossiers-ngi-templates.sh +# Apply on koopa (host-local): +# bash ~/bin/apply-dossiers-ngi-edition-switch.sh # sudo ~/bin/caddy-apply # -# Keep the existing line: header Alt-Svc "clear" -# Keep any existing basic_auth in that same block. -# scripts/caddy/caddy-apply.sh --snippet reports an existing block and -# does not merge these lines. Leave that script unused for this change. -# Leave scripts/caddy/apply-goa-wdGet-full-replace.sh unused. -# Leave scripts/caddy/apply-dossiers-ngi-basicauth.sh unused. -# Stock Caddy templates only. No replace-response plugin. +# Sticky bar switches build via cookie tdpd_edition → editions//. +# Stock Caddy only (templates + map + handle). No replace-response plugin. +# Keep .2 / .3 blocks untouched. # -# Tab-indented lines inserted after header Alt-Svc: +# Site-block body (no outer braces): + header Alt-Svc "clear" + + @use path_regexp use ^/_cascade/use/([A-Za-z0-9_-]+)/?$ + handle @use { + header Set-Cookie "tdpd_edition={re.use.1}; Path=/; Max-Age=31536000; SameSite=Lax" + redir * / 302 + } + + handle /_cascade/* { + root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch + file_server + } + handle /branch-status.json { + root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch + file_server + } + + # Dynamic editions: cookie value is the dir name under editions/ + # (hourly builder stages every origin/* tip as slug with / → -). + map {http.request.cookie.tdpd_edition} {edition_dir} { + default flask + flask flask + ~^[A-Za-z0-9_-]+$ {http.request.cookie.tdpd_edition} + } + + root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/editions/{edition_dir} templates { mime text/html between "{%" "%}" + root /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch } + file_server + try_files {path} {path}/ {path}/index.html diff --git a/scripts/dossiers-ngi/inject-cascade-bar.py b/scripts/dossiers-ngi/inject-cascade-bar.py new file mode 100755 index 0000000..9659dde --- /dev/null +++ b/scripts/dossiers-ngi/inject-cascade-bar.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Insert {% include "_cascade/bar.html" . %} after in freeze HTML. + +Idempotent. Skips files that already contain the include or live under +_cascade/. Usage: + + python3 inject-cascade-bar.py /path/to/edition/root +""" +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: + return None + if "_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:] + + +def main() -> int: + if len(sys.argv) != 2: + print(f"usage: {sys.argv[0]} EDITION_ROOT", file=sys.stderr) + return 2 + root = Path(sys.argv[1]).resolve() + if not root.is_dir(): + print(f"not a dir: {root}", file=sys.stderr) + return 2 + changed = 0 + scanned = 0 + for path in root.rglob("*.html"): + if "/_cascade/" in path.as_posix() + "/": + continue + scanned += 1 + raw = path.read_text(encoding="utf-8", errors="ignore") + out = inject(raw) + if out is None: + continue + path.write_text(out, encoding="utf-8") + changed += 1 + print(f"OK: scanned={scanned} injected={changed} root={root}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/dossiers-ngi/smoke-branch-tree.sh b/scripts/dossiers-ngi/smoke-branch-tree.sh new file mode 100755 index 0000000..6c6340b --- /dev/null +++ b/scripts/dossiers-ngi/smoke-branch-tree.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Local smoke: temp origin with extra branches → tree JSON + stub rebuild → cleanup. +# Does not touch origin on git.ngi-0.eu. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SCRIPT="$ROOT/scripts/dossiers-ngi/branch-status-hourly.py" +TMP="$(mktemp -d "${TMPDIR:-/tmp}/tdpd-smoke-XXXXXX")" +cleanup() { rm -rf "$TMP"; } +trap cleanup EXIT + +echo "TMP=$TMP" + +BARE="$TMP/origin.git" +CLONE="$TMP/tdpd" +EDITIONS="$TMP/editions" +OUT="$TMP/branch-status.json" + +git init --bare "$BARE" >/dev/null +git clone "$BARE" "$CLONE" >/dev/null 2>&1 +cd "$CLONE" +git config user.email "smoke@example.test" +git config user.name "smoke" +echo "root" > README +git add README +git commit -m "main root" >/dev/null +git branch -M main +git push -u origin main >/dev/null 2>&1 + +# ladder: main → feat/a → feat/b +git checkout -b feat/a >/dev/null 2>&1 +echo a > a.txt && git add a.txt && git commit -m "feat a" >/dev/null +git push -u origin feat/a >/dev/null 2>&1 +git checkout -b feat/b >/dev/null 2>&1 +echo b > b.txt && git add b.txt && git commit -m "feat b" >/dev/null +git push -u origin feat/b >/dev/null 2>&1 + +# sibling off main +git checkout main >/dev/null 2>&1 +git checkout -b smoke/temp-sibling >/dev/null 2>&1 +echo s > s.txt && git add s.txt && git commit -m "smoke sibling" >/dev/null +git push -u origin smoke/temp-sibling >/dev/null 2>&1 + +# stub freeze: write website/build/index.html +FREEZE_CMD='mkdir -p "{root}/website/build" && printf "%s\n" "

      {branch}

      " > "{root}/website/build/index.html"' + +mkdir -p "$EDITIONS" +# seed a Makefile so non-stub path would find one; stub overrides via FREEZE_CMD +FETCH=0 TDPD_ROOT="$CLONE" OUT="$OUT" EDITIONS_ROOT="$EDITIONS" \ + FREEZE_CMD="$FREEZE_CMD" REBUILD=1 \ + python3 "$SCRIPT" --rebuild --no-fetch + +python3 - "$OUT" <<'PY' +import json, sys +from pathlib import Path +data = json.loads(Path(sys.argv[1]).read_text()) +branches = {b["branch"]: b for b in data["branches"]} +assert "main" in branches +assert "feat/a" in branches +assert "feat/b" in branches +assert "smoke/temp-sibling" in branches +assert branches["feat/a"]["parent"] == "main", branches["feat/a"] +assert branches["feat/b"]["parent"] == "feat/a", branches["feat/b"] +assert branches["smoke/temp-sibling"]["parent"] == "main", branches["smoke/temp-sibling"] +# prefixes semi-graphic +prefs = {b["branch"]: b["prefix"] for b in data["branches"]} +assert any("├──" in p or "└──" in p for p in prefs.values()), prefs +# editions built +print("OK tree:") +for b in data["branches"]: + print(f" {b['prefix']}{b['branch']} @ {b['tip']} -> {b['edition']} parent={b['parent']!r}") +print("rebuild", data.get("rebuild")) +PY + +# editions exist with stub content +test -f "$EDITIONS/feat-a/index.html" +test -f "$EDITIONS/smoke-temp-sibling/index.html" +grep -q "feat/a" "$EDITIONS/feat-a/index.html" +grep -q "smoke/temp-sibling" "$EDITIONS/smoke-temp-sibling/index.html" + +# delete smoke branch on origin; fetch --prune drops remote-tracking ref +cd "$CLONE" +git push origin --delete smoke/temp-sibling >/dev/null 2>&1 +git branch -D smoke/temp-sibling >/dev/null 2>&1 || true +TDPD_ROOT="$CLONE" OUT="$OUT" EDITIONS_ROOT="$EDITIONS" \ + FREEZE_CMD="$FREEZE_CMD" \ + python3 "$SCRIPT" +python3 - "$OUT" <<'PY' +import json, sys +from pathlib import Path +data = json.loads(Path(sys.argv[1]).read_text()) +names = {b["branch"] for b in data["branches"]} +assert "smoke/temp-sibling" not in names, names +assert names == {"main", "feat/a", "feat/b"}, names +print("OK smoke branch removed from tree") +PY + +echo "SMOKE_OK" diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css index 0f9e1cc..2a5e185 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css @@ -2,28 +2,50 @@ position: sticky; top: 0; z-index: 10; - display: flex; - flex-wrap: wrap; - gap: 0.35rem 0.85rem; - align-items: baseline; margin: -1.5rem -1.5rem 1rem; - padding: 0.55rem 1.5rem; + padding: 0.55rem 1.5rem 0.65rem; 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; } -.cascade-bar a { +.cascade-tree-lines { + white-space: pre; + overflow-x: auto; +} +a.cascade-tree-row { + display: block; color: #7ec8ff; text-decoration: none; + padding: 0.05rem 0.15rem; + border-radius: 2px; } -.cascade-bar a:hover { text-decoration: underline; } -.cascade-bar a.is-active { - color: #e8eef4; +a.cascade-tree-row:hover { + background: #1a2430; text-decoration: none; - box-shadow: inset 0 -2px 0 #e8eef4; } -.cascade-bar a.is-active:hover { text-decoration: none; } -.cascade-bar code { color: inherit; } +a.cascade-tree-row.is-active { + color: #e8eef4; + background: #1e2a38; + box-shadow: inset 3px 0 0 #e8eef4; +} +a.cascade-tree-row.is-active:hover { + text-decoration: none; +} +.cascade-bar code { color: inherit; font-size: inherit; } .cascade-bar .tip { color: #8a96a3; - font-size: 12px; +} +.cascade-bar .cascade-default { + color: #6a9080; +} +.cascade-timing { + margin-top: 0.35rem; + color: #6a7888; + font-size: 11px; +} +.cascade-bar .tip { + white-space: pre-wrap; } diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html index d6ed145..ed51952 100644 --- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html +++ b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html @@ -1,11 +1,5 @@ -{% $p := .Req.URL.Path %} -