Compare commits
21 commits
master
...
clementine
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750aa819e9 | ||
|
|
7880893181 | ||
|
|
d3948e970e | ||
|
|
ac73624909 | ||
|
|
3198e12591 | ||
|
|
261c25c87f | ||
|
|
7397cddb83 | ||
|
|
da6f384399 | ||
|
|
52e5c1d328 | ||
|
|
27c7f4e37e | ||
|
|
31af42538a | ||
|
|
a4ec0305bc | ||
|
|
10d445c712 | ||
|
|
5677318ae2 | ||
|
|
ee82ef9b6e | ||
|
|
ebecb26c7c | ||
|
|
467092db1f | ||
|
|
c1fefcc766 | ||
|
|
c406352e1a | ||
|
|
ef0ff7f60d | ||
|
|
2b9f6c6b83 |
24 changed files with 207 additions and 528 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/* {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
83
scripts/pepsi/add-mail-v6-static.sh
Executable file
83
scripts/pepsi/add-mail-v6-static.sh
Executable 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
|
||||
100
scripts/pepsi/todo631-pepsi-kreis.sh
Executable file
100
scripts/pepsi/todo631-pepsi-kreis.sh
Executable 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, "&")
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, "&")
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue