# dossiers-ngi — dynamic edition bar + 10‑min rebuild 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/…/_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` | 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: `%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 # 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 | | `freeze_ok` | `true` when the current origin tip freeze succeeded (sticky bar green ✓); `false` when it failed, is a placeholder, or the recorded tip is stale (red ✗) | | `freeze_error` | last freeze error text (only when not ok) | | `vs_parent_plus` / `vs_parent_minus` | `git diff --numstat` tip vs **parent** tip (sticky bar +grün/−rot; omitted when `parent` is empty or not in the tree, e.g. `main` and `feature/nl-net-injestion-injestor-update`) | | `loc` | text-file line count of the tip vs the empty tree (gray `N LOC` in the plus/minus columns; only when there is no parent) | Rebuild runs **only when** the current origin tip is not already a successful freeze (`freeze_ok`; timer never forces). Cookie alias `editions/flask` copies from `DEFAULT_BRANCH` unless that alias is already `freeze_ok` for the same tip **and** `.edition-built-at` matches the source freeze. 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) ``` ## Edition freeze missing the bar (cookie trap) `feat/website-views` freeze can omit `{% include "_cascade/bar.html" %}`. Cookie `tdpd_edition` then has no switcher. **No zypper / Caddy plugin.** Way back: `https://dossiers.ngi.hacktivism.ch/_cascade/use/flask` or `/_cascade/` (`index.html` switcher). On koopa (hernani, no root): ```bash bash scripts/dossiers-ngi/inject-editions-bar.sh ``` Hourly rebuild injects on skip **and** freeze-error (keep existing HTML + include). The bar is `position: fixed` (not sticky); `100vw` overflow is avoided. ## Deploy landing (Freigabe) Do **not** `--delete` the whole www root. Sync: ```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 ``` ## Sticky cascade bar `bar.js` fetches `/branch-status.json` and draws: ``` feature/nl-net-injestion-injestor-update … N LOC main … N LOC ├── feat/sm-content … +N −M # vs main │ └── feat/website-features … +N −M # vs sm-content └── project_template … +N −M # vs main ``` Visible cells: freeze mark (green ✓ / red ✗) · name (tree prefix + branch) · tip `monDD HH:MM` · `Ns monDD HH:MM` (build seconds + freeze time) · plus column · minus column. `+N −M` vs the **parent** tip, in two right-aligned columns so `+898` and `+1200` share an edge. Roots with no parent show gray `N` in the plus column and `LOC` in the minus column. No `@sha`, no `default` label (`data-default` stays for flask/cookie). Inner tree `max-width: 45rem` (same as site `#navigation` / `#content` 45em at root 16px), so names line up with About and the minus column with the page edge; below 45rem the tree is full width with a small pad. Rear cells are right-aligned `tabular-nums`. Poll every 30s **updates mark/when/built/plus/minus in place** when the branch structure is unchanged (no full tree remount). Each name links to `/_cascade/use/` (cookie + 302 `/`). Caddy map: ``` ~^[A-Za-z0-9_-]+$ {http.request.cookie.tdpd_edition} ``` Also serve `/cascade/*` from the site www root (path stubs). Live Caddy must not use a trailing `~.* flask` catch-all. Edition purity: responses send `Vary: Cookie` and `Cache-Control: private, no-cache` so `/static/css/style.css` (same path, different bodies per edition) cannot stick the green design CSS onto `main` after a views visit. so new branches need no Caddy edit — only an `editions//` tree. ```bash bash ~/bin/apply-dossiers-ngi-edition-switch.sh sudo ~/bin/caddy-apply ``` Stanza SoT: `scripts/dossiers-ngi/caddy-dossiers-ngi.snippet`. ## HTTP → HTTPS 308 Shared Caddy HTTP list already `redir https://{host}{uri} permanent` (308). `dossiers.ngi.hacktivism.ch` was missing there → HTTP `200` empty body / Firefox `NS_ERROR_NET_EMPTY_RESPONSE`. HTTPS site body stays untouched. On koopa (hernani, then root apply): ```bash # SoT: scripts/dossiers-ngi/apply-dossiers-ngi-http-308.sh cp /path/to/apply-dossiers-ngi-http-308.sh ~/bin/ chmod +x ~/bin/apply-dossiers-ngi-http-308.sh bash ~/bin/apply-dossiers-ngi-http-308.sh sudo ~/bin/caddy-apply ``` Smoke after apply: ```bash curl -sSI http://dossiers.ngi.hacktivism.ch/ | grep -Ei 'HTTP|location' # 308 Location: https://dossiers.ngi.hacktivism.ch/ ``` `.2` / `.3` stay off this list unless `SITE=` is set. Caddy edition map must list each `editions/` explicitly. A map output of `{http.request.cookie.tdpd_edition}` did not expand on Caddy v2.11.4 (only `flask` worked → 404 for `main`/views). After new tips appear, refresh the snippet map (or regenerate from `ls editions`) before `apply-dossiers-ngi-edition-switch` + `caddy-apply`.