Compare commits

..

21 commits

Author SHA1 Message Date
Hernâni Marques
750aa819e9
pepsi: add static mail IPv6 host helper (::25) 2026-10-01 13:02:10 +02:00
Hernâni Marques
7880893181
pepsi: add g0v mon-circle local inject test script 2026-10-01 12:41:38 +02:00
Hernâni Marques
d3948e970e
pepsi: host-net highports + ensure mon-* users 2026-10-01 12:24:09 +02:00
Hernâni Marques
ac73624909
AGENTS: §0 secrets I/O-frei pointer 2026-10-01 12:00:47 +02:00
Hernâni Marques
3198e12591
fix(admin-logs): update log file format 2026-10-01 11:56:19 +02:00
Hernâni Marques
261c25c87f
fix(dossiers-ngi): update README and scripts for branch status 2026-10-01 11:39:45 +02:00
Hernâni Marques
7397cddb83
feat(dossiers-ngi): sticky bar vs-parent loc-on-roots split plus/minus 45rem 2026-09-30 23:45:02 +02:00
Hernâni Marques
da6f384399
feat(dossiers-ngi): sticky bar vs-main compact dates tabular wider tree 2026-09-30 23:23:14 +02:00
Hernâni Marques
52e5c1d328
feat(dossiers-ngi): inject-editions-bar.sh for freeze HTML missing include 2026-09-30 22:25:38 +02:00
Hernâni Marques
27c7f4e37e
feat(dossiers-ngi): sticky bar + JSON diffstat vs parent tip 2026-09-30 22:17:03 +02:00
Hernâni Marques
31af42538a
feat: dossiers-ngi sticky bar vs-main +green/−red diffstat 2026-09-30 16:07:28 +02:00
Hernâni Marques
a4ec0305bc
fix: dossiers-ngi Caddy explicit edition_dir map (cookie placeholder 404) 2026-09-30 16:00:55 +02:00
Hernâni Marques
10d445c712
ops: dossiers-ngi Caddy Vary Cookie + no-cache for edition-pure CSS 2026-09-30 15:56:50 +02:00
Hernâni Marques
5677318ae2
ops: dossiers-ngi cascade bar dates, center/mobile, soft refresh; serve /cascade 2026-09-30 15:52:25 +02:00
Hernâni Marques
ee82ef9b6e
ops: dossiers-ngi edition tree bar, 10min tip-skip rebuild, cleanup 2026-09-30 15:35:35 +02:00
Hernâni Marques
ebecb26c7c
scripts/dossiers-ngi/README.md: update branch-status-hourly.py reference 2026-09-30 12:52:38 +02:00
Hernâni Marques
467092db1f
docs: update README.md with new content 2026-09-30 12:50:54 +02:00
Hernâni Marques
c1fefcc766
ops: dossiers-ngi hourly branch-status emit tip fields only 2026-09-30 03:41:53 +02:00
Hernâni Marques
c406352e1a
sites/dossiers.ngi: strip meta keep links and last-built 2026-09-30 03:41:53 +02:00
Hernâni Marques
ef0ff7f60d
docs: daylog tech-dossier Freeze cascade tip correction 2026-09-30 03:41:53 +02:00
Hernâni Marques
2b9f6c6b83
ops: dossiers-ngi hourly branch-status builder + systemd timer 2026-09-30 03:41:53 +02:00
24 changed files with 207 additions and 528 deletions

View file

@ -1,13 +0,0 @@
# 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

View file

@ -1,23 +0,0 @@
# 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

View file

@ -1,41 +0,0 @@
# 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.

View file

@ -1,5 +1,12 @@
# 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.
**Sync (must-sync trees / wenn Git allein nicht reicht):** siehe [SYNC.md](SYNC.md).

View file

@ -1,21 +0,0 @@
# 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

View file

@ -1,24 +0,0 @@
# 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"]

View file

@ -1,18 +0,0 @@
# 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

View file

@ -1,30 +1,13 @@
# Tor on koopa
**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`).
**Host `tor.service`:** disabled / must stay off (port clash on ORPort 8080 + ControlPort 9051).
**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`.
**Runtime:** host **`tor.service`** (not a podman container).
**Ports:** ORPort **8080** · ControlPort **9051** (loopback).
**Non-exit** relay (`ExitRelay 0`).
```bash
# status
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
systemctl status tor
ss -lntp | grep -E '8080|9051'
```
Apply / recreate (after host tor off):
```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.
login-health: WARN if `tor.service` not active.
Legacy `koopa-tor-relay` / `configs/tor/` removed from live inventory (git history keeps them).

View file

@ -41,10 +41,6 @@ Host runtime: `/etc/caddy/secrets/dossiers-ngi-basicauth.txt` (root-only).
SoT: `koopa-admin-secrets-enc` (git-crypt) → `koopa/home-hernani/caddy/`.
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
After podman cutover (`scripts/notes/cutover-to-silverbullet.sh --apply`) and DNS:

View file

@ -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`) |
| `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.
Rebuild runs **only when** the current origin tip is not already a successful freeze (`freeze_ok`; timer never forces).
Each run also: `git fetch --prune`, `worktree prune`, drop stale
`editions/<slug>/` (gone remotes), purge leftover `tdpd-*` tmp dirs.
@ -136,30 +136,5 @@ 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/<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`.

View file

@ -534,26 +534,15 @@ def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict:
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:
"""Keep editions/flask in sync with DEFAULT_BRANCH freeze when present."""
src_branch = default_branch()
if src_branch not in tips:
return
src_slug = edition_slug(src_branch)
src = editions / src_slug
src = editions / edition_slug(src_branch)
dst = editions / "flask"
tip = tips[src_branch]
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:
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)

View file

@ -19,10 +19,7 @@
handle @use {
header Set-Cookie "tdpd_edition={re.use.1}; Path=/; Max-Age=31536000; SameSite=Lax"
header Cache-Control "private, no-store"
# 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
redir * / 302
}
handle /_cascade/* {

View file

@ -100,24 +100,6 @@ PY
printf '1\n' > "$EDITIONS/feat-a/.edition-freeze-ok"
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
cd "$CLONE"
git push origin --delete smoke/temp-sibling >/dev/null 2>&1

View file

@ -0,0 +1,83 @@
#!/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

View file

@ -0,0 +1,100 @@
#!/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

View file

@ -118,24 +118,3 @@ a.cascade-tree-row.is-active:hover {
color: #8a96a3;
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;
}

View file

@ -1,6 +1,5 @@
<link rel="stylesheet" href="/_cascade/bar.css">
<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>
<script src="/_cascade/bar.js" defer></script>

View file

@ -25,99 +25,6 @@
}
}
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) {
return String(s)
.replace(/&/g, "&amp;")
@ -369,8 +276,8 @@
var parent = row.parent || "";
var isDefault = branch === defaultBranch;
html.push(
'<a class="cascade-tree-row" role="treeitem" href="' +
esc(useHref(edition)) +
'<a class="cascade-tree-row" role="treeitem" href="/_cascade/use/' +
esc(edition) +
'" data-edition="' +
esc(edition) +
'" data-branch="' +
@ -426,9 +333,7 @@
.catch(function () {
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
mount.innerHTML =
'<a class="cascade-tree-row" href="' +
esc(useHref("flask")) +
'" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
'<span class="tip"> (branch-status unavailable)</span>';
markActive(mount, cookieEdition());
pinBar();
@ -436,26 +341,6 @@
});
}
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);
setInterval(function () {
loadStatus(true);

View file

@ -9,7 +9,6 @@
<body>
<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>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>

View file

@ -1,17 +0,0 @@
<!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>

View file

@ -118,24 +118,3 @@ a.cascade-tree-row.is-active:hover {
color: #8a96a3;
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;
}

View file

@ -1,6 +1,5 @@
<link rel="stylesheet" href="/_cascade/bar.css">
<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>
<script src="/_cascade/bar.js" defer></script>

View file

@ -25,99 +25,6 @@
}
}
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) {
return String(s)
.replace(/&/g, "&amp;")
@ -369,8 +276,8 @@
var parent = row.parent || "";
var isDefault = branch === defaultBranch;
html.push(
'<a class="cascade-tree-row" role="treeitem" href="' +
esc(useHref(edition)) +
'<a class="cascade-tree-row" role="treeitem" href="/_cascade/use/' +
esc(edition) +
'" data-edition="' +
esc(edition) +
'" data-branch="' +
@ -426,9 +333,7 @@
.catch(function () {
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
mount.innerHTML =
'<a class="cascade-tree-row" href="' +
esc(useHref("flask")) +
'" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><span class="cascade-name"><code>flask</code></span></a>' +
'<span class="tip"> (branch-status unavailable)</span>';
markActive(mount, cookieEdition());
pinBar();
@ -436,26 +341,6 @@
});
}
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);
setInterval(function () {
loadStatus(true);

View file

@ -9,7 +9,6 @@
<body>
<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>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>