Compare commits
22 commits
clementine
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f74662202 | ||
|
|
8370ad2e33 | ||
|
|
eb4349085d | ||
|
|
fe0c1e2b53 | ||
|
|
37ac6a2171 | ||
|
|
197e0ab19a | ||
|
|
8ad49de231 | ||
|
|
c14e215ad8 | ||
|
|
8cb55e57e8 | ||
|
|
3feb694084 | ||
|
|
b24dd17ee9 | ||
|
|
c2bf7a4263 | ||
|
|
ce5269b745 | ||
|
|
686a9fb906 | ||
|
|
84623b7c9a | ||
|
|
737d37a76d | ||
|
|
e35205b6cd | ||
|
|
8a09314eaf | ||
|
|
ff934ce5e9 | ||
|
|
f2a0d533b8 | ||
|
|
9035b9600b | ||
|
|
6e57fceb19 |
24 changed files with 528 additions and 207 deletions
13
2026/2026-10-21--castopod-php-stay-off.md
Normal file
13
2026/2026-10-21--castopod-php-stay-off.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Castopod stays off
|
||||||
|
|
||||||
|
Rechecked 2026-10-21 after the stop note. Web unit container-koopa-castopod.service is disabled, inactive, dead. Container koopa-castopod is exited, finished 2026-10-21 22:48:06 +0200. Nothing listens on :9020. MariaDB and Redis user units stay loaded and active. No second stop. Caddy vhost unchanged.
|
||||||
|
|
||||||
|
Log review found no sign of a successful remote code execution. Overlay file /tmp/fw-cp.php (1448 bytes, mode 600, sha256 7bb279bffa12dbb81787183108a2641b78a6657b069cbef7967950d80e13b5bc, mtime 2026-09-07 03:25:59 +0200) is a local database script for cp_users, outside the docroot, not a web shell. /tmp/os-dry.out is an apt dry-run. Five theme files differ by one branding line from 2026-07-13. Media volume has no scripts. Access logs run 2026-10-17 through the stop and show no 2xx on a non-index script. Critical lines on 2026-09-07 match other days.
|
||||||
|
|
||||||
|
Limits: access logs start 2026-10-17, so HTTP from 2026-09-07 is absent. A memory-only or already rotated request is not ruled out. Whether the local script ran is not in the logs. MariaDB was not opened.
|
||||||
|
|
||||||
|
Stays down until an image ships PHP newer than 8.4.18. Hub tag :1 from 2026-02-24 is not that image. Parent 537, slug castopod-restart-after-php-fix.
|
||||||
|
|
||||||
|
Restore only then:
|
||||||
|
|
||||||
|
systemctl --user enable --now container-koopa-castopod.service
|
||||||
23
2026/2026-10-21--castopod-php-stop.md
Normal file
23
2026/2026-10-21--castopod-php-stop.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Castopod PHP stopped
|
||||||
|
|
||||||
|
Checked 2026-10-21. Host has no php-fpm and no php package. The only PHP process was Castopod FrankenPHP in container koopa-castopod.
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|--------|
|
||||||
|
| image | docker.io/castopod/castopod:1 |
|
||||||
|
| repo digest | sha256:4e4f0440520f45257bfeac7be4347defd20048b4efef8f53d73ec9ed3a4f7966 |
|
||||||
|
| amd64 digest | sha256:757f3c2a96b31b81753c23bc092dfcf05039d9df4d13cc12c428bb752fce882b |
|
||||||
|
| image created | 2026-02-24 |
|
||||||
|
| hub tags 1 / latest / 1.15.5 | same digest, pushed 2026-02-24 |
|
||||||
|
| PHP | 8.4.18 (12 Feb 2026) |
|
||||||
|
| FrankenPHP | v2.10.2 |
|
||||||
|
| listen | 0.0.0.0:9020 -> 8080 |
|
||||||
|
| public | https://castopod.hacktivism.ch/ |
|
||||||
|
|
||||||
|
Stable Hub tags have not moved since 2026-02-24. develop last push 2026-06-05 and is not a stable security update. php.net lists 8.4.26 (24 Sep 2026) and 8.4.27 (22 Oct 2026) after 8.4.18, with a banner to update for critical remote code execution. Pulling :1 repeats the same image. FrankenPHP ships its own PHP binary.
|
||||||
|
|
||||||
|
Stopped and disabled user unit container-koopa-castopod.service. ExecStop is `podman stop -t 30 koopa-castopod`. Container status after stop: Exited (0). :9020 not listening. MariaDB and Redis units left running. Caddy vhost unchanged.
|
||||||
|
|
||||||
|
Bring the web container back only when a newer image exists:
|
||||||
|
|
||||||
|
systemctl --user enable --now container-koopa-castopod.service
|
||||||
41
2026/2026-10-24--todo704-tor-cachyos-cookieauth.md
Normal file
41
2026/2026-10-24--todo704-tor-cachyos-cookieauth.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# TODO=704 Tor CachyOS CTR + CookieAuth (koopa)
|
||||||
|
|
||||||
|
Relay was down (stale PIDFile / start-timeout on forking unit), keys intact. Recreated **same identity** on CachyOS container; CookieAuth for ControlPort; host tor disabled.
|
||||||
|
|
||||||
|
## Stand after APPLY (2026-10-24 ~21:47–21:51 Europe/Zurich)
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|--------|
|
||||||
|
| CTR | `koopa-tor-relay` Up, image `localhost/koopa-tor-relay:latest` (=`:cachyos`) |
|
||||||
|
| Tor | 0.4.9.14 (CachyOS/Arch extra) |
|
||||||
|
| Unit | `container-koopa-tor-relay.service` Type=simple, active |
|
||||||
|
| Host tor | `tor.service` disabled + failed/inactive |
|
||||||
|
| FP | `02C7EECCDF7814FBA2DB1CBAED000D06E8A32EE8` (keys mtime 2024-11-14) |
|
||||||
|
| ORPort | 8080 |
|
||||||
|
| ControlPort | 127.0.0.1:9051 CookieAuthentication=1, CookieAuthFile=/var/lib/tor/control_auth_cookie |
|
||||||
|
| Bootstrap | PROGRESS=100 done; status/circuit-established=1 |
|
||||||
|
| Unauth ControlPort | 514 Authentication required |
|
||||||
|
| Cookie AUTH | 250 OK |
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Dry-run `/tmp/tor.sh` on koopa → STATUS=dry-run-ok; NEXT=`sudo APPLY=1 /tmp/tor.sh` (host tor was still enabled).
|
||||||
|
2. Human: `systemctl disable --now tor.service`, then `sudo APPLY=1 /tmp/tor.sh`.
|
||||||
|
3. APPLY: wrote CachyOS Containerfile, pulled `docker.io/cachyos/cachyos:latest`, build (cache hit), `podman create --replace --network host --user 1000:1000 --userns keep-id`, torrc bak `torrc.bak-cookie-20261024-214720`, CookieAuth on.
|
||||||
|
4. First APPLY hit dbus fail under sudo (`$DBUS_SESSION_BUS_ADDRESS` / `$XDG_RUNTIME_DIR` unset). Unit rewrite + enable finished with XDG/DBUS set; script path re-exec as SUDO_USER for later runs.
|
||||||
|
5. Verify from magikoopa SSH: unit active, CTR Up, FP match, circuit-established=1, unauth 514.
|
||||||
|
|
||||||
|
## Live paths
|
||||||
|
|
||||||
|
- `~/koopa-tor-relay/{torrc,data,log,build/Containerfile}`
|
||||||
|
- `~/.config/systemd/user/container-koopa-tor-relay.service`
|
||||||
|
- Driver: `/tmp/tor.sh` (TARGET=koopa); upgrade later: `~/upgrade-containers.sh --apply --rebuild-local --only koopa-tor-relay`
|
||||||
|
|
||||||
|
## Akte mirrors
|
||||||
|
|
||||||
|
- `configs/tor/torrc`
|
||||||
|
- `configs/tor/build/Containerfile`
|
||||||
|
- `configs/systemd/user/container-koopa-tor-relay.service`
|
||||||
|
- `host/tor/README.md` updated (was stale host `tor.service`)
|
||||||
|
|
||||||
|
Sibling: firecuda brew tor CookieAuth same day (FP `52BB94DDC1292F950CF728708AC48523E018A718`, MyFamily both ways). Parent TODO=704.
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## §0 Secrets (HIGH — pointer)
|
|
||||||
|
|
||||||
**Common SoT:** `$HOME/git/llm/llm-configs/operator-rules/secrets-policy.md` (`PATH[admin-secrets]`).
|
|
||||||
Secret **values** never enter chat/ctx as **input or output** (prompts, pasted tool results, commit subjects, admin-log prose). Handle via **scripts** only (disk `*.env`; report `STATUS=` / `KEY_PRESENT=` / paths) or else a **path-only local-model instruction**. Session stub: `$HOME/.grok/AGENTS.md` §0. Do not fork a second full copy here.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Session Must / Must not: `$HOME/.grok/AGENTS.md` only — do not duplicate.
|
Session Must / Must not: `$HOME/.grok/AGENTS.md` only — do not duplicate.
|
||||||
|
|
||||||
**Sync (must-sync trees / wenn Git allein nicht reicht):** siehe [SYNC.md](SYNC.md).
|
**Sync (must-sync trees / wenn Git allein nicht reicht):** siehe [SYNC.md](SYNC.md).
|
||||||
|
|
|
||||||
21
configs/systemd/user/container-koopa-tor-relay.service
Normal file
21
configs/systemd/user/container-koopa-tor-relay.service
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# container-koopa-tor-relay.service — regenerated by tor.sh 2026-10-24T21:47:24+02:00
|
||||||
|
# Prefer simple over podman-generate-systemd forking+PIDFile (stale CID storm).
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Podman koopa-tor-relay
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
RequiresMountsFor=%t/containers
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment=PODMAN_SYSTEMD_UNIT=%n
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
TimeoutStartSec=120
|
||||||
|
TimeoutStopSec=70
|
||||||
|
ExecStart=/usr/bin/podman start --attach koopa-tor-relay
|
||||||
|
ExecStop=/usr/bin/podman stop -t 10 koopa-tor-relay
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
24
configs/tor/build/Containerfile
Normal file
24
configs/tor/build/Containerfile
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# koopa-tor-relay — Tor non-exit relay on CachyOS (Arch extra/tor)
|
||||||
|
# Same pattern as koopa-meet-web:cachyos. Rootless: tor user -> uid/gid 1000.
|
||||||
|
# Recreate: --user 1000:1000 --userns=keep-id (see README / tor.sh)
|
||||||
|
ARG CACHYOS_IMAGE=docker.io/cachyos/cachyos:latest
|
||||||
|
FROM ${CACHYOS_IMAGE}
|
||||||
|
ENV TZ=Europe/Zurich
|
||||||
|
RUN pacman -Syu --noconfirm --needed tor nyx ca-certificates \
|
||||||
|
&& pacman -Scc --noconfirm \
|
||||||
|
&& rm -rf /var/cache/pacman/pkg/* /tmp/* \
|
||||||
|
&& mkdir -p /var/lib/tor /var/log/tor \
|
||||||
|
&& if id tor >/dev/null 2>&1; then \
|
||||||
|
groupmod -g 1000 tor 2>/dev/null || true; \
|
||||||
|
usermod -u 1000 -g 1000 tor 2>/dev/null || true; \
|
||||||
|
elif id debian-tor >/dev/null 2>&1; then \
|
||||||
|
groupmod -g 1000 debian-tor; usermod -u 1000 -g 1000 debian-tor; \
|
||||||
|
else \
|
||||||
|
groupadd -g 1000 tor; useradd -u 1000 -g 1000 -d /var/lib/tor -s /usr/bin/nologin tor; \
|
||||||
|
fi \
|
||||||
|
&& chown -R 1000:1000 /var/lib/tor /var/log/tor \
|
||||||
|
&& chmod 700 /var/lib/tor
|
||||||
|
EXPOSE 8080 9051
|
||||||
|
USER 1000:1000
|
||||||
|
ENTRYPOINT ["/usr/bin/tor"]
|
||||||
|
CMD ["-f", "/etc/tor/torrc", "--runasdaemon", "0"]
|
||||||
18
configs/tor/torrc
Normal file
18
configs/tor/torrc
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# KoopaRelay — same policy as live /etc/tor/torrc + admin-log configs/tor/torrc
|
||||||
|
# Identity: keys in DataDirectory (migrate from host /var/lib/tor)
|
||||||
|
Log notice file /var/log/tor/notices.log
|
||||||
|
Nickname KoopaRelay
|
||||||
|
ContactInfo info+koopa@hacktivism.ch
|
||||||
|
# dual-stack: bind all addresses on 8080 (IPv4 + IPv6), same as host tor
|
||||||
|
ORPort 8080
|
||||||
|
ControlPort 127.0.0.1:9051
|
||||||
|
CookieAuthentication 1
|
||||||
|
CookieAuthFile /var/lib/tor/control_auth_cookie
|
||||||
|
ExitRelay 0
|
||||||
|
SocksPort 0
|
||||||
|
MyFamily 52BB94DDC1292F950CF728708AC48523E018A718
|
||||||
|
BandwidthRate 2000 MBytes
|
||||||
|
BandwidthBurst 2000 MBytes
|
||||||
|
RelayBandwidthRate 2000 MBytes
|
||||||
|
RelayBandwidthBurst 2000 MBytes
|
||||||
|
DataDirectory /var/lib/tor
|
||||||
|
|
@ -1,13 +1,30 @@
|
||||||
# Tor on koopa
|
# Tor on koopa
|
||||||
|
|
||||||
**Runtime:** host **`tor.service`** (not a podman container).
|
**Runtime:** rootless podman container **`koopa-tor-relay`** (CachyOS base, Arch `extra/tor`), managed by user unit **`container-koopa-tor-relay.service`** (`Type=simple`, `podman start --attach`).
|
||||||
**Ports:** ORPort **8080** · ControlPort **9051** (loopback).
|
**Host `tor.service`:** disabled / must stay off (port clash on ORPort 8080 + ControlPort 9051).
|
||||||
**Non-exit** relay (`ExitRelay 0`).
|
**Ports:** ORPort **8080** · ControlPort **127.0.0.1:9051** (CookieAuthentication + CookieAuthFile).
|
||||||
|
**Non-exit** relay (`ExitRelay 0`). Nickname **KoopaRelay**.
|
||||||
|
**Fingerprint (stable keys under `~/koopa-tor-relay/data/keys/`):** `02C7EECCDF7814FBA2DB1CBAED000D06E8A32EE8`
|
||||||
|
**Family:** firecuda FireCudaRelay `52BB94DDC1292F950CF728708AC48523E018A718`
|
||||||
|
|
||||||
|
Live tree: `~/koopa-tor-relay/` (`torrc`, `data/`, `log/`, `build/Containerfile`).
|
||||||
|
Mirrors in this repo: `configs/tor/torrc`, `configs/tor/build/Containerfile`, `configs/systemd/user/container-koopa-tor-relay.service`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl status tor
|
# status
|
||||||
ss -lntp | grep -E '8080|9051'
|
systemctl --user status container-koopa-tor-relay.service
|
||||||
|
podman ps --filter name=koopa-tor-relay
|
||||||
|
# ControlPort cookie auth (hex)
|
||||||
|
COOKIE=$(xxd -p -c 256 ~/koopa-tor-relay/data/control_auth_cookie)
|
||||||
|
printf 'AUTHENTICATE %s\r\nGETINFO fingerprint\r\nGETINFO status/circuit-established\r\nQUIT\r\n' "$COOKIE" | nc -w 3 127.0.0.1 9051
|
||||||
```
|
```
|
||||||
|
|
||||||
login-health: WARN if `tor.service` not active.
|
Apply / recreate (after host tor off):
|
||||||
Legacy `koopa-tor-relay` / `configs/tor/` removed from live inventory (git history keeps them).
|
|
||||||
|
```bash
|
||||||
|
sudo APPLY=1 /tmp/tor.sh
|
||||||
|
# later upgrades:
|
||||||
|
~/upgrade-containers.sh --apply --rebuild-local --only koopa-tor-relay
|
||||||
|
```
|
||||||
|
|
||||||
|
login-health: expect user unit active + CTR Up; host `tor.service` inactive. Parent TODO=704.
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ Host runtime: `/etc/caddy/secrets/dossiers-ngi-basicauth.txt` (root-only).
|
||||||
SoT: `koopa-admin-secrets-enc` (git-crypt) → `koopa/home-hernani/caddy/`.
|
SoT: `koopa-admin-secrets-enc` (git-crypt) → `koopa/home-hernani/caddy/`.
|
||||||
Hosts: `dossiers.ngi` / `dossiers.2.ngi` / `dossiers.3.ngi`.hacktivism.ch
|
Hosts: `dossiers.ngi` / `dossiers.2.ngi` / `dossiers.3.ngi`.hacktivism.ch
|
||||||
|
|
||||||
|
HTTP→HTTPS 308 for `dossiers.ngi` (add host to the shared ACME list, leave
|
||||||
|
HTTPS body alone): `scripts/dossiers-ngi/apply-dossiers-ngi-http-308.sh`
|
||||||
|
then `sudo ~/bin/caddy-apply`.
|
||||||
|
|
||||||
## silverbullet.hacktivism.ch — merge + basic_auth
|
## silverbullet.hacktivism.ch — merge + basic_auth
|
||||||
|
|
||||||
After podman cutover (`scripts/notes/cutover-to-silverbullet.sh --apply`) and DNS:
|
After podman cutover (`scripts/notes/cutover-to-silverbullet.sh --apply`) and DNS:
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ Per branch in JSON / sticky bar:
|
||||||
| `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`) |
|
| `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) |
|
| `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).
|
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
|
Each run also: `git fetch --prune`, `worktree prune`, drop stale
|
||||||
`editions/<slug>/` (gone remotes), purge leftover `tdpd-*` tmp dirs.
|
`editions/<slug>/` (gone remotes), purge leftover `tdpd-*` tmp dirs.
|
||||||
|
|
||||||
|
|
@ -136,5 +136,30 @@ sudo ~/bin/caddy-apply
|
||||||
|
|
||||||
Stanza SoT: `scripts/dossiers-ngi/caddy-dossiers-ngi.snippet`.
|
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/<slug>` 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`.
|
Caddy edition map must list each `editions/<slug>` 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`.
|
||||||
|
|
|
||||||
|
|
@ -534,15 +534,26 @@ def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict:
|
||||||
shutil.rmtree(wt, ignore_errors=True)
|
shutil.rmtree(wt, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _edition_built_at(editions: Path, slug: str) -> str:
|
||||||
|
p = editions / slug / ".edition-built-at"
|
||||||
|
if not p.is_file():
|
||||||
|
return ""
|
||||||
|
return p.read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
|
|
||||||
def sync_flask_alias(editions: Path, tips: dict[str, str]) -> None:
|
def sync_flask_alias(editions: Path, tips: dict[str, str]) -> None:
|
||||||
"""Keep editions/flask in sync with DEFAULT_BRANCH freeze when present."""
|
"""Keep editions/flask in sync with DEFAULT_BRANCH freeze when present."""
|
||||||
src_branch = default_branch()
|
src_branch = default_branch()
|
||||||
if src_branch not in tips:
|
if src_branch not in tips:
|
||||||
return
|
return
|
||||||
src = editions / edition_slug(src_branch)
|
src_slug = edition_slug(src_branch)
|
||||||
|
src = editions / src_slug
|
||||||
dst = editions / "flask"
|
dst = editions / "flask"
|
||||||
tip = tips[src_branch]
|
tip = tips[src_branch]
|
||||||
if tip_recorded(editions, "flask") == tip and (dst / "index.html").is_file():
|
flask_ok, _ = freeze_ok_for(editions, "flask", tip)
|
||||||
|
src_built = _edition_built_at(editions, src_slug)
|
||||||
|
flask_built = _edition_built_at(editions, "flask")
|
||||||
|
if flask_ok and src_built and src_built == flask_built:
|
||||||
return
|
return
|
||||||
if not (src / "index.html").is_file():
|
if not (src / "index.html").is_file():
|
||||||
ensure_placeholder(editions, "flask", tip)
|
ensure_placeholder(editions, "flask", tip)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@
|
||||||
handle @use {
|
handle @use {
|
||||||
header Set-Cookie "tdpd_edition={re.use.1}; Path=/; Max-Age=31536000; SameSite=Lax"
|
header Set-Cookie "tdpd_edition={re.use.1}; Path=/; Max-Age=31536000; SameSite=Lax"
|
||||||
header Cache-Control "private, no-store"
|
header Cache-Control "private, no-store"
|
||||||
redir * / 302
|
# Keep the caller's path (?next=). Never bounce to /. No next → bar page.
|
||||||
|
@next query_regexp next ^(/|/[^/].*)$
|
||||||
|
redir @next {query.next} 302
|
||||||
|
redir * /_cascade/ 302
|
||||||
}
|
}
|
||||||
|
|
||||||
handle /_cascade/* {
|
handle /_cascade/* {
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,24 @@ PY
|
||||||
printf '1\n' > "$EDITIONS/feat-a/.edition-freeze-ok"
|
printf '1\n' > "$EDITIONS/feat-a/.edition-freeze-ok"
|
||||||
rm -f "$EDITIONS/feat-a/.edition-freeze-error"
|
rm -f "$EDITIONS/feat-a/.edition-freeze-error"
|
||||||
|
|
||||||
|
# flask alias: matching tip + stale HTML must copy from DEFAULT_BRANCH
|
||||||
|
MAIN_TIP="$(tr -d '\n' < "$EDITIONS/main/.edition-tip")"
|
||||||
|
mkdir -p "$EDITIONS/flask"
|
||||||
|
printf '%s\n' "$MAIN_TIP" > "$EDITIONS/flask/.edition-tip"
|
||||||
|
printf '%s\n' '<html>stale-flask</html>' > "$EDITIONS/flask/index.html"
|
||||||
|
printf '%s\n' '2026-09-30T17:35:26+02:00' > "$EDITIONS/flask/.edition-built-at"
|
||||||
|
FETCH=0 TDPD_ROOT="$CLONE" OUT="$OUT" EDITIONS_ROOT="$EDITIONS" \
|
||||||
|
FREEZE_CMD="$FREEZE_CMD" REBUILD=1 DEFAULT_BRANCH=main \
|
||||||
|
python3 "$SCRIPT" --rebuild --no-fetch
|
||||||
|
grep -q "main" "$EDITIONS/flask/index.html"
|
||||||
|
if grep -q "stale-flask" "$EDITIONS/flask/index.html"; then
|
||||||
|
echo "FAIL flask alias kept stale HTML"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
test "$(tr -d '\n' < "$EDITIONS/flask/.edition-freeze-ok")" = "1"
|
||||||
|
test "$(tr -d '\n' < "$EDITIONS/flask/.edition-built-at")" = "$(tr -d '\n' < "$EDITIONS/main/.edition-built-at")"
|
||||||
|
echo "OK flask alias copied after stale skip"
|
||||||
|
|
||||||
# delete smoke branch on origin; fetch --prune drops remote-tracking ref
|
# delete smoke branch on origin; fetch --prune drops remote-tracking ref
|
||||||
cd "$CLONE"
|
cd "$CLONE"
|
||||||
git push origin --delete smoke/temp-sibling >/dev/null 2>&1
|
git push origin --delete smoke/temp-sibling >/dev/null 2>&1
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Add dedicated mail IPv6 on koopa eno1 (not Tor SLAAC).
|
|
||||||
# Addr: 2a02:168:53a8::25/64 (hernani-dns KOOPA_WAN_AAAA)
|
|
||||||
# Keeps ipv6.method=auto so SLAAC (Tor) stays. Adds one static address.
|
|
||||||
# Freigabe: human this session. Run on koopa as root / sudo.
|
|
||||||
# Live: ip -6 addr add; persist: nmcli connection modify +ipv6.addresses
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
IFACE="${IFACE:-eno1}"
|
|
||||||
ADDR="${MAIL_V6_ADDR:-2a02:168:53a8::25}"
|
|
||||||
PREFIX="${MAIL_V6_PREFIX:-64}"
|
|
||||||
CIDR="${ADDR}/${PREFIX}"
|
|
||||||
CON="${NM_CON:-}"
|
|
||||||
|
|
||||||
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
|
|
||||||
echo "STATUS=need_root HINT='sudo $0'"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "STATUS=start HOST=$(hostname -s) IFACE=$IFACE CIDR=$CIDR"
|
|
||||||
|
|
||||||
if ! ip link show "$IFACE" >/dev/null 2>&1; then
|
|
||||||
echo "STATUS=no_iface IFACE=$IFACE"
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Resolve NM connection for IFACE if not set
|
|
||||||
if [[ -z "$CON" ]]; then
|
|
||||||
CON=$(nmcli -g NAME,DEVICE con show --active 2>/dev/null \
|
|
||||||
| awk -F: -v d="$IFACE" '$2==d {print $1; exit}')
|
|
||||||
if [[ -z "$CON" ]]; then
|
|
||||||
CON=$(nmcli -g NAME,DEVICE con show 2>/dev/null \
|
|
||||||
| awk -F: -v d="$IFACE" '$2==d {print $1; exit}')
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [[ -z "$CON" ]]; then
|
|
||||||
CON="$IFACE"
|
|
||||||
fi
|
|
||||||
echo "NM_CON=$CON"
|
|
||||||
|
|
||||||
# Idempotent live address
|
|
||||||
if ip -6 addr show dev "$IFACE" | grep -qE "inet6[[:space:]]+${ADDR}/"; then
|
|
||||||
echo "LIVE=present ADDR=$ADDR"
|
|
||||||
LIVE_ADDED=0
|
|
||||||
else
|
|
||||||
ip -6 addr add "$CIDR" dev "$IFACE"
|
|
||||||
echo "LIVE=added ADDR=$CIDR"
|
|
||||||
LIVE_ADDED=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Persist via NetworkManager (append; do not replace method=auto)
|
|
||||||
EXISTING=$(nmcli -g ipv6.addresses con show "$CON" 2>/dev/null || true)
|
|
||||||
if echo "$EXISTING" | grep -qF "$ADDR"; then
|
|
||||||
echo "NM=present ADDR=$ADDR"
|
|
||||||
NM_ADDED=0
|
|
||||||
else
|
|
||||||
nmcli con mod "$CON" +ipv6.addresses "$CIDR"
|
|
||||||
echo "NM=added ADDR=$CIDR"
|
|
||||||
NM_ADDED=1
|
|
||||||
# Apply without full down/up if possible
|
|
||||||
if nmcli con up "$CON" >/dev/null 2>&1; then
|
|
||||||
echo "NM=reapplied CON=$CON"
|
|
||||||
else
|
|
||||||
echo "NM=reapply_warn HINT='nmcli con up $CON' (addr already live)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
if ip -6 addr show dev "$IFACE" | grep -qE "inet6[[:space:]]+${ADDR}/"; then
|
|
||||||
echo "VERIFY=ok ADDR=$ADDR"
|
|
||||||
echo "STATUS=done LIVE_ADDED=$LIVE_ADDED NM_ADDED=$NM_ADDED CIDR=$CIDR"
|
|
||||||
# Show Tor SLAAC still present (informational)
|
|
||||||
if ip -6 addr show dev "$IFACE" | grep -q '2a02:168:53a8:0:d584:9e34:13cf:15d6'; then
|
|
||||||
echo "TOR_SLAAC=present"
|
|
||||||
else
|
|
||||||
echo "TOR_SLAAC=absent_or_changed HINT=ok_if_privacy_temp_rotated"
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "STATUS=verify_fail ADDR=$ADDR"
|
|
||||||
ip -6 addr show dev "$IFACE" || true
|
|
||||||
exit 1
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# TODO=631: g0v mon-* Kreis-Test (local circle via pepsi TRUST_LOOPBACK / :25).
|
|
||||||
# Inject via koopa container loopback :25. Empfang = Maildir MARK grep.
|
|
||||||
# Real OS users (ensure-mon-users.sh): mon-smtp maildir dkim bounce local.
|
|
||||||
# No secrets. Freigabe-free (local inject only).
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
KOOPA_HOST="${KOOPA_HOST:-koopa}"
|
|
||||||
CONTAINER="${PEPSI_CONTAINER:-koopa-pepsi}"
|
|
||||||
OUTDIR="${HOME}/git/llm/llm-work-input"
|
|
||||||
MARK="${MARK:-$(date +%Y%m%dT%H%M%S%z)-kreis}"
|
|
||||||
USERS=(mon-smtp mon-maildir mon-dkim mon-bounce mon-local)
|
|
||||||
DRAIN_SEC="${DRAIN_SEC:-6}"
|
|
||||||
|
|
||||||
echo "STATUS=start MARK=${MARK} CONTAINER=${CONTAINER} N=${#USERS[@]}"
|
|
||||||
|
|
||||||
# --- preflight: users exist ---
|
|
||||||
for u in "${USERS[@]}"; do
|
|
||||||
ssh -o BatchMode=yes "${KOOPA_HOST}" \
|
|
||||||
"podman exec ${CONTAINER} getent passwd ${u}" >/dev/null \
|
|
||||||
|| { echo "STATUS=fail PREFLIGHT user=${u} missing"; exit 2; }
|
|
||||||
done
|
|
||||||
echo "STATUS=preflight USERS_OK=${#USERS[@]}"
|
|
||||||
|
|
||||||
# --- send circle: each → next ---
|
|
||||||
SEND_PY=$(mktemp)
|
|
||||||
trap 'rm -f "${SEND_PY}"' EXIT
|
|
||||||
{
|
|
||||||
echo 'import smtplib, time, sys'
|
|
||||||
echo 'from email.message import EmailMessage'
|
|
||||||
echo "MARK = ${MARK@Q}"
|
|
||||||
echo -n 'USERS = ['
|
|
||||||
for u in "${USERS[@]}"; do printf '%s,' "${u@Q}"; done
|
|
||||||
echo ']'
|
|
||||||
cat <<'PY'
|
|
||||||
ok = fail = 0
|
|
||||||
n = len(USERS)
|
|
||||||
for i, u in enumerate(USERS):
|
|
||||||
to_u = USERS[(i + 1) % n]
|
|
||||||
frm = f"{u}@g0v.ch"
|
|
||||||
to = f"{to_u}@g0v.ch"
|
|
||||||
msg = EmailMessage()
|
|
||||||
msg["From"] = frm
|
|
||||||
msg["To"] = to
|
|
||||||
msg["Subject"] = f"[pepsi-kreis] TODO631 {i+1}/{n} {MARK}"
|
|
||||||
msg["Message-ID"] = f"<todo631-kreis-{i}.{MARK}@g0v.ch"
|
|
||||||
msg.set_content(
|
|
||||||
f"TODO=631 kreis-test.\nFrom={frm}\nTo={to}\ni={i}\nmark={MARK}\n"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with smtplib.SMTP("127.0.0.1", 25, timeout=30) as s:
|
|
||||||
# force MAIL FROM / RCPT explicit for clear errors
|
|
||||||
s.sendmail(frm, [to], msg.as_string())
|
|
||||||
print(f"STATUS=sent i={i} FROM={frm} TO={to}")
|
|
||||||
ok += 1
|
|
||||||
except Exception as e:
|
|
||||||
print(f"STATUS=fail i={i} FROM={frm} TO={to} ERR={type(e).__name__}:{e}")
|
|
||||||
fail += 1
|
|
||||||
time.sleep(1.2)
|
|
||||||
print(f"STATUS=send_done OK={ok} FAIL={fail} MARK={MARK}")
|
|
||||||
sys.exit(0 if fail == 0 else 1)
|
|
||||||
PY
|
|
||||||
} >"${SEND_PY}"
|
|
||||||
|
|
||||||
ssh -o BatchMode=yes "${KOOPA_HOST}" "podman exec -i ${CONTAINER} python3 -" <"${SEND_PY}"
|
|
||||||
echo "MARK=${MARK}" >"${OUTDIR}/todo631-kreis.stamp"
|
|
||||||
|
|
||||||
sleep "${DRAIN_SEC}"
|
|
||||||
|
|
||||||
# --- Maildir HIT per recipient (next-hop of each sender) ---
|
|
||||||
OUT="${OUTDIR}/todo631-kreis-test.txt"
|
|
||||||
{
|
|
||||||
echo "STATUS=ok MARK=kreis-test TODO=631 TS=$(date -Iseconds)"
|
|
||||||
echo "MARK_PROBE=${MARK} USERS=$(IFS=,; echo "${USERS[*]}")"
|
|
||||||
echo "SEND=via container :25 loopback"
|
|
||||||
echo "EXPECT=each user MARK_HITS>=1 (circle next-hop)"
|
|
||||||
echo "NOTE_vacation=PONG may also land; SPF-fail path skips vacation"
|
|
||||||
} >"${OUT}"
|
|
||||||
|
|
||||||
HIT_TOTAL=0
|
|
||||||
MISS=0
|
|
||||||
for u in "${USERS[@]}"; do
|
|
||||||
hits=$(ssh -o BatchMode=yes "${KOOPA_HOST}" \
|
|
||||||
"podman exec ${CONTAINER} bash -c 'grep -lF ${MARK@Q} /home/${u}/Maildir/new/* /home/${u}/Maildir/cur/* 2>/dev/null | wc -l'" \
|
|
||||||
| tr -d '[:space:]')
|
|
||||||
hits=${hits:-0}
|
|
||||||
echo "HIT_${u}=${hits}" | tee -a "${OUT}"
|
|
||||||
if [[ "${hits}" -ge 1 ]]; then
|
|
||||||
HIT_TOTAL=$((HIT_TOTAL + 1))
|
|
||||||
else
|
|
||||||
MISS=$((MISS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "${MISS}" -eq 0 ]]; then
|
|
||||||
echo "STATUS=PASS HIT_USERS=${HIT_TOTAL}/5 MARK=${MARK}" | tee -a "${OUT}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "STATUS=PARTIAL HIT_USERS=${HIT_TOTAL}/5 MISS=${MISS} MARK=${MARK}" | tee -a "${OUT}"
|
|
||||||
exit 1
|
|
||||||
|
|
@ -118,3 +118,24 @@ a.cascade-tree-row.is-active:hover {
|
||||||
color: #8a96a3;
|
color: #8a96a3;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.cascade-hint {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 45rem;
|
||||||
|
margin: 0.15rem auto 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.2rem 0.35rem;
|
||||||
|
color: #f0c674;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cascade-hint[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.cascade-missing-msg {
|
||||||
|
max-width: 45rem;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
color: #c8d0d8;
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<link rel="stylesheet" href="/_cascade/bar.css">
|
<link rel="stylesheet" href="/_cascade/bar.css">
|
||||||
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
||||||
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
||||||
|
<div class="cascade-hint" id="cascade-hint" role="status" hidden></div>
|
||||||
</nav>
|
</nav>
|
||||||
<script src="/_cascade/bar.js" defer></script>
|
<script src="/_cascade/bar.js" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,99 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setEditionCookie(edition) {
|
||||||
|
document.cookie =
|
||||||
|
"tdpd_edition=" +
|
||||||
|
encodeURIComponent(edition) +
|
||||||
|
"; Path=/; Max-Age=31536000; SameSite=Lax";
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepPath() {
|
||||||
|
return location.pathname + location.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useHref(edition) {
|
||||||
|
var path = keepPath();
|
||||||
|
var href = "/_cascade/use/" + encodeURIComponent(edition);
|
||||||
|
if (!path || path.indexOf("/_cascade/use/") === 0) return href;
|
||||||
|
return href + "?next=" + encodeURIComponent(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hintEl() {
|
||||||
|
return document.getElementById("cascade-hint");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHint(msg) {
|
||||||
|
var el = hintEl();
|
||||||
|
if (!el) {
|
||||||
|
var bar = document.getElementById("cascade-bar");
|
||||||
|
if (!bar) return;
|
||||||
|
el = document.createElement("div");
|
||||||
|
el.id = "cascade-hint";
|
||||||
|
el.className = "cascade-hint";
|
||||||
|
el.setAttribute("role", "status");
|
||||||
|
bar.appendChild(el);
|
||||||
|
}
|
||||||
|
el.hidden = false;
|
||||||
|
el.textContent = msg;
|
||||||
|
pinBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHint() {
|
||||||
|
var el = hintEl();
|
||||||
|
if (!el) return;
|
||||||
|
el.hidden = true;
|
||||||
|
el.textContent = "";
|
||||||
|
pinBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseKeepsBar(res, text) {
|
||||||
|
if (res && res.ok) return true;
|
||||||
|
var body = text || "";
|
||||||
|
return (
|
||||||
|
body.indexOf("cascade-bar") !== -1 ||
|
||||||
|
body.indexOf("cascade-missing") !== -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchEdition(edition, branchLabel) {
|
||||||
|
var prev = cookieEdition();
|
||||||
|
var here = keepPath() || "/";
|
||||||
|
var label = branchLabel || edition;
|
||||||
|
if (!edition) return;
|
||||||
|
if (edition === prev) {
|
||||||
|
clearHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditionCookie(edition);
|
||||||
|
fetch(here, { cache: "no-store", credentials: "same-origin" })
|
||||||
|
.then(function (res) {
|
||||||
|
return res.text().then(function (text) {
|
||||||
|
return { res: res, text: text };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function (got) {
|
||||||
|
if (responseKeepsBar(got.res, got.text)) {
|
||||||
|
location.replace(here + location.hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditionCookie(prev);
|
||||||
|
showHint(
|
||||||
|
"This path is not in " +
|
||||||
|
label +
|
||||||
|
". Choose another branch above."
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
setEditionCookie(prev);
|
||||||
|
showHint(
|
||||||
|
"This path is not in " +
|
||||||
|
label +
|
||||||
|
". Choose another branch above."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
return String(s)
|
return String(s)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
|
|
@ -276,8 +369,8 @@
|
||||||
var parent = row.parent || "";
|
var parent = row.parent || "";
|
||||||
var isDefault = branch === defaultBranch;
|
var isDefault = branch === defaultBranch;
|
||||||
html.push(
|
html.push(
|
||||||
'<a class="cascade-tree-row" role="treeitem" href="/_cascade/use/' +
|
'<a class="cascade-tree-row" role="treeitem" href="' +
|
||||||
esc(edition) +
|
esc(useHref(edition)) +
|
||||||
'" data-edition="' +
|
'" data-edition="' +
|
||||||
esc(edition) +
|
esc(edition) +
|
||||||
'" data-branch="' +
|
'" data-branch="' +
|
||||||
|
|
@ -333,7 +426,9 @@
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
|
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
|
||||||
mount.innerHTML =
|
mount.innerHTML =
|
||||||
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
|
'<a class="cascade-tree-row" href="' +
|
||||||
|
esc(useHref("flask")) +
|
||||||
|
'" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
|
||||||
'<span class="tip"> (branch-status unavailable)</span>';
|
'<span class="tip"> (branch-status unavailable)</span>';
|
||||||
markActive(mount, cookieEdition());
|
markActive(mount, cookieEdition());
|
||||||
pinBar();
|
pinBar();
|
||||||
|
|
@ -341,6 +436,26 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", function (ev) {
|
||||||
|
var a = ev.target.closest
|
||||||
|
? ev.target.closest("a.cascade-tree-row[data-edition]")
|
||||||
|
: null;
|
||||||
|
if (!a || ev.defaultPrevented) return;
|
||||||
|
if (ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)
|
||||||
|
return;
|
||||||
|
ev.preventDefault();
|
||||||
|
switchEdition(
|
||||||
|
a.getAttribute("data-edition"),
|
||||||
|
a.getAttribute("data-branch") || a.getAttribute("data-edition")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.body && document.body.classList.contains("cascade-missing")) {
|
||||||
|
showHint(
|
||||||
|
"This path is not in the current branch. Choose another branch above."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadStatus(false);
|
loadStatus(false);
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
loadStatus(true);
|
loadStatus(true);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<body>
|
<body>
|
||||||
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
||||||
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
||||||
|
<div class="cascade-hint" id="cascade-hint" role="status" hidden></div>
|
||||||
</nav>
|
</nav>
|
||||||
<p>Stuck on an edition without a bar? Pick a build above, or <a href="/_cascade/use/flask">reset to flask</a>.</p>
|
<p>Stuck on an edition without a bar? Pick a build above, or <a href="/_cascade/use/flask">reset to flask</a>.</p>
|
||||||
<script src="/_cascade/bar.js" defer></script>
|
<script src="/_cascade/bar.js" defer></script>
|
||||||
|
|
|
||||||
17
sites/dossiers.ngi.hacktivism.ch/_cascade/missing.html
Normal file
17
sites/dossiers.ngi.hacktivism.ch/_cascade/missing.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>path not in this branch</title>
|
||||||
|
<link rel="stylesheet" href="/_cascade/bar.css">
|
||||||
|
</head>
|
||||||
|
<body class="cascade-missing">
|
||||||
|
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
||||||
|
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
||||||
|
<div class="cascade-hint" id="cascade-hint" role="status" hidden></div>
|
||||||
|
</nav>
|
||||||
|
<p class="cascade-missing-msg">This address is not in the current branch. Choose another branch above.</p>
|
||||||
|
<script src="/_cascade/bar.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -118,3 +118,24 @@ a.cascade-tree-row.is-active:hover {
|
||||||
color: #8a96a3;
|
color: #8a96a3;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.cascade-hint {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 45rem;
|
||||||
|
margin: 0.15rem auto 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.2rem 0.35rem;
|
||||||
|
color: #f0c674;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cascade-hint[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.cascade-missing-msg {
|
||||||
|
max-width: 45rem;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
color: #c8d0d8;
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<link rel="stylesheet" href="/_cascade/bar.css">
|
<link rel="stylesheet" href="/_cascade/bar.css">
|
||||||
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
||||||
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
||||||
|
<div class="cascade-hint" id="cascade-hint" role="status" hidden></div>
|
||||||
</nav>
|
</nav>
|
||||||
<script src="/_cascade/bar.js" defer></script>
|
<script src="/_cascade/bar.js" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,99 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setEditionCookie(edition) {
|
||||||
|
document.cookie =
|
||||||
|
"tdpd_edition=" +
|
||||||
|
encodeURIComponent(edition) +
|
||||||
|
"; Path=/; Max-Age=31536000; SameSite=Lax";
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepPath() {
|
||||||
|
return location.pathname + location.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useHref(edition) {
|
||||||
|
var path = keepPath();
|
||||||
|
var href = "/_cascade/use/" + encodeURIComponent(edition);
|
||||||
|
if (!path || path.indexOf("/_cascade/use/") === 0) return href;
|
||||||
|
return href + "?next=" + encodeURIComponent(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hintEl() {
|
||||||
|
return document.getElementById("cascade-hint");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHint(msg) {
|
||||||
|
var el = hintEl();
|
||||||
|
if (!el) {
|
||||||
|
var bar = document.getElementById("cascade-bar");
|
||||||
|
if (!bar) return;
|
||||||
|
el = document.createElement("div");
|
||||||
|
el.id = "cascade-hint";
|
||||||
|
el.className = "cascade-hint";
|
||||||
|
el.setAttribute("role", "status");
|
||||||
|
bar.appendChild(el);
|
||||||
|
}
|
||||||
|
el.hidden = false;
|
||||||
|
el.textContent = msg;
|
||||||
|
pinBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHint() {
|
||||||
|
var el = hintEl();
|
||||||
|
if (!el) return;
|
||||||
|
el.hidden = true;
|
||||||
|
el.textContent = "";
|
||||||
|
pinBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseKeepsBar(res, text) {
|
||||||
|
if (res && res.ok) return true;
|
||||||
|
var body = text || "";
|
||||||
|
return (
|
||||||
|
body.indexOf("cascade-bar") !== -1 ||
|
||||||
|
body.indexOf("cascade-missing") !== -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchEdition(edition, branchLabel) {
|
||||||
|
var prev = cookieEdition();
|
||||||
|
var here = keepPath() || "/";
|
||||||
|
var label = branchLabel || edition;
|
||||||
|
if (!edition) return;
|
||||||
|
if (edition === prev) {
|
||||||
|
clearHint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditionCookie(edition);
|
||||||
|
fetch(here, { cache: "no-store", credentials: "same-origin" })
|
||||||
|
.then(function (res) {
|
||||||
|
return res.text().then(function (text) {
|
||||||
|
return { res: res, text: text };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function (got) {
|
||||||
|
if (responseKeepsBar(got.res, got.text)) {
|
||||||
|
location.replace(here + location.hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditionCookie(prev);
|
||||||
|
showHint(
|
||||||
|
"This path is not in " +
|
||||||
|
label +
|
||||||
|
". Choose another branch above."
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
setEditionCookie(prev);
|
||||||
|
showHint(
|
||||||
|
"This path is not in " +
|
||||||
|
label +
|
||||||
|
". Choose another branch above."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
return String(s)
|
return String(s)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
|
|
@ -276,8 +369,8 @@
|
||||||
var parent = row.parent || "";
|
var parent = row.parent || "";
|
||||||
var isDefault = branch === defaultBranch;
|
var isDefault = branch === defaultBranch;
|
||||||
html.push(
|
html.push(
|
||||||
'<a class="cascade-tree-row" role="treeitem" href="/_cascade/use/' +
|
'<a class="cascade-tree-row" role="treeitem" href="' +
|
||||||
esc(edition) +
|
esc(useHref(edition)) +
|
||||||
'" data-edition="' +
|
'" data-edition="' +
|
||||||
esc(edition) +
|
esc(edition) +
|
||||||
'" data-branch="' +
|
'" data-branch="' +
|
||||||
|
|
@ -333,7 +426,9 @@
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
|
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
|
||||||
mount.innerHTML =
|
mount.innerHTML =
|
||||||
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
|
'<a class="cascade-tree-row" href="' +
|
||||||
|
esc(useHref("flask")) +
|
||||||
|
'" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
|
||||||
'<span class="tip"> (branch-status unavailable)</span>';
|
'<span class="tip"> (branch-status unavailable)</span>';
|
||||||
markActive(mount, cookieEdition());
|
markActive(mount, cookieEdition());
|
||||||
pinBar();
|
pinBar();
|
||||||
|
|
@ -341,6 +436,26 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", function (ev) {
|
||||||
|
var a = ev.target.closest
|
||||||
|
? ev.target.closest("a.cascade-tree-row[data-edition]")
|
||||||
|
: null;
|
||||||
|
if (!a || ev.defaultPrevented) return;
|
||||||
|
if (ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)
|
||||||
|
return;
|
||||||
|
ev.preventDefault();
|
||||||
|
switchEdition(
|
||||||
|
a.getAttribute("data-edition"),
|
||||||
|
a.getAttribute("data-branch") || a.getAttribute("data-edition")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.body && document.body.classList.contains("cascade-missing")) {
|
||||||
|
showHint(
|
||||||
|
"This path is not in the current branch. Choose another branch above."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadStatus(false);
|
loadStatus(false);
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
loadStatus(true);
|
loadStatus(true);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<body>
|
<body>
|
||||||
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
<nav class="cascade-bar" aria-label="cascade builds" id="cascade-bar">
|
||||||
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
<div class="cascade-tree" id="cascade-tree" role="tree">Loading builds…</div>
|
||||||
|
<div class="cascade-hint" id="cascade-hint" role="status" hidden></div>
|
||||||
</nav>
|
</nav>
|
||||||
<p>Stuck on an edition without a bar? Pick a build above, or <a href="/_cascade/use/flask">reset to flask</a>.</p>
|
<p>Stuck on an edition without a bar? Pick a build above, or <a href="/_cascade/use/flask">reset to flask</a>.</p>
|
||||||
<script src="/_cascade/bar.js" defer></script>
|
<script src="/_cascade/bar.js" defer></script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue