diff --git a/2026/2026-07-19.md b/2026/2026-07-19.md
index 8b5722b..4345769 100644
--- a/2026/2026-07-19.md
+++ b/2026/2026-07-19.md
@@ -3,8 +3,3 @@
- **Caddy mon bare paths:** `redir /path/ 302` inside `handle` → Location `302` → merchant code 21; fix `redir * /path/ 302`
- **apply-monitoring-live.sh:** smoke for-loop syntax (missing `\`); use absolute `/home/hernani/koopa-caddy/…` (root `~` = `/root`)
- Re-apply: `sudo /home/hernani/koopa-caddy/apply-monitoring-live.sh`
-- **clementine-magikoopa-share** (`~/clementine-magikoopa-share/`): openssl AES-256-CBC + PBKDF2 iter 600000; `lock.sh`/`unlock.sh` + `ANLEITUNG.md` (usage). Inventory only after decrypt: `EXPECTED-REPOS.md` (includes vecigate-admin-log/secrets expected from magikoopa). Usage mirror in `koopa-admin-secrets/docs/clementine-magikoopa-share.md`.
-- **Share payload = all local-only / never-pushed repos:** canonical list in `koopa-admin-secrets/docs/share-payload-dirs.txt` (+ `local-only-share-repos.md`). On clementine archived: `taler-monitoring-env`, `llm`, `grok-work-log`, **`koopa-admin-secrets`**, **`francpaysan-secrets`**, **`dot-grok`** (`~/.grok` full profile); still MISSING until magikoopa locks them: `vecigate-admin-log`, `vecigate-admin-secrets`.
-- **Share README:** other machine must treat `dot-grok` / profile rules **system-wide** (install to `~/.grok`, same operator rules on clementine and magikoopa).
-
-- **secret-languages:** extracted from local branches `local/merchant-secret-ui` / `local/secret-ui-scrub` into standalone local-only repo `~/src/koopa/secret-languages` (never push). Those branches deleted on this clone. Pack via clementine-magikoopa-share.
diff --git a/2026/2026-08-17--decidim.md b/2026/2026-08-17--decidim.md
deleted file mode 100644
index f2481c1..0000000
--- a/2026/2026-08-17--decidim.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# Decidim — decidim.hacktivism.ch (2026-08-17)
-
-Site: **https://decidim.hacktivism.ch/**
-Port **9027** → Caddy. Stack: `koopa-decidim` + `koopa-decidim-sidekiq` + `koopa-decidim-db` + `koopa-decidim-redis`. Image `decidim/decidim:0.30.8`.
-
-Live: `~/koopa-decidim/` (compose mirror `configs/decidim/`). Secrets only on host (`.env`, `users.env`).
-
-Instance name **hacktivism**. Theme: exchange-dark (gold `#e8a838`, bg `#1a1410`) via `theme/hacktivism.css`. Analog: `configs/lemmy/`.
-
-Admin users are on the host (`~/koopa-decidim/users.env`), not in this repo.
-
-Caddy: `sudo /home/hernani/koopa-caddy/apply.sh`
-Boot: `scripts/decidim/install-systemd.sh` (linger already yes on koopa).
-
-## Cookie / privacy
-
-Stock Decidim still emits `#dc-dialog-wrapper` (Accept all / essential / Settings). This instance has no third-party trackers; privacy is default.
-
-- CSS: `#dc-dialog-wrapper, .cookies__container { display: none !important; }`
-- JS: `/hacktivism-consent.js` sets `decidim-consent={"essential":true}` if missing
-- Live 2026-08-17: HTML 200, CSS/JS 200, hide rules in public `/hacktivism.css`
-
-## Reboot (checked 2026-08-17)
-
-| Item | Value |
-|------|--------|
-| linger `hernani` | yes |
-| `container-koopa-decidim-db` | enabled + active |
-| `container-koopa-decidim` | enabled + active |
-| `container-koopa-lemmy-db` / `container-koopa-lemmy` | enabled + active |
-| listen | `:9027` `:9026` |
-
-login-health: probes + containers + linger + these units enabled. Loopback HTTP on :9027 is **301** (app wants HTTPS; TLS is Caddy). Probe allows 301. After reboot: login-health only unless asked.
-
-## Public example + isoc
-
-- Process **example** (published, not private): `https://decidim.hacktivism.ch/processes/example`
- — title **Öffentliche digitale Allmende** (public-by-default reading).
-- Published meeting **Offene Runde: Lesen ohne Konto** + one public proposal + background page.
-- Homepage block `highlighted_processes` published.
-- Participant `isoc` on the host (`users.env`); password not in this repo.
-- Avatar: gold ISOC globe (same crop as Bonfire `@isoc`); host file `~/koopa-decidim/assets/img/isoc-gold.png`, not in this repo.
-- Seed: `scripts/decidim/seed-example.sh` / `configs/decidim/seed_example.rb`.
diff --git a/2026/2026-09-05--hernani-ollama-user+power.md b/2026/2026-09-05--hernani-ollama-user+power.md
deleted file mode 100644
index b6a6e22..0000000
--- a/2026/2026-09-05--hernani-ollama-user+power.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# koopa: hernani user Ollama + power sampling (2026-09-05)
-
-Freigabe: operator — install Ollama under hernani (no root package); loopback only; laptop tunnels.
-
-## Ollama
-
-- Binary: `~hernani/.local/bin/ollama` (v0.33.3 from GitHub `ollama-linux-amd64.tar.zst`)
-- Unit: `~/.config/systemd/user/ollama.service` · `OLLAMA_HOST=127.0.0.1:11434` · enabled
-- Access: SSH as **hernani** from clementine/magikoopa → LocalForward (helper `koopa-ollama-tunnel` → laptop `:11438`)
-- Models: small + exotic pulls (see pull log `/tmp/ollama-koopa-pull-20260905.log`) — prefer ≤3B
-
-## Power / energy
-
-- Deployed `host-power.py` to `~hernani/.local/bin/host-power`
-- Laptop: `remote-power koopa` / `koopa-power` → JSON watts; combo derives Wh
-- RAPL `energy_uj` root-only → quality **estimated** until Freigabe for `enable-host-power-access.sh --apply`
diff --git a/2026/2026-09-07--git-meta-origin-forgejo.md b/2026/2026-09-07--git-meta-origin-forgejo.md
index c70e899..d795832 100644
--- a/2026/2026-09-07--git-meta-origin-forgejo.md
+++ b/2026/2026-09-07--git-meta-origin-forgejo.md
@@ -7,7 +7,7 @@
| Host | **Forgejo rootless** `https://git.hacktivism.ch/` |
| Repo | **hernani/git** (meta-repo: non-secret globals only) |
| Visibility | **private** (`DEFAULT_PRIVATE` template is public — create with `private:true`) |
-| Default branch | `master` (forgejo/ht SoT — never `main`) |
+| Default branch | `main` |
| SSH clone (FQDN form) | `git@git.hacktivism.ch:hernani/git.git` |
| SSH URL (API form) | `ssh://git@git.hacktivism.ch:9200/hernani/git.git` |
| Local tree | `$HOME/git/` (`PATH[git-root]`) |
@@ -19,7 +19,7 @@ Unauth HTTPS browse of a private repo → **404** (expected).
```bash
cd "$HOME/git"
git remote add origin git@git.hacktivism.ch:hernani/git.git
-git push -u origin master
+git push -u origin main
```
SSH Host `git.hacktivism.ch`: `HostName 127.0.0.1`, `Port 9200`, `User git`, `ProxyJump hernani@koopa`, `IdentityFile ~/.ssh/id_ed25519`.
@@ -40,7 +40,7 @@ podman exec koopa-forgejo gitea admin user generate-access-token \
curl -sS -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
- -d '{"name":"git","private":true,"auto_init":false,"default_branch":"master"}' \
+ -d '{"name":"git","private":true,"auto_init":false,"default_branch":"main"}' \
http://127.0.0.1:9024/api/v1/admin/users/hernani/repos
# expect HTTP 201, full_name=hernani/git
```
diff --git a/2026/2026-09-09--forgejo-tip-master-only.md b/2026/2026-09-09--forgejo-tip-master-only.md
deleted file mode 100644
index e8c54f6..0000000
--- a/2026/2026-09-09--forgejo-tip-master-only.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Forgejo HT tip = `master` only (2026-09-09)
-
-## Rule
-
-On `git.hacktivism.ch` every hernani tip is **`master` only** — no dual `main`+`master`, no tip named `main`.
-
-- Instance: `[repository] DEFAULT_BRANCH = master` in Forgejo `app.ini` (live after container restart).
-- Sync/clone/push defaults: `origin/master`.
-- Docs: `llm-configs` → `git-skills.md`, `ESYNC-RULE.md`, `REPOS-INDEX.md`, `forgejo-llm-config.md`.
-
-## Unify (done)
-
-Authenticated inventory: 32 hernani repos. Mutated 15 NEED → verify **OK 32 / BAD 0**.
-
-Notable:
-
-| Case | Repos |
-|------|--------|
-| MAIN_ONLY → master + drop main | `_grok_sync`, admin-logs, `elelmon` (kept `main-legacy`), `silverbucket` (ex-`freenotes` space), `grok-work-log`, `llm-work-output-enc`, `openkknd-ng`, `taler-monitoring`, `taler-wallets-enc`, `vecirex-www`, … |
-| EMPTY | `vecigate-admin-log` (PATCH default only) |
-| DUAL_DIFF (stale master dropped; tip from former main) | `git`, `llm-configs` |
-
-Feature branches left in place where present (`koopa-admin-log`, `taler-monitoring`, …).
-
-## Rename (same day)
-
-Forgejo space repo **`hernani/freenotes` → `hernani/silverbucket`** (tip `master`). Local clone: `~/git/freenotes-space` → origin `silverbucket`. **Not** the FreeNotes tool/apps tree (`~/git/freenotes`).
-
-## Local clones
-
-Retarget checkout/tracking to `origin/master` on magikoopa trees that had `origin`.
diff --git a/2026/2026-09-11--forgejo-gitleaks-enc-gate.md b/2026/2026-09-11--forgejo-gitleaks-enc-gate.md
deleted file mode 100644
index 8a89620..0000000
--- a/2026/2026-09-11--forgejo-gitleaks-enc-gate.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# 2026-09-11 — Forgejo/ht gitleaks + *enc.git Klartext-Sperre (TODO320)
-
-**Status:** live on koopa Forgejo (`koopa-forgejo` / `git.hacktivism.ch`)
-**Freigabe:** Auftrag `TODO=320` this session (host mutation ht)
-**Analog:** firefly/taler `admin-log/firefly/28-gitolite-enc-gitleaks.md` + `hooks/common/pre-receive.d-gitleaks`
-**Tip:** k stays **319** (no tip-advance)
-
-## What
-
-1. **gitleaks** pre-receive on non-`*enc.git` — reject pushes with detected secrets.
-2. **Skip** `*enc.git` for gitleaks (ciphertext / FP).
-3. **`git-crypt-enforce`** on `*enc.git` — require `.gitattributes` + `.git-crypt/`; reject blobs under `filter=git-crypt` missing `\0GITCRYPT` (9-byte header).
-
-## Live paths (koopa)
-
-| Item | Path |
-|------|------|
-| Data root | `~/koopa-forgejo/data` → container `/var/lib/gitea` |
-| Binary | `data/custom/bin/gitleaks` (v8.21.2 linux_x64) |
-| Shared hooks | `data/custom/hooks/pre-receive.d/{gitleaks,git-crypt-enforce}` |
-| Smoke | `data/custom/bin/smoke-todo320.sh` (run **inside** container) |
-| Per-repo | `data/git/repositories/*/*.git/hooks/pre-receive.d/` → symlinks to shared hooks (container paths) |
-| Staging / installer SoT | laptop `PATH[llm-configs]/bin/forgejo-hooks/` + koopa `~/koopa-forgejo/hooks-staging/` |
-
-Installer: `install-hooks-ht.sh` (idempotent; podman; does not restart container).
-After install: **~32** repos linked. **New repos:** re-run installer (custom/hooks alone not relied on for auto-sync into every bare repo).
-
-## Smoke (Done-when a/b/c) — PASSED 2026-09-11
-
-```bash
-podman exec koopa-forgejo /var/lib/gitea/custom/bin/smoke-todo320.sh
-# ALL SMOKES PASSED
-# (a) secret push denied — private-key fixture (not AWS EXAMPLE keys)
-# (b) enc ciphertext allowed
-# (c) enc plaintext denied
-```
-
-Fixes needed during smoke: park incoming objects on `refs/tmp/*` (not `refs/heads/*`, else `rev-list tip --not --branches` is empty); magic check `head -c 9` for `\0GITCRYPT`.
-
-## Rollback
-
-```bash
-# Per repo or all: remove custom hooks from pre-receive.d (keep Forgejo `gitea` hook)
-rm -f ~/koopa-forgejo/data/git/repositories/*/*.git/hooks/pre-receive.d/gitleaks
-rm -f ~/koopa-forgejo/data/git/repositories/*/*.git/hooks/pre-receive.d/git-crypt-enforce
-# Optional: remove shared copies under data/custom/hooks/pre-receive.d/
-```
-
-## Ops notes
-
-- Use **podman** (not docker sock) for `koopa-forgejo`.
-- Symlink targets must be **container** paths (`/var/lib/gitea/custom/...`).
-- Hook logs redact via gitleaks `--redact`; do not echo secrets.
-- Laptop SSH: `Host git.hacktivism.ch` ProxyJump koopa, Port 9200, User git.
-- Local SoT scripts: `$HOME/git/llm/llm-configs/bin/forgejo-hooks/`.
diff --git a/2026/2026-09-12--enc-inventur-rewrite-hooks.md b/2026/2026-09-12--enc-inventur-rewrite-hooks.md
deleted file mode 100644
index 980f31d..0000000
--- a/2026/2026-09-12--enc-inventur-rewrite-hooks.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# 2026-09-12 — *enc inventur → rewrite → hooks smoke (REPORT ONLY)
-
-**Status:** report only — **no** admin-log / firefly / Taler push this stretch
-**Freigabe:** YES rewrite / force-with-lease / hooks; admin-log = report only
-**Host:** magikoopa (ops) + koopa Forgejo (`koopa-forgejo`)
-**Credit stand:** PCT=0.0 @14:29 renew (authoritative); host `/c` also 0%
-**Inventur park:** `$HOME/git/llm/llm-work-input/tmp/enc-inventur-20260912-STATUS.txt`
-
-## Priority table (tip)
-
-| Priority | Repo | Outcome |
-|----------|------|---------|
-| 1 | `grok-work-log-credit-enc` | **DONE** — was Klartext; git-crypt + orphan tip `877d3e7`; force-with-lease to `hernani/grok-work-log-credit-enc.git` (`e21c1fe`→`877d3e7`). Subject: `git-crypt: encrypt *enc tree (attrs+keys; wipe plain remote next)`. Dirty parked on `backup/pre-crypt-20260912` (`7f00501`). |
-| 2 | `llm-work-output-enc` | **ORIGIN FIXED** — was empty index + wrong origin `hernani/git.git`. Reattached: clone correct `hernani/llm-work-output-enc.git`, unlock, overlay live. Tip `f853969` already `\0GITCRYPT`. `ORIGIN_OK` `LAYOUT_OK`. Local dirty overlay ≈2090 (not pushed this turn). |
-| 2 | `llm-control-data-enc` | **LOCAL CRYPT ONLY** — wrong origin cleared; `git init` + sibling attrs + both GPG users; tip `6aec2c7` (encrypted `status.json`/`stats.json`/sqlite). Origin URL set to `hernani/llm-control-data-enc.git`. **Forgejo repo MISSING** — **no push** until create Freigabe. |
-| 3 | `osaurus-work-log-enc` | **OPEN** — tree at `$HOME/git/llm/llm-work-logs/osaurus-work-log-enc`; no commits, no attrs/crypt, no usable remote. |
-| 3 | `freenotes-data-enc` | **OPEN** — tree at `$HOME/git/src/freenotes-data-enc`; has `.gitattributes` + `.git-crypt`; **NO_REMOTE**. |
-| tip OK | `dot-grok-enc`, `secrets-enc` family, `buerokratie-enc`, `llm-work-db-data-enc`, `grok-work-log-enc`, (+ credit-enc now) | tip encrypted; history-clean handoff list unchanged for remaining. |
-
-## Wrong origin (cleared)
-
-Both control + output previously pointed at **`hernani/git.git`** (ASC board). Cleared. **HALT** remains: never force-push *enc content to `hernani/git.git`.
-
-## Hooks (TODO320) — reinstall + smoke 2026-09-12
-
-Installer SoT: `$HOME/git/llm/llm-configs/bin/forgejo-hooks/`
-Synced → koopa `~/koopa-forgejo/hooks-staging/` → `install-hooks-ht.sh`
-
-```
-OK gitleaks=8.21.2 repos_linked=32
-podman exec koopa-forgejo /var/lib/gitea/custom/bin/smoke-todo320.sh
-ALL SMOKES PASSED
- (a) secret push denied
- (b) enc ciphertext allowed
- (c) enc plaintext denied
-```
-
-`grok-work-log-credit-enc.git` bare has `pre-receive.d/{gitleaks,git-crypt-enforce}` symlinks.
-Bare `hernani/llm-control-data-enc.git` **absent** on Forgejo (matches ls-remote MISSING).
-
-**Re-smoke 2026-09-12T15:37Z:** `ALL SMOKES PASSED` again (a deny secret / b allow ciphertext / c deny plaintext).
-Inventur STATUS close block appended same turn.
-
-## GPG recipients (credit + control)
-
-- `31733E0C598D3A1CF70955D6CB5738652768F7E9` (pep)
-- `8DB88672C5C9FD08373194B6EA1C30038C1D6498` (magikoopa)
-
-## Blocked / needs next Freigabe
-
-1. **Create** Forgejo `hernani/llm-control-data-enc` (private) → then first push of `6aec2c7` + re-run installer for new bare hooks.
-2. Optional: commit/push work-output dirty overlay (2090) after review.
-3. P3: osaurus init+crypt+remote; freenotes add correct remote + push policy.
-4. Admin-log **push** of this file (report-only until Freigabe).
-
-## HALT (unchanged)
-
-- No ladder restart `@START_N=333`
-- No admin-log / firefly / Taler push without Freigabe
-- No force-push to `hernani/git.git`
diff --git a/2026/2026-09-15--goa-ansible-merchant-first.md b/2026/2026-09-15--goa-ansible-merchant-first.md
deleted file mode 100644
index 97f738d..0000000
--- a/2026/2026-09-15--goa-ansible-merchant-first.md
+++ /dev/null
@@ -1,124 +0,0 @@
-# 2026-09-15 — GOA Ansible Merchant-first
-
-**Freigabe:** Live-Stop / Caddy = nein bis Chat-OK.
-**Feed:** `taler-deployment` `origin/dev/hernani/regio-goa-explorational-currency`
-**Work branch:** `dev/hernani/goa-regio-ng-vanilla`
-
-## Naming
-
-| | |
-|--|--|
-| **vanilla** | Ansible Vollkomposition (Merchant+Exchange+Bank+nginx/pg); GOA-Ersatz-Kandidat |
-| **local** | Lab only: loopback / `goa.local` |
-
-`configs/lab-local/goa-regio-ng` = **vanilla** @ **local**. Nie `REGIO_PUBLISH_KOOPA_PORTS=1` neben Live.
-
-## Ist (lab green)
-
-```bash
-ssh hernani@koopa 'podman ps --format "{{.Names}} {{.Status}} {{.Ports}}" | grep -E "goa-regio|taler-hacktivism"'
-# taler-hacktivism* Up 9010–9015
-# goa-regio-ng Up 127.0.0.1:9180→80, 127.0.0.1:9322→22
-
-ssh hernani@koopa "podman exec goa-regio-ng sh -c '
- for u in nginx postgresql libeufin-bank taler-merchant-httpd taler-exchange-httpd taler-merchant.target; do
- printf \"%s=\"; systemctl is-active \$u
- done'"
-# all=active
-
-ssh hernani@koopa 'for h in bank.goa.local exchange.goa.local backend.goa.local; do
- curl -sS -o /dev/null -w "$h %{http_code}\n" -H "Host: $h" http://127.0.0.1:9180/config
-done'
-# all 200
-
-ssh hernani@koopa 'curl -sS -H "Host: exchange.goa.local" http://127.0.0.1:9180/keys' \
- | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["master_public_key"],d["currency"],len(d.get("denominations",[])))'
-# TW6K5… GOA 27
-```
-
-| Piece | Live | Lab vanilla@local |
-|-------|------|-------------------|
-| Merchant | :9010 | **active** · instance `default` |
-| Exchange | :9011 | **active** · master TW6K5 · 27 denoms |
-| Bank | :9012 | libeufin-bank OK |
-| MASTER | TW6K5… | sha256 match enc `current/exchange-offline/master.priv` |
-
-Playbook: `ok=92 changed=20 failed=0` after `/etc/hosts` pin (bank.goa.local NXDOMAIN once).
-
-## Lab hosts (ephemeral)
-
-Container recreate drops hosts. Re-pin:
-
-```bash
-cd $HOME/git/admin-logs/local/koopa-admin-log/configs/lab-local
-ansible-playbook -i goa-regio-ng-koopa.yml lab-hosts.yml
-# unsafe_writes — /etc/hosts EBUSY on atomic rename inside podman
-```
-
-## Merchant token (lab)
-
-SoT: `koopa-admin-secrets-enc/backups/goa/lab-local/merchant/secrets/merchant-default-access-token`
-passwd = PLAIN; Bearer = file value as-is (`secret-token:…`). `/instances/default/private/products` **200**.
-`/config` `"currency":"CHF"` = tip dual-merchant; GOA under `currencies.GOA`.
-
-## t2 — wire + order + withdraw + pay (lab) **done**
-
-Bank acct `lab-merchant` via `libeufin-bank create-account`; secrets under `…/lab-local/merchant/secrets/`. Wire `x-taler-bank`.
-
-```bash
-# POST /instances/default/private/accounts → 200 salt/h_wire
-# POST orders GOA:1 → 200 unpaid
-# create-token labwallet; POST withdrawals; wallet-cli withdraw; POST confirm → 204
-# handle-uri --yes "$taler_pay_uri"
-```
-
-| | |
-|--|--|
-| WID | `1eab9b0d-a3a8-41a8-b070-d7645b25ab25` |
-| withdraw | `txn:withdrawal:2EBFWF7GK415A151KP56MWCVK2CYMDHMR55Q9BG11VT6NR55YC80` final |
-| pay | `txn:payment:D4H75B5YDNJ52G8T6ADBDCXVWXCVZBG5HVSGE7G4S0XBQ6K8H3Y0` done |
-| order | `2026.257-6E75WGDBR32M4` **paid** |
-| wallet | GOA:9 (GOA:10 − GOA:1); CLI 1.6.43 **inside** `goa-regio-ng` |
-
-## Zielbild
-
-Ansible = SoT. Zwei Formen, gleiche Playbooks: **Full-stack Podman** · **Recreate-Fabrik**.
-Branch: **master = stage**; anderer = **prod**. Kein `STAGE_ENV`.
-**Reihenfolge:** **1 GOA** → **2 FP/Stage** (zwei Stacks) → **3 mytops-***. Merchant-Probleme → mytops vorziehen.
-Gate: elelmon moderate → purse/withdraw → lab harden → Live-Merchant cutover nur wenn 1–3 grün → restore-back. Greenfield `*.2.*` PARK.
-
-## APPLY 20260915T0013Z
-
-BACKUP+APT+COMMIT OK. APPLY_SMOKE_FAIL was `sm=$(smoke)` capture; driver now `if smoke; then` → **APPLY_OK**. Smoke endpoints 200. Live bank/exchange/merchant Up; lab `goa-regio-ng` Up. Images retagged `:live` / `:landing`.
-
-## Arbeit 1 — live confirm overflow (2026-09-16) **FAIL**
-
-Smoke: `$HOME/git/llm/llm-work-input/goa-confirm-smoke.sh` (fresh scratch wallet; WCLI `$HOME/.local/bin/taler-wallet-cli.mjs`).
-
-| | |
-|--|--|
-| user | `cfm18923` 2026-09-16T00:35:23Z |
-| max_debt | acct#1=1e12 · **#2=1e8** · #3=1e12 |
-| credit | GOA:4798.5 **200** |
-| TOS | `exchanges add` + `update` + `accept-tos` **OK** |
-| GOA:20 WID | `062e3112-e235-4fea-95d1-644773100224` |
-| create | body `amount` + `exchange_url` → **200** |
-| force-select | rpub + epayto from `/keys` → **200** `selected` |
-| confirm | POST `/accounts/{user}/withdrawals/{wid}/confirm` → **500** code **5110** P0001 `amount value overflowed` |
-| GOA:4200 | same scratch rpub → **409** code **5114** Reserve pub already used (script; skip until GOA:20 green) |
-
-`CONFIRM_SMOKE_FAIL`. Select+TOS+RPUB green for GOA:20; overflow still at confirm. Next: dump `make_incoming` / `bank_wire_transfer` for that WID; lower remaining 1e12 if dump confirms.
-
-## Pointers
-
-| | |
-|--|--|
-| inventory | `configs/lab-local/goa-regio-ng-koopa.yml` |
-| lab hosts | `configs/lab-local/lab-hosts.yml` |
-| Lab README | `configs/lab-local/README.md` |
-| backup daylog | `2026/2026-09-04--goa-backup-regio-ng.md` |
-| APPLY driver | `scripts/taler-shared/goa-live-backup-upgrade.sh` |
-| dumps | koopa `~/backups/goa/current` → `20260904T1238Z` |
-| Caddy | taler→9010 · exchange→9011 · bank→9012 |
-
-Leave-off: t1+t2 lab green. Live confirm **5110**. No Live-Stop / no Caddy.
diff --git a/2026/2026-09-18--amanita-external-probe.md b/2026/2026-09-18--amanita-external-probe.md
deleted file mode 100644
index 954f18d..0000000
--- a/2026/2026-09-18--amanita-external-probe.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# 2026-09-18 — koopa as external amanita probe logger
-
-## Why
-
-Amanita hard hangs leave no panic; internal heartbeat stops with the box.
-External ICMP+SSH from **koopa** records last-alive independently so hang vs
-power/PSU can be separated after the next freeze.
-
-## Live (hernani@koopa)
-
-| Piece | Detail |
-|-------|--------|
-| Key | `~/.ssh/id_ed25519_amanita_probe` — fingerprint `SHA256:R2i2nZfI1svR6Ts3DaSw5eZj2fvnGgm1R9PoMFS4Jj0` |
-| SSH Host | `amanita-probe` → `vx@192.168.100.6`, IdentityFile that key only |
-| Script | `~/.local/bin/koopa-amanita-probe` (SoT: `scripts/amanita-probe/`) |
-| Timer | user `koopa-amanita-probe.timer` ~15 s, linger yes |
-| Log | `~/.local/state/amanita-probe/amanita-probe.{log,last,meta}` — owned by hernani, mode 644 |
-
-## amanita vx (only)
-
-Restricted `authorized_keys` line (not on root):
-
-```text
-from="192.168.100.95",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,command="/bin/cat /var/lib/amanita/heartbeat.last" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBeMenvp0D9EarfAEUtu49Q7bCbGhw+R/E1nJPQSSmZc koopa-amanita-probe@hernani-koopa
-```
-
-Forced command only cats the world-readable HB mirror. No shell, no agent/X11/port forward.
-
-## Verify (2026-09-18 ~15:09 CEST)
-
-- `systemctl --user is-active koopa-amanita-probe.timer` → active
-- `ssh amanita-probe` → EXIT 0, one HB line
-- meta: `icmp=ok ssh=ok gap_s≈15`
-- hernani can read the probe log dir on koopa (owner)
-
-## Docs / install
-
-`scripts/amanita-probe/README.md` + `install-amanita-probe.sh`.
-Do **not** put this key on root AK; do **not** bare `modprobe ramoops`.
diff --git a/2026/2026-09-19--goa-regio-ng-login-health+monitoring.md b/2026/2026-09-19--goa-regio-ng-login-health+monitoring.md
deleted file mode 100644
index df7efc8..0000000
--- a/2026/2026-09-19--goa-regio-ng-login-health+monitoring.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# 2026-09-19 — GOA single-CTR: login-health + monitoring
-
-## Why
-
-`koopa-login-health` FAIL×3 on dead `taler-hacktivism*` + WARN `koopa-tor-relay`.
-Live stack is **`goa-regio-ng`** (pasta 9010–9015) + host **`tor.service`** (ORPort 8080).
-
-## Live SoT (hernani@koopa)
-
-| Piece | Detail |
-|-------|--------|
-| GOA CTR | `goa-regio-ng` — image `localhost/goa-regio-ng:empty-current` |
-| Scripts | `~/scripts/taler-shared/goa-regio-ng/` · `goa-all-in-one.sh` |
-| Bak | `~/backups/goa/current` |
-| Landings | host `/var/www/*-landing/` + `~/goa-landing-deploy` · Caddy outside CTR |
-| Tor | host `tor.service` — **not** container `koopa-tor-relay` |
-| Forgejo | `:9024/:9200` · edge Caddy `:9000/:9001` |
-
-## Repo changes (magikoopa, no push yet)
-
-**koopa-admin-log**
-- `scripts/login-health/`: CRITICAL/EXPECT → `goa-regio-ng` (+ forgejo); drop legacy + tor-relay; host `tor.service` check; probes 9010–9012
-- delete `host/overview/{LIVE,services}.*` · delete `configs/tor/*`
-- rewrite `host/tor/README.md` · shorten README / ports / koopa-setup / login-health README
-- ISSUES I-07 → host Tor SoT
-
-**elelmon + taler-monitoring** (EQ scripts mirrored)
-- BANK=EX=MER=`goa-regio-ng`; drop nginx-in-bank; host stats.json fallback
-- `check_{server,inside,urls,versions,e2e,sanity}.sh` · `metrics.sh` · `lib.sh` · docs
-
-## Verify (after deploy on koopa — Freigabe)
-
-```bash
-koopa-login-health show -v
-# expect: goa-regio-ng OK · no taler-hacktivism* · tor.service OK|WARN
-```
-
-STATE dir name `taler-hacktivism-monitoring` kept (host-agent path, not CTR).
-
-## Follow-up (same day) — elelmon-only SoT
-
-- Forgejo `hernani/taler-monitoring` **archived**; push fails. Do not maintain twin.
-- Parity leftover (mail-deliverability) → **elelmon** only (`check_mail_deliverability.sh`,
- `mail_deliv_probe.py`, host-agent timer, wrapper phase `mail-deliverability|maildeliv`).
-- Canonical clone/install path: `~/src/elelmon` — see `scripts/taler-monitoring/README.md`.
-- Modular Python rewrite scaffold under elelmon package `elelmon/` (not flat TM `check_*.sh` layout).
diff --git a/2026/2026-09-23--goa-merchant-terms-privacy+mon.md b/2026/2026-09-23--goa-merchant-terms-privacy+mon.md
deleted file mode 100644
index 11ce7d3..0000000
--- a/2026/2026-09-23--goa-merchant-terms-privacy+mon.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# 2026-09-23 — GOA merchant /terms+/privacy + mon green
-
-## Why
-
-Public merchant `taler.hacktivism.ch` lacked `/terms` + `/privacy` (501).
-elelmon still assumed legacy multi-CTR `taler-hacktivism*`; live SoT is single
-CTR **`goa-regio-ng`** (pasta 9010–9015).
-
-## Live (hernani@koopa)
-
-| Piece | Detail |
-|-------|--------|
-| CTR | `goa-regio-ng` · BANK=EX=MER same |
-| Terms | `TERMS_ETAG=merchant-tos-dual-v2` · en under `/var/lib/taler-merchant/terms/` |
-| Privacy | `PRIVACY_ETAG=merchant-pp-swiss-v0` |
-| Drop-in | `/etc/taler-merchant/conf.d/terms-privacy.conf` · httpd restart |
-| Public | `https://taler.hacktivism.ch/{terms,privacy}` → **200** |
-| Mon stamp | `20260923T075931Z` · LOG `~/.local/state/elelmon-runs/hacktivism-full-….log` |
-| Mon result | EXIT=0 · 182 OK · 0 ERROR · 21 WARN · 21 INFO |
-| Public mon | staging → `/var/www/monitoring-sites/{host}/monitoring/` |
-
-## Repo SoT (magikoopa)
-
-**koopa-admin-log** — profile/scripts already had ETAGs + install helpers;
-runtime apply on CTR today. Caddyfile dossiers `try_files` → `{path} {path}/ {path}/index.html`
-(separate commit).
-
-**elelmon** — laptop tip goa-aware (clean); koopa WT may still need SSH pull.
-
-## Hold
-
-Cutover locked. No `taler-deployment` master push. Next: T3 vanilla Ansible lokal → T2 APT (Freigabe).
diff --git a/2026/2026-09-25--qbittorrent-bt-net.md b/2026/2026-09-25--qbittorrent-bt-net.md
deleted file mode 100644
index a7c31fb..0000000
--- a/2026/2026-09-25--qbittorrent-bt-net.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# 2026-09-25 — qBittorrent BT net (flame / firewalled)
-
-## Goal
-Inbound BT listen **6881** tcp+udp reachable so WebUI connection status leaves the firewalled (flame) state.
-
-## Live on koopa
-- CTR `koopa-prime-qbittorrent` (linuxserver): host **6881** tcp+udp, WebUI **9023**→8080 → `bt.hacktivism.ch`
-- Conf: `Session\Port=6881`, `PortForwardingEnabled=true`, `Connection\UPnP=true`
-- Temp: `/downloads/incomplete/` (abc / host uid 100999)
-- Durable apply: `scripts/prime/apply-qbittorrent-bt-net.sh` (+ wrapper `koopa-bt-fix.sh`)
-- Clone on host: `~/koopa-admin-log/scripts/prime/`
-
-## firewalld
-- Doc: `configs/firewalld/public-ports.md` — **6881/tcp+udp**
-- Agent cannot list without sudo/polkit; human opens with:
- ```bash
- sudo firewall-cmd --permanent --add-port=6881/tcp
- sudo firewall-cmd --permanent --add-port=6881/udp
- sudo firewall-cmd --reload
- ```
-- WAN probe success (below) implies the path through host firewall is open.
-
-## VeciGate (2026-09-25)
-- wan `dstnat` tcp+udp **6881** → `192.168.100.95:6881`
-- hairpin LAN `192.168.100.0/24` and LAN2 `192.168.88.0/24` → `212.51.151.254:6881` tcp+udp
-- Comments: `koopa: qBittorrent BT …` / `hairpin: LAN[2] qBittorrent BT …`
-- Detail: vecigate-admin-log `2026/2026-09-25--qbittorrent-bt-dnat.md`
-
-## Verify (2026-09-25, from magikoopa)
-| Probe | Result |
-|-------|--------|
-| `nc -vz 192.168.100.95 6881` | LAN_TCP_OK |
-| `nc -vz 212.51.151.254 6881` | WAN_TCP_OK |
-| `nc -zu 212.51.151.254 6881` | WAN_UDP_OK |
-
-WebUI API (`/api/v2/…`) still auth-gated from agent — human glance at connection icon on `bt.hacktivism.ch`.
-
-## Freigabe
-Session Freigabe covered podman conf/restart + VeciGate NAT. No git push of admin-log unless asked.
diff --git a/2026/2026-09-28--tech-dossier-freeze-cascade.md b/2026/2026-09-28--tech-dossier-freeze-cascade.md
deleted file mode 100644
index b24c6cd..0000000
--- a/2026/2026-09-28--tech-dossier-freeze-cascade.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# tech-dossier Freeze cascade — 2026-09-28
-
-Repo: `$HOME/git/src/ngi/tech-dossier-project-data`
-Ladder (each tip autonomously `make html`; each depends on the previous):
-
-| # | Branch | Tip | Local serve | Live ht root |
-|---|--------|-----|-------------|--------------|
-| 1 | `main` | `d2fbd79` | http://127.0.0.1:8101/ | — |
-| 2 | `feat/sm-content` | `089ef1f` | http://127.0.0.1:8102/ | `dossiers.3.ngi.hacktivism.ch` (optional) |
-| 3 | `feat/website-features` | `53a3176` | http://127.0.0.1:8103/ | `dossiers.2.ngi.hacktivism.ch` (optional) |
-| 4 | `feat/website-design` | `2e26c80` | http://127.0.0.1:8104/ | — (no public host after views deploy) |
-| 5 | `feat/website-views` | `bbafc38` (origin tip; local soft-reset dropped bad `1b2fcf7`/`7aca0e8`) | http://127.0.0.1:8105/ | path roots under blank landing (see Live) |
-
-Caddy roots (SoT `scripts/caddy/Caddyfile.koopa-live-wdGet`):
-
-- `/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch`
-- `/var/www/monitoring-sites/dossiers.2.ngi.hacktivism.ch`
-- `/var/www/monitoring-sites/dossiers.3.ngi.hacktivism.ch`
-
-basic_auth: `scripts/caddy/apply-dossiers-ngi-basicauth.sh`
-
-## Local cascade links
-
-- [main](http://127.0.0.1:8101/)
-- [feat/sm-content](http://127.0.0.1:8102/)
-- [feat/website-features](http://127.0.0.1:8103/)
-- [feat/website-design](http://127.0.0.1:8104/)
-- [feat/website-views](http://127.0.0.1:8105/)
-
-## Live
-
-- [dossiers.ngi](https://dossiers.ngi.hacktivism.ch/) ← **blank cascade landing** (kal `sites/dossiers.ngi.hacktivism.ch/`; EN + ASCII ladder; views tip cite `bbafc38`)
-- Path editions: `/main/`, `/feat-sm-content/`, `/feat-website-features/`, `/feat-website-design/`, `/feat-website-views/`
-- Hourly branch-status builder: **kal** `scripts/dossiers-ngi/branch-status-hourly.py` → `sites/…/branch-status.json` (+ user timer); **not** inside tdpd
-- tdpd `feat/website-views` local HEAD = origin = `bbafc38` (NO_PUSH; bad tips discarded locally)
-- [dossiers.2.ngi](https://dossiers.2.ngi.hacktivism.ch/) ← website-features (optional)
-- [dossiers.3.ngi](https://dossiers.3.ngi.hacktivism.ch/) ← sm-content (optional)
-
-## Deploy (Freigabe 2026-09-28 design; Freigabe 2026-09-29 views; Freigabe blank landing same day)
-
-Earlier primary: rsync `website/build/` from tip `feat/website-views` →
-`hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/`
-(`--delete`, exclude `.DS_Store`).
-
-**Result 2026-09-28 ~22:25+02 (design @ `2e26c80`):** `RSYNC_OK` — ~2698 files / ~27M; live `dossier/sm/index.html` 41693 bytes.
-
-**Result 2026-09-29 ~04:30+02 (views-era freeze):** `RSYNC_OK` — live `/` HTTP 200. Local tdpd later briefly carried wrong tips `1b2fcf7` → `7aca0e8` (ladder rewrite + hourly in tdpd); those were **dropped** locally back to `bbafc38` (TODO=430). Never push those tips.
-
-**Result 2026-09-29 (blank landing kal @ `56ee97a`):** `RSYNC_OK` — live `/` HTTP 200 SIZE=3505 title `cascade landing`; path-root editions owned by koopa-admin-log `sites/dossiers.ngi.hacktivism.ch/`; NO_PUSH.
-
-**TODO=430 follow-up (local, NO_PUSH):** kal landing EN/ASCII + tip cite `bbafc38`; hourly builder + systemd under kal; daylog tip table corrected. Deploy landing / enable timer on koopa needs Freigabe.
diff --git a/2026/2026-10-20--forgejo-host-ro-users.md b/2026/2026-10-20--forgejo-host-ro-users.md
deleted file mode 100644
index e602ad3..0000000
--- a/2026/2026-10-20--forgejo-host-ro-users.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# Forgejo host RO users (rabbithole-ro, amanita-ro, firecuda-ro)
-
-Created 2026-10-20 on koopa Forgejo for targeted **read-only** clones of selected `hernani/*` repos (replace host-local tree copies via git, no rsync/scp of SoT).
-
-## Users
-
-| Username | ID | Email | Flags |
-|----------|----|-------|-------|
-| `rabbithole-ro` | 182 | `rabbithole-ro@hosts.hacktivism.ch` | active, restricted, not admin |
-| `amanita-ro` | 183 | `amanita-ro@hosts.hacktivism.ch` | active, restricted, not admin |
-| `firecuda-ro` | 184 | `firecuda-ro@hosts.hacktivism.ch` | active, restricted, not admin |
-
-## Repo access
-
-- Collaborator **read** on `hernani/waldpfad` (all three).
-- SSH user keys titled `*-host-ro` (ed25519) uploaded via oneshot admin token.
-
-## Secrets (enc only)
-
-Laptop SoT (git-crypt):
-
-`PATH[koopa-admin-secrets-enc]/koopa/home-hernani/koopa-forgejo/hosts/{rabbithole,amanita,firecuda}-ro/`
-
-Per host: `password.txt` (600), `ssh/id_ed25519` (+ `.pub`), `README.md`. Never commit plaintext; never grant write collab.
-
-## Create recipe (idempotent)
-
-On koopa as `hernani`, Forgejo HTTP **9024**:
-
-```bash
-podman exec koopa-forgejo gitea admin user create \
- --username USER --email USER@hosts.hacktivism.ch \
- --random-password --must-change-password=false --restricted
-```
-
-SSH key + read collab via oneshot token (`write:admin,write:repository,write:user`) against `http://127.0.0.1:9024/api/v1`:
-
-- `POST /admin/users/USER/keys` with `{"title","key"}`
-- `PUT /repos/hernani/waldpfad/collaborators/USER` with `{"permission":"read"}`
-
-Purge leftover `oneshot-*` tokens in Forgejo UI (API delete often 403). Wipe `/tmp/*.pub` / oneshot scripts after use.
-
-## Clone pattern (on target host)
-
-```bash
-GIT_SSH_COMMAND='ssh -i /path/to/id_ed25519 -o IdentitiesOnly=yes -o BatchMode=yes' \
- git clone --depth 1 --single-branch --branch master \
- ssh://git@git.hacktivism.ch:9200/hernani/waldpfad.git
-```
-
-Or Host stanza with `IdentityFile` for that RO key and `User git` / Port 9200 (ProxyJump as needed).
-
-## Related
-
-- Forgejo rootless: `2026-07-10--forgejo-rootless.md`
-- Oneshot token pattern: `2026-09-07--git-meta-origin-forgejo.md`
-- Host skill: Forgejo ports 9200 SSH / 9024 HTTP; secrets always `*enc`
diff --git a/2026/2026-10-21--castopod-php-stay-off.md b/2026/2026-10-21--castopod-php-stay-off.md
deleted file mode 100644
index 0455ac3..0000000
--- a/2026/2026-10-21--castopod-php-stay-off.md
+++ /dev/null
@@ -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
diff --git a/2026/2026-10-21--castopod-php-stop.md b/2026/2026-10-21--castopod-php-stop.md
deleted file mode 100644
index e951d06..0000000
--- a/2026/2026-10-21--castopod-php-stop.md
+++ /dev/null
@@ -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
diff --git a/2026/2026-10-24--todo704-tor-cachyos-cookieauth.md b/2026/2026-10-24--todo704-tor-cachyos-cookieauth.md
deleted file mode 100644
index 46665b5..0000000
--- a/2026/2026-10-24--todo704-tor-cachyos-cookieauth.md
+++ /dev/null
@@ -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.
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index ba7a565..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# AGENTS.md
-
-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).
diff --git a/ISSUES.md b/ISSUES.md
index a9efa6a..efb5921 100644
--- a/ISSUES.md
+++ b/ISSUES.md
@@ -12,7 +12,7 @@ Stand **2026-07-18** (koopa, live). Keine Secrets.
| [I-04](#i-2026-07-16-04) | low | open | Stray `kind_taussig` |
| [I-05](#i-2026-07-16-05) | low | open | Alte exited Podman-Container |
| [I-06](#i-2026-07-16-06) | low–medium | open | Nym HTTP API `*:9080` |
-| [I-07](#i-2026-07-16-07) | medium | **fixed** | Tor = host `tor.service` (not container) |
+| [I-07](#i-2026-07-16-07) | medium | **fixed** | Tor host vs `koopa-tor-relay` |
| [I-08](#i-2026-07-17-08) | medium | open | Landing-stats → standalone `taler-landing-stats` |
| [I-09](#i-2026-07-18-09) | high | open | Monitoring-HTML öffentlich: Caddy/www (code 21) |
@@ -36,7 +36,7 @@ sudo ~/koopa-caddy/apply-monitoring-live.sh
```
Details: `2026/2026-07-18--monitoring-public-caddy.md`
-Suite: https://git.hacktivism.ch/hernani/elelmon (`taler-monitoring.sh`; archived twin not SoT)
+Suite: https://git.hacktivism.ch/hernani/taler-monitoring (v1.7.1+)
### I-2026-07-16-01
@@ -134,18 +134,24 @@ Hinweis: `nym-node` startet intern mit `--http-bind-address [::]:8080` (Containe
### I-2026-07-16-07
-**Tor = host `tor.service` (ORPort 8080)**
-Severity: medium · Host: koopa · Status: **fixed** (SoT 2026-09)
+**`koopa-tor-relay` vs host `tor.service` (Port-Clash)**
+Severity: medium · Host: koopa · Status: **fixed** 2026-07-16
-**Live:** host **`tor.service`** · ORPort **8080** · ControlPort **9051** · non-exit.
-Legacy CTR `koopa-tor-relay` / `configs/tor/` removed from inventory (git history keeps them).
+**War:** Nach Reboot crash-loopte `koopa-tor-relay` — Host-`tor.service` hielt **8080**/**9051**.
+**Soll:** nur Container (`KoopaRelay`, ORPort **8080**, VeciGate WAN→koopa:8080).
+
+**Fix:** Host-Tor gestoppt/disabled; ggf. hängenden Prozess gekillt; User-Unit:
```bash
-systemctl status tor
+sudo systemctl disable --now tor
+sudo systemctl mask tor # optional
+systemctl --user enable --now container-koopa-tor-relay.service
ss -lntp | grep -E '8080|9051'
-# login-health: WARN if tor.service not active
+podman ps --filter name=koopa-tor-relay
```
+**Live (nach Fix):** Host-`tor` inactive/disabled; `koopa-tor-relay` Up; 8080 + 127.0.0.1:9051 vom Relay-Prozess.
+
---
### I-2026-07-17-08
@@ -171,4 +177,4 @@ Severity: medium · Host: koopa · Status: open · **TODO (noch nicht umsetzen a
- Prod-Container mit Boot-Autostart: Up
- Taler-Ports 9010–9015 antworten (401/302 OK)
- Linger=yes, container-*-Units enabled
-- Tor-OR: host `tor.service` (I-07)
+- Tor-OR: Container-Pfad (I-07 fixed)
diff --git a/README.md b/README.md
index 916b125..47aab15 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,8 @@ Ops log and config mirror for host **koopa** (openSUSE Tumbleweed).
## FIND / setup entry
-Plain file **`koopa-setup`** — lab + host pointers (goa-regio-ng, ports, daylogs). Start there.
+Plain file **`koopa-setup`** — lab + host pointers (mytops-local, regio-ng,
+ports, daylogs). Start there before hunting under `configs/` / `2026/`.
## Open issues
@@ -27,32 +28,69 @@ Hosted on **Forgejo rootless** (this host). See `2026/2026-07-10--admin-log-orig
## Layout
```
-koopa-setup # FIND: lab/host setup pointers
-host/ # openSUSE host-level (caddy, tor, systemd, …)
-configs/ # mirrored app configs (caddy, forgejo, …)
-scripts/ # login-health, GOA helpers, …
- login-health/ # SSH login snapshot
- taler-shared/goa-regio-ng/ # live GOA SoT
+host/ # openSUSE host-level config + overview
+ overview/services.md # graphical service map
+ systemd/ firewalld/ caddy/ network/
+configs/ # mirrored app configs (caddy, taler, forgejo, …)
+ taler-hacktivism/ # merchant container taler-hacktivism
+ taler-hacktivism-bank/ # bank container (image …-banking)
+ taler-exchange/ # exchange conf (inside ansible container)
+ taler-exchange-ansible/ # container taler-hacktivism-exchange-ansible
+ forgejo/ # rootless git.hacktivism.ch
+ castopod/ bonfire/ lemmy/ prime/ # compose mirrors (no secret values)
+ tops/ # koopa-tops-ng1…ng3 + autostart unit
+ tor/ # KoopaRelay ORPort 8080
+scripts/ # merchant, exchange, monitoring helpers
+ taler-merchant/ # merchant-model start scripts
+ taler-exchange/ # same model; archive/ = greenfield bootstrap
2026/ # day logs + dated topic notes
```
-## Live GOA + public stack
+## Live containers (podman)
-| Name | Role | Host ports |
-|------|------|------------|
-| **`goa-regio-ng`** | bank + exchange + merchant (one CTR, pasta) | **9010–9012** APIs · **9013–9015** landings (host nginx) |
+**Authoritative snapshot of host koopa:** [`host/overview/LIVE.md`](host/overview/LIVE.md) (refreshed from live).
+
+| Name (podman) | Role | Host port |
+|---------------|------|-----------|
+| `taler-hacktivism` | merchant | **9010** (+ landing **9015**) |
+| **`taler-hacktivism-exchange-ansible`** | exchange (Ansible/systemd) | **9011** (+ landing **9014**) |
+| `taler-hacktivism-bank` | libeufin-bank (GOA) | **9012** (+ landing **9013**) |
+| `koopa-castopod` (+ mariadb/redis) | Castopod | **9020** |
+| `koopa-bonfire` (+ postgres) | Bonfire | **9021** |
+| `koopa-lemmy-proxy` (+ lemmy/ui/pictrs/db) | Lemmy | **9026** |
+| `koopa-prime-jellyfin` | Jellyfin | **9022** |
+| `koopa-prime-qbittorrent` | qBittorrent | **9023** |
| `koopa-forgejo` (+ postgres) | Forgejo rootless | **9024** + SSH **9200** |
-| `koopa-castopod` / bonfire / lemmy / decidim / … | optional apps | **9020+** |
-| `koopa-tops-ng1`…`ng3` | static nginx | **9090–9092** |
-| host **`tor.service`** | Tor relay | ORPort **8080** · ControlPort **9051** |
+| `koopa-tops-ng1` … `ng3` | static nginx | **9090**–**9092** |
-Start GOA: `scripts/taler-shared/goa-regio-ng/` · bak `~/backups/goa/current` · landings `~/goa-landing-deploy`.
-Edge: Caddy **9000/9001** → 127.0.0.1 backends.
-Login snapshot: `scripts/login-health/` (`koopa-login-health`).
+Start model (Taler merchant/bank): **root** `start_base_services_*` → `/usr/local/bin/start_*.sh`.
+Start model (exchange): `~/ansible-taler-exchange/` → `run-container-koopa.sh` + `deploy-hacktivism-goa.sh`.
+Start model (user apps): `hernani` → `cd ~/koopa-* && podman-compose up -d`.
+Start model (tops): user unit `container-koopa-tops.service` (linger).
+Public hosts: `taler.` / `exchange.` / `bank.` / `castopod.` / `bonfire.` / `prime.` / `bt.` / **`git.hacktivism.ch`** / `lemmy.` / `tops.ng1`–`ng3`.hacktivism.ch
+(Caddy → 9010–9015 / 9020–**9026** / **9090–9092**; git-SSH host **9200** direct).
+
+Service **details**: `configs/taler-hacktivism*`, `configs/taler-exchange*`, `configs/forgejo/`, `configs/tops/`, `scripts/*`.
+Host-wide picture: **`host/overview/services.md`**.
Port table: **`configs/ports.md`**.
-Tor: **`host/tor/README.md`**.
Router: **`../vecigate-admin-log`**.
-**Benchmarks:** `benchmarks/`
-**Forgejo:** `2026/2026-07-10--forgejo-rootless.md` → https://git.hacktivism.ch/
+**Benchmarks** (drive from the local machine, measure on koopa): **`benchmarks/`**
+→ amount ladder: `benchmarks/amount-ladder/`.
+
+**Exchange snapshot/restore (GOA, local images only):**
+→ `2026/2026-07-09--exchange-snapshot-and-restore.md`
+
+**Exchange `/keys` + Wire + bank account (diagram):**
+`configs/taler-exchange/README.md` (section “/keys, Wire and Account”).
+
+**Castopod (stack/accounts/boot — not podcast content):**
+`2026/2026-07-09--castopod-content.md` → https://castopod.hacktivism.ch/
+
+**Bonfire FEDERATE + guest public (2026-07-10):**
+`2026/2026-07-10--bonfire-federate.md` → https://bonfire.hacktivism.ch/
+(ground zero: `2026/2026-07-09--bonfire-ground-zero.md`)
+
+**Forgejo rootless (git.hacktivism.ch, no free registration, SSH :9200):**
+`2026/2026-07-10--forgejo-rootless.md` → https://git.hacktivism.ch/
diff --git a/SECRETS.md b/SECRETS.md
index 87d49fa..7500471 100644
--- a/SECRETS.md
+++ b/SECRETS.md
@@ -1,12 +1,12 @@
# Secrets — not stored in this repo
-Live passwords/keys live in sibling **`koopa-admin-secrets-enc-enc`**.
+Live passwords/keys live in sibling **`koopa-admin-secrets`**.
## Map
### Taler — host `/root`
-| What | Mirror (`koopa-admin-secrets-enc/…`) | Live |
+| What | Mirror (`koopa-admin-secrets/…`) | Live |
|------|----------------------------------|------|
| Bank admin/exchange/explorer/demo/bench | `koopa/host-root/taler-bank/*.txt` | `/root/bank-*-password.txt` |
| Merchant instance goa-demo | `koopa/host-root/taler-merchant/…` | `/root/merchant-*-password.txt` |
@@ -28,8 +28,8 @@ Live passwords/keys live in sibling **`koopa-admin-secrets-enc-enc`**.
## Rules
- **koopa-admin-log**: paths only; `@inline-secret@` in configs, never password/key material.
-- Pull/deploy: `koopa-admin-secrets-enc/scripts/{pull,deploy}-from-koopa.sh`
-- Full inventory: `koopa-admin-secrets-enc/MAP.md`
+- Pull/deploy: `koopa-admin-secrets/scripts/{pull,deploy}-from-koopa.sh`
+- Full inventory: `koopa-admin-secrets/MAP.md`
## Public (OK here)
diff --git a/SYNC.md b/SYNC.md
deleted file mode 100644
index c9d1199..0000000
--- a/SYNC.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# SYNC.md — must-sync (pull.sh / push.sh)
-
-**SoT:** Git tip auf ht/Forgejo (`hernani/*`), nicht grok-all-Pack, nicht ASC-only.
-**Dieses Set:** Profile `grok` + `llm` (=`combo`) + `hernani`.
-**Nicht in diesem Set:** `taler` / `admin-logs/taler/` (eigenes Profil).
-**Parties (local crossover):** magikoopa · clementine · koopatroopa (plus weitere Hosts unter `admin-logs/local/`).
-**Meta-Skripte:** `$HOME/git/pull.sh` · `$HOME/git/push.sh` · `$HOME/git/profiles/*.repos`
-
-Agents sollen diese Bäume **laufend** auf dem gleichen Tip halten (pull oft; push nur mit Freigabe).
-
----
-
-## 1. Schnell: was Agents tun
-
-| Wann | Aktion | Freigabe? |
-|------|--------|-----------|
-| Session-Start / Host-Wechsel / „sync“ | `$HOME/git/pull.sh grok llm hernani` | nein (read/ff) |
-| Repo fehlt auf Disk | gleiches `pull.sh` (cloned + unlock) | nein |
-| Nur Status / Dry-Run | `$HOME/git/pull.sh --list grok llm hernani` bzw. `--dry-run …` | nein |
-| Commits hoch | `$HOME/git/push.sh grok llm hernani` (oder `--dry-run`) | **ja** Push-Freigabe |
-| Dirty tree committen | `/g` bzw. git-worker — **kein** Push aus Worker | Commit-Freigabe ≠ Push |
-
-`combo` ≡ `llm` (Alias). Nie `$HOME/git/dot-grok-enc` anlegen — Live-SoT ist flat `$HOME/.grok`.
-
----
-
-## 2. Wenn Git allein nicht reicht
-
-Reines `git pull` / `git clone` reicht oft **nicht**, weil: Profile-DEST, git-crypt, ff-only-Policy, ACCESS_DENIED, retired grok-all.
-
-### A) Clone / Update über Profile (bevorzugt)
-
-```bash
-$HOME/git/pull.sh grok llm hernani
-# optional:
-$HOME/git/pull.sh --dry-run grok llm hernani
-$HOME/git/pull.sh --list grok llm hernani # zeigt u.a. ACCESS_DENIED
-```
-
-- Existierendes Repo: fetch + **ff-only** pull (kein stilles Merge).
-- `--force`: fetch + `reset --hard` auf Remote (tracked überschreiben; untracked bleiben). **Nur mit Absicht / Freigabe**, nie blind auf dirty Work.
-- Kein `.git`: init + fetch + reset, dann Unlock.
-
-### B) git-crypt (*-enc und `$HOME/.grok`)
-
-Nach erfolgreichem Git-Stand kann der Tree noch **Ciphertext** sein:
-
-```bash
-cd "$DEST" && git-crypt unlock
-# oder: git-crypt status -e # noch encrypted?
-```
-
-| Symptom | Ursache | Fix |
-|---------|---------|-----|
-| Dateien sind Blob/binär / `git-crypt status` zeigt encrypted | Key fehlt / nie unlocked | `git-crypt unlock` mit GPG-Key der Collaborators |
-| Unlock schlägt fehl, Tree dirty | Unlock überspringt dirty+already-unlocked Policy in pull.sh | erst commit/stash/clean klären, dann unlock |
-| Agent liest Müll / leere Secrets | Unlock vergessen | unlock, dann erneut lesen — nicht raten |
-| „Permission denied“ SSH ht | Key/ACL | SSH-Agent laden; ACL auf Forgejo — nicht Pfade erfinden |
-
-### C) Push / Divergenz
-
-```bash
-$HOME/git/push.sh --dry-run grok llm hernani
-$HOME/git/push.sh grok llm hernani # nur mit Freigabe
-```
-
-| Farbe/Lage (push.sh) | Bedeutung | Agent |
-|----------------------|-----------|-------|
-| GREEN | synced / push ok | weiter |
-| YELLOW | behind / mergeable | erst `pull.sh` (ff); diverged → Human |
-| RED | conflict / diverged hard | **HALT** — kein force ohne Freigabe |
-| INFO blue | Repo fehlt auf Disk | `pull.sh` clone |
-
-### D) Explizit verboten / retired
-
-- **grok-all / pack-push / passphrase-Pack** — retired. `magikoopa-unlock-enc.sh unlock` → exit 2.
-- ASC-Board (`SYNC-TODO.md.asc`) optional; Sync läuft über Git-Tip + diese SYNC.md.
-- `taler`-Profile hier **nicht** mitziehen (außer Human sagt explizit taler).
-- Keine Secrets in Klartext-Chat; *enc nur nach unlock lesen.
-
----
-
-## 3. DEST-Übersicht (dieses Set)
-
-### Profile `grok`
-
-| DEST | Remote (kurz) |
-|------|----------------|
-| `$HOME/.grok` | `hernani/dot-grok-enc` |
-| `$HOME/git/src/_hernani-private/grokd` | `hernani/grokd` |
-
-### Profile `llm` (= `combo`)
-
-Alles unter `$HOME/git/llm/…` laut `profiles/llm.repos` (configs, knowledge-mirror, work-*-enc, input, control-data-enc, work-logs).
-
-### Profile `hernani` (local crossover — **nicht** taler)
-
-| DEST-Bereich | Inhalt |
-|--------------|--------|
-| `$HOME/git/admin-logs/local/*` | Host admin-logs + `*-secrets-enc` (amanita, clementine, firecuda, koopa, koopatroopa, magikoopa, vecigate, …) + `hernani-common-secrets-enc` |
-| `$HOME/git/buerokratie-enc` | Bürokratie *enc |
-
-firefly-Vault-Pfad laut HOSTS: unter shared secrets (`admin-secrets-enc/firefly/` bzw. common) — **nicht** Clem-only Blob; Host-Zeile in `$HOME/git/llm/HOSTS.md`. SSH Host+Key für firefly braucht Freigabe / ACL, nicht nur Sync-Datei.
-
----
-
-## 4. Dauerbetrieb — Ansätze (llm-controld · grokd · hernanid · …)
-
-Agents halten Tips **laufend**; dafür gibt es (bzw. sollen) Host-Daemons/`ensure`-Hooks geben. **Nicht verwechseln:** Git-Tip-Sync (`pull.sh`) ≠ Controld-Snapshots ≠ Statusline-Keepalive ≠ Combo-Write-Push.
-
-| Name | Rolle | SoT / Bin | Start-Ansatz | Freigabe? |
-|------|--------|-----------|--------------|-----------|
-| **llm-controld** | Fleet/Letter-Probes → `status.json` (Statusline-Daten) | `$HOME/git/llm/llm-configs/bin/llm-controld` · DATA `llm-control-data-enc` | **Mit Tunnel**, nicht Boot: Tunnel-Skripte → `llm-controld ensure`. Nach Unlock ggf. `llm-control-data-peel.sh` (Doppel-*enc*). Stats: `$HOME/.local/state/llm-controld/` | `ensure` ok; dauerhafte Boot-Units nur mit Freigabe |
-| **grokd** | Grok-Statusline keep-alive (`ensure-statusline.sh`) | `$HOME/git/src/_hernani-private/grokd` (Profil `grok`) | `grokd ensure` (auch via `ensure-statusline`). Optional Scheduler: `install-grokd.sh --load` — macOS LaunchAgent `ai.hernani.grokd`; Clementine/Linux user-crontab `* * * * * grokd ensure` | Dateien/`--link-local` ok; **`--load` braucht Freigabe** |
-| **hernanid** | **Ansatz / Stub:** laufend Tip für Profil `hernani` (`admin-logs/local/*`, `buerokratie-enc`) | noch kein festes Bin — Muster wie grokd: `hernanid ensure` → `$HOME/git/pull.sh hernani` (+ unlock laut §2B) | Gleiches Host-Muster: magikoopa LaunchAgent `ai.hernani.hernanid`; Clementine crontab `hernanid ensure`. Bis es existiert: Agents/`pull.sh hernani` manuell bzw. Session-Start | Scheduler-Install / `--load` nur mit Freigabe |
-| **combo-git-sync** | Combo **Write-Trees** commit+push (OpenPGP+SSH), ≠ Tip-Pull | `$HOME/git/llm/llm-configs/bin/combo-git-sync.py` · LaunchAgent `ai.llm-configs.combo-git-sync-hourly` | `install-combo-git-sync-hourly.sh` (macOS LaunchAgent / Clem crontab stündlich). Quit-on-conflict → `--clear-quit` | Install/Load + jeder Push-Pfad nur mit Freigabe-Policy der Settings |
-
-### Agent-Regeln zu Daemons
-
-1. Vor Arbeit: wenn Controld/Statusline/Grok nötig → `llm-controld ensure` / `grokd ensure` (billig, Singleton/flock).
-2. Tip-Sync weiter über `pull.sh grok llm hernani` — Controld ersetzt das **nicht**.
-3. Keine neuen LaunchAgents/crontab-Zeilen ohne Freigabe; Soft-Park = Dateien + `ensure` on-demand.
-4. Clementine (sysvinit): **kein** systemctl — user crontab wie in den `install-*.sh`.
-5. `hernanid` fehlt noch als Binary: nicht erfinden; Ansatz oben dokumentieren / Human+Clem abstimmen; bis dahin Session-`pull.sh`.
-
----
-
-## 5. Agent-Pflicht (laufend)
-
-1. Vor Arbeit an diesen Bäumen: Tip prüfen (`pull.sh` oder `git status` + behind).
-2. Nach lokalen Commits: Human auf Push ansprechen; **kein** eigenmächtiger `git push`.
-3. Bei Ciphertext / fehlendem Unlock: SYNC.md §2B befolgen, nicht Inhalt erfinden.
-4. Neuer technischer Ordner in diesem Set: SYNC.md + AGENTS-Verweis auf SYNC.md mitziehen.
-5. Katalog/Index-Refresh und SSH-Mutationen: nur mit Freigabe.
-6. Dauerbetrieb: §4 — `ensure` nutzen; Scheduler nur mit Freigabe; `hernanid` als Ansatz bis Binary da ist.
-
----
-
-## 6. Verweise
-
-- Profile: `$HOME/git/profiles/{grok,llm,combo,hernani}.repos`
-- Session Must: `$HOME/.grok/AGENTS.md` (Freigabe, §7 Git)
-- Hosts: `$HOME/git/llm/HOSTS.md`
-- grokd docs: `$HOME/git/src/_hernani-private/grokd/docs/README.md`
-- llm-controld / peel: host skill magikoopa · `llm-configs/bin/llm-controld` · `llm-control-data-peel.sh`
-- Diese Datei: neben `AGENTS.md` in jedem technischen Ordner dieses Sets
diff --git a/configs/README.md b/configs/README.md
index 34b65f4..61e422b 100644
--- a/configs/README.md
+++ b/configs/README.md
@@ -14,8 +14,7 @@ Directories are named to match **live podman container names** where possible.
| `caddy/` `firewalld/` `systemd/` | host services | |
| `tor/` | **`koopa-tor-relay`** (podman host net) | `localhost/koopa-tor-relay:latest` (**non-root** uid 1000) |
| `nym/` | **`koopa-nym`** (nym.com nym-node) | `localhost/koopa-nym:latest` (**non-root** uid 1000) |
-| `paivana/` | **`koopa-paivana`** + paywalls + upstream | `localhost/koopa-paivana:latest` (**non-root**); public **9025/9028–9030** |
-| `notes/` | **`koopa-silverbullet`** (SilverBullet) | `localhost/koopa-silverbullet:cachyos` (backend **127.0.0.1:19128**; paywall **9028**) |
+| `paivana/` | **`koopa-paivana`** (+ upstream) | `localhost/koopa-paivana:latest` (**non-root**); upstream unprivileged nginx |
| `forgejo/` | **`koopa-forgejo`** | rootless image + `user: 1000` + `userns keep-id` |
| `prime/` | jellyfin / qbittorrent | linuxserver **PUID/PGID=1000** |
diff --git a/configs/bank-landing/index.html b/configs/bank-landing/index.html
index 4d1b748..09d1c5d 100644
--- a/configs/bank-landing/index.html
+++ b/configs/bank-landing/index.html
@@ -1118,18 +1118,12 @@
-
-
+
stack ·
- vanilla
+ libeufin-bank 1.6.6
·
- libeufin-bank 1.6.10
- ·
- libeufin-common 1.6.10
- ·
- libeufin-bank-webui 1.6.38
+ libeufin-common 1.6.6
-
Intergalactic · GOA · no IBAN
@@ -1564,10 +1558,10 @@ tw run-until-done && tw balance
/privacy .
-
- Bank landing v45
- · content 2026-09-16 23:19 CEST
- · 0d881cd
+
+ Bank landing v44
+ · content 2026-07-17 18:31 CEST
+ · 689ee59
diff --git a/configs/bank-landing/nginx-landing.conf b/configs/bank-landing/nginx-landing.conf
index e0ff9fb..251a713 100644
--- a/configs/bank-landing/nginx-landing.conf
+++ b/configs/bank-landing/nginx-landing.conf
@@ -49,16 +49,6 @@ server {
add_header Cache-Control "no-store" always;
add_header Access-Control-Allow-Origin * always;
}
- # FP pattern: GET Integration via demo-api (inject suggested/required_exchange)
- # Caddy @wdGet → :9013 → here → :19096; POST/other Integration stays on :9012
- location /taler-integration/withdrawal-operation/ {
- proxy_pass http://127.0.0.1:19096;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_read_timeout 70s;
- add_header Cache-Control "no-store" always;
- add_header Access-Control-Allow-Origin * always;
- }
location /intro/ {
alias /var/www/bank-landing/;
}
diff --git a/configs/bbb/Containerfile b/configs/bbb/Containerfile
deleted file mode 100644
index ae85793..0000000
--- a/configs/bbb/Containerfile
+++ /dev/null
@@ -1,36 +0,0 @@
-# koopa-bbb-front — BigBlueButton front (Ubuntu)
-# Exception to CachyOS policy: BBB packages and install scripts target Ubuntu.
-# Build: bash ~/koopa-admin-log/scripts/bbb/build.sh → localhost/koopa-bbb-front:ubuntu
-#
-# Placeholder reverse-proxy target (nginx) until Greenlight and/or BBB core
-# (bbb-install on Ubuntu) is chosen. Not production BBB media.
-
-ARG UBUNTU_IMAGE=docker.io/library/ubuntu:24.04
-FROM ${UBUNTU_IMAGE}
-
-ENV DEBIAN_FRONTEND=noninteractive \
- TZ=Europe/Zurich
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends \
- ca-certificates curl nginx tini \
- && rm -rf /var/lib/apt/lists/*
-
-RUN printf '%s\n' \
- 'server {' \
- ' listen 80 default_server;' \
- ' root /var/www/html;' \
- ' index index.html;' \
- ' location = /health { access_log off; default_type text/plain; return 200 "ok\n"; }' \
- ' location / { try_files $uri $uri/ =404; }' \
- '}' > /etc/nginx/sites-available/default \
- && printf '%s\n' \
- 'bbb.hacktivism.ch ' \
- 'BBB front stub (Ubuntu 24.04). Full BigBlueButton / Greenlight is a separate Ubuntu install step.
' \
- > /var/www/html/index.html
-
-COPY entrypoint.sh /docker-entrypoint.sh
-RUN chmod 755 /docker-entrypoint.sh
-
-EXPOSE 80
-ENTRYPOINT ["/usr/bin/tini", "--", "/docker-entrypoint.sh"]
diff --git a/configs/bbb/README.md b/configs/bbb/README.md
deleted file mode 100644
index ac655e2..0000000
--- a/configs/bbb/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# BBB (BigBlueButton) — Ubuntu container (koopa)
-
-CachyOS exception: BBB packages and `bbb-install` target **Ubuntu**. Front stub image is Ubuntu 24.04.
-
-| Item | Value |
-|------|--------|
-| Stack | BigBlueButton (front stub → core later) |
-| Image | `localhost/koopa-bbb-front:ubuntu` (`FROM docker.io/library/ubuntu:24.04`) |
-| Live dir | `/home/hernani/koopa-bbb/` |
-| Container | `koopa-bbb-front` |
-| Host port | **9032** |
-| Public site | `bbb.hacktivism.ch` |
-
-## Phases
-
-1. **Front stub (this tree)** — nginx placeholder on `:9032` so Caddy + DNS can land.
-2. **Greenlight (optional)** — Rails UI talking to a real BBB API; still Ubuntu-based images.
-3. **BBB core** — FreeSWITCH / mediasoup / akka-apps. Prefer official Ubuntu install (`bbb-install.sh`) on a dedicated Ubuntu host/VM, or a known multi-container BBB compose. Not CachyOS.
-
-Media UDP/TCP and TURN are documented when core is chosen — not in this stub.
-
-## Build + up (koopa)
-
-```bash
-bash /path/to/koopa-admin-log/scripts/bbb/build.sh
-mkdir -p ~/koopa-bbb && cp -a /path/to/koopa-admin-log/configs/bbb/{compose.yml,Containerfile,entrypoint.sh} ~/koopa-bbb/
-cd ~/koopa-bbb && HOST_PORT=9032 podman-compose up -d
-```
-
-## Caddy
-
-```bash
-sudo bash /path/to/koopa-admin-log/scripts/caddy/caddy-apply.sh \
- --snippet /path/to/koopa-admin-log/configs/caddy/bbb.hacktivism.ch.caddy \
- --site bbb.hacktivism.ch --port 9032 --label bbb
-```
-
-DNS + VeciGate for the hostname after apply Freigabe.
diff --git a/configs/bbb/compose.yml b/configs/bbb/compose.yml
deleted file mode 100644
index 961e9fd..0000000
--- a/configs/bbb/compose.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-# koopa-bbb — BigBlueButton front on Ubuntu (exception to CachyOS)
-# Live: /home/hernani/koopa-bbb/
-# Build: bash ~/koopa-admin-log/scripts/bbb/build.sh → localhost/koopa-bbb-front:ubuntu
-#
-# HOST_PORT: 9032 → Caddy bbb.hacktivism.ch
-# Core BBB / Greenlight: separate follow-up (Ubuntu), not this stub.
-services:
- bbb-front:
- image: localhost/koopa-bbb-front:ubuntu
- build:
- context: .
- dockerfile: Containerfile
- container_name: koopa-bbb-front
- restart: unless-stopped
- environment:
- - TZ=Europe/Zurich
- ports:
- - "${HOST_PORT:-9032}:80"
- healthcheck:
- test: ["CMD", "curl", "-fsS", "http://127.0.0.1/health"]
- interval: 30s
- timeout: 5s
- retries: 3
- start_period: 10s
- labels:
- org.hacktivism.service: bbb
- org.hacktivism.host_port: "${HOST_PORT:-9032}"
- org.hacktivism.site: bbb.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.port_status: allocated-9032
- org.hacktivism.image_base: ubuntu
diff --git a/configs/bbb/entrypoint.sh b/configs/bbb/entrypoint.sh
deleted file mode 100755
index d73c4e6..0000000
--- a/configs/bbb/entrypoint.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-set -euo pipefail
-exec nginx -g 'daemon off;'
diff --git a/configs/bbb/vm/defaults.env b/configs/bbb/vm/defaults.env
deleted file mode 100644
index f7ba7dd..0000000
--- a/configs/bbb/vm/defaults.env
+++ /dev/null
@@ -1,27 +0,0 @@
-# koopa BBB Ubuntu VM — lean defaults (host ~15 Gi / 4 cores)
-# Override before sourcing: RAM_MIB=4096 VCPUS=2 …
-
-VM_NAME=koopa-bbb
-# Official prod=16G; local-dev floor=8G. Koopa lean default=6G + guest swap.
-RAM_MIB=6144
-VCPUS=2
-DISK_GB=50
-# Ubuntu 22.04 LTS (bbb-install 3.0). 24.04 only if docs for your branch say so.
-OS_VARIANT=ubuntu22.04
-CLOUD_IMAGE_URL=https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img
-HOSTNAME=bbb.hacktivism.ch
-# Bridged LAN (preferred for media DNAT). Set BRIDGE= after host-prep creates br0.
-BRIDGE=
-# Fallback: libvirt default NAT (media harder; ok for first boot/ssh)
-NETWORK_SOURCE=default
-# Guest swap (MiB) — important when RAM_MIB < 8192
-SWAP_MIB=4096
-# SSH pubkey injected via cloud-init (path on koopa host)
-SSH_PUBKEY_FILE=/home/hernani/.ssh/id_ed25519.pub
-# Disk pool
-POOL_DIR=/var/lib/libvirt/images
-# bbb-install branch + Greenlight
-BBB_INSTALL_BRANCH=v3.0.x-release
-BBB_INSTALL_EXTRA=-g
-# After VM has IP: Caddy may stay on :9032 stub until cutover, or point at VM:80
-CADDY_BACKEND_HINT=VM_HTTP_80_OR_DNAT
diff --git a/configs/caddy/Caddyfile b/configs/caddy/Caddyfile
index ee8dee5..58fb8a5 100644
--- a/configs/caddy/Caddyfile
+++ b/configs/caddy/Caddyfile
@@ -1,7 +1,7 @@
# Internal only (not in the browser URL):
# 9010 merchant API | 9011 exchange API | 9012 bank API
# 9013 bank landing | 9014 exchange landing | 9015 merchant landing
-# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9029 galene | 9030 cryptpad | 9031 meet | 9032 bbb | 9028 silverbullet | 9200 forgejo-ssh
+# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9200 forgejo-ssh
# 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3
{
@@ -39,28 +39,28 @@ taler.hacktivism.ch {
# surface + aptdeploy HTML (taler.hacktivism.ch only)
handle /taler-monitoring-surface_err {
- redir /taler-monitoring-surface_err/ 302
+ redir * /taler-monitoring-surface_err/ 302
}
handle /taler-monitoring-surface_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-surface {
- redir /taler-monitoring-surface/ 302
+ redir * /taler-monitoring-surface/ 302
}
handle /taler-monitoring-surface* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-aptdeploy_err {
- redir /taler-monitoring-aptdeploy_err/ 302
+ redir * /taler-monitoring-aptdeploy_err/ 302
}
handle /taler-monitoring-aptdeploy_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-aptdeploy {
- redir /taler-monitoring-aptdeploy/ 302
+ redir * /taler-monitoring-aptdeploy/ 302
}
handle /taler-monitoring-aptdeploy* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -68,14 +68,14 @@ taler.hacktivism.ch {
}
handle /taler-monitoring-mattermost_err {
- redir /taler-monitoring-mattermost_err/ 302
+ redir * /taler-monitoring-mattermost_err/ 302
}
handle /taler-monitoring-mattermost_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-mattermost {
- redir /taler-monitoring-mattermost/ 302
+ redir * /taler-monitoring-mattermost/ 302
}
handle /taler-monitoring-mattermost* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -83,14 +83,14 @@ taler.hacktivism.ch {
}
handle /taler-monitoring-mail_err {
- redir /taler-monitoring-mail_err/ 302
+ redir * /taler-monitoring-mail_err/ 302
}
handle /taler-monitoring-mail_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-mail {
- redir /taler-monitoring-mail/ 302
+ redir * /taler-monitoring-mail/ 302
}
handle /taler-monitoring-mail* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -99,14 +99,14 @@ taler.hacktivism.ch {
# taler-monitoring console HTML (host static — not in app containers)
handle /monitoring_err {
- redir /monitoring_err/ 302
+ redir * /monitoring_err/ 302
}
handle /monitoring_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /monitoring {
- redir /monitoring/ 302
+ redir * /monitoring/ 302
}
handle /monitoring* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -143,14 +143,14 @@ exchange.hacktivism.ch {
# taler-monitoring console HTML (host static — not in app containers)
handle /monitoring_err {
- redir /monitoring_err/ 302
+ redir * /monitoring_err/ 302
}
handle /monitoring_err* {
root * /var/www/monitoring-sites/exchange.hacktivism.ch
file_server
}
handle /monitoring {
- redir /monitoring/ 302
+ redir * /monitoring/ 302
}
handle /monitoring* {
root * /var/www/monitoring-sites/exchange.hacktivism.ch
@@ -177,14 +177,14 @@ bank.hacktivism.ch {
# taler-monitoring console HTML (host static — not in app containers)
handle /monitoring_err {
- redir /monitoring_err/ 302
+ redir * /monitoring_err/ 302
}
handle /monitoring_err* {
root * /var/www/monitoring-sites/bank.hacktivism.ch
file_server
}
handle /monitoring {
- redir /monitoring/ 302
+ redir * /monitoring/ 302
}
handle /monitoring* {
root * /var/www/monitoring-sites/bank.hacktivism.ch
@@ -204,19 +204,6 @@ bank.hacktivism.ch {
}
}
- # FP pattern (francpaysan-admin-log files/caddy/Caddyfile @wdGet):
- # GET withdrawal-operation → landing :9013 → demo-api :19096
- # (injects suggested_exchange / required_exchange for Android/iOS)
- @wdGet {
- method GET
- path /taler-integration/withdrawal-operation/*
- }
- handle @wdGet {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
reverse_proxy 127.0.0.1:9012 {
import proxy_public
}
@@ -328,118 +315,7 @@ lemmy.hacktivism.ch {
}
}
-# 9027 decidim (rails)
-decidim.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9027 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9028 silverbullet / SilverBullet — allocated 2026-09-08
-# Merge into /etc/caddy/Caddyfile (sudo).
-# Also add http://silverbullet.hacktivism.ch to the shared ACME/redirect list.
-silverbullet.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9028 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9029 galene — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site galene.hacktivism.ch --port 9029 --label galene
-galene.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9029 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9030 cryptpad — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site cryptpad.hacktivism.ch --port 9030 --label cryptpad
-# CryptPad also needs sandbox subdomain (cryptpad-ui / sandbox) — wire after first boot.
-cryptpad.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9030 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9031 meet (Jitsi) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site meet.hacktivism.ch --port 9031 --label meet
-# WebRTC media (UDP/JVB) is separate from this HTTPS vhost — VeciGate/firewall later.
-meet.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9031 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9032 bbb (BigBlueButton) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site bbb.hacktivism.ch --port 9032 --label bbb
-# Stack image base: Ubuntu (not CachyOS). Media/UDP ports TBD with install.
-bbb.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9032 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch, http://galene.hacktivism.ch, http://cryptpad.hacktivism.ch, http://meet.hacktivism.ch, http://bbb.hacktivism.ch {
+http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch {
handle /.well-known/acme-challenge/* {
root * /var/www/acme
file_server
diff --git a/configs/caddy/Caddyfile.bak-caddy-apply-20260908-090954 b/configs/caddy/Caddyfile.bak-caddy-apply-20260908-090954
deleted file mode 100644
index bacff1e..0000000
--- a/configs/caddy/Caddyfile.bak-caddy-apply-20260908-090954
+++ /dev/null
@@ -1,343 +0,0 @@
-# Internal only (not in the browser URL):
-# 9010 merchant API | 9011 exchange API | 9012 bank API
-# 9013 bank landing | 9014 exchange landing | 9015 merchant landing
-# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9200 forgejo-ssh
-# 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3
-
-{
- email info+koopa@hacktivism.ch
- http_port 9000
- https_port 9001
- auto_https disable_redirects
- # Caddy listens on 9001 behind VeciGate/https-proxy :443.
- # Default HTTP/3 would send Alt-Svc: h3=":9001" — break public HTTPS on :443.
- servers {
- protocols h1 h2
- }
-}
-
-(proxy_public) {
- header_up Host {host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- header_down Location "^https?://[^/]+:90[0-9]{2}(.*)$" "https://{host}$1"
-}
-
-taler.hacktivism.ch {
- tls /etc/caddy/certs/taler.hacktivism.ch/fullchain.pem /etc/caddy/certs/taler.hacktivism.ch/privkey.pem
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9015 {
- import proxy_public
- }
- }
-
- # surface + aptdeploy HTML (taler.hacktivism.ch only)
- handle /taler-monitoring-surface_err {
- redir /taler-monitoring-surface_err/ 302
- }
- handle /taler-monitoring-surface_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-surface {
- redir /taler-monitoring-surface/ 302
- }
- handle /taler-monitoring-surface* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy_err {
- redir /taler-monitoring-aptdeploy_err/ 302
- }
- handle /taler-monitoring-aptdeploy_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy {
- redir /taler-monitoring-aptdeploy/ 302
- }
- handle /taler-monitoring-aptdeploy* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mattermost_err {
- redir /taler-monitoring-mattermost_err/ 302
- }
- handle /taler-monitoring-mattermost_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mattermost {
- redir /taler-monitoring-mattermost/ 302
- }
- handle /taler-monitoring-mattermost* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mail_err {
- redir /taler-monitoring-mail_err/ 302
- }
- handle /taler-monitoring-mail_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mail {
- redir /taler-monitoring-mail/ 302
- }
- handle /taler-monitoring-mail* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
-
-
-
- # SPA: /webui → /webui/
- redir /webui /webui/ 302
-
- # Merchant API + WebUI (nginx :9010 → unix socket)
- reverse_proxy https://127.0.0.1:9010 {
- transport http {
- tls_insecure_skip_verify
- }
- import proxy_public
- }
-}
-
-exchange.hacktivism.ch {
- tls /etc/caddy/certs/exchange.hacktivism.ch/fullchain.pem /etc/caddy/certs/exchange.hacktivism.ch/privkey.pem
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9014 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
-
-
- reverse_proxy 127.0.0.1:9011 {
- import proxy_public
- }
-}
-
-bank.hacktivism.ch {
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
-
-
- # Static terms/privacy on landing nginx :9013
- handle /terms* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
- handle /privacy* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- reverse_proxy 127.0.0.1:9012 {
- import proxy_public
- }
-}
-
-castopod.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9020 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- }
-}
-
-bonfire.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9021 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- flush_interval -1
- }
-}
-
-prime.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9022 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- flush_interval -1
- }
-}
-
-bt.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9023 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- }
-}
-
-# 9024 forgejo HTTP (SSH :9200 host-direct, not via Caddy)
-git.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9024 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-
-# Taler Operations design previews (static nginx)
-tops.ng1.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9090 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-tops.ng2.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9091 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-tops.ng3.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9092 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-
-# 9025 paivana-httpd (GOA paywall)
-paivana.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9025 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- }
-}
-
-# 9026 lemmy (+ ui/pictrs via internal nginx)
-lemmy.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9026 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9027 decidim (rails)
-decidim.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9027 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch {
- handle /.well-known/acme-challenge/* {
- root * /var/www/acme
- file_server
- }
- handle {
- redir https://{host}{uri} permanent
- }
-}
diff --git a/configs/caddy/Caddyfile.bak-caddy-apply-20260908-090955 b/configs/caddy/Caddyfile.bak-caddy-apply-20260908-090955
deleted file mode 100644
index 9c7ed06..0000000
--- a/configs/caddy/Caddyfile.bak-caddy-apply-20260908-090955
+++ /dev/null
@@ -1,399 +0,0 @@
-# Internal only (not in the browser URL):
-# 9010 merchant API | 9011 exchange API | 9012 bank API
-# 9013 bank landing | 9014 exchange landing | 9015 merchant landing
-# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9029 galene | 9030 cryptpad | 9031 meet | 9200 forgejo-ssh
-# 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3
-
-{
- email info+koopa@hacktivism.ch
- http_port 9000
- https_port 9001
- auto_https disable_redirects
- # Caddy listens on 9001 behind VeciGate/https-proxy :443.
- # Default HTTP/3 would send Alt-Svc: h3=":9001" — break public HTTPS on :443.
- servers {
- protocols h1 h2
- }
-}
-
-(proxy_public) {
- header_up Host {host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- header_down Location "^https?://[^/]+:90[0-9]{2}(.*)$" "https://{host}$1"
-}
-
-taler.hacktivism.ch {
- tls /etc/caddy/certs/taler.hacktivism.ch/fullchain.pem /etc/caddy/certs/taler.hacktivism.ch/privkey.pem
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9015 {
- import proxy_public
- }
- }
-
- # surface + aptdeploy HTML (taler.hacktivism.ch only)
- handle /taler-monitoring-surface_err {
- redir /taler-monitoring-surface_err/ 302
- }
- handle /taler-monitoring-surface_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-surface {
- redir /taler-monitoring-surface/ 302
- }
- handle /taler-monitoring-surface* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy_err {
- redir /taler-monitoring-aptdeploy_err/ 302
- }
- handle /taler-monitoring-aptdeploy_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy {
- redir /taler-monitoring-aptdeploy/ 302
- }
- handle /taler-monitoring-aptdeploy* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mattermost_err {
- redir /taler-monitoring-mattermost_err/ 302
- }
- handle /taler-monitoring-mattermost_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mattermost {
- redir /taler-monitoring-mattermost/ 302
- }
- handle /taler-monitoring-mattermost* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mail_err {
- redir /taler-monitoring-mail_err/ 302
- }
- handle /taler-monitoring-mail_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mail {
- redir /taler-monitoring-mail/ 302
- }
- handle /taler-monitoring-mail* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
-
-
-
- # SPA: /webui → /webui/
- redir /webui /webui/ 302
-
- # Merchant API + WebUI (nginx :9010 → unix socket)
- reverse_proxy https://127.0.0.1:9010 {
- transport http {
- tls_insecure_skip_verify
- }
- import proxy_public
- }
-}
-
-exchange.hacktivism.ch {
- tls /etc/caddy/certs/exchange.hacktivism.ch/fullchain.pem /etc/caddy/certs/exchange.hacktivism.ch/privkey.pem
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9014 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
-
-
- reverse_proxy 127.0.0.1:9011 {
- import proxy_public
- }
-}
-
-bank.hacktivism.ch {
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
-
-
- # Static terms/privacy on landing nginx :9013
- handle /terms* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
- handle /privacy* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- reverse_proxy 127.0.0.1:9012 {
- import proxy_public
- }
-}
-
-castopod.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9020 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- }
-}
-
-bonfire.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9021 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- flush_interval -1
- }
-}
-
-prime.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9022 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- flush_interval -1
- }
-}
-
-bt.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9023 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- }
-}
-
-# 9024 forgejo HTTP (SSH :9200 host-direct, not via Caddy)
-git.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9024 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-
-# Taler Operations design previews (static nginx)
-tops.ng1.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9090 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-tops.ng2.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9091 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-tops.ng3.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9092 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-
-# 9025 paivana-httpd (GOA paywall)
-paivana.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9025 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- }
-}
-
-# 9026 lemmy (+ ui/pictrs via internal nginx)
-lemmy.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9026 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9027 decidim (rails)
-decidim.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9027 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9029 galene — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site galene.hacktivism.ch --port 9029 --label galene
-galene.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9029 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9030 cryptpad — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site cryptpad.hacktivism.ch --port 9030 --label cryptpad
-# CryptPad also needs sandbox subdomain (cryptpad-ui / sandbox) — wire after first boot.
-cryptpad.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9030 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9031 meet (Jitsi) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site meet.hacktivism.ch --port 9031 --label meet
-# WebRTC media (UDP/JVB) is separate from this HTTPS vhost — VeciGate/firewall later.
-meet.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9031 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch, http://galene.hacktivism.ch, http://cryptpad.hacktivism.ch, http://meet.hacktivism.ch {
- handle /.well-known/acme-challenge/* {
- root * /var/www/acme
- file_server
- }
- handle {
- redir https://{host}{uri} permanent
- }
-}
diff --git a/configs/caddy/Caddyfile.bak-caddy-apply-20260908-143821 b/configs/caddy/Caddyfile.bak-caddy-apply-20260908-143821
deleted file mode 100644
index 70d8b08..0000000
--- a/configs/caddy/Caddyfile.bak-caddy-apply-20260908-143821
+++ /dev/null
@@ -1,418 +0,0 @@
-# Internal only (not in the browser URL):
-# 9010 merchant API | 9011 exchange API | 9012 bank API
-# 9013 bank landing | 9014 exchange landing | 9015 merchant landing
-# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9029 galene | 9030 cryptpad | 9031 meet | 9032 bbb | 9200 forgejo-ssh
-# 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3
-
-{
- email info+koopa@hacktivism.ch
- http_port 9000
- https_port 9001
- auto_https disable_redirects
- # Caddy listens on 9001 behind VeciGate/https-proxy :443.
- # Default HTTP/3 would send Alt-Svc: h3=":9001" — break public HTTPS on :443.
- servers {
- protocols h1 h2
- }
-}
-
-(proxy_public) {
- header_up Host {host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- header_down Location "^https?://[^/]+:90[0-9]{2}(.*)$" "https://{host}$1"
-}
-
-taler.hacktivism.ch {
- tls /etc/caddy/certs/taler.hacktivism.ch/fullchain.pem /etc/caddy/certs/taler.hacktivism.ch/privkey.pem
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9015 {
- import proxy_public
- }
- }
-
- # surface + aptdeploy HTML (taler.hacktivism.ch only)
- handle /taler-monitoring-surface_err {
- redir /taler-monitoring-surface_err/ 302
- }
- handle /taler-monitoring-surface_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-surface {
- redir /taler-monitoring-surface/ 302
- }
- handle /taler-monitoring-surface* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy_err {
- redir /taler-monitoring-aptdeploy_err/ 302
- }
- handle /taler-monitoring-aptdeploy_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy {
- redir /taler-monitoring-aptdeploy/ 302
- }
- handle /taler-monitoring-aptdeploy* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mattermost_err {
- redir /taler-monitoring-mattermost_err/ 302
- }
- handle /taler-monitoring-mattermost_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mattermost {
- redir /taler-monitoring-mattermost/ 302
- }
- handle /taler-monitoring-mattermost* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mail_err {
- redir /taler-monitoring-mail_err/ 302
- }
- handle /taler-monitoring-mail_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mail {
- redir /taler-monitoring-mail/ 302
- }
- handle /taler-monitoring-mail* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
-
-
-
- # SPA: /webui → /webui/
- redir /webui /webui/ 302
-
- # Merchant API + WebUI (nginx :9010 → unix socket)
- reverse_proxy https://127.0.0.1:9010 {
- transport http {
- tls_insecure_skip_verify
- }
- import proxy_public
- }
-}
-
-exchange.hacktivism.ch {
- tls /etc/caddy/certs/exchange.hacktivism.ch/fullchain.pem /etc/caddy/certs/exchange.hacktivism.ch/privkey.pem
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9014 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
-
-
- reverse_proxy 127.0.0.1:9011 {
- import proxy_public
- }
-}
-
-bank.hacktivism.ch {
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
-
-
- # Static terms/privacy on landing nginx :9013
- handle /terms* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
- handle /privacy* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- reverse_proxy 127.0.0.1:9012 {
- import proxy_public
- }
-}
-
-castopod.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9020 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- }
-}
-
-bonfire.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9021 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- flush_interval -1
- }
-}
-
-prime.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9022 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- flush_interval -1
- }
-}
-
-bt.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9023 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- }
-}
-
-# 9024 forgejo HTTP (SSH :9200 host-direct, not via Caddy)
-git.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9024 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-
-# Taler Operations design previews (static nginx)
-tops.ng1.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9090 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-tops.ng2.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9091 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-tops.ng3.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9092 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- }
-}
-
-
-# 9025 paivana-httpd (GOA paywall)
-paivana.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9025 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- }
-}
-
-# 9026 lemmy (+ ui/pictrs via internal nginx)
-lemmy.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9026 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9027 decidim (rails)
-decidim.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9027 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9029 galene — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site galene.hacktivism.ch --port 9029 --label galene
-galene.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9029 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9030 cryptpad — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site cryptpad.hacktivism.ch --port 9030 --label cryptpad
-# CryptPad also needs sandbox subdomain (cryptpad-ui / sandbox) — wire after first boot.
-cryptpad.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9030 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9031 meet (Jitsi) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site meet.hacktivism.ch --port 9031 --label meet
-# WebRTC media (UDP/JVB) is separate from this HTTPS vhost — VeciGate/firewall later.
-meet.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9031 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9032 bbb (BigBlueButton) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site bbb.hacktivism.ch --port 9032 --label bbb
-# Stack image base: Ubuntu (not CachyOS). Media/UDP ports TBD with install.
-bbb.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9032 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch, http://galene.hacktivism.ch, http://cryptpad.hacktivism.ch, http://meet.hacktivism.ch, http://bbb.hacktivism.ch {
- handle /.well-known/acme-challenge/* {
- root * /var/www/acme
- file_server
- }
- handle {
- redir https://{host}{uri} permanent
- }
-}
diff --git a/configs/caddy/README.md b/configs/caddy/README.md
index 66283ff..c59d05e 100644
--- a/configs/caddy/README.md
+++ b/configs/caddy/README.md
@@ -4,7 +4,6 @@
|------|------|
| **`Caddyfile`** | Multi-vhost config mirror (`/etc/caddy/Caddyfile` on host) |
| `git.hacktivism.ch.caddy` | Site-block snippet for Forgejo |
-| `silverbullet.hacktivism.ch.caddy` | Site-block snippet for SilverBullet |
| `Caddyfile.taler-host` | **Legacy** Taler-only sketch — do not deploy |
Also: `host/caddy/Caddyfile` (same as `Caddyfile` here).
diff --git a/configs/caddy/bbb.hacktivism.ch.caddy b/configs/caddy/bbb.hacktivism.ch.caddy
deleted file mode 100644
index c1c3b4b..0000000
--- a/configs/caddy/bbb.hacktivism.ch.caddy
+++ /dev/null
@@ -1,18 +0,0 @@
-# 9032 bbb (BigBlueButton) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site bbb.hacktivism.ch --port 9032 --label bbb
-# Stack image base: Ubuntu (not CachyOS). Media/UDP ports TBD with install.
-bbb.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9032 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
diff --git a/configs/caddy/cryptpad.hacktivism.ch.caddy b/configs/caddy/cryptpad.hacktivism.ch.caddy
deleted file mode 100644
index 759c6ad..0000000
--- a/configs/caddy/cryptpad.hacktivism.ch.caddy
+++ /dev/null
@@ -1,18 +0,0 @@
-# 9030 cryptpad — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site cryptpad.hacktivism.ch --port 9030 --label cryptpad
-# Backend is koopa-cryptpad-edge (HTTP+WS). Sandbox subdomain (cryptpad-ui) — wire after first boot.
-cryptpad.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9030 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
diff --git a/configs/caddy/galene.hacktivism.ch.caddy b/configs/caddy/galene.hacktivism.ch.caddy
deleted file mode 100644
index 59aaef9..0000000
--- a/configs/caddy/galene.hacktivism.ch.caddy
+++ /dev/null
@@ -1,17 +0,0 @@
-# 9029 galene — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site galene.hacktivism.ch --port 9029 --label galene
-galene.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9029 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
diff --git a/configs/caddy/meet.hacktivism.ch.caddy b/configs/caddy/meet.hacktivism.ch.caddy
deleted file mode 100644
index 5e54f99..0000000
--- a/configs/caddy/meet.hacktivism.ch.caddy
+++ /dev/null
@@ -1,18 +0,0 @@
-# 9031 meet (Jitsi) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site meet.hacktivism.ch --port 9031 --label meet
-# WebRTC media (UDP/JVB) is separate from this HTTPS vhost — VeciGate/firewall later.
-meet.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9031 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
diff --git a/configs/caddy/silverbullet.hacktivism.ch.caddy b/configs/caddy/silverbullet.hacktivism.ch.caddy
deleted file mode 100644
index 868b065..0000000
--- a/configs/caddy/silverbullet.hacktivism.ch.caddy
+++ /dev/null
@@ -1,18 +0,0 @@
-# 9028 silverbullet / SilverBullet — allocated 2026-09-08
-# Merge into /etc/caddy/Caddyfile (sudo).
-# Also add http://silverbullet.hacktivism.ch to the shared ACME/redirect list.
-silverbullet.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9028 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
diff --git a/configs/cryptpad/Containerfile b/configs/cryptpad/Containerfile
deleted file mode 100644
index e20fe10..0000000
--- a/configs/cryptpad/Containerfile
+++ /dev/null
@@ -1,47 +0,0 @@
-# koopa-cryptpad — CryptPad on CachyOS base + Node from pacman
-# Build on koopa (x86_64):
-# bash ~/koopa-admin-log/scripts/cryptpad/build.sh
-# → localhost/koopa-cryptpad:cachyos
-#
-# App tree from upstream release tarball (not the official Debian-ish image).
-
-ARG CACHYOS_IMAGE=docker.io/cachyos/cachyos:latest
-FROM ${CACHYOS_IMAGE}
-
-ARG CRYPTPAD_VERSION=2025.9.0
-ARG CRYPTPAD_URL=https://github.com/cryptpad/cryptpad/archive/refs/tags/${CRYPTPAD_VERSION}.tar.gz
-
-ENV TZ=Europe/Zurich \
- PATH=/usr/local/bin:$PATH \
- CRYPTPAD_HOME=/cryptpad
-
-RUN pacman -Syu --noconfirm --needed \
- bash curl ca-certificates catatonit tar git \
- nodejs npm \
- && pacman -Scc --noconfirm \
- && rm -rf /var/cache/pacman/pkg/* /tmp/*
-
-# npm may block git deps (EALLOWGIT); rewrite git+ssh → https and allow git installs.
-RUN curl -fsSL "$CRYPTPAD_URL" -o /tmp/cryptpad.tgz \
- && mkdir -p /cryptpad \
- && tar -xzf /tmp/cryptpad.tgz -C /cryptpad --strip-components=1 \
- && rm -f /tmp/cryptpad.tgz \
- && cd /cryptpad \
- && git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" \
- && git config --global url."https://github.com/".insteadOf "git@github.com:" \
- && git config --global url."https://".insteadOf "git://" \
- && printf 'allow-git=all\nallow-remote=all\n' > /root/.npmrc \
- && (npm ci --omit=dev --allow-git=all --allow-remote=all \
- || npm install --omit=dev --allow-git=all --allow-remote=all) \
- && (npm run install:components || true) \
- && rm -rf /root/.npm /tmp/* /var/cache/pacman/pkg/*
-
-COPY entrypoint.sh /docker-entrypoint.sh
-RUN mkdir -p /cryptpad/config /cryptpad/data /cryptpad/datastore /cryptpad/block /cryptpad/blob \
- /cryptpad/blobstage /cryptpad/data/logs
-COPY config.example.js /cryptpad/config/config.js
-RUN chmod 755 /docker-entrypoint.sh
-
-EXPOSE 3000
-WORKDIR /cryptpad
-ENTRYPOINT ["/usr/bin/catatonit", "--", "/docker-entrypoint.sh"]
diff --git a/configs/cryptpad/README.md b/configs/cryptpad/README.md
deleted file mode 100644
index 2a91ced..0000000
--- a/configs/cryptpad/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# CryptPad — on CachyOS (koopa)
-
-| Item | Value |
-|------|--------|
-| Stack | [CryptPad](https://cryptpad.org/) |
-| Image | `localhost/koopa-cryptpad:cachyos` |
-| Live dir | `/home/hernani/koopa-cryptpad/` |
-| Host port | **9030** = Paivana paywall; nginx edge backend **172.17.0.1:19130** → app (pasta host IP; Paivana DEST) |
-| App ports (internal) | **3000** HTTP, **3003** websocket |
-| Public site | `cryptpad.hacktivism.ch` |
-
-`websocketPort` must differ from `httpPort` (upstream default 3003). Compose runs `koopa-cryptpad-edge` (nginx) so Caddy still only targets **9030**.
-Upstream wants a separate **safe/sandbox** origin for some builds — add a second vhost after first boot if the admin UI requires it.
-
-## Build + up
-
-```bash
-bash /path/to/koopa-admin-log/scripts/cryptpad/build.sh
-mkdir -p ~/koopa-cryptpad
-cp -a /path/to/koopa-admin-log/configs/cryptpad/{compose.yml,config.example.js} ~/koopa-cryptpad/
-cp ~/koopa-cryptpad/config.example.js ~/koopa-cryptpad/config.js
-# edit httpUnsafeOrigin / httpSafeOrigin in config.js
-cd ~/koopa-cryptpad && podman-compose up -d
-```
-
-## Caddy
-
-```bash
-sudo bash /path/to/koopa-admin-log/scripts/caddy/caddy-apply.sh \
- --snippet …/configs/caddy/cryptpad.hacktivism.ch.caddy \
- --site cryptpad.hacktivism.ch --port 9030 --label cryptpad
-```
diff --git a/configs/cryptpad/compose.yml b/configs/cryptpad/compose.yml
deleted file mode 100644
index 6452461..0000000
--- a/configs/cryptpad/compose.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-# koopa-cryptpad — CryptPad on CachyOS + nginx edge (HTTP+WS on :9030)
-# Live: /home/hernani/koopa-cryptpad/
-# Build: bash ~/koopa-admin-log/scripts/cryptpad/build.sh → localhost/koopa-cryptpad:cachyos
-#
-# Public :9030 = koopa-paivana-cryptpad (shared paivana image).
-# Edge on pasta host IP — Paivana DEST → 172.17.0.1:19130 (not LAN 0.0.0.0)
-# App listens 3000 (http) + 3003 (websocket) inside the net.
-services:
- cryptpad:
- image: localhost/koopa-cryptpad:cachyos
- build:
- context: .
- dockerfile: Containerfile
- args:
- CRYPTPAD_VERSION: "${CRYPTPAD_VERSION:-2025.9.0}"
- container_name: koopa-cryptpad
- restart: unless-stopped
- environment:
- - TZ=Europe/Zurich
- volumes:
- - ./data:/cryptpad/data
- - ./datastore:/cryptpad/datastore
- - ./block:/cryptpad/block
- - ./blob:/cryptpad/blob
- - ./config.js:/cryptpad/config/config.js:ro
- expose:
- - "3000"
- - "3003"
- labels:
- org.hacktivism.service: cryptpad
- org.hacktivism.host_port: "9030"
- org.hacktivism.backend_port: "${BACKEND_PORT:-19130}"
- org.hacktivism.site: cryptpad.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.port_status: backend-19130-paywall-9030
- org.hacktivism.image_base: cachyos
-
- edge:
- image: docker.io/library/nginx:alpine
- container_name: koopa-cryptpad-edge
- restart: unless-stopped
- depends_on:
- - cryptpad
- volumes:
- - ./edge.conf:/etc/nginx/conf.d/default.conf:ro
- ports:
- - "172.17.0.1:${BACKEND_PORT:-19130}:80"
- labels:
- org.hacktivism.service: cryptpad-edge
- org.hacktivism.host_port: "9030"
- org.hacktivism.backend_port: "${BACKEND_PORT:-19130}"
- org.hacktivism.site: cryptpad.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
diff --git a/configs/cryptpad/config.example.js b/configs/cryptpad/config.example.js
deleted file mode 100644
index 6766565..0000000
--- a/configs/cryptpad/config.example.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/* CryptPad config — http on 3000, websockets on 3003 (must differ).
- * Live: mount ./config.js over /cryptpad/config/config.js
- * Edge nginx in compose merges both onto host :9030 (Caddy unchanged).
- */
-module.exports = {
- httpUnsafeOrigin: 'https://cryptpad.hacktivism.ch',
- httpSafeOrigin: 'https://cryptpad.hacktivism.ch',
- httpAddress: '0.0.0.0',
- httpPort: 3000,
- websocketPort: 3003,
- logToStdout: true,
- maxWorkers: 2,
-};
diff --git a/configs/cryptpad/edge.conf b/configs/cryptpad/edge.conf
deleted file mode 100644
index 3d5b305..0000000
--- a/configs/cryptpad/edge.conf
+++ /dev/null
@@ -1,37 +0,0 @@
-# Front for koopa-cryptpad: HTTP :3000 + /cryptpad_websocket → :3003
-upstream cryptpad_http {
- server cryptpad:3000;
-}
-upstream cryptpad_ws {
- server cryptpad:3003;
-}
-
-server {
- listen 80 default_server;
- server_name _;
- client_max_body_size 150m;
-
- location /cryptpad_websocket {
- proxy_pass http://cryptpad_ws;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_read_timeout 3600s;
- proxy_send_timeout 3600s;
- }
-
- location / {
- proxy_pass http://cryptpad_http;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Forwarded-Host $host;
- proxy_read_timeout 3600s;
- proxy_send_timeout 3600s;
- }
-}
diff --git a/configs/cryptpad/entrypoint.sh b/configs/cryptpad/entrypoint.sh
deleted file mode 100755
index e128249..0000000
--- a/configs/cryptpad/entrypoint.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-set -euo pipefail
-cd /cryptpad
-# Expect config.js already present (compose mounts or image default).
-exec node server.js "$@"
diff --git a/configs/exchange-landing/index.html b/configs/exchange-landing/index.html
index 75908bb..c57b486 100644
--- a/configs/exchange-landing/index.html
+++ b/configs/exchange-landing/index.html
@@ -289,22 +289,18 @@
-
-
+
stack ·
- vanilla
+ taler-exchange 1.6.6
·
- taler-exchange 1.6.15
+ libtalerexchange 1.6.6
·
- libtalerexchange 1.6.15
+ taler-exchange-database 1.6.6
·
- taler-exchange-database 1.6.15
+ taler-exchange-offline 1.6.6
·
- taler-exchange-offline 1.6.15
- ·
- taler-terms-generator 1.6.15
+ taler-terms-generator 1.6.6
-
GOA · Exchange
@@ -504,10 +500,10 @@ sudo apt-get install -y taler-wallet-cli
/privacy .
-
- Exchange landing v44
- · content 2026-09-16 23:19 CEST
- · 0d881cd
+
+ Exchange landing v43
+ · content 2026-07-17 18:33 CEST
+ · d0594cb
diff --git a/configs/firewalld/public-ports.md b/configs/firewalld/public-ports.md
index e9ce9de..2ae7378 100644
--- a/configs/firewalld/public-ports.md
+++ b/configs/firewalld/public-ports.md
@@ -18,7 +18,6 @@ sudo firewall-cmd --list-ports
| 8080/tcp | Tor OR | — |
| 23235/tcp | (legacy list; host SSH is :22, WAN via VeciGate 23235→22) | — |
| **9200/tcp** | **Forgejo git-SSH** (podman rootlessport) | **OK** after add+reload |
-| **6881/tcp+udp** | qBittorrent BT listen (`koopa-prime-qbittorrent`) | apply: `scripts/prime/apply-qbittorrent-bt-net.sh` |
### Not opened on purpose
@@ -62,7 +61,6 @@ Forgejo advertises `SSH_PORT=9200`. Traffic hits **host:9200** (not Caddy). With
|------|--------|
| 2026-07-09 | 443/tcp re-added for IPv6 |
| 2026-07-10 | **9200/tcp** for Forgejo git-SSH (verified LAN + hairpin + SSH auth) |
-| 2026-09-25 | **6881/tcp+udp** for qBittorrent BT (`scripts/prime/apply-qbittorrent-bt-net.sh`); VeciGate wan+hairpin DNAT live; WAN/LAN `nc` OK |
## Nym (`koopa-nym`, nym.com)
diff --git a/configs/forgejo/README.md b/configs/forgejo/README.md
index 9359a0b..a5d9a6a 100644
--- a/configs/forgejo/README.md
+++ b/configs/forgejo/README.md
@@ -58,7 +58,3 @@ Or: Profile → Appearance → theme **hacktivism**, then hard-reload.
### Registration
Public signup is disabled (`DISABLE_REGISTRATION`, no registration button).
-
-### pre-receive: gitleaks + *enc.git gate (TODO320, 2026-09-11)
-
-Live hooks under `~/koopa-forgejo/data/custom/hooks/pre-receive.d/` (gitleaks skip `*enc.git`; `git-crypt-enforce` Klartext-Sperre). Binary `data/custom/bin/gitleaks` v8.21.2. Installer + smoke: laptop `llm-configs/bin/forgejo-hooks/`. Ops note: `2026/2026-09-11--forgejo-gitleaks-enc-gate.md`. New repos → re-run `install-hooks-ht.sh`.
diff --git a/configs/galene/Containerfile b/configs/galene/Containerfile
deleted file mode 100644
index a2e620c..0000000
--- a/configs/galene/Containerfile
+++ /dev/null
@@ -1,41 +0,0 @@
-# koopa-galene — Galene SFU on CachyOS base
-# Build on koopa (x86_64):
-# bash ~/koopa-admin-log/scripts/galene/build.sh
-# → localhost/koopa-galene:cachyos
-#
-# Upstream no longer ships release tarballs; build from git tag/branch.
-
-ARG CACHYOS_IMAGE=docker.io/cachyos/cachyos:latest
-FROM ${CACHYOS_IMAGE}
-
-ARG GALENE_REF=galene-0.96.3
-ARG GALENE_REPO=https://github.com/jech/galene.git
-
-ENV TZ=Europe/Zurich \
- PATH=/usr/local/bin:$PATH \
- GALENE_DATA=/data \
- GOPATH=/tmp/go \
- GOTOOLCHAIN=auto
-
-RUN pacman -Syu --noconfirm --needed \
- bash curl ca-certificates catatonit tar git go \
- && pacman -Scc --noconfirm \
- && rm -rf /var/cache/pacman/pkg/* /tmp/*
-
-RUN set -eux \
- && git clone --depth 1 --branch "$GALENE_REF" "$GALENE_REPO" /src/galene \
- && cd /src/galene \
- && CGO_ENABLED=0 go build -ldflags='-s -w' -o /usr/local/bin/galene . \
- && test -x /usr/local/bin/galene \
- && mkdir -p /opt/galene \
- && cp -a /src/galene/static /opt/galene/static \
- && rm -rf /src/galene /tmp/go /root/go /var/cache/pacman/pkg/*
-
-COPY entrypoint.sh /docker-entrypoint.sh
-RUN chmod 755 /docker-entrypoint.sh \
- && mkdir -p /data/groups /data/recordings /data/data \
- && ln -sfn /opt/galene/static /data/static
-
-EXPOSE 8443
-WORKDIR /data
-ENTRYPOINT ["/usr/bin/catatonit", "--", "/docker-entrypoint.sh"]
diff --git a/configs/galene/README.md b/configs/galene/README.md
deleted file mode 100644
index 88eb6bb..0000000
--- a/configs/galene/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Galene — SFU on CachyOS (koopa)
-
-| Item | Value |
-|------|--------|
-| Stack | [Galene](https://galene.org/) |
-| Image | `localhost/koopa-galene:cachyos` (`FROM docker.io/cachyos/cachyos`) |
-| Build | Go from git tag `GALENE_REF` (default `galene-0.96.3`; no release tarballs) |
-| Live dir | `/home/hernani/koopa-galene/` |
-| Container | `koopa-galene` |
-| Host port | **9029** = Paivana paywall; Galene HTTP backend **127.0.0.1:19129** (`-insecure`) |
-| Public site | `galene.hacktivism.ch` |
-
-## Build + up (koopa)
-
-```bash
-bash /path/to/koopa-admin-log/scripts/galene/build.sh
-mkdir -p ~/koopa-galene && cp -a /path/to/koopa-admin-log/configs/galene/compose.yml ~/koopa-galene/
-cd ~/koopa-galene && podman-compose up -d
-```
-
-## Caddy
-
-```bash
-sudo bash /path/to/koopa-admin-log/scripts/caddy/caddy-apply.sh \
- --snippet /path/to/koopa-admin-log/configs/caddy/galene.hacktivism.ch.caddy \
- --site galene.hacktivism.ch --port 9029 --label galene
-```
-
-DNS + VeciGate for the hostname; WebRTC UDP ports documented when first room is live.
-
-## Notes
-
-- Volume `./data` must not hide UI assets; entrypoint passes `-static /opt/galene/static`.
-- Example group: copy `groups/hacktivism.json.example` → `~/koopa-galene/data/groups/hacktivism.json` (change op password).
-- WebRTC UDP: compose publishes **10000–10099/udp**; set the same range in VeciGate DNAT → koopa. Entrypoint uses `-udp-range` (`GALENE_UDP_RANGE`).
-- Built-in TURN (`-turn auto`) logs `no public addresses` until the host has a reachable public/mapped address; fix UDP/NAT first, then re-check logs.
diff --git a/configs/galene/compose.yml b/configs/galene/compose.yml
deleted file mode 100644
index 1919f87..0000000
--- a/configs/galene/compose.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-# koopa-galene — Galene on CachyOS
-# Live: /home/hernani/koopa-galene/
-# Build: bash ~/koopa-admin-log/scripts/galene/build.sh → localhost/koopa-galene:cachyos
-#
-# Public :9029 = koopa-paivana-galene (shared paivana image).
-# Backend on pasta host IP — Paivana DEST → 172.17.0.1:19129 (HTTP -insecure; not LAN 0.0.0.0).
-# WebRTC UDP: still published (see README / ports.md)
-services:
- galene:
- image: localhost/koopa-galene:cachyos
- container_name: koopa-galene
- restart: unless-stopped
- environment:
- - TZ=Europe/Zurich
- - GALENE_UDP_RANGE=${GALENE_UDP_RANGE:-10000-10099}
- volumes:
- - ./data:/data
- - ./entrypoint.sh:/docker-entrypoint.sh:ro
- ports:
- - "172.17.0.1:${BACKEND_PORT:-19129}:8443"
- - "${GALENE_UDP_LO:-10000}-${GALENE_UDP_HI:-10099}:${GALENE_UDP_LO:-10000}-${GALENE_UDP_HI:-10099}/udp"
- labels:
- org.hacktivism.service: galene
- org.hacktivism.host_port: "9029"
- org.hacktivism.backend_port: "${BACKEND_PORT:-19129}"
- org.hacktivism.site: galene.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.port_status: backend-19129-paywall-9029
- org.hacktivism.image_base: cachyos
-
diff --git a/configs/galene/entrypoint.sh b/configs/galene/entrypoint.sh
deleted file mode 100755
index c686f21..0000000
--- a/configs/galene/entrypoint.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-set -euo pipefail
-# Galene listens HTTPS by default; behind Caddy we serve plain HTTP on :8443
-# with -insecure (TLS terminated at Caddy).
-# Static assets stay in the image (/opt/galene/static); the ./data volume must
-# not shadow them — pass -static explicitly.
-DATA="${GALENE_DATA:-/data}"
-STATIC="${GALENE_STATIC:-/opt/galene/static}"
-# Host/UDP map: VeciGate WAN → koopa 10000-10099/udp (see configs/ports.md).
-UDP_RANGE="${GALENE_UDP_RANGE:-10000-10099}"
-mkdir -p "$DATA/groups" "$DATA/recordings" "$DATA/data"
-cd "$DATA"
-exec /usr/local/bin/galene \
- -http "0.0.0.0:8443" \
- -insecure \
- -static "$STATIC" \
- -udp-range "$UDP_RANGE" \
- -data "$DATA/data" \
- -groups "$DATA/groups" \
- -recordings "$DATA/recordings" \
- "$@"
diff --git a/configs/galene/groups/hacktivism.json.example b/configs/galene/groups/hacktivism.json.example
deleted file mode 100644
index 69bda83..0000000
--- a/configs/galene/groups/hacktivism.json.example
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "displayName": "hacktivism",
- "public": true,
- "op": [{"username": "op", "password": "changeme"}],
- "presenter": [{}],
- "other": []
-}
diff --git a/configs/lab-local/goa-regio-ng-koopa.yml b/configs/lab-local/goa-regio-ng-koopa.yml
index 00450d8..04d502e 100644
--- a/configs/lab-local/goa-regio-ng-koopa.yml
+++ b/configs/lab-local/goa-regio-ng-koopa.yml
@@ -53,7 +53,8 @@ regional_currency:
regional_currency_common_amounts: "GOA:10 GOA:20 GOA:50 GOA:100 GOA:200 GOA:1000"
regional_currency_static_coins_file: goa-exchange-coins.conf
regional_currency_coin_withdraw_duration: 7 days
- regional_currency_default_debt_limit: "GOA:4503599627370496"
+ # Must stay < 2^52 (libeufin amount overflow on withdraw confirm).
+ regional_currency_default_debt_limit: "GOA:1000000000000"
regional_currency_exchange_master_public_key_expected: "TW6K5FXF81VYCAH0YWYX0SX98KBBSJ42VX27WAX01FTFH400QG10"
regional_currency_exchange_master_priv_src: "{{ lookup('ansible.builtin.env', 'HOME') }}/git/admin-logs/koopa-admin-secrets/backups/goa/current/exchange-offline/master.priv"
diff --git a/configs/lab-local/lab-hosts.yml b/configs/lab-local/lab-hosts.yml
deleted file mode 100644
index e899299..0000000
--- a/configs/lab-local/lab-hosts.yml
+++ /dev/null
@@ -1,19 +0,0 @@
----
-# lab-only: pin *.goa.local → 127.0.0.1 inside goa-regio-ng
-# ephemeral on container recreate — re-run after recreate / before site.yml wait-on-bank
-# inventory: goa-regio-ng-koopa.yml (same SSH ProxyCommand)
-#
-# ansible-playbook -i goa-regio-ng-koopa.yml lab-hosts.yml
-
-- name: lab hosts goa.local
- hosts: regional_currency
- gather_facts: false
- tasks:
- - name: /etc/hosts *.goa.local → 127.0.0.1
- ansible.builtin.blockinfile:
- path: /etc/hosts
- marker: "# {mark} ANSIBLE LAB goa.local"
- # podman systemd: /etc/hosts often EBUSY on atomic rename
- unsafe_writes: true
- block: |
- 127.0.0.1 bank.goa.local exchange.goa.local backend.goa.local goa.local
diff --git a/configs/meet/Containerfile b/configs/meet/Containerfile
deleted file mode 100644
index de1f36d..0000000
--- a/configs/meet/Containerfile
+++ /dev/null
@@ -1,55 +0,0 @@
-# koopa-meet-web — Jitsi Meet web assets on CachyOS
-# Build: bash ~/koopa-admin-log/scripts/meet/build.sh → localhost/koopa-meet-web:cachyos
-#
-# Full Meet needs prosody + jicofo + jvb (see compose.yml). This image serves
-# the static web UI via nginx from Arch/CachyOS packages + upstream meet tarball.
-# Signaling/media containers are separate services in the same compose.
-
-ARG CACHYOS_IMAGE=docker.io/cachyos/cachyos:latest
-FROM ${CACHYOS_IMAGE}
-
-ARG JITSI_MEET_VERSION=stable
-ARG JITSI_MEET_URL=https://download.jitsi.org/jitsi-meet/src/jitsi-meet-1.0.0.tar.bz2
-
-ENV TZ=Europe/Zurich
-
-RUN pacman -Syu --noconfirm --needed \
- bash curl ca-certificates catatonit tar nginx \
- && pacman -Scc --noconfirm \
- && rm -rf /var/cache/pacman/pkg/* /tmp/*
-
-# CachyOS/Arch nginx.conf embeds a default server and does NOT include http.d/.
-# Replace it with a minimal conf that serves the branded landing.
-RUN mkdir -p /usr/share/jitsi-meet /etc/nginx/http.d \
- && printf '%s\n' \
- 'worker_processes 1;' \
- 'events { worker_connections 1024; }' \
- 'http {' \
- ' include mime.types;' \
- ' default_type application/octet-stream;' \
- ' sendfile on;' \
- ' keepalive_timeout 65;' \
- ' server {' \
- ' listen 80 default_server;' \
- ' server_name _;' \
- ' root /usr/share/jitsi-meet;' \
- ' index index.html;' \
- ' location / { try_files $uri $uri/ /index.html; }' \
- ' }' \
- '}' > /etc/nginx/nginx.conf \
- && printf '%s\n' \
- ' ' \
- ' ' \
- 'meet.hacktivism.ch ' \
- '' \
- 'meet.hacktivism.ch ' \
- 'Jitsi Meet web (CachyOS). Signaling/media (prosody / jicofo / jvb) is Phase 2 — see compose + README.
' \
- 'HTTP OK on host port 9031. Full rooms need UDP/JVB after Phase 2.
' \
- > /usr/share/jitsi-meet/index.html
-
-COPY entrypoint.sh /docker-entrypoint.sh
-RUN chmod 755 /docker-entrypoint.sh
-
-EXPOSE 80
-ENTRYPOINT ["/usr/bin/catatonit", "--", "/docker-entrypoint.sh"]
diff --git a/configs/meet/README.md b/configs/meet/README.md
deleted file mode 100644
index bc8f200..0000000
--- a/configs/meet/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Meet (Jitsi) — CachyOS web stub (koopa)
-
-| Item | Value |
-|------|--------|
-| Public site | `meet.hacktivism.ch` |
-| Public port | **9031** (`koopa-paivana-meet`) |
-| Backend | `127.0.0.1:19131` (`koopa-meet-web`) |
-| Image (web) | `localhost/koopa-meet-web:cachyos` |
-| Live dir | `/home/hernani/koopa-meet/` |
-
-Phase 1 = Caddy vhost + branded nginx landing on CachyOS (`koopa-meet-web`) behind Paivana (GOA:42).
-Phase 2 = prosody + jicofo + jvb — scaffold in `compose.phase2.yml` (`profiles: [phase2]`); images under `configs/meet/phase2/` still TODO. Not `docker-jitsi-meet` Debian by default.
-UDP/JVB and TURN need VeciGate once rooms work (range TBD in `ports.md`).
-
-## Caddy
-
-Caddy stays on `127.0.0.1:9031` (paywall). App bind is loopback `19131` only.
-
-```bash
-sudo bash /path/to/koopa-admin-log/scripts/caddy/caddy-apply.sh \
- --snippet …/configs/caddy/meet.hacktivism.ch.caddy \
- --site meet.hacktivism.ch --port 9031 --label meet
-```
diff --git a/configs/meet/compose.phase2.yml b/configs/meet/compose.phase2.yml
deleted file mode 100644
index f1a3373..0000000
--- a/configs/meet/compose.phase2.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-# Phase-2 stubs — NOT started by default.
-# Usage (after images exist):
-# cd ~/koopa-meet && podman-compose -f compose.yml -f compose.phase2.yml up -d
-#
-# Do not pull official docker-jitsi-meet Debian images as the default base.
-# Build CachyOS (or pinned binary) images under configs/meet/phase2/ when ready.
-
-services:
- prosody:
- profiles: ["phase2"]
- image: localhost/koopa-meet-prosody:cachyos
- container_name: koopa-meet-prosody
- restart: unless-stopped
- environment:
- - TZ=Europe/Zurich
- # ports / volumes: fill when Containerfile exists
- labels:
- org.hacktivism.service: meet-prosody
- org.hacktivism.site: meet.hacktivism.ch
- org.hacktivism.phase: "2"
-
- jicofo:
- profiles: ["phase2"]
- image: localhost/koopa-meet-jicofo:cachyos
- container_name: koopa-meet-jicofo
- restart: unless-stopped
- environment:
- - TZ=Europe/Zurich
- depends_on:
- - prosody
- labels:
- org.hacktivism.service: meet-jicofo
- org.hacktivism.site: meet.hacktivism.ch
- org.hacktivism.phase: "2"
-
- jvb:
- profiles: ["phase2"]
- image: localhost/koopa-meet-jvb:cachyos
- container_name: koopa-meet-jvb
- restart: unless-stopped
- environment:
- - TZ=Europe/Zurich
- # UDP host publish TBD in ports.md when allocated
- depends_on:
- - prosody
- labels:
- org.hacktivism.service: meet-jvb
- org.hacktivism.site: meet.hacktivism.ch
- org.hacktivism.phase: "2"
diff --git a/configs/meet/compose.yml b/configs/meet/compose.yml
deleted file mode 100644
index 9aacb27..0000000
--- a/configs/meet/compose.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# koopa-meet — Jitsi Meet (CachyOS web stub + follow-up signaling/media)
-# Live: /home/hernani/koopa-meet/
-# Public :9031 = koopa-paivana-meet (shared paivana image).
-# Backend on pasta host IP — Paivana DEST → 172.17.0.1:19131 (not LAN 0.0.0.0)
-#
-# Next: add prosody / jicofo / jvb services (CachyOS images or pinned binaries).
-# Official docker-jitsi-meet is Debian-based — do not pull those as default here.
-services:
- meet-web:
- image: localhost/koopa-meet-web:cachyos
- build:
- context: .
- dockerfile: Containerfile
- container_name: koopa-meet-web
- restart: unless-stopped
- environment:
- - TZ=Europe/Zurich
- ports:
- - "172.17.0.1:${BACKEND_PORT:-19131}:80"
- labels:
- org.hacktivism.service: meet
- org.hacktivism.backend_port: "${BACKEND_PORT:-19131}"
- org.hacktivism.site: meet.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.port_status: backend-19131-paywall-9031
- org.hacktivism.image_base: cachyos
diff --git a/configs/meet/entrypoint.sh b/configs/meet/entrypoint.sh
deleted file mode 100755
index d73c4e6..0000000
--- a/configs/meet/entrypoint.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-set -euo pipefail
-exec nginx -g 'daemon off;'
diff --git a/configs/merchant-landing/index.html b/configs/merchant-landing/index.html
index 14af99e..77330f8 100644
--- a/configs/merchant-landing/index.html
+++ b/configs/merchant-landing/index.html
@@ -530,22 +530,18 @@
-
-
+
stack ·
- vanilla
+ taler-merchant 1.6.9
·
- taler-merchant 1.6.23
+ taler-merchant-webui 1.6.8~dev6
·
- taler-merchant-webui 1.6.38
+ libtalermerchant 1.6.9
·
- libtalermerchant 1.6.23
+ libtalerexchange 1.6.7~dev2
·
- libtalerexchange 1.6.15
- ·
- taler-terms-generator 1.6.15
+ taler-terms-generator 1.6.7~dev2
-
Merchant · dual currency
@@ -837,10 +833,10 @@ sudo apt-get install -y taler-wallet-cli
/privacy .
-
- Merchant landing v43
- · content 2026-09-16 23:19 CEST
- · 0d881cd
+
+ Merchant landing v42
+ · content 2026-07-17 18:27 CEST
+ · 37ea4bb
diff --git a/configs/notes/.env.example b/configs/notes/.env.example
deleted file mode 100644
index 03eb62b..0000000
--- a/configs/notes/.env.example
+++ /dev/null
@@ -1,5 +0,0 @@
-# Live on koopa: ~/koopa-silverbullet/.env (not git)
-# Allocate HOST_PORT only after live `ss` + Freigabe + ports.md update.
-HOST_PORT=9028
-# Optional SilverBullet app auth (prefer Caddy basicauth):
-# SB_USER=hernani:changeme
diff --git a/configs/notes/Containerfile b/configs/notes/Containerfile
deleted file mode 100644
index f922feb..0000000
--- a/configs/notes/Containerfile
+++ /dev/null
@@ -1,44 +0,0 @@
-# koopa-silverbullet — SilverBullet on CachyOS base + server binary
-# Build on koopa (x86_64):
-# bash ~/koopa-admin-log/scripts/notes/build.sh
-# → localhost/koopa-silverbullet:cachyos
-#
-# Base: docker.io/cachyos/cachyos (Arch-derived). App binary from upstream
-# silverbullet-server-linux-x86_64 release (not the Alpine ghcr image).
-
-ARG CACHYOS_IMAGE=docker.io/cachyos/cachyos:latest
-FROM ${CACHYOS_IMAGE}
-
-ARG SB_VERSION=2.10.0
-ARG SB_ZIP_URL=https://github.com/silverbulletmd/silverbullet/releases/download/${SB_VERSION}/silverbullet-server-linux-x86_64.zip
-
-ENV SB_HOSTNAME=0.0.0.0 \
- SB_FOLDER=/space \
- SB_PORT=3000 \
- SB_NAME=SilverBullet \
- TZ=Europe/Zurich \
- PATH=/usr/local/bin:$PATH
-
-# catatonit = Arch/CachyOS container init (tini not in repos)
-RUN pacman -Syu --noconfirm --needed \
- bash curl git openssh catatonit unzip \
- && pacman -Scc --noconfirm \
- && rm -rf /var/cache/pacman/pkg/* /tmp/*
-
-# Fetch + install server binary (build-time; no Deno in image)
-RUN curl -fsSL "$SB_ZIP_URL" -o /tmp/sb.zip \
- && unzip -o /tmp/sb.zip -d /tmp \
- && install -m 755 /tmp/silverbullet /silverbullet \
- && /silverbullet --version || true \
- && rm -rf /tmp/sb.zip /tmp/silverbullet
-
-COPY entrypoint.sh /docker-entrypoint.sh
-RUN chmod 755 /docker-entrypoint.sh \
- && mkdir -p /space
-
-EXPOSE 3000
-HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
- CMD curl -fsS "http://127.0.0.1:${SB_PORT}/.instance" || exit 1
-
-WORKDIR /space
-ENTRYPOINT ["/usr/bin/catatonit", "--", "/docker-entrypoint.sh"]
diff --git a/configs/notes/README.md b/configs/notes/README.md
deleted file mode 100644
index 38b6deb..0000000
--- a/configs/notes/README.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# SilverBullet — SilverBullet on CachyOS (koopa)
-
-Cross-host markdown notes (magikoopa + clementine). Stack name **SilverBullet** (parked web UI). Product **FreeNotes** = separate native enc notes (not this stack).
-
-| Item | Value |
-|------|--------|
-| Stack | [SilverBullet](https://silverbullet.md/) server binary |
-| Image | `localhost/koopa-silverbullet:cachyos` (`FROM docker.io/cachyos/cachyos`) |
-| Live dir | `/home/hernani/koopa-silverbullet/` |
-| Container | `koopa-silverbullet` |
-| Host port | **9028** = Paivana paywall; app backend **127.0.0.1:19128** |
-| Public site | `silverbullet.hacktivism.ch` |
-| Auth | Paivana (GOA:42); no Caddy basic_auth |
-| Forgejo space backup | private `hernani/freenotes` (historical name; SB space only) |
-
-## Cutover freenotes → silverbullet (koopa)
-
-Live still may be `~/koopa-freenotes` / CT `koopa-freenotes`. Scripts default dry-run; `--apply` needs Freigabe.
-
-```bash
-bash /path/to/koopa-admin-log/scripts/notes/status.sh
-bash /path/to/koopa-admin-log/scripts/notes/cutover-to-silverbullet.sh
-# Freigabe:
-bash /path/to/koopa-admin-log/scripts/notes/cutover-to-silverbullet.sh --apply
-```
-
-See `scripts/notes/README.md` for seed + Caddy order. DNS: `silverbullet.hacktivism.ch`.
-
-## Build + recreate (koopa)
-
-```bash
-# clone koopa-admin-log from Forgejo when needed (no permanent ~/ clone)
-bash /path/to/koopa-admin-log/scripts/notes/build.sh
-cd ~/koopa-silverbullet
-podman-compose down
-podman-compose up -d
-bash /path/to/koopa-admin-log/scripts/notes/up.sh
-```
-
-## Caddy apply (sudo on koopa)
-
-```bash
-sudo bash /path/to/koopa-admin-log/scripts/caddy/merge-notes-site.sh
-# seed from secrets-enc SoT so apply does not rotate PASS
-sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt \
- bash /path/to/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh
-```
-
-Runtime: `/etc/caddy/secrets/silverbullet-basicauth.txt` (root-only). Never recreate `~/koopa-secrets`.
-
-## Space SoT (Forgejo)
-
-Private `hernani/freenotes` — live tree `~/koopa-silverbullet/space/` (git). On-koopa push uses oneshot HTTP token (no hernani SSH key in `~/.ssh`); laptop push via `git@git.hacktivism.ch:hernani/freenotes.git` (ProxyJump).
-
-## Client apps (hosts)
-
-Notes-like **system apps** (standalone Brave/Chrome `--app=` window + own profile): `configs/notes/client-apps/`.
-
-- magikoopa: `~/Applications/SilverBullet.app` — `bash client-apps/install-magikoopa.sh`
-- clementine (on-box): `silverbullet.desktop` — `bash client-apps/install-clementine.sh`
-- optional: browser **Install app** (PWA) for Dock identity
-
-## Plugs (SB)
-
-`space/_plug/`: `treeview.plug.js`, `silversearch.plug.js` — see `PLUGS.md`.
diff --git a/configs/notes/client-apps/README.md b/configs/notes/client-apps/README.md
deleted file mode 100644
index cf1687b..0000000
--- a/configs/notes/client-apps/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# SilverBullet — system Notes apps (hosts)
-
-Goal: **like Apple Notes** — a real system app window on each laptop, not a browser bookmark and not SilverBullet plugs.
-
-| Host | App | Behavior |
-|------|-----|----------|
-| magikoopa | `SilverBullet.app` → `~/Applications/` | Brave/Chrome **`--app=`** + own profile (`SilverBullet-BraveProfile`) — no tab bar |
-| clementine | `silverbullet.desktop` | same `--app=` via Brave/Chromium + `~/.local/share/SilverBullet-browser` |
-| both (optional) | Browser **Install app** / PWA | even closer to native Dock/menu identity |
-
-**Not this:** `space/_plug/` (TreeView/SilverSearch) — those are in-editor extensions.
-
-Install:
-
-```bash
-# magikoopa
-bash configs/notes/client-apps/install-magikoopa.sh
-
-# clementine (on-box)
-bash configs/notes/client-apps/install-clementine.sh
-```
-
-First launch: log in once (Caddy basicauth); credentials stay in the app profile.
diff --git a/configs/notes/client-apps/SilverBullet.app/Contents/Info.plist b/configs/notes/client-apps/SilverBullet.app/Contents/Info.plist
deleted file mode 100644
index 495c9f3..0000000
--- a/configs/notes/client-apps/SilverBullet.app/Contents/Info.plist
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
- CFBundleName SilverBullet
- CFBundleDisplayName SilverBullet
- CFBundleIdentifier ch.hacktivism.silverbullet
- CFBundleVersion 1
- CFBundleShortVersionString 1.0
- CFBundleExecutable SilverBullet
- CFBundlePackageType APPL
- LSMinimumSystemVersion 12.0
- CFBundleIconFile AppIcon
-
-
diff --git a/configs/notes/client-apps/SilverBullet.app/Contents/MacOS/SilverBullet b/configs/notes/client-apps/SilverBullet.app/Contents/MacOS/SilverBullet
deleted file mode 100755
index 6fd407e..0000000
--- a/configs/notes/client-apps/SilverBullet.app/Contents/MacOS/SilverBullet
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-# SilverBullet — Notes-like system app (standalone window, not a browser tab)
-URL="https://silverbullet.hacktivism.ch/"
-USER_DATA="${HOME}/Library/Application Support/SilverBullet-BraveProfile"
-mkdir -p "$USER_DATA"
-
-BRAVE="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
-CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
-
-# Dedicated Chromium profile so this feels like its own app (Dock, cookies, no tab chrome)
-if [[ -x "$BRAVE" ]]; then
- exec "$BRAVE" \
- --user-data-dir="$USER_DATA" \
- --class=SilverBullet \
- --app="$URL" \
- --name=SilverBullet
-elif [[ -x "$CHROME" ]]; then
- exec "$CHROME" \
- --user-data-dir="$USER_DATA" \
- --class=SilverBullet \
- --app="$URL" \
- --name=SilverBullet
-else
- # last resort: Safari has no --app; open dedicated window via osascript
- exec osascript <<'OSA'
-tell application "Safari"
- activate
- make new document with properties {URL:"https://silverbullet.hacktivism.ch/"}
-end tell
-OSA
-fi
diff --git a/configs/notes/client-apps/install-clementine.sh b/configs/notes/client-apps/install-clementine.sh
deleted file mode 100755
index b568c1f..0000000
--- a/configs/notes/client-apps/install-clementine.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env bash
-# Install SilverBullet XFCE launcher on clementine (on-box). No Freigabe (user home).
-set -euo pipefail
-SRC="$(cd "$(dirname "$0")" && pwd)/silverbullet.desktop"
-APPDIR="${HOME}/.local/share/applications"
-DESK="${HOME}/Desktop"
-mkdir -p "$APPDIR" "$DESK"
-install -m 0644 "$SRC" "$APPDIR/silverbullet.desktop"
-install -m 0644 "$SRC" "$DESK/silverbullet.desktop"
-# refresh menu cache if available
-command -v update-desktop-database >/dev/null && update-desktop-database "$APPDIR" || true
-echo "STATUS=ok DESKTOP=$APPDIR/silverbullet.desktop SHORTCUT=$DESK/silverbullet.desktop"
-echo "TIP=open https://silverbullet.hacktivism.ch then browser Install app for PWA"
diff --git a/configs/notes/client-apps/install-magikoopa.sh b/configs/notes/client-apps/install-magikoopa.sh
deleted file mode 100755
index 887e365..0000000
--- a/configs/notes/client-apps/install-magikoopa.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-# Install SilverBullet.app into ~/Applications (magikoopa). No Freigabe (user home).
-set -euo pipefail
-SRC="$(cd "$(dirname "$0")" && pwd)/SilverBullet.app"
-DEST="${HOME}/Applications/SilverBullet.app"
-URL="${HOME}/Desktop/SilverBullet.url"
-mkdir -p "${HOME}/Applications" "${HOME}/Desktop"
-if [[ -d "$SRC" ]]; then
- rm -rf "$DEST"
- cp -R "$SRC" "$DEST"
-else
- # bootstrap minimal app if scaffold app tree missing
- mkdir -p "$DEST/Contents/MacOS" "$DEST/Contents/Resources"
- cat >"$DEST/Contents/Info.plist" <<'PLIST'
-
-
-
-
- CFBundleName SilverBullet
- CFBundleDisplayName SilverBullet
- CFBundleIdentifier ch.hacktivism.silverbullet
- CFBundleVersion 1
- CFBundleShortVersionString 1.0
- CFBundleExecutable SilverBullet
- CFBundlePackageType APPL
- LSMinimumSystemVersion 12.0
-
-
-PLIST
- printf '%s\n' '#!/bin/bash' 'open "https://silverbullet.hacktivism.ch/"' >"$DEST/Contents/MacOS/SilverBullet"
- chmod +x "$DEST/Contents/MacOS/SilverBullet"
-fi
-printf '%s\n' '[InternetShortcut]' 'URL=https://silverbullet.hacktivism.ch/' >"$URL"
-echo "STATUS=ok APP=$DEST URL=$URL"
diff --git a/configs/notes/client-apps/silverbullet.desktop b/configs/notes/client-apps/silverbullet.desktop
deleted file mode 100644
index c2de6fb..0000000
--- a/configs/notes/client-apps/silverbullet.desktop
+++ /dev/null
@@ -1,12 +0,0 @@
-[Desktop Entry]
-Version=1.0
-Type=Application
-Name=SilverBullet
-Comment=Notes-like SilverBullet on koopa — standalone window
-TryExec=brave-browser
-Exec=sh -c 'B=""; for c in brave-browser brave chromium chromium-browser google-chrome; do command -v "$c" >/dev/null && B="$c" && break; done; if [ -n "$B" ]; then exec "$B" --user-data-dir="$HOME/.local/share/SilverBullet-browser" --class=SilverBullet --app=https://silverbullet.hacktivism.ch/; else exec xdg-open https://silverbullet.hacktivism.ch/; fi'
-Terminal=false
-Categories=Office;Network;
-Keywords=notes;todo;silverbullet;
-StartupWMClass=SilverBullet
-StartupNotify=true
diff --git a/configs/notes/compose.yml b/configs/notes/compose.yml
deleted file mode 100644
index fec8d42..0000000
--- a/configs/notes/compose.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-# koopa-silverbullet — SilverBullet on CachyOS base (parked; FreeNotes = native enc)
-# Live: /home/hernani/koopa-silverbullet/
-# Build: bash ~/koopa-admin-log/scripts/notes/build.sh → localhost/koopa-silverbullet:cachyos
-#
-# Public :9028 = koopa-paivana-silverbullet (shared paivana image).
-# Backend on pasta host IP — Paivana DEST → 172.17.0.1:19128 (not LAN 0.0.0.0)
-services:
- notes:
- image: localhost/koopa-silverbullet:cachyos
- build:
- context: .
- dockerfile: Containerfile
- args:
- SB_VERSION: "${SB_VERSION:-2.10.0}"
- container_name: koopa-silverbullet
- restart: unless-stopped
- environment:
- - PUID=1000
- - PGID=1000
- - TZ=Europe/Zurich
- - SB_INDEX_PAGE=index
- - SB_NAME=SilverBullet
- - SB_SHELL_BACKEND=off
- # Optional app-layer auth (prefer Caddy basicauth; leave empty in .env to skip):
- # - SB_USER=${SB_USER}
- volumes:
- - ./space:/space
- ports:
- - "172.17.0.1:${BACKEND_PORT:-19128}:3000"
- labels:
- org.hacktivism.service: silverbullet
- org.hacktivism.host_port: "9028"
- org.hacktivism.backend_port: "${BACKEND_PORT:-19128}"
- org.hacktivism.site: silverbullet.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.port_status: backend-19128-paywall-9028
- org.hacktivism.image_base: cachyos
-
diff --git a/configs/notes/container-koopa-silverbullet.service b/configs/notes/container-koopa-silverbullet.service
deleted file mode 100644
index 375cf9f..0000000
--- a/configs/notes/container-koopa-silverbullet.service
+++ /dev/null
@@ -1,28 +0,0 @@
-# container-koopa-silverbullet.service
-# Install on koopa as hernani (after compose create):
-# mkdir -p ~/.config/systemd/user
-# cp configs/notes/container-koopa-silverbullet.service ~/.config/systemd/user/
-# systemctl --user daemon-reload
-# systemctl --user enable --now container-koopa-silverbullet.service
-# Requires: loginctl enable-linger hernani (root, once)
-#
-# Container must already exist (podman-compose up -d once).
-
-[Unit]
-Description=Podman container-koopa-silverbullet.service (SilverBullet)
-Documentation=https://silverbullet.md/
-Wants=network-online.target
-After=network-online.target
-RequiresMountsFor=/run/user/1000/containers
-
-[Service]
-Environment=PODMAN_SYSTEMD_UNIT=%n
-Restart=always
-TimeoutStopSec=90
-ExecStart=/usr/bin/podman start koopa-silverbullet
-ExecStop=/usr/bin/podman stop -t 20 koopa-silverbullet
-ExecStopPost=/usr/bin/podman stop -t 20 koopa-silverbullet
-Type=forking
-
-[Install]
-WantedBy=default.target
diff --git a/configs/notes/entrypoint.sh b/configs/notes/entrypoint.sh
deleted file mode 100755
index 6386786..0000000
--- a/configs/notes/entrypoint.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-# SilverBullet entrypoint (CachyOS/Arch user tools).
-# Adapted from upstream docker-entrypoint.sh (busybox → shadow useradd).
-set -euo pipefail
-
-if [[ -f /space/CONTAINER_BOOT.md ]]; then
- echo "Executing CONTAINER_BOOT.md script"
- bash /space/CONTAINER_BOOT.md &
-fi
-
-SB_FOLDER="${SB_FOLDER:-/space}"
-
-if [[ -z "${PUID:-}" && "$(id -u)" == "0" ]]; then
- PUID=$(stat -c "%u" "$SB_FOLDER")
- echo "Will run SilverBullet with UID $PUID (from owner of $SB_FOLDER)"
-fi
-if [[ -z "${PGID:-}" ]]; then
- PGID=$(stat -c "%g" "$SB_FOLDER")
-fi
-
-if [[ "${PUID:-0}" == "0" || "$(id -u)" != "0" ]]; then
- exec /silverbullet "$@"
-fi
-
-echo "Creating silverbullet user UID=$PUID GID=$PGID"
-getent group silverbullet >/dev/null 2>&1 || groupadd -g "$PGID" silverbullet || groupadd -o -g "$PGID" silverbullet
-getent passwd silverbullet >/dev/null 2>&1 || useradd -M -N -g silverbullet -u "$PUID" -d "$SB_FOLDER" -s /bin/bash silverbullet \
- || useradd -o -M -N -g silverbullet -u "$PUID" -d "$SB_FOLDER" -s /bin/bash silverbullet
-
-echo "Running SilverBullet as PUID=$PUID PGID=$PGID"
-# quote-safe: pass args via "$@" under su -c is awkward; use runuser when present
-if command -v runuser >/dev/null 2>&1; then
- exec runuser -u silverbullet -- /silverbullet "$@"
-fi
-args=$(printf '%q ' "$@")
-exec su silverbullet -s /bin/bash -c "/silverbullet $args"
diff --git a/configs/notes/space/PLUGS.md b/configs/notes/space/PLUGS.md
deleted file mode 100644
index 38d6454..0000000
--- a/configs/notes/space/PLUGS.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Plugs (apps)
-
-Third-party SilverBullet plugs in (loaded automatically).
-
-| Plug | Role |
-|------|------|
-| treeview.plug.js | Folder sidebar (Apple Notes-like) |
-| silversearch.plug.js | Full-text search |
-
-Install/update: drop into , reload SilverBullet.
-
-Builtin SB plugs stay in the image; only extras live here.
diff --git a/configs/notes/space/TODO.md b/configs/notes/space/TODO.md
deleted file mode 100644
index 8d665d3..0000000
--- a/configs/notes/space/TODO.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# TODO
-
-Canonical ADHD todo (SilverBullet space SoT; FreeNotes = separate native enc).
-
-## Now (max 3) — active quest board
-- [x] magikoopa SilverBullet.app (`~/Applications`) + Desktop SilverBullet.url
-- [ ] Install silverbullet.desktop on clementine (on-box: client-apps/install-clementine.sh)
-- [ ] One-shot migrate: Apple Notes export + Dokumente/TODO.md → here (or FreeNotes later)
-- [ ] Human sudo: confirm `sudo ls -la /etc/caddy/secrets/silverbullet-basicauth.txt`
-- [ ] Live cutover Freigabe: dir/container → koopa-silverbullet; Caddy → silverbullet.hacktivism.ch; DNS
-
-## Next
-- [x] Private Forgejo backup remote for space/ (hernani/freenotes — historical name)
-- [x] Seed notes space on koopa
-- [x] Caddy merge + basicauth scripts (live cutover pending Freigabe)
-- [ ] DNS / public hostname silverbullet.hacktivism.ch (human)
-- [x] Update productivity-adhd-notes 02 / 02b → single SoT pointing here
-- [x] Wire magikoopa SSH push to hernani/freenotes (ProxyJump) — clone ~/git/freenotes
-
-## Someday / amnesty
-- [ ] PWA install both laptops (browser Install app)
-- [ ] Second-layer SB_USER if basicauth alone is not enough
-- [ ] TreeView / SilverSearch UX polish after first week
-
-## Stats (gamification — agent updates)
-- Session streak: 1
-- Quests finished today: 6
-- XP today: 60
-- Last win: rename stack freenotes → silverbullet
-- Boss: silverbullet cutover
-- Boss HP: 70%
diff --git a/configs/notes/space/_plug/silversearch.plug.js b/configs/notes/space/_plug/silversearch.plug.js
deleted file mode 100644
index e3f1cfd..0000000
--- a/configs/notes/space/_plug/silversearch.plug.js
+++ /dev/null
@@ -1,21 +0,0 @@
-var Un=Object.create;var Ce=Object.defineProperty;var Vn=Object.getOwnPropertyDescriptor;var qn=Object.getOwnPropertyNames;var Gn=Object.getPrototypeOf,Kn=Object.prototype.hasOwnProperty;var Fe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),Q=(t,e)=>{for(var n in e)Ce(t,n,{get:e[n],enumerable:!0})},Hn=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of qn(e))!Kn.call(t,i)&&i!==n&&Ce(t,i,{get:()=>e[i],enumerable:!(r=Vn(e,i))||r.enumerable});return t};var Et=(t,e,n)=>(n=t!=null?Un(Gn(t)):{},Hn(e||!t||!t.__esModule?Ce(n,"default",{value:t,enumerable:!0}):n,t));var bn=Fe((yu,_e)=>{function mn(t){return Array.isArray(t)?t:[t]}var Ds=void 0,mt="",hn=" ",dt="\\",Is=/^\s+$/,zs=/(?:[^\\]|^)\\$/,Os=/^\\!/,Rs=/^\\#/,Ls=/\r?\n/g,js=/^\.{0,2}\/|^\.{1,2}$/,Ns=/\/$/,H="/",yn="node-ignore";typeof Symbol<"u"&&(yn=Symbol.for("node-ignore"));var vn=yn,X=(t,e,n)=>(Object.defineProperty(t,e,{value:n}),n),$s=/([0-z])-([0-z])/g,gn=()=>!1,Ws=t=>t.replace($s,(e,n,r)=>n.charCodeAt(0)<=r.charCodeAt(0)?e:mt),Bs=t=>{let{length:e}=t;return t.slice(0,e-e%2)},Us=[[/^\uFEFF/,()=>mt],[/((?:\\\\)*?)(\\?\s+)$/,(t,e,n)=>e+(n.indexOf("\\")===0?hn:mt)],[/(\\+?)\s/g,(t,e)=>{let{length:n}=e;return e.slice(0,n-n%2)+hn}],[/[\\$.|*+(){^]/g,t=>`\\${t}`],[/(?!\\)\?/g,()=>"[^/]"],[/^\//,()=>"^"],[/\//g,()=>"\\/"],[/^\^*\\\*\\\*\\\//,()=>"^(?:.*\\/)?"],[/^(?=[^^])/,function(){return/\/(?!$)/.test(this)?"^":"(?:^|\\/)"}],[/\\\/\\\*\\\*(?=\\\/|$)/g,(t,e,n)=>e+6{let r=n.replace(/\\\*/g,"[^\\/]*");return e+r}],[/\\\\\\(?=[$.|*+(){^])/g,()=>dt],[/\\\\/g,()=>dt],[/(\\)?\[([^\]/]*?)(\\*)($|\])/g,(t,e,n,r,i)=>e===dt?`\\[${n}${Bs(r)}${i}`:i==="]"&&r.length%2===0?`[${Ws(n)}${r}]`:"[]"],[/(?:[^*])$/,t=>/\/$/.test(t)?`${t}$`:`${t}(?=$|\\/$)`]],Vs=/(^|\\\/)?\\\*$/,ue="regex",be="checkRegex",dn="_",qs={[ue](t,e){return`${e?`${e}[^/]+`:"[^/]*"}(?=$|\\/$)`},[be](t,e){return`${e?`${e}[^/]*`:"[^/]*"}(?=$|\\/$)`}},Gs=t=>Us.reduce((e,[n,r])=>e.replace(n,r.bind(t)),t),ke=t=>typeof t=="string",Ks=t=>t&&ke(t)&&!Is.test(t)&&!zs.test(t)&&t.indexOf("#")!==0,Hs=t=>t.split(Ls).filter(Boolean),yt=class{constructor(e,n,r,i,s,o){this.pattern=e,this.mark=n,this.negative=s,X(this,"body",r),X(this,"ignoreCase",i),X(this,"regexPrefix",o)}get regex(){let e=dn+ue;return this[e]?this[e]:this._make(ue,e)}get checkRegex(){let e=dn+be;return this[e]?this[e]:this._make(be,e)}_make(e,n){let r=this.regexPrefix.replace(Vs,qs[e]),i=this.ignoreCase?new RegExp(r,"i"):new RegExp(r);return X(this,n,i)}},Xs=({pattern:t,mark:e},n)=>{let r=!1,i=t;i.indexOf("!")===0&&(r=!0,i=i.substr(1)),i=i.replace(Os,"!").replace(Rs,"#");let s=Gs(i);return new yt(t,e,i,n,r,s)},vt=class{constructor(e){this._ignoreCase=e,this._rules=[]}_add(e){if(e&&e[vn]){this._rules=this._rules.concat(e._rules._rules),this._added=!0;return}if(ke(e)&&(e={pattern:e}),Ks(e.pattern)){let n=Xs(e,this._ignoreCase);this._added=!0,this._rules.push(n)}}add(e){return this._added=!1,mn(ke(e)?Hs(e):e).forEach(this._add,this),this._added}test(e,n,r){let i=!1,s=!1,o;this._rules.forEach(a=>{let{negative:l}=a;s===l&&i!==s||l&&!i&&!s&&!n||!a[r].test(e)||(i=!l,s=l,o=l?Ds:a)});let u={ignored:i,unignored:s};return o&&(u.rule=o),u}},Js=(t,e)=>{throw new e(t)},L=(t,e,n)=>ke(t)?t?L.isNotRelative(t)?n(`path should be a \`path.relative()\`d string, but got "${e}"`,RangeError):!0:n("path must not be empty",TypeError):n(`path must be a string, but got \`${e}\``,TypeError),xn=t=>js.test(t);L.isNotRelative=xn;L.convert=t=>t;var gt=class{constructor({ignorecase:e=!0,ignoreCase:n=e,allowRelativePaths:r=!1}={}){X(this,vn,!0),this._rules=new vt(n),this._strictPathCheck=!r,this._initCache()}_initCache(){this._ignoreCache=Object.create(null),this._testCache=Object.create(null)}add(e){return this._rules.add(e)&&this._initCache(),this}addPattern(e){return this.add(e)}_test(e,n,r,i){let s=e&&L.convert(e);return L(s,e,this._strictPathCheck?Js:gn),this._t(s,n,r,i)}checkIgnore(e){if(!Ns.test(e))return this.test(e);let n=e.split(H).filter(Boolean);if(n.pop(),n.length){let r=this._t(n.join(H)+H,this._testCache,!0,n);if(r.ignored)return r}return this._rules.test(e,!1,be)}_t(e,n,r,i){if(e in n)return n[e];if(i||(i=e.split(H).filter(Boolean)),i.pop(),!i.length)return n[e]=this._rules.test(e,r,ue);let s=this._t(i.join(H)+H,n,r,i);return n[e]=s.ignored?s:this._rules.test(e,r,ue)}ignores(e){return this._test(e,this._ignoreCache,!1).ignored}createFilter(){return e=>!this.ignores(e)}filter(e){return mn(e).filter(this.createFilter())}test(e){return this._test(e,this._testCache,!0)}},xt=t=>new gt(t),Qs=t=>L(t&&L.convert(t),t,gn),wn=()=>{let t=n=>/^\\\\\?\\/.test(n)||/["<>|\u0000-\u001F]+/u.test(n)?n:n.replace(/\\/g,"/");L.convert=t;let e=/^[a-z]:\//i;L.isNotRelative=n=>e.test(n)||xn(n)};typeof process<"u"&&process.platform==="win32"&&wn();_e.exports=xt;xt.default=xt;_e.exports.isPathValid=Qs;X(_e.exports,Symbol.for("setupWindows"),wn)});var En=Fe(Ee=>{Ee.parse=function(t,e){if(e?e.offsets=typeof e.offsets>"u"?!0:e.offsets:e={offsets:!0},t||(t=""),t.indexOf(":")===-1&&!e.tokenize)return t;if(!e.keywords&&!e.ranges&&!e.tokenize)return t;var n={text:[]};e.offsets&&(n.offsets=[]);for(var r={},i=[],s=/(\S+:'(?:[^'\\]|\\.)*')|(\S+:"(?:[^"\\]|\\.)*")|(-?"(?:[^"\\]|\\.)*")|(-?'(?:[^'\\]|\\.)*')|\S+|\S+:\S+/g,o;(o=s.exec(t))!==null;){var h=o[0],u=h.indexOf(":");if(u!==-1){var a=h.split(":"),l=h.slice(0,u),f=h.slice(u+1);f=f.replace(/^\"|\"$|^\'|\'$/g,""),f=(f+"").replace(/\\(.?)/g,function(T,M){switch(M){case"\\":return"\\";case"0":return"\0";case"":return"";default:return M}}),i.push({keyword:l,value:f,offsetStart:o.index,offsetEnd:o.index+h.length})}else{var p=!1;h[0]==="-"&&(p=!0,h=h.slice(1)),h=h.replace(/^\"|\"$|^\'|\'$/g,""),h=(h+"").replace(/\\(.?)/g,function(T,M){switch(M){case"\\":return"\\";case"0":return"\0";case"":return"";default:return M}}),p?r.text?(r.text instanceof Array||(r.text=[r.text]),r.text.push(h)):r.text=h:i.push({text:h,offsetStart:o.index,offsetEnd:o.index+h.length})}}i.reverse();for(var h;h=i.pop();)if(h.text)n.text.push(h.text),e.offsets&&n.offsets.push(h);else{var l=h.keyword;e.keywords=e.keywords||[];var d=!1,g=!1;if(!/^-/.test(l))d=e.keywords.indexOf(l)!==-1;else if(l[0]==="-"){var v=l.slice(1);d=e.keywords.indexOf(v)!==-1,d&&(l=v,g=!0)}e.ranges=e.ranges||[];var m=e.ranges.indexOf(l)!==-1;if(d){e.offsets&&n.offsets.push({keyword:l,value:h.value,offsetStart:g?h.offsetStart+1:h.offsetStart,offsetEnd:h.offsetEnd});var y=h.value;if(y.length){var x=y.split(",");g?r[l]?r[l]instanceof Array?x.length>1?r[l]=r[l].concat(x):r[l].push(y):(r[l]=[r[l]],r[l].push(y)):x.length>1?r[l]=x:e.alwaysArray?r[l]=[y]:r[l]=y:n[l]?n[l]instanceof Array?x.length>1?n[l]=n[l].concat(x):n[l].push(y):(n[l]=[n[l]],n[l].push(y)):x.length>1?n[l]=x:e.alwaysArray?n[l]=[y]:n[l]=y}}else if(m){e.offsets&&n.offsets.push(h);var y=h.value,w=y.split("-");n[l]={},w.length===2?(n[l].from=w[0],n[l].to=w[1]):!w.length%2||(n[l].from=y)}else{var k=h.keyword+":"+h.value;n.text.push(k),e.offsets&&n.offsets.push({text:k,offsetStart:h.offsetStart,offsetEnd:h.offsetEnd})}}return n.text.length?e.tokenize||(n.text=n.text.join(" ").trim()):delete n.text,n.exclude=r,n};Ee.stringify=function(t,e,n){if(e||(e={offsets:!0}),!t)return"";if(typeof t=="string")return t;if(Array.isArray(t))return t.join(" ");if(!Object.keys(t).length)return"";if(Object.keys(t).length===3&&t.text&&t.offsets&&t.exclude&&typeof t.text=="string")return t.text;n||(n="");var r=function(u){return u.indexOf(" ")>-1?JSON.stringify(u):u},i=function(u){return n+u},s=[];if(t.text){var o=[];typeof t.text=="string"?o.push(t.text):o.push.apply(o,t.text),o.length>0&&s.push(o.map(r).map(i).join(" "))}return e.keywords&&e.keywords.forEach(function(u){if(t[u]){var a=[];typeof t[u]=="string"?a.push(t[u]):a.push.apply(a,t[u]),a.length>0&&s.push(i(u+":"+a.map(r).join(",")))}}),e.ranges&&e.ranges.forEach(function(u){if(t[u]){var a=t[u].from,l=t[u].to;l&&(a=a+"-"+l),a&&s.push(i(u+":"+a))}}),t.exclude&&Object.keys(t.exclude).length>0&&s.push(Ee.stringify(t.exclude,e,"-")),s.join(" ")}});var An=Fe((Lu,Pn)=>{Pn.exports=En()});function Xn(t){let e=atob(t),n=e.length,r=new Uint8Array(n);for(let i=0;i(...n)=>{let r=this.prefix?[this.prefix,...n]:n;this.originalConsole[e](...r),this.captureLog(e,n)};console.log=t("log"),console.info=t("info"),console.warn=t("warn"),console.error=t("error"),console.debug=t("debug")}captureLog(t,e){let n={level:t,timestamp:Date.now(),message:e.map(r=>{if(typeof r=="string")return r;try{return JSON.stringify(r)}catch{return String(r)}}).join(" ")};this.logBuffer.push(n),this.logBuffer.length>this.maxCaptureSize&&this.logBuffer.shift()}async postToServer(t,e){if(this.logBuffer.length>0){let r=[...this.logBuffer];this.logBuffer=[];try{if(!(await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r.map(s=>({...s,source:e})))})).ok)throw new Error("Failed to post logs to server")}catch(i){console.warn("Could not post logs to server",i.message),this.logBuffer.unshift(...r)}}}},Pt;function Qn(t=""){return Pt=new Jn(t),Pt}var Z=t=>{throw new Error("Not initialized yet")},Ie=typeof window>"u"&&typeof globalThis.WebSocketPair>"u",De=new Map,Me=0;Ie&&(globalThis.syscall=async(t,...e)=>await new Promise((n,r)=>{Me++,De.set(Me,{resolve:n,reject:r}),Z({type:"sys",id:Me,name:t,args:e})}));function St(t,e,n){Ie&&(Z=n,self.addEventListener("message",r=>{(async()=>{let i=r.data;switch(i.type){case"inv":{let s=t[i.name];if(!s)throw new Error(`Function not loaded: ${i.name}`);try{let o=await Promise.resolve(s(...i.args||[]));Z({type:"invr",id:i.id,result:o})}catch(o){console.error("An exception was thrown as a result of invoking function",i.name,"error:",o.message),Z({type:"invr",id:i.id,error:o.message})}}break;case"sysr":{let s=i.id,o=De.get(s);if(!o)throw Error("Invalid request id");De.delete(s),i.error?o.reject(new Error(i.error)):o.resolve(i.result)}break}})().catch(console.error)}),Z({type:"manifest",manifest:e}),Qn(`[${e.name} plug]`))}async function Zn(t,e){if(typeof t!="string"){let n=new Uint8Array(await t.arrayBuffer()),r=n.length>0?At(n):void 0;e={method:t.method,headers:Object.fromEntries(t.headers.entries()),base64Body:r},t=t.url}return syscall("sandboxFetch.fetch",t,e)}globalThis.nativeFetch=globalThis.fetch;function Yn(){globalThis.fetch=async(t,e)=>{let n=e?.body?At(new Uint8Array(await new Response(e.body).arrayBuffer())):void 0,r=await Zn(t,e&&{method:e.method,headers:e.headers,base64Body:n});return new Response(r.base64Body?Xn(r.base64Body):null,{status:r.status,headers:r.headers})}}Ie&&Yn();var C={};Q(C,{alert:()=>Lr,configureVimMode:()=>ei,confirm:()=>Rr,copyToClipboard:()=>Hr,deleteLine:()=>Xr,dispatch:()=>zr,downloadFile:()=>kr,filterBox:()=>Pr,flashNotification:()=>Er,fold:()=>$r,foldAll:()=>Ur,getCurrentEditor:()=>ir,getCurrentPage:()=>er,getCurrentPageMeta:()=>tr,getCurrentPath:()=>nr,getCursor:()=>ur,getRecentlyOpenedPages:()=>rr,getSelection:()=>lr,getText:()=>sr,getUiOption:()=>jr,goHistory:()=>br,hidePanel:()=>Sr,insertAtCursor:()=>Ir,insertAtPos:()=>Cr,invokeCommand:()=>cr,isMobile:()=>ni,moveCursor:()=>Mr,moveCursorToLine:()=>Dr,moveLineDown:()=>Zr,moveLineUp:()=>Qr,navigate:()=>pr,newWindow:()=>wr,openCommandPalette:()=>dr,openPageNavigator:()=>hr,openSearchPanel:()=>Kr,openUrl:()=>xr,prompt:()=>Or,rebuildEditorState:()=>vr,redo:()=>Gr,reloadConfigAndCommands:()=>gr,reloadPage:()=>mr,reloadUI:()=>yr,replaceRange:()=>Fr,save:()=>fr,sendMessage:()=>ti,setSelection:()=>ar,setText:()=>or,setUiOption:()=>Nr,showPanel:()=>Ar,showProgress:()=>Tr,toggleComment:()=>Jr,toggleFold:()=>Br,undo:()=>qr,unfold:()=>Wr,unfoldAll:()=>Vr,uploadFile:()=>_r,vimEx:()=>Yr});typeof globalThis.syscall>"u"&&(globalThis.syscall=()=>{throw new Error("Not implemented here")});function c(t,...e){return globalThis.syscall(t,...e)}function er(){return c("editor.getCurrentPage")}function tr(){return c("editor.getCurrentPageMeta")}function nr(){return c("editor.getCurrentPath")}function rr(){return c("editor.getRecentlyOpenedPages")}function ir(){return c("editor.getCurrentEditor")}function sr(){return c("editor.getText")}function or(t,e=!1){return c("editor.setText",t,e)}function ur(){return c("editor.getCursor")}function lr(){return c("editor.getSelection")}function ar(t,e){return c("editor.setSelection",t,e)}function cr(t,e){return c("editor.invokeCommand",t,e)}function fr(){return c("editor.save")}function pr(t,e=!1,n=!1){return c("editor.navigate",t,e,n)}function hr(t="page"){return c("editor.openPageNavigator",t)}function dr(){return c("editor.openCommandPalette")}function mr(){return c("editor.reloadPage")}function yr(){return c("editor.reloadUI")}function vr(){return c("editor.rebuildEditorState")}function gr(){return c("editor.reloadConfigAndCommands")}function xr(t,e=!1){return c("editor.openUrl",t,e)}function wr(){return c("editor.newWindow")}function br(t){return c("editor.goHistory",t)}function kr(t,e){return c("editor.downloadFile",t,e)}function _r(t,e){return c("editor.uploadFile",t,e)}function Er(t,e="info"){return c("editor.flashNotification",t,e)}function Pr(t,e,n="",r=""){return c("editor.filterBox",t,e,n,r)}function Ar(t,e,n,r=""){return c("editor.showPanel",t,e,n,r)}function Sr(t){return c("editor.hidePanel",t)}function Tr(t,e){return c("editor.showProgress",t,e)}function Cr(t,e){return c("editor.insertAtPos",t,e)}function Fr(t,e,n){return c("editor.replaceRange",t,e,n)}function Mr(t,e=!1){return c("editor.moveCursor",t,e)}function Dr(t,e=1,n=!1){return c("editor.moveCursorToLine",t,e,n)}function Ir(t,e=!1,n=!1){return c("editor.insertAtCursor",t,e,n)}function zr(t){return c("editor.dispatch",t)}function Or(t,e=""){return c("editor.prompt",t,e)}function Rr(t){return c("editor.confirm",t)}function Lr(t){return c("editor.alert",t)}function jr(t){return c("editor.getUiOption",t)}function Nr(t,e){return c("editor.setUiOption",t,e)}function $r(){return c("editor.fold")}function Wr(){return c("editor.unfold")}function Br(){return c("editor.toggleFold")}function Ur(){return c("editor.foldAll")}function Vr(){return c("editor.unfoldAll")}function qr(){return c("editor.undo")}function Gr(){return c("editor.redo")}function Kr(){return c("editor.openSearchPanel")}function Hr(t){return c("editor.copyToClipboard",t)}function Xr(){return c("editor.deleteLine")}function Jr(){return c("editor.toggleComment")}function Qr(){return c("editor.moveLineUp")}function Zr(){return c("editor.moveLineDown")}function Yr(t){return c("editor.vimEx",t)}function ei(){return c("editor.configureVimMode")}function ti(t,e){return c("editor.sendMessage",t,e)}function ni(){return c("editor.isMobile")}var D={};Q(D,{deleteDocument:()=>yi,deleteFile:()=>_i,deletePage:()=>ci,fileExists:()=>Ei,getDocumentMeta:()=>hi,getFileMeta:()=>bi,getPageMeta:()=>si,listDocuments:()=>pi,listFiles:()=>vi,listPages:()=>ii,listPlugs:()=>fi,pageExists:()=>oi,readDocument:()=>di,readFile:()=>gi,readFileWithMeta:()=>wi,readPage:()=>ui,readPageWithMeta:()=>li,readRef:()=>xi,writeDocument:()=>mi,writeFile:()=>ki,writePage:()=>ai});function ii(){return c("space.listPages")}function si(t){return c("space.getPageMeta",t)}function oi(t){return c("space.pageExists",t)}function ui(t){return c("space.readPage",t)}function li(t){return c("space.readPageWithMeta",t)}function ai(t,e){return c("space.writePage",t,e)}function ci(t){return c("space.deletePage",t)}function fi(){return c("space.listPlugs")}function pi(){return c("space.listDocuments")}function hi(t){return c("space.getDocumentMeta",t)}function di(t){return c("space.readDocument",t)}function mi(t,e){return c("space.writeDocument",t,e)}function yi(t){return c("space.deleteDocument",t)}function vi(){return c("space.listFiles")}function gi(t){return c("space.readFile",t)}function xi(t){return c("space.readRef",t)}function wi(t){return c("space.readFileWithMeta",t)}function bi(t){return c("space.getFileMeta",t)}function ki(t,e){return c("space.writeFile",t,e)}function _i(t){return c("space.deleteFile",t)}function Ei(t){return c("space.fileExists",t)}var ae={};Q(ae,{cleanDatabases:()=>Ri,getBaseURI:()=>Di,getConfig:()=>zi,getMode:()=>Fi,getURLPrefix:()=>Mi,getVersion:()=>Ii,invokeCommand:()=>Ai,invokeFunction:()=>Pi,listCommands:()=>Si,listSyscalls:()=>Ti,reloadPlugs:()=>Ci,wipeClient:()=>Oi});function Pi(t,...e){return c("system.invokeFunction",t,...e)}function Ai(t,e){return c("system.invokeCommand",t,e)}function Si(){return c("system.listCommands")}function Ti(){return c("system.listSyscalls")}function Ci(){return c("system.reloadPlugs")}function Fi(){return c("system.getMode")}function Mi(){return c("system.getURLPrefix")}function Di(){return c("system.getBaseURI")}function Ii(){return c("system.getVersion")}function zi(t,e=void 0){return c("system.getConfig",t,e)}function Oi(t=!1){return c("system.wipeClient",t)}function Ri(){return c("system.cleanDatabases")}var F={};Q(F,{del:()=>Ni,get:()=>ji,set:()=>Li});function Li(t,e){return c("clientStore.set",t,e)}function ji(t){return c("clientStore.get",t)}function Ni(t){return c("clientStore.delete",t)}var po=new Uint8Array(16);var ce={};Q(ce,{dispatchEvent:()=>Vi,listEvents:()=>qi});function Vi(t,e,n){return new Promise((r,i)=>{let s=-1;n&&(s=setTimeout(()=>{console.log("Timeout!"),i("timeout")},n)),c("event.dispatch",t,e).then(o=>{s!==-1&&clearTimeout(s),r(o)}).catch(i)})}function qi(){return c("event.listEvents")}var ze='var vl=Object.defineProperty;var fi=P=>{throw TypeError(P)};var pl=(P,D,F)=>D in P?vl(P,D,{enumerable:!0,configurable:!0,writable:!0,value:F}):P[D]=F;var ge=(P,D,F)=>pl(P,typeof D!="symbol"?D+"":D,F),Kn=(P,D,F)=>D.has(P)||fi("Cannot "+F);var u=(P,D,F)=>(Kn(P,D,"read from private field"),F?F.call(P):D.get(P)),T=(P,D,F)=>D.has(P)?fi("Cannot add the same private member more than once"):D instanceof WeakSet?D.add(P):D.set(P,F),y=(P,D,F,ke)=>(Kn(P,D,"write to private field"),ke?ke.call(P,F):D.set(P,F),F),N=(P,D,F)=>(Kn(P,D,"access private method"),F);(function(){"use strict";var Et,xt,Tt,St,Wt,Ue,kt,Ve,be,At,B,Yn,Wn,Gn,Qn,ai,fe,Vn,Ee,st,Q,xe,$,pe,Pe,lt,He,Mt,Dt,Pt,Ce,an,O,_l,gl,ml,Xn,cn,dn,Zn,li,_e,Te,ee,ot,Gt,Qt,un,oi;var D=Array.isArray,F=Array.prototype.indexOf,ke=Array.prototype.includes,Jt=Array.from,ui=Object.defineProperty,Ct=Object.getOwnPropertyDescriptor,ci=Object.getOwnPropertyDescriptors,di=Object.prototype,hi=Array.prototype,Jn=Object.getPrototypeOf,$n=Object.isExtensible;const It=()=>{};function vi(e){return e()}function hn(e){for(var t=0;t{e=r,t=i});return{promise:n,resolve:e,reject:t}}const q=2,ct=4,Rt=8,tr=1<<24,Ie=16,ae=32,Be=64,vn=128,re=512,L=1024,z=2048,ue=4096,X=8192,ce=16384,Ke=32768,pn=1<<25,Ye=65536,nr=1<<17,pi=1<<18,dt=1<<19,rr=1<<20,Ae=1<<25,We=65536,_n=1<<21,gn=1<<22,Re=1<<23,ht=Symbol("$state"),Me=new class extends Error{constructor(){super(...arguments);ge(this,"name","StaleReactionError");ge(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};function ir(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function _i(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function gi(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function mi(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function wi(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function yi(e){throw new Error("https://svelte.dev/e/effect_orphan")}function bi(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Ei(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function xi(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Ti(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Si(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const ki=1,Ai=2,Mi=16,Di=1,Pi=2,V=Symbol(),Ci="http://www.w3.org/1999/xhtml",Ii="http://www.w3.org/2000/svg",Ri="http://www.w3.org/1998/Math/MathML";function Ni(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}function sr(e){return e===this.v}function Fi(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function lr(e){return!Fi(e,this.v)}let Nt=!1,wl=!1;function Oi(){Nt=!0}let M=null;function vt(e){M=e}function or(e){return qi().get(e)}function pt(e,t=!1,n){M={p:M,i:!1,c:null,e:null,s:e,x:null,r:S,l:Nt&&!t?{s:null,u:null,$:[]}:null}}function _t(e){var t=M,n=t.e;if(n!==null){t.e=null;for(var r of n)Rr(r)}return t.i=!0,M=t.p,{}}function Ft(){return!Nt||M!==null&&M.l===null}function qi(e){return M===null&&ir(),M.c??(M.c=new Map(Li(M)||void 0))}function Li(e){let t=e.p;for(;t!==null;){const n=t.c;if(n!==null)return n;t=t.p}return null}let Ge=[];function fr(){var e=Ge;Ge=[],hn(e)}function Qe(e){if(Ge.length===0&&!qt){var t=Ge;queueMicrotask(()=>{t===Ge&&fr()})}Ge.push(e)}function ji(){for(;Ge.length>0;)fr()}function ar(e){var t=S;if(t===null)return b.f|=Re,e;if((t.f&Ke)===0&&(t.f&ct)===0)throw e;Ne(e,t)}function Ne(e,t){for(;t!==null;){if((t.f&vn)!==0){if((t.f&Ke)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}const Ui=-7169;function I(e,t){e.f=e.f&Ui|t}function mn(e){(e.f&re)!==0||e.deps===null?I(e,L):I(e,ue)}function ur(e){if(e!==null)for(const t of e)(t.f&q)===0||(t.f&We)===0||(t.f^=We,ur(t.deps))}function cr(e,t,n){(e.f&z)!==0?t.add(e):(e.f&ue)!==0&&n.add(e),ur(e.deps),I(e,L)}const Ot=new Set;let m=null,H=null,wn=null,qt=!1,yn=!1,gt=null,$t=null;var dr=0;let Vi=1;const fn=class fn{constructor(){T(this,B);ge(this,"id",Vi++);ge(this,"current",new Map);ge(this,"previous",new Map);T(this,Et,new Set);T(this,xt,new Set);T(this,Tt,0);T(this,St,0);T(this,Wt,null);T(this,Ue,[]);T(this,kt,new Set);T(this,Ve,new Set);T(this,be,new Map);ge(this,"is_fork",!1);T(this,At,!1)}skip_effect(t){u(this,be).has(t)||u(this,be).set(t,{d:[],m:[]})}unskip_effect(t){var n=u(this,be).get(t);if(n){u(this,be).delete(t);for(var r of n.d)I(r,z),this.schedule(r);for(r of n.m)I(r,ue),this.schedule(r)}}capture(t,n){n!==V&&!this.previous.has(t)&&this.previous.set(t,n),(t.f&Re)===0&&(this.current.set(t,t.v),H==null||H.set(t,t.v))}activate(){m=this}deactivate(){m=null,H=null}flush(){try{if(yn=!0,m=this,!N(this,B,Yn).call(this)){for(const t of u(this,kt))u(this,Ve).delete(t),I(t,z),this.schedule(t);for(const t of u(this,Ve))I(t,ue),this.schedule(t)}N(this,B,Wn).call(this)}finally{dr=0,wn=null,gt=null,$t=null,yn=!1,m=null,H=null,Fe.clear()}}discard(){for(const t of u(this,xt))t(this);u(this,xt).clear()}increment(t){y(this,Tt,u(this,Tt)+1),t&&y(this,St,u(this,St)+1)}decrement(t,n){y(this,Tt,u(this,Tt)-1),t&&y(this,St,u(this,St)-1),!(u(this,At)||n)&&(y(this,At,!0),Qe(()=>{y(this,At,!1),this.flush()}))}oncommit(t){u(this,Et).add(t)}ondiscard(t){u(this,xt).add(t)}settled(){return(u(this,Wt)??y(this,Wt,er())).promise}static ensure(){if(m===null){const t=m=new fn;yn||(Ot.add(m),qt||Qe(()=>{m===t&&t.flush()}))}return m}apply(){}schedule(t){var i;if(wn=t,(i=t.b)!=null&&i.is_pending&&(t.f&(ct|Rt|tr))!==0&&(t.f&Ke)===0){t.b.defer_effect(t);return}for(var n=t;n.parent!==null;){n=n.parent;var r=n.f;if(gt!==null&&n===S&&(b===null||(b.f&q)===0))return;if((r&(Be|ae))!==0){if((r&L)===0)return;n.f^=L}}u(this,Ue).push(n)}};Et=new WeakMap,xt=new WeakMap,Tt=new WeakMap,St=new WeakMap,Wt=new WeakMap,Ue=new WeakMap,kt=new WeakMap,Ve=new WeakMap,be=new WeakMap,At=new WeakMap,B=new WeakSet,Yn=function(){return this.is_fork||u(this,St)>0},Wn=function(){var l,o;dr++>1e3&&zi();const t=u(this,Ue);y(this,Ue,[]),this.apply();var n=gt=[],r=[],i=$t=[];for(const a of t)N(this,B,Gn).call(this,a,n,r);if(m=null,i.length>0){var s=fn.ensure();for(const a of i)s.schedule(a)}if(gt=null,$t=null,N(this,B,Yn).call(this)){N(this,B,Qn).call(this,r),N(this,B,Qn).call(this,n);for(const[a,c]of u(this,be))_r(a,c)}else{u(this,kt).clear(),u(this,Ve).clear();for(const a of u(this,Et))a(this);u(this,Et).clear(),hr(r),hr(n),u(this,Tt)===0&&N(this,B,ai).call(this),(l=u(this,Wt))==null||l.resolve()}var f=m;f!==null&&(Ot.add(f),N(o=f,B,Wn).call(o))},Gn=function(t,n,r){t.f^=L;for(var i=t.first;i!==null;){var s=i.f,f=(s&(ae|Be))!==0,l=f&&(s&L)!==0,o=l||(s&X)!==0||u(this,be).has(i);if(!o&&i.fn!==null){f?i.f^=L:(s&ct)!==0?n.push(i):Vt(i)&&((s&Ie)!==0&&u(this,Ve).add(i),yt(i));var a=i.first;if(a!==null){i=a;continue}}for(;i!==null;){var c=i.next;if(c!==null){i=c;break}i=i.parent}}},Qn=function(t){for(var n=0;n1){this.previous.clear();var t=m,n=H,r=!0;for(const s of Ot){if(s===this){r=!1;continue}const f=[];for(const[o,a]of this.current){if(s.current.has(o))if(r&&a!==s.current.get(o))s.current.set(o,a);else continue;f.push(o)}if(f.length===0)continue;const l=[...s.current.keys()].filter(o=>!this.current.has(o));if(l.length>0){s.activate();const o=new Set,a=new Map;for(const c of f)vr(c,l,o,a);if(u(s,Ue).length>0){s.apply();for(const c of u(s,Ue))N(i=s,B,Gn).call(i,c,[],[])}s.deactivate()}}m=t,H=n}u(this,be).clear(),Ot.delete(this)};let Xe=fn;function Hi(e){var t=qt;qt=!0;try{for(var n;;){if(ji(),m===null)return n;m.flush()}}finally{qt=t}}function zi(){try{bi()}catch(e){Ne(e,wn)}}let de=null;function hr(e){var t=e.length;if(t!==0){for(var n=0;n0)){Fe.clear();for(const i of de){if((i.f&(ce|X))!==0)continue;const s=[i];let f=i.parent;for(;f!==null;)de.has(f)&&(de.delete(f),s.push(f)),f=f.parent;for(let l=s.length-1;l>=0;l--){const o=s[l];(o.f&(ce|X))===0&&yt(o)}}de.clear()}}de=null}}function vr(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const i of e.reactions){const s=i.f;(s&q)!==0?vr(i,t,n,r):(s&(gn|Ie))!==0&&(s&z)===0&&pr(i,t,r)&&(I(i,z),bn(i))}}function pr(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const i of e.deps){if(ke.call(t,i))return!0;if((i.f&q)!==0&&pr(i,t,n))return n.set(i,!0),!0}return n.set(e,!1),!1}function bn(e){m.schedule(e)}function _r(e,t){if(!((e.f&ae)!==0&&(e.f&L)!==0)){(e.f&z)!==0?t.d.push(e):(e.f&ue)!==0&&t.m.push(e),I(e,L);for(var n=e.first;n!==null;)_r(n,t),n=n.next}}function Bi(e){let t=0,n=Ze(0),r;return()=>{Sn()&&(E(n),kn(()=>(t===0&&(r=zt(()=>e(()=>Lt(n)))),t+=1,()=>{Qe(()=>{t-=1,t===0&&(r==null||r(),r=void 0,Lt(n))})})))}}var Ki=Ye|dt;function Yi(e,t,n,r){new Wi(e,t,n,r)}class Wi{constructor(t,n,r,i){T(this,O);ge(this,"parent");ge(this,"is_pending",!1);ge(this,"transform_error");T(this,fe);T(this,Vn,null);T(this,Ee);T(this,st);T(this,Q);T(this,xe,null);T(this,$,null);T(this,pe,null);T(this,Pe,null);T(this,lt,0);T(this,He,0);T(this,Mt,!1);T(this,Dt,new Set);T(this,Pt,new Set);T(this,Ce,null);T(this,an,Bi(()=>(y(this,Ce,Ze(u(this,lt))),()=>{y(this,Ce,null)})));var s;y(this,fe,t),y(this,Ee,n),y(this,st,f=>{var l=S;l.b=this,l.f|=vn,r(f)}),this.parent=S.b,this.transform_error=i??((s=this.parent)==null?void 0:s.transform_error)??(f=>f),y(this,Q,rn(()=>{N(this,O,Xn).call(this)},Ki))}defer_effect(t){cr(t,u(this,Dt),u(this,Pt))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!u(this,Ee).pending}update_pending_count(t,n){N(this,O,Zn).call(this,t,n),y(this,lt,u(this,lt)+t),!(!u(this,Ce)||u(this,Mt))&&(y(this,Mt,!0),Qe(()=>{y(this,Mt,!1),u(this,Ce)&&mt(u(this,Ce),u(this,lt))}))}get_effect_pending(){return u(this,an).call(this),E(u(this,Ce))}error(t){var n=u(this,Ee).onerror;let r=u(this,Ee).failed;if(!n&&!r)throw t;u(this,xe)&&(W(u(this,xe)),y(this,xe,null)),u(this,$)&&(W(u(this,$)),y(this,$,null)),u(this,pe)&&(W(u(this,pe)),y(this,pe,null));var i=!1,s=!1;const f=()=>{if(i){Ni();return}i=!0,s&&Si(),u(this,pe)!==null&&$e(u(this,pe),()=>{y(this,pe,null)}),N(this,O,dn).call(this,()=>{N(this,O,Xn).call(this)})},l=o=>{try{s=!0,n==null||n(o,f),s=!1}catch(a){Ne(a,u(this,Q)&&u(this,Q).parent)}r&&y(this,pe,N(this,O,dn).call(this,()=>{try{return ie(()=>{var a=S;a.b=this,a.f|=vn,r(u(this,fe),()=>o,()=>f)})}catch(a){return Ne(a,u(this,Q).parent),null}}))};Qe(()=>{var o;try{o=this.transform_error(t)}catch(a){Ne(a,u(this,Q)&&u(this,Q).parent);return}o!==null&&typeof o=="object"&&typeof o.then=="function"?o.then(l,a=>Ne(a,u(this,Q)&&u(this,Q).parent)):l(o)})}}fe=new WeakMap,Vn=new WeakMap,Ee=new WeakMap,st=new WeakMap,Q=new WeakMap,xe=new WeakMap,$=new WeakMap,pe=new WeakMap,Pe=new WeakMap,lt=new WeakMap,He=new WeakMap,Mt=new WeakMap,Dt=new WeakMap,Pt=new WeakMap,Ce=new WeakMap,an=new WeakMap,O=new WeakSet,_l=function(){try{y(this,xe,ie(()=>u(this,st).call(this,u(this,fe))))}catch(t){this.error(t)}},gl=function(t){const n=u(this,Ee).failed;n&&y(this,pe,ie(()=>{n(u(this,fe),()=>t,()=>()=>{})}))},ml=function(){const t=u(this,Ee).pending;if(t){this.is_pending=!0,y(this,$,ie(()=>t(u(this,fe))));var n=m;Qe(()=>{var r=y(this,Pe,document.createDocumentFragment()),i=Oe();r.append(i),y(this,xe,N(this,O,dn).call(this,()=>ie(()=>u(this,st).call(this,i)))),u(this,He)===0&&(u(this,fe).before(r),y(this,Pe,null),$e(u(this,$),()=>{y(this,$,null)}),N(this,O,cn).call(this,n))})}},Xn=function(){var t=m;try{if(this.is_pending=this.has_pending_snippet(),y(this,He,0),y(this,lt,0),y(this,xe,ie(()=>{u(this,st).call(this,u(this,fe))})),u(this,He)>0){var n=y(this,Pe,document.createDocumentFragment());Dn(u(this,xe),n);const r=u(this,Ee).pending;y(this,$,ie(()=>r(u(this,fe))))}else N(this,O,cn).call(this,t)}catch(r){this.error(r)}},cn=function(t){this.is_pending=!1;for(const n of u(this,Dt))I(n,z),t.schedule(n);for(const n of u(this,Pt))I(n,ue),t.schedule(n);u(this,Dt).clear(),u(this,Pt).clear()},dn=function(t){var n=S,r=b,i=M;ye(u(this,Q)),se(u(this,Q)),vt(u(this,Q).ctx);try{return Xe.ensure(),t()}catch(s){return ar(s),null}finally{ye(n),se(r),vt(i)}},Zn=function(t,n){var r;if(!this.has_pending_snippet()){this.parent&&N(r=this.parent,O,Zn).call(r,t,n);return}y(this,He,u(this,He)+t),u(this,He)===0&&(N(this,O,cn).call(this,n),u(this,$)&&$e(u(this,$),()=>{y(this,$,null)}),u(this,Pe)&&(u(this,fe).before(u(this,Pe)),y(this,Pe,null)))};function Gi(e,t,n,r){const i=Ft()?tn:wr;var s=e.filter(v=>!v.settled);if(n.length===0&&s.length===0){r(t.map(i));return}var f=S,l=Qi(),o=s.length===1?s[0].promise:s.length>1?Promise.all(s.map(v=>v.promise)):null;function a(v){l();try{r(v)}catch(p){(f.f&ce)===0&&Ne(p,f)}en()}if(n.length===0){o.then(()=>a(t.map(i)));return}var c=gr();function h(){Promise.all(n.map(v=>Xi(v))).then(v=>a([...t.map(i),...v])).catch(v=>Ne(v,f)).finally(()=>c())}o?o.then(()=>{l(),h(),en()}):h()}function Qi(){var e=S,t=b,n=M,r=m;return function(s=!0){ye(e),se(t),vt(n),s&&(e.f&ce)===0&&(r==null||r.activate(),r==null||r.apply())}}function en(e=!0){ye(null),se(null),vt(null),e&&(m==null||m.deactivate())}function gr(){var e=S.b,t=m,n=e.is_rendered();return e.update_pending_count(1,t),t.increment(n),(r=!1)=>{e.update_pending_count(-1,t),t.decrement(n,r)}}function tn(e){var t=q|z,n=b!==null&&(b.f&q)!==0?b:null;return S!==null&&(S.f|=dt),{ctx:M,deps:null,effects:null,equals:sr,f:t,fn:e,reactions:null,rv:0,v:V,wv:0,parent:n??S,ac:null}}function Xi(e,t,n){let r=S;r===null&&_i();var i=void 0,s=Ze(V),f=!b,l=new Map;return us(()=>{var p;var o=S,a=er();i=a.promise;try{Promise.resolve(e()).then(a.resolve,a.reject).finally(en)}catch(d){a.reject(d),en()}var c=m;if(f){if((o.f&Ke)!==0)var h=gr();if(r.b.is_rendered())(p=l.get(c))==null||p.reject(Me),l.delete(c);else{for(const d of l.values())d.reject(Me);l.clear()}l.set(c,a)}const v=(d,_=void 0)=>{if(h){var x=_===Me;h(x)}if(!(_===Me||(o.f&ce)!==0)){if(c.activate(),_)s.f|=Re,mt(s,_);else{(s.f&Re)!==0&&(s.f^=Re),mt(s,d);for(const[w,A]of l){if(l.delete(w),w===c)break;A.reject(Me)}}c.deactivate()}};a.promise.then(v,d=>v(null,d||"unknown"))}),Ir(()=>{for(const o of l.values())o.reject(Me)}),new Promise(o=>{function a(c){function h(){c===i?o(s):a(i)}c.then(h,h)}a(i)})}function mr(e){const t=tn(e);return Ur(t),t}function wr(e){const t=tn(e);return t.equals=lr,t}function Zi(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n0&&!Er&&ts()}return t}function ts(){Er=!1;for(const e of xn)(e.f&L)!==0&&I(e,ue),Vt(e)&&yt(e);xn.clear()}function xr(e,t=1){var n=E(e),r=t===1?n++:n--;return j(e,n),r}function Lt(e){j(e,e.v+1)}function Tr(e,t,n){var r=e.reactions;if(r!==null)for(var i=Ft(),s=r.length,f=0;f{if(nt===s)return l();var o=b,a=nt;se(null),Hr(s);var c=l();return se(o),Hr(a),c};return r&&n.set("length",Z(e.length)),new Proxy(e,{defineProperty(l,o,a){(!("value"in a)||a.configurable===!1||a.enumerable===!1||a.writable===!1)&&Ei();var c=n.get(o);return c===void 0?f(()=>{var h=Z(a.value);return n.set(o,h),h}):j(c,a.value,!0),!0},deleteProperty(l,o){var a=n.get(o);if(a===void 0){if(o in l){const c=f(()=>Z(V));n.set(o,c),Lt(i)}}else j(a,V),Lt(i);return!0},get(l,o,a){var p;if(o===ht)return e;var c=n.get(o),h=o in l;if(c===void 0&&(!h||(p=Ct(l,o))!=null&&p.writable)&&(c=f(()=>{var d=Je(h?l[o]:V),_=Z(d);return _}),n.set(o,c)),c!==void 0){var v=E(c);return v===V?void 0:v}return Reflect.get(l,o,a)},getOwnPropertyDescriptor(l,o){var a=Reflect.getOwnPropertyDescriptor(l,o);if(a&&"value"in a){var c=n.get(o);c&&(a.value=E(c))}else if(a===void 0){var h=n.get(o),v=h==null?void 0:h.v;if(h!==void 0&&v!==V)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return a},has(l,o){var v;if(o===ht)return!0;var a=n.get(o),c=a!==void 0&&a.v!==V||Reflect.has(l,o);if(a!==void 0||S!==null&&(!c||(v=Ct(l,o))!=null&&v.writable)){a===void 0&&(a=f(()=>{var p=c?Je(l[o]):V,d=Z(p);return d}),n.set(o,a));var h=E(a);if(h===V)return!1}return c},set(l,o,a,c){var U;var h=n.get(o),v=o in l;if(r&&o==="length")for(var p=a;pZ(V)),n.set(p+"",d))}if(h===void 0)(!v||(U=Ct(l,o))!=null&&U.writable)&&(h=f(()=>Z(void 0)),j(h,Je(a)),n.set(o,h));else{v=h.v!==V;var _=f(()=>Je(a));j(h,_)}var x=Reflect.getOwnPropertyDescriptor(l,o);if(x!=null&&x.set&&x.set.call(c,a),!v){if(r&&typeof o=="string"){var w=n.get("length"),A=Number(o);Number.isInteger(A)&&A>=w.v&&j(w,A+1)}Lt(i)}return!0},ownKeys(l){E(i);var o=Reflect.ownKeys(l).filter(h=>{var v=n.get(h);return v===void 0||v.v!==V});for(var[a,c]of n)c.v!==V&&!(a in l)&&o.push(a);return o},setPrototypeOf(){xi()}})}var Tn,Sr,kr,Ar;function ns(){if(Tn===void 0){Tn=window,Sr=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;kr=Ct(t,"firstChild").get,Ar=Ct(t,"nextSibling").get,$n(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),$n(n)&&(n.__t=void 0)}}function Oe(e=""){return document.createTextNode(e)}function qe(e){return kr.call(e)}function jt(e){return Ar.call(e)}function K(e,t){return qe(e)}function me(e,t=!1){{var n=qe(e);return n instanceof Comment&&n.data===""?jt(n):n}}function he(e,t=1,n=!1){let r=e;for(;t--;)r=jt(r);return r}function rs(e){e.textContent=""}function Mr(){return!1}function Dr(e,t,n){return document.createElementNS(t??Ci,e,void 0)}let Pr=!1;function is(){Pr||(Pr=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{var t;if(!e.defaultPrevented)for(const n of e.target.elements)(t=n.__on_r)==null||t.call(n)})},{capture:!0}))}function nn(e){var t=b,n=S;se(null),ye(null);try{return e()}finally{se(t),ye(n)}}function ss(e,t,n,r=n){e.addEventListener(t,()=>nn(n));const i=e.__on_r;i?e.__on_r=()=>{i(),r(!0)}:e.__on_r=()=>r(!0),is()}function Cr(e){S===null&&(b===null&&yi(),wi()),et&&mi()}function ls(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function we(e,t){var n=S;n!==null&&(n.f&X)!==0&&(e|=X);var r={ctx:M,deps:null,nodes:null,f:e|z|re,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null},i=r;if((e&ct)!==0)gt!==null?gt.push(r):Xe.ensure().schedule(r);else if(t!==null){try{yt(r)}catch(f){throw W(r),f}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&(i.f&dt)===0&&(i=i.first,(e&Ie)!==0&&(e&Ye)!==0&&i!==null&&(i.f|=Ye))}if(i!==null&&(i.parent=n,n!==null&&ls(i,n),b!==null&&(b.f&q)!==0&&(e&Be)===0)){var s=b;(s.effects??(s.effects=[])).push(i)}return r}function Sn(){return b!==null&&!ve}function Ir(e){const t=we(Rt,null);return I(t,L),t.teardown=e,t}function wt(e){Cr();var t=S.f,n=!b&&(t&ae)!==0&&(t&Ke)===0;if(n){var r=M;(r.e??(r.e=[])).push(e)}else return Rr(e)}function Rr(e){return we(ct|rr,e)}function os(e){return Cr(),we(Rt|rr,e)}function fs(e){Xe.ensure();const t=we(Be|dt,e);return(n={})=>new Promise(r=>{n.outro?$e(t,()=>{W(t),r(void 0)}):(W(t),r(void 0))})}function as(e){return we(ct,e)}function us(e){return we(gn|dt,e)}function kn(e,t=0){return we(Rt|t,e)}function Ut(e,t=[],n=[],r=[]){Gi(r,t,n,i=>{we(Rt,()=>e(...i.map(E)))})}function rn(e,t=0){var n=we(Ie|t,e);return n}function ie(e){return we(ae|dt,e)}function Nr(e){var t=e.teardown;if(t!==null){const n=et,r=b;jr(!0),se(null);try{t.call(null)}finally{jr(n),se(r)}}}function An(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const i=n.ac;i!==null&&nn(()=>{i.abort(Me)});var r=n.next;(n.f&Be)!==0?n.parent=null:W(n,t),n=r}}function cs(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&ae)===0&&W(t),t=n}}function W(e,t=!0){var n=!1;(t||(e.f&pi)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(Fr(e.nodes.start,e.nodes.end),n=!0),I(e,pn),An(e,t&&!n),Ht(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(const s of r)s.stop();Nr(e),e.f^=pn,e.f|=ce;var i=e.parent;i!==null&&i.first!==null&&Or(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=null}function Fr(e,t){for(;e!==null;){var n=e===t?null:jt(e);e.remove(),e=n}}function Or(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function $e(e,t,n=!0){var r=[];qr(e,r,!0);var i=()=>{n&&W(e),t&&t()},s=r.length;if(s>0){var f=()=>--s||i();for(var l of r)l.out(f)}else i()}function qr(e,t,n){if((e.f&X)===0){e.f^=X;var r=e.nodes&&e.nodes.t;if(r!==null)for(const l of r)(l.is_global||n)&&t.push(l);for(var i=e.first;i!==null;){var s=i.next,f=(i.f&Ye)!==0||(i.f&ae)!==0&&(e.f&Ie)!==0;qr(i,t,f?n:!1),i=s}}}function Mn(e){Lr(e,!0)}function Lr(e,t){if((e.f&X)!==0){e.f^=X,(e.f&L)===0&&(I(e,z),Xe.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=(n.f&Ye)!==0||(n.f&ae)!==0;Lr(n,i?t:!1),n=r}var s=e.nodes&&e.nodes.t;if(s!==null)for(const f of s)(f.is_global||t)&&f.in()}}function Dn(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:jt(n);t.append(n),n=i}}let sn=!1,et=!1;function jr(e){et=e}let b=null,ve=!1;function se(e){b=e}let S=null;function ye(e){S=e}let le=null;function Ur(e){b!==null&&(le===null?le=[e]:le.push(e))}let G=null,J=0,oe=null;function ds(e){oe=e}let Vr=1,tt=0,nt=tt;function Hr(e){nt=e}function zr(){return++Vr}function Vt(e){var t=e.f;if((t&z)!==0)return!0;if(t&q&&(e.f&=~We),(t&ue)!==0){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}(t&re)!==0&&H===null&&I(e,L)}return!1}function Br(e,t,n=!0){var r=e.reactions;if(r!==null&&!(le!==null&&ke.call(le,e)))for(var i=0;i{e.ac.abort(Me)}),e.ac=null);try{e.f|=_n;var c=e.fn,h=c();e.f|=Ke;var v=e.deps,p=m==null?void 0:m.is_fork;if(G!==null){var d;if(p||Ht(e,J),v!==null&&J>0)for(v.length=J+G.length,d=0;dn==null?void 0:n.call(this,s))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?Qe(()=>{t.addEventListener(e,i,r)}):t.addEventListener(e,i,r),i}function Xr(e,t,n,r,i){var s={capture:r,passive:i},f=gs(e,t,n,s);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&Ir(()=>{t.removeEventListener(e,f,s)})}function Bt(e,t,n){(t[rt]??(t[rt]={}))[e]=n}function In(e){for(var t=0;t{throw A});throw v}}finally{e[rt]=t,delete e.currentTarget,se(c),ye(h)}}}const Nn=((li=globalThis==null?void 0:globalThis.window)==null?void 0:li.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function ms(e){return(Nn==null?void 0:Nn.createHTML(e))??e}function ws(e){var t=Dr("template");return t.innerHTML=ms(e.replaceAll("","")),t.content}function bt(e,t){var n=S;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function Y(e,t){var n=(t&Di)!==0,r=(t&Pi)!==0,i,s=!e.startsWith("");return()=>{i===void 0&&(i=ws(s?e:""+e),n||(i=qe(i)));var f=r||Sr?document.importNode(i,!0):i.cloneNode(!0);if(n){var l=qe(f),o=f.lastChild;bt(l,o)}else bt(f,f);return f}}function ys(e=""){{var t=Oe(e+"");return bt(t,t),t}}function Le(){var e=document.createDocumentFragment(),t=document.createComment(""),n=Oe();return e.append(t,n),bt(t,n),e}function C(e,t){e!==null&&e.before(t)}function Fn(e,t){var n=t==null?"":typeof t=="object"?`${t}`:t;n!==(e.__t??(e.__t=e.nodeValue))&&(e.__t=n,e.nodeValue=`${n}`)}function bs(e,t){return Es(e,t)}const ln=new Map;function Es(e,{target:t,anchor:n,props:r={},events:i,context:s,intro:f=!0,transformError:l}){ns();var o=void 0,a=fs(()=>{var c=n??t.appendChild(Oe());Yi(c,{pending:()=>{}},p=>{pt({});var d=M;s&&(d.c=s),i&&(r.$$events=i),o=e(p,r)||{},_t()},l);var h=new Set,v=p=>{for(var d=0;d{var x;for(var p of h)for(const w of[t,document]){var d=ln.get(w),_=d.get(p);--_==0?(w.removeEventListener(p,Rn),d.delete(p),d.size===0&&ln.delete(w)):d.set(p,_)}Cn.delete(v),c!==n&&((x=c.parentNode)==null||x.removeChild(c))}});return xs.set(o,a),o}let xs=new WeakMap;class Jr{constructor(t,n=!0){ge(this,"anchor");T(this,_e,new Map);T(this,Te,new Map);T(this,ee,new Map);T(this,ot,new Set);T(this,Gt,!0);T(this,Qt,t=>{if(u(this,_e).has(t)){var n=u(this,_e).get(t),r=u(this,Te).get(n);if(r)Mn(r),u(this,ot).delete(n);else{var i=u(this,ee).get(n);i&&(u(this,Te).set(n,i.effect),u(this,ee).delete(n),i.fragment.lastChild.remove(),this.anchor.before(i.fragment),r=i.effect)}for(const[s,f]of u(this,_e)){if(u(this,_e).delete(s),s===t)break;const l=u(this,ee).get(f);l&&(W(l.effect),u(this,ee).delete(f))}for(const[s,f]of u(this,Te)){if(s===n||u(this,ot).has(s))continue;const l=()=>{if(Array.from(u(this,_e).values()).includes(s)){var a=document.createDocumentFragment();Dn(f,a),a.append(Oe()),u(this,ee).set(s,{effect:f,fragment:a})}else W(f);u(this,ot).delete(s),u(this,Te).delete(s)};u(this,Gt)||!r?(u(this,ot).add(s),$e(f,l,!1)):l()}}});T(this,un,t=>{u(this,_e).delete(t);const n=Array.from(u(this,_e).values());for(const[r,i]of u(this,ee))n.includes(r)||(W(i.effect),u(this,ee).delete(r))});this.anchor=t,y(this,Gt,n)}ensure(t,n){var r=m,i=Mr();if(n&&!u(this,Te).has(t)&&!u(this,ee).has(t))if(i){var s=document.createDocumentFragment(),f=Oe();s.append(f),u(this,ee).set(t,{effect:ie(()=>n(f)),fragment:s})}else u(this,Te).set(t,ie(()=>n(this.anchor)));if(u(this,_e).set(r,t),i){for(const[l,o]of u(this,Te))l===t?r.unskip_effect(o):r.skip_effect(o);for(const[l,o]of u(this,ee))l===t?r.unskip_effect(o.effect):r.skip_effect(o.effect);r.oncommit(u(this,Qt)),r.ondiscard(u(this,un))}else u(this,Qt).call(this,r)}}_e=new WeakMap,Te=new WeakMap,ee=new WeakMap,ot=new WeakMap,Gt=new WeakMap,Qt=new WeakMap,un=new WeakMap;function De(e,t,n=!1){var r=new Jr(e),i=n?Ye:0;function s(f,l){r.ensure(f,l)}rn(()=>{var f=!1;t((l,o=0)=>{f=!0,s(o,l)}),f||s(-1,null)},i)}function Ts(e,t){return t}function Ss(e,t,n){for(var r=[],i=t.length,s,f=t.length,l=0;l{if(s){if(s.pending.delete(h),s.done.add(h),s.pending.size===0){var v=e.outrogroups;On(e,Jt(s.done)),v.delete(s),v.size===0&&(e.outrogroups=null)}}else f-=1},!1)}if(f===0){var o=r.length===0&&n!==null;if(o){var a=n,c=a.parentNode;rs(c),c.append(a),e.items.clear()}On(e,t,!o)}else s={pending:new Set(t),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(s)}function On(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(const f of e.pending.values())for(const l of f)r.add(e.items.get(l).e)}for(var i=0;i{var w=n();return D(w)?w:w==null?[]:Jt(w)}),c,h=new Map,v=!0;function p(w){(x.effect.f&ce)===0&&(x.pending.delete(w),x.fallback=o,As(x,c,f,t,r),o!==null&&(c.length===0?(o.f&Ae)===0?Mn(o):(o.f^=Ae,Yt(o,null,f)):$e(o,()=>{o=null})))}function d(w){x.pending.delete(w)}var _=rn(()=>{c=E(a);for(var w=c.length,A=new Set,U=m,ze=Mr(),te=0;tes(f)):(o=ie(()=>s($r??($r=Oe()))),o.f|=Ae)),w>A.size&&gi(),!v)if(h.set(U,A),ze){for(const[ne,Hn]of l)A.has(ne)||U.skip_effect(Hn.e);U.oncommit(p),U.ondiscard(d)}else p(U);E(a)}),x={effect:_,items:l,pending:h,outrogroups:null,fallback:o};v=!1}function Kt(e){for(;e!==null&&(e.f&ae)===0;)e=e.next;return e}function As(e,t,n,r,i){var R;var s=t.length,f=e.items,l=Kt(e.effect.first),o,a=null,c=[],h=[],v,p,d,_;for(_=0;_0){var g=null;Ss(e,te,g)}}}function Ms(e,t,n,r,i,s,f,l){var o=(f&ki)!==0?(f&Mi)===0?es(n,!1,!1):Ze(n):null,a=(f&Ai)!==0?Ze(i):null;return{v:o,i:a,e:ie(()=>(s(t,o??n,a??i,l),()=>{e.delete(r)}))}}function Yt(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,s=t&&(t.f&Ae)===0?t.nodes.start:n;r!==null;){var f=jt(r);if(s.before(r),r===i)return;r=f}}function je(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}function on(e,t,n=!1,r=!1,i=!1,s=!1){var f=e,l="";if(n)var o=e;Ut(()=>{var a=S;if(l!==(l=t()??"")){if(n){a.nodes=null,o.innerHTML=l,l!==""&&bt(qe(o),o.lastChild);return}if(a.nodes!==null&&(Fr(a.nodes.start,a.nodes.end),a.nodes=null),l!==""){var c=r?Ii:i?Ri:void 0,h=Dr(r?"svg":i?"math":"template",c);h.innerHTML=l;var v=r||i?h:h.content;if(bt(qe(v),v.lastChild),r||i)for(;qe(v);)f.before(qe(v));else f.before(v)}}})}function it(e,t,...n){var r=new Jr(e);rn(()=>{const i=t()??null;r.ensure(i,i&&(s=>i(s,...n)))},Ye)}const ei=[...` \n\\r\\f\xA0\\v\\uFEFF`];function Ds(e,t,n){var r=""+e;if(n){for(var i of Object.keys(n))if(n[i])r=r?r+" "+i:i;else if(r.length)for(var s=i.length,f=0;(f=r.indexOf(i,f))>=0;){var l=f+s;(f===0||ei.includes(r[f-1]))&&(l===r.length||ei.includes(r[l]))?r=(f===0?"":r.substring(0,f))+r.substring(l+1):f=l}}return r===""?null:r}function ti(e,t=!1){var n=t?" !important;":";",r="";for(var i of Object.keys(e)){var s=e[i];s!=null&&s!==""&&(r+=" "+i+": "+s+n)}return r}function Ps(e,t){if(t){var n="",r,i;return Array.isArray(t)?(r=t[0],i=t[1]):r=t,r&&(n+=ti(r)),i&&(n+=ti(i,!0)),n=n.trim(),n===""?null:n}return String(e)}function Cs(e,t,n,r,i,s){var f=e.__className;if(f!==n||f===void 0){var l=Ds(n,r,s);l==null?e.removeAttribute("class"):e.className=l,e.__className=n}else if(s&&i!==s)for(var o in s){var a=!!s[o];(i==null||a!==!!i[o])&&e.classList.toggle(o,a)}return s}function qn(e,t={},n,r){for(var i in n){var s=n[i];t[i]!==s&&(n[i]==null?e.style.removeProperty(i):e.style.setProperty(i,s,r))}}function Is(e,t,n,r){var i=e.__style;if(i!==t){var s=Ps(t,r);s==null?e.removeAttribute("style"):e.style.cssText=s,e.__style=t}else r&&(Array.isArray(r)?(qn(e,n==null?void 0:n[0],r[0]),qn(e,n==null?void 0:n[1],r[1],"important")):qn(e,n,r));return r}function Rs(e,t,n=t){var r=new WeakSet;ss(e,"input",async i=>{var s=i?e.defaultValue:e.value;if(s=Ln(e)?jn(s):s,n(s),m!==null&&r.add(m),await Yr(),s!==(s=t())){var f=e.selectionStart,l=e.selectionEnd,o=e.value.length;if(e.value=s??"",l!==null){var a=e.value.length;f===l&&l===o&&a>o?(e.selectionStart=a,e.selectionEnd=a):(e.selectionStart=f,e.selectionEnd=Math.min(l,a))}}}),zt(t)==null&&e.value&&(n(Ln(e)?jn(e.value):e.value),m!==null&&r.add(m)),kn(()=>{var i=t();if(e===document.activeElement){var s=m;if(r.has(s))return}Ln(e)&&i===jn(e.value)||e.type==="date"&&!i&&!e.value||i!==e.value&&(e.value=i??"")})}function Ln(e){var t=e.type;return t==="number"||t==="range"}function jn(e){return e===""?null:+e}function ni(e,t){return e===t||(e==null?void 0:e[ht])===t}function Ns(e={},t,n,r){var i=M.r,s=S;return as(()=>{var f,l;return kn(()=>{f=l,l=[],zt(()=>{e!==n(...l)&&(t(e,...l),f&&ni(n(...f),e)&&t(null,...f))})}),()=>{let o=s;for(;o!==i&&o.parent!==null&&o.parent.f&pn;)o=o.parent;const a=()=>{l&&ni(n(...l),e)&&t(null,...l)},c=o.teardown;o.teardown=()=>{a(),c==null||c()}}}),e}function Fs(e=!1){const t=M,n=t.l.u;if(!n)return;let r=()=>vs(t.s);if(e){let i=0,s={};const f=tn(()=>{let l=!1;const o=t.s;for(const a in o)o[a]!==s[a]&&(s[a]=o[a],l=!0);return l&&i++,i});r=()=>E(f)}n.b.length&&os(()=>{ri(t,r),hn(n.b)}),wt(()=>{const i=zt(()=>n.m.map(vi));return()=>{for(const s of i)typeof s=="function"&&s()}}),n.a.length&&wt(()=>{ri(t,r),hn(n.a)})}function ri(e,t){if(e.l.s)for(const n of e.l.s)E(n);t()}function Os(e){M===null&&ir(),Nt&&M.l!==null?qs(M).m.push(e):wt(()=>{const t=zt(e);if(typeof t=="function")return t})}function qs(e){var t=e.l;return t.u??(t.u={a:[],b:[],m:[]})}const Ls="5";typeof window<"u"&&((oi=window.__svelte??(window.__svelte={})).v??(oi.v=new Set)).add(Ls);var js=Y(\' \'),Us=Y(\' \'),Vs=Y(\'
\'),Hs=Y(\'\');function zs(e,t){var n=Hs();let r;var i=K(n);{var s=o=>{var a=Vs(),c=K(a);{var h=d=>{var _=js(),x=K(_);it(x,()=>t.title),C(d,_)};De(c,d=>{t.title&&d(h)})}var v=he(c,2);{var p=d=>{var _=Us(),x=K(_);it(x,()=>t.info),C(d,_)};De(v,d=>{t.info&&d(p)})}C(o,a)};De(i,o=>{(t.title||t.info)&&o(s)})}var f=he(i,2),l=K(f);it(l,()=>t.description),Ut(()=>r=Cs(n,1,"sb-option",null,r,{"silversearch-selected":t.selected})),Bt("click",n,function(...o){var a;(a=t.onclick)==null||a.apply(this,o)}),Bt("mousemove",n,function(...o){var a;(a=t.onmousemove)==null||a.apply(this,o)}),C(e,n)}In(["click","mousemove"]),Oi();var Bs=Y(\' Silversearch found 0 results for your query
\');function Ks(e){var t=Bs();C(e,t)}var Ys=Y(\'\');function Ws(e,t){var n=Ys(),r=K(n);let i;Ut(()=>i=Is(r,"",i,{width:t.progress===null?"30%":`${t.progress}%`,animation:t.progress===null?"slide 1s linear infinite":"none"})),C(e,n)}var Gs=Y(\'Indexed out of documents
\'),Qs=Y(\'Loading ...
\'),Xs=Y(\'
\');function Zs(e,t){pt(t,!0);let n=Z(null),r=mr(()=>E(n)?E(n).done/E(n).all*100:null);Os(()=>{const a=setInterval(async()=>{const c=await syscall("clientStore.get","silversearch-progress");c&&j(n,c,!0)},50);return()=>clearInterval(a)});var i=Xs(),s=K(i);Ws(s,{get progress(){return E(r)}});var f=he(s,2);{var l=a=>{var c=Gs(),h=he(K(c)),v=K(h),p=he(h,2),d=K(p);Ut(()=>{Fn(v,E(n).done),Fn(d,E(n).all)}),C(a,c)},o=a=>{var c=Qs();C(a,c)};De(f,a=>{E(n)!==null?a(l):a(o,-1)})}C(e,i),_t()}var Js=Y(`Add the following to your query to refine the search:
Use path:"Library" to restrict your results\n to paths which include "Library" Use ext:pdf,jpg or ext:png, or\n a plain .png to restrict your search to certain filetypes Use "exact expressions" in quotes to further filter\n the results returned by the query Use -apple -pie to exclude notes containing the words\n "apple" and "pie" `);function $s(e){var t=Js();C(e,t)}let k=Je({text:"",buffer:"",historyIndex:-1,history:[]});var el=Y(\'
\');function ii(e,t){pt(t,!0);let n;wt(()=>{n.showModal()});function r(){syscall("editor.hidePanel","modal")}let i=Z(Je([])),s=Z(!1),f=Z(0);wt(()=>{k.historyIndex>=0&&(k.history[k.historyIndex]=k.text)}),wt(()=>{k.text?o():(j(i,[],!0),j(s,!1))});let l=null;async function o(){j(s,!0),l&&(l.reject(),l=null),l=Promise.withResolvers();try{j(i,await Promise.race([t.search(k.text),l.promise]),!0),l=null,j(f,0),h(),j(s,!1)}catch{}}async function a(g,R){E(i).length!==0&&(k.text&&k.history.unshift(k.text),k.history=k.history.filter(ne=>ne),k.history.length>50&&k.history.pop(),syscall("clientStore.set","silversearch-history",[...k.history]),await t.open(E(i)[E(f)],g,R),await syscall("editor.hidePanel","modal"))}function c(g){if(g.key==="Enter"){a(g.ctrlKey,g.altKey);return}if(g.ctrlKey){const R=k.historyIndex;if(R===-1&&(k.buffer=k.text),g.key==="ArrowUp")k.historyIndex++;else if(g.key==="ArrowDown")k.historyIndex--;else return;k.historyIndex=Math.max(-1,Math.min(k.historyIndex,k.history.length-1)),R!=k.historyIndex&&k.historyIndex>=0?k.text=k.history[k.historyIndex]:k.historyIndex===-1&&(k.text=k.buffer)}else{if(g.key==="ArrowUp")xr(f,-1);else if(g.key==="ArrowDown")xr(f);else return;j(f,Math.max(0,Math.min(E(f),E(i).length-1)),!0)}g.preventDefault(),h()}async function h(){await Yr();const g=document.querySelector(".silversearch-selected");g&&g.scrollIntoView({block:"nearest"})}var v=el();Xr("click",Tn,r);var p=K(v),d=he(K(p),2),_=he(p,2),x=K(_);it(x,()=>t.helpText);var w=he(_,2),A=K(w);ks(A,17,()=>E(i),Ts,(g,R,ne)=>{{const Hn=at=>{var Se=Le(),Xt=me(Se);{var zn=ut=>{var Zt=Le(),Bn=me(Zt);it(Bn,()=>t.elementTitle,()=>E(R)),C(ut,Zt)};De(Xt,ut=>{t.elementTitle&&ut(zn)})}C(at,Se)},cl=at=>{var Se=Le(),Xt=me(Se);it(Xt,()=>t.elementDescription,()=>E(R)),C(at,Se)},dl=at=>{var Se=Le(),Xt=me(Se);{var zn=ut=>{var Zt=Le(),Bn=me(Zt);it(Bn,()=>t.elementInfo,()=>E(R)),C(ut,Zt)};De(Xt,ut=>{t.elementInfo&&ut(zn)})}C(at,Se)};let hl=mr(()=>ne===E(f));zs(g,{get selected(){return E(hl)},onclick:({ctrlKey:at,altKey:Se})=>a(at,Se),onmousemove:()=>j(f,ne,!0),title:Hn,description:cl,info:dl})}});var U=he(A,2);{var ze=g=>{Ks(g)},te=g=>{$s(g)},ft=g=>{Zs(g,{})};De(U,g=>{!E(i).length&&!E(s)&&k.text?g(ze):!E(i).length&&!E(s)?g(te,1):!E(i).length&&E(s)&&g(ft,2)})}Ns(v,g=>n=g,()=>n),Xr("cancel",v,g=>{g.preventDefault(),syscall("editor.hidePanel","modal")}),Bt("click",p,g=>g.stopPropagation()),Bt("keydown",p,c),Rs(d,()=>k.text,g=>k.text=g),C(e,v),_t()}In(["click","keydown"]);function tl(e){return e.replace(/[.*+?^${}()|[\\]\\\\]/g,"\\\\$&")}const si=/( |<|>|"|')/g;function Un(e,t){const n="silversearch-highlight";if(!t.length)return e;try{return e.split(si).map(r=>r.match(si)?r:r.replace(new RegExp(`(${t.map(i=>tl(i.match)).join("|")})`,"giu"),`$1 `)).join("")}catch(r){return console.error("[Silversearch] Error in highlightText()",r),e}}var nl=Y("Press Ctrl-Enter to open in new Tab. Use Tab to switch scope.",1);function rl(e,t){pt(t,!0);let n=[],r=null;async function i(f){let l=await syscall("silversearch.search",f,{singleFilePath:t.currentPath,silent:!0});return n=l[0].matches,r=l[0].name,l[0].excerpts}async function s(f,l,o){r&&await syscall("editor.navigate",`${r}@${f.offset}`,!1,l)}ii(e,{search:i,open:s,helpText:o=>{var a=nl();C(o,a)},elementDescription:(o,a=It)=>{var c=Le(),h=me(c);on(h,()=>Un(a().excerpt,n)),C(o,c)},$$slots:{helpText:!0,elementDescription:!0}}),_t()}function il(e,t){for(const n of e)if(n.type==="range"&&n.from<=t&&t<=n.to)return n.tail;return""}var sl=Y("Use Tab to switch scope.",1),ll=Y("Press Ctrl-Enter to open in new Tab and Alt-Enter to insert a link. ",1);function ol(e,t){pt(t,!1);const n=or("isDocumentEditor");async function r(f,l,o){var h,v;if(o)return await i(f);const a=((v=(h=f.matches)==null?void 0:h[0])==null?void 0:v.offset)??0;let c=`@${a}`;f.navigationMap&&(c=il(f.navigationMap,a)),await syscall("editor.navigate",`${f.name}${c}`,!1,l)}async function i(f){const l=`[[${f.name}]]`;await syscall("editor.insertAtCursor",l),await syscall("editor.hidePanel","modal")}async function s(f){return syscall("silversearch.search",f,{silent:!0})}Fs(),ii(e,{search:s,open:r,helpText:c=>{var h=ll(),v=he(me(h),5);{var p=d=>{var _=sl();C(d,_)};De(v,d=>{n||d(p)})}C(c,h)},elementTitle:(c,h=It)=>{var v=Le(),p=me(v);on(p,()=>Un(h().name,h().matchesName)),C(c,v)},elementDescription:(c,h=It)=>{var v=Le(),p=me(v);on(p,()=>Un(h().excerpts[0].excerpt,h().matches)),C(c,v)},elementInfo:(c,h=It)=>{var v=ys();Ut(()=>Fn(v,`${h().matches.length??""} Matches`)),C(c,v)},$$slots:{helpText:!0,elementTitle:!0,elementDescription:!0,elementInfo:!0}}),_t()}var fl=Y("
",1);function al(e,t){pt(t,!0),syscall("clientStore.get","silversearch-history").then(h=>{k.history=h||[]});const n=or("isDocumentEditor");k.text=t.defaultQuery;let r=Z(!0);function i(h){h.stopPropagation(),h.key==="Tab"&&(n||(j(r,!E(r)),h.preventDefault()))}var s=fl(),f=me(s);on(f,()=>t.customStyles);var l=he(f,2),o=K(l);{var a=h=>{ol(h,{})},c=h=>{rl(h,{get currentPath(){return t.currentPath}})};De(o,h=>{E(r)?h(a):h(c,-1)})}Bt("keydown",l,i),C(e,s),_t()}In(["keydown"]);async function ul(){const e=syscall("editor.getCurrentPath"),t=syscall("editor.getCurrentEditor"),n=syscall("editor.getUiOption","customStyles");await Promise.race([new Promise(r=>setTimeout(r,75)),new Promise(r=>{const i=document.querySelector("link#stylesheet");i&&(i.onload=r)})]),bs(al,{target:document.querySelector("#container"),props:{defaultQuery:globalThis.DEFAULT_QUERY??"",currentPath:await e,customStyles:await n},context:new Map([["isDocumentEditor",await t!=="page"]])})}ul()})();\n';var Tt=`
-
-
`;var Yi="ENTRIES",It="KEYS",zt="VALUES";var Y=class{constructor(e,n){let r=e._tree,i=Array.from(r.keys());this.set=e,this._type=n,this._path=i.length>0?[{node:r,keys:i}]:[]}next(){let e=this.dive();return this.backtrack(),e}dive(){if(this._path.length===0)return{done:!0,value:void 0};let{node:e,keys:n}=B(this._path);if(B(n)==="")return{done:!1,value:this.result()};let r=e.get(B(n));return this._path.push({node:r,keys:Array.from(r.keys())}),this.dive()}backtrack(){if(this._path.length===0)return;let e=B(this._path).keys;e.pop(),!(e.length>0)&&(this._path.pop(),this.backtrack())}key(){return this.set._prefix+this._path.map(({keys:e})=>B(e)).filter(e=>e!=="").join("")}value(){return B(this._path).node.get("")}result(){switch(this._type){case zt:return this.value();case It:return this.key();default:return[this.key(),this.value()]}}[Symbol.iterator](){return this}},B=t=>t[t.length-1],es=(t,e,n)=>{let r=new Map;if(e===void 0)return r;let i=e.length+1,s=i+n,o=new Uint8Array(s*i).fill(n+1);for(let u=0;u{let a=s*o;e:for(let l of t.keys())if(l===""){let f=i[a-1];f<=n&&r.set(u,[t.get(l),f])}else{let f=s;for(let p=0;pn)continue e}Ot(t.get(l),e,n,r,i,f,o,u+l)}},ee=class t{constructor(e=new Map,n=""){this._size=void 0,this._tree=e,this._prefix=n}atPrefix(e){if(!e.startsWith(this._prefix))throw new Error("Mismatched prefix");let[n,r]=he(this._tree,e.slice(this._prefix.length));if(n===void 0){let[i,s]=Be(r);for(let o of i.keys())if(o!==""&&o.startsWith(s)){let u=new Map;return u.set(o.slice(s.length),i.get(o)),new t(u,e)}}return new t(n,e)}clear(){this._size=void 0,this._tree.clear()}delete(e){return this._size=void 0,ts(this._tree,e)}entries(){return new Y(this,Yi)}forEach(e){for(let[n,r]of this)e(n,r,this)}fuzzyGet(e,n){return es(this._tree,e,n)}get(e){let n=Ne(this._tree,e);return n!==void 0?n.get(""):void 0}has(e){let n=Ne(this._tree,e);return n!==void 0&&n.has("")}keys(){return new Y(this,It)}set(e,n){if(typeof e!="string")throw new Error("key must be a string");return this._size=void 0,Oe(this._tree,e).set("",n),this}get size(){if(this._size)return this._size;this._size=0;let e=this.entries();for(;!e.next().done;)this._size+=1;return this._size}update(e,n){if(typeof e!="string")throw new Error("key must be a string");this._size=void 0;let r=Oe(this._tree,e);return r.set("",n(r.get(""))),this}fetch(e,n){if(typeof e!="string")throw new Error("key must be a string");this._size=void 0;let r=Oe(this._tree,e),i=r.get("");return i===void 0&&r.set("",i=n()),i}values(){return new Y(this,zt)}[Symbol.iterator](){return this.entries()}static from(e){let n=new t;for(let[r,i]of e)n.set(r,i);return n}static fromObject(e){return t.from(Object.entries(e))}},he=(t,e,n=[])=>{if(e.length===0||t==null)return[t,n];for(let r of t.keys())if(r!==""&&e.startsWith(r))return n.push([t,r]),he(t.get(r),e.slice(r.length),n);return n.push([t,e]),he(void 0,"",n)},Ne=(t,e)=>{if(e.length===0||t==null)return t;for(let n of t.keys())if(n!==""&&e.startsWith(n))return Ne(t.get(n),e.slice(n.length))},Oe=(t,e)=>{let n=e.length;e:for(let r=0;t&&r{let[n,r]=he(t,e);if(n!==void 0){if(n.delete(""),n.size===0)Rt(r);else if(n.size===1){let[i,s]=n.entries().next().value;Lt(r,i,s)}}},Rt=t=>{if(t.length===0)return;let[e,n]=Be(t);if(e.delete(n),e.size===0)Rt(t.slice(0,-1));else if(e.size===1){let[r,i]=e.entries().next().value;r!==""&&Lt(t.slice(0,-1),r,i)}},Lt=(t,e,n)=>{if(t.length===0)return;let[r,i]=Be(t);r.set(i+e,n),r.delete(i)},Be=t=>t[t.length-1],Ue="or",jt="and",ns="and_not",U=class t{constructor(e){if(e?.fields==null)throw new Error('MiniSearch: option "fields" must be provided');let n=e.autoVacuum==null||e.autoVacuum===!0?je:e.autoVacuum;this._options={...Le,...e,autoVacuum:n,searchOptions:{...Ct,...e.searchOptions||{}},autoSuggestOptions:{...us,...e.autoSuggestOptions||{}}},this._index=new ee,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldIds={},this._fieldLength=new Map,this._avgFieldLength=[],this._nextId=0,this._storedFields=new Map,this._dirtCount=0,this._currentVacuum=null,this._enqueuedVacuum=null,this._enqueuedVacuumConditions=We,this.addFields(this._options.fields)}add(e){let{extractField:n,stringifyField:r,tokenize:i,processTerm:s,fields:o,idField:u}=this._options,a=n(e,u);if(a==null)throw new Error(`MiniSearch: document does not have ID field "${u}"`);if(this._idToShortId.has(a))throw new Error(`MiniSearch: duplicate ID ${a}`);let l=this.addDocumentId(a);this.saveStoredFields(l,e);for(let f of o){let p=n(e,f);if(p==null)continue;let h=i(r(p,f),f),d=this._fieldIds[f],g=new Set(h).size;this.addFieldLength(l,d,this._documentCount-1,g);for(let v of h){let m=s(v,f);if(Array.isArray(m))for(let y of m)this.addTerm(d,l,y);else m&&this.addTerm(d,l,m)}}}addAll(e){for(let n of e)this.add(n)}addAllAsync(e,n={}){let{chunkSize:r=10}=n,i={chunk:[],promise:Promise.resolve()},{chunk:s,promise:o}=e.reduce(({chunk:u,promise:a},l,f)=>(u.push(l),(f+1)%r===0?{chunk:[],promise:a.then(()=>new Promise(p=>setTimeout(p,0))).then(()=>this.addAll(u))}:{chunk:u,promise:a}),i);return o.then(()=>this.addAll(s))}remove(e){let{tokenize:n,processTerm:r,extractField:i,stringifyField:s,fields:o,idField:u}=this._options,a=i(e,u);if(a==null)throw new Error(`MiniSearch: document does not have ID field "${u}"`);let l=this._idToShortId.get(a);if(l==null)throw new Error(`MiniSearch: cannot remove document with ID ${a}: it is not in the index`);for(let f of o){let p=i(e,f);if(p==null)continue;let h=n(s(p,f),f),d=this._fieldIds[f],g=new Set(h).size;this.removeFieldLength(l,d,this._documentCount,g);for(let v of h){let m=r(v,f);if(Array.isArray(m))for(let y of m)this.removeTerm(d,l,y);else m&&this.removeTerm(d,l,m)}}this._storedFields.delete(l),this._documentIds.delete(l),this._idToShortId.delete(a),this._fieldLength.delete(l),this._documentCount-=1}removeAll(e){if(e)for(let n of e)this.remove(n);else{if(arguments.length>0)throw new Error("Expected documents to be present. Omit the argument to remove all documents.");this._index=new ee,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldLength=new Map,this._avgFieldLength=[],this._storedFields=new Map,this._nextId=0}}discard(e){let n=this._idToShortId.get(e);if(n==null)throw new Error(`MiniSearch: cannot discard document with ID ${e}: it is not in the index`);this._idToShortId.delete(e),this._documentIds.delete(n),this._storedFields.delete(n),(this._fieldLength.get(n)||[]).forEach((r,i)=>{this.removeFieldLength(n,i,this._documentCount,r)}),this._fieldLength.delete(n),this._documentCount-=1,this._dirtCount+=1,this.maybeAutoVacuum()}maybeAutoVacuum(){if(this._options.autoVacuum===!1)return;let{minDirtFactor:e,minDirtCount:n,batchSize:r,batchWait:i}=this._options.autoVacuum;this.conditionalVacuum({batchSize:r,batchWait:i},{minDirtCount:n,minDirtFactor:e})}discardAll(e){let n=this._options.autoVacuum;try{this._options.autoVacuum=!1;for(let r of e)this.discard(r)}finally{this._options.autoVacuum=n}this.maybeAutoVacuum()}replace(e){let{idField:n,extractField:r}=this._options,i=r(e,n);this.discard(i),this.add(e)}vacuum(e={}){return this.conditionalVacuum(e)}conditionalVacuum(e,n){return this._currentVacuum?(this._enqueuedVacuumConditions=this._enqueuedVacuumConditions&&n,this._enqueuedVacuum!=null?this._enqueuedVacuum:(this._enqueuedVacuum=this._currentVacuum.then(()=>{let r=this._enqueuedVacuumConditions;return this._enqueuedVacuumConditions=We,this.performVacuuming(e,r)}),this._enqueuedVacuum)):this.vacuumConditionsMet(n)===!1?Promise.resolve():(this._currentVacuum=this.performVacuuming(e),this._currentVacuum)}async performVacuuming(e,n){let r=this._dirtCount;if(this.vacuumConditionsMet(n)){let i=e.batchSize||$e.batchSize,s=e.batchWait||$e.batchWait,o=1;for(let[u,a]of this._index){for(let[l,f]of a)for(let[p]of f)this._documentIds.has(p)||(f.size<=1?a.delete(l):f.delete(p));this._index.get(u).size===0&&this._index.delete(u),o%i===0&&await new Promise(l=>setTimeout(l,s)),o+=1}this._dirtCount-=r}await null,this._currentVacuum=this._enqueuedVacuum,this._enqueuedVacuum=null}vacuumConditionsMet(e){if(e==null)return!0;let{minDirtCount:n,minDirtFactor:r}=e;return n=n||je.minDirtCount,r=r||je.minDirtFactor,this.dirtCount>=n&&this.dirtFactor>=r}get isVacuuming(){return this._currentVacuum!=null}get dirtCount(){return this._dirtCount}get dirtFactor(){return this._dirtCount/(1+this._documentCount+this._dirtCount)}has(e){return this._idToShortId.has(e)}getStoredFields(e){let n=this._idToShortId.get(e);if(n!=null)return this._storedFields.get(n)}search(e,n={}){let{searchOptions:r}=this._options,i={...r,...n},s=this.executeQuery(e,n),o=[];for(let[u,{score:a,terms:l,match:f}]of s){let p=l.length||1,h={id:this._documentIds.get(u),score:a*p,terms:Object.keys(f),queryTerms:l,match:f};Object.assign(h,this._storedFields.get(u)),(i.filter==null||i.filter(h))&&o.push(h)}return e===t.wildcard&&i.boostDocument==null||o.sort(Mt),o}autoSuggest(e,n={}){n={...this._options.autoSuggestOptions,...n};let r=new Map;for(let{score:s,terms:o}of this.search(e,n)){let u=o.join(" "),a=r.get(u);a!=null?(a.score+=s,a.count+=1):r.set(u,{score:s,terms:o,count:1})}let i=[];for(let[s,{score:o,terms:u,count:a}]of r)i.push({suggestion:s,terms:u,score:o/a});return i.sort(Mt),i}get documentCount(){return this._documentCount}get termCount(){return this._index.size}static loadJSON(e,n){if(n==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJS(JSON.parse(e),n)}static async loadJSONAsync(e,n){if(n==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJSAsync(JSON.parse(e),n)}static getDefault(e){if(Le.hasOwnProperty(e))return Re(Le,e);throw new Error(`MiniSearch: unknown option "${e}"`)}static loadJS(e,n){let{index:r,documentIds:i,fieldLength:s,storedFields:o,serializationVersion:u}=e,a=this.instantiateMiniSearch(e,n);a._documentIds=fe(i),a._fieldLength=fe(s),a._storedFields=fe(o);for(let[l,f]of a._documentIds)a._idToShortId.set(f,l);for(let[l,f]of r){let p=new Map;for(let h of Object.keys(f)){let d=f[h];u===1&&(d=d.ds),p.set(parseInt(h,10),fe(d))}a._index.set(l,p)}return a}static async loadJSAsync(e,n){let{index:r,documentIds:i,fieldLength:s,storedFields:o,serializationVersion:u}=e,a=this.instantiateMiniSearch(e,n);a._documentIds=await pe(i),a._fieldLength=await pe(s),a._storedFields=await pe(o);for(let[f,p]of a._documentIds)a._idToShortId.set(p,f);let l=0;for(let[f,p]of r){let h=new Map;for(let d of Object.keys(p)){let g=p[d];u===1&&(g=g.ds),h.set(parseInt(d,10),await pe(g))}++l%1e3===0&&await Nt(0),a._index.set(f,h)}return a}static instantiateMiniSearch(e,n){let{documentCount:r,nextId:i,fieldIds:s,averageFieldLength:o,dirtCount:u,serializationVersion:a}=e;if(a!==1&&a!==2)throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version");let l=new t(n);return l._documentCount=r,l._nextId=i,l._idToShortId=new Map,l._fieldIds=s,l._avgFieldLength=o,l._dirtCount=u||0,l._index=new ee,l}executeQuery(e,n={}){if(e===t.wildcard)return this.executeWildcardQuery(n);if(typeof e!="string"){let h={...n,...e,queries:void 0},d=e.queries.map(g=>this.executeQuery(g,h));return this.combineResults(d,h.combineWith)}let{tokenize:r,processTerm:i,searchOptions:s}=this._options,o={tokenize:r,processTerm:i,...s,...n},{tokenize:u,processTerm:a}=o,p=u(e).flatMap(h=>a(h)).filter(h=>!!h).map(os(o)).map(h=>this.executeQuerySpec(h,o));return this.combineResults(p,o.combineWith)}executeQuerySpec(e,n){let r={...this._options.searchOptions,...n},i=(r.fields||this._options.fields).reduce((v,m)=>({...v,[m]:Re(r.boost,m)||1}),{}),{boostDocument:s,weights:o,maxFuzzy:u,bm25:a}=r,{fuzzy:l,prefix:f}={...Ct.weights,...o},p=this._index.get(e.term),h=this.termResults(e.term,e.term,1,e.termBoost,p,i,s,a),d,g;if(e.prefix&&(d=this._index.atPrefix(e.term)),e.fuzzy){let v=e.fuzzy===!0?.2:e.fuzzy,m=v<1?Math.min(u,Math.round(e.term.length*v)):v;m&&(g=this._index.fuzzyGet(e.term,m))}if(d)for(let[v,m]of d){let y=v.length-e.term.length;if(!y)continue;g?.delete(v);let x=f*v.length/(v.length+.3*y);this.termResults(e.term,v,x,e.termBoost,m,i,s,a,h)}if(g)for(let v of g.keys()){let[m,y]=g.get(v);if(!y)continue;let x=l*v.length/(v.length+y);this.termResults(e.term,v,x,e.termBoost,m,i,s,a,h)}return h}executeWildcardQuery(e){let n=new Map,r={...this._options.searchOptions,...e};for(let[i,s]of this._documentIds){let o=r.boostDocument?r.boostDocument(s,"",this._storedFields.get(i)):1;n.set(i,{score:o,terms:[],match:{}})}return n}combineResults(e,n=Ue){if(e.length===0)return new Map;let r=n.toLowerCase(),i=rs[r];if(!i)throw new Error(`Invalid combination operator: ${n}`);return e.reduce(i)||new Map}toJSON(){let e=[];for(let[n,r]of this._index){let i={};for(let[s,o]of r)i[s]=Object.fromEntries(o);e.push([n,i])}return{documentCount:this._documentCount,nextId:this._nextId,documentIds:Object.fromEntries(this._documentIds),fieldIds:this._fieldIds,fieldLength:Object.fromEntries(this._fieldLength),averageFieldLength:this._avgFieldLength,storedFields:Object.fromEntries(this._storedFields),dirtCount:this._dirtCount,index:e,serializationVersion:2}}termResults(e,n,r,i,s,o,u,a,l=new Map){if(s==null)return l;for(let f of Object.keys(o)){let p=o[f],h=this._fieldIds[f],d=s.get(h);if(d==null)continue;let g=d.size,v=this._avgFieldLength[h];for(let m of d.keys()){if(!this._documentIds.has(m)){this.removeTerm(h,m,n),g-=1;continue}let y=u?u(this._documentIds.get(m),n,this._storedFields.get(m)):1;if(!y)continue;let x=d.get(m),w=this._fieldLength.get(m)[h],k=ss(x,g,this._documentCount,w,v,a),E=r*i*p*y*k,T=l.get(m);if(T){T.score+=E,ls(T.terms,e);let M=Re(T.match,n);M?M.push(f):T.match[n]=[f]}else l.set(m,{score:E,terms:[e],match:{[n]:[f]}})}}return l}addTerm(e,n,r){let i=this._index.fetch(r,Dt),s=i.get(e);if(s==null)s=new Map,s.set(n,1),i.set(e,s);else{let o=s.get(n);s.set(n,(o||0)+1)}}removeTerm(e,n,r){if(!this._index.has(r)){this.warnDocumentChanged(n,e,r);return}let i=this._index.fetch(r,Dt),s=i.get(e);s==null||s.get(n)==null?this.warnDocumentChanged(n,e,r):s.get(n)<=1?s.size<=1?i.delete(e):s.delete(n):s.set(n,s.get(n)-1),this._index.get(r).size===0&&this._index.delete(r)}warnDocumentChanged(e,n,r){for(let i of Object.keys(this._fieldIds))if(this._fieldIds[i]===n){this._options.logger("warn",`MiniSearch: document with ID ${this._documentIds.get(e)} has changed before removal: term "${r}" was not present in field "${i}". Removing a document after it has changed can corrupt the index!`,"version_conflict");return}}addDocumentId(e){let n=this._nextId;return this._idToShortId.set(e,n),this._documentIds.set(n,e),this._documentCount+=1,this._nextId+=1,n}addFields(e){for(let n=0;nObject.prototype.hasOwnProperty.call(t,e)?t[e]:void 0,rs={[Ue]:(t,e)=>{for(let n of e.keys()){let r=t.get(n);if(r==null)t.set(n,e.get(n));else{let{score:i,terms:s,match:o}=e.get(n);r.score=r.score+i,r.match=Object.assign(r.match,o),Ft(r.terms,s)}}return t},[jt]:(t,e)=>{let n=new Map;for(let r of e.keys()){let i=t.get(r);if(i==null)continue;let{score:s,terms:o,match:u}=e.get(r);Ft(i.terms,o),n.set(r,{score:i.score+s,terms:i.terms,match:Object.assign(i.match,u)})}return n},[ns]:(t,e)=>{for(let n of e.keys())t.delete(n);return t}},is={k:1.2,b:.7,d:.5},ss=(t,e,n,r,i,s)=>{let{k:o,b:u,d:a}=s;return Math.log(1+(n-e+.5)/(e+.5))*(a+t*(o+1)/(t+o*(1-u+u*r/i)))},os=t=>(e,n,r)=>{let i=typeof t.fuzzy=="function"?t.fuzzy(e,n,r):t.fuzzy||!1,s=typeof t.prefix=="function"?t.prefix(e,n,r):t.prefix===!0,o=typeof t.boostTerm=="function"?t.boostTerm(e,n,r):1;return{term:e,fuzzy:i,prefix:s,termBoost:o}},Le={idField:"id",extractField:(t,e)=>t[e],stringifyField:(t,e)=>t.toString(),tokenize:t=>t.split(as),processTerm:t=>t.toLowerCase(),fields:void 0,searchOptions:void 0,storeFields:[],logger:(t,e)=>{typeof console?.[t]=="function"&&console[t](e)},autoVacuum:!0},Ct={combineWith:Ue,prefix:!1,fuzzy:!1,maxFuzzy:6,boost:{},weights:{fuzzy:.45,prefix:.375},bm25:is},us={combineWith:jt,prefix:(t,e,n)=>e===n.length-1},$e={batchSize:1e3,batchWait:10},We={minDirtFactor:.1,minDirtCount:20},je={...$e,...We},ls=(t,e)=>{t.includes(e)||t.push(e)},Ft=(t,e)=>{for(let n of e)t.includes(n)||t.push(n)},Mt=({score:t},{score:e})=>e-t,Dt=()=>new Map,fe=t=>{let e=new Map;for(let n of Object.keys(t))e.set(parseInt(n,10),t[n]);return e},pe=async t=>{let e=new Map,n=0;for(let r of Object.keys(t))e.set(parseInt(r,10),t[r]),++n%1e3===0&&await Nt(0);return e},Nt=t=>new Promise(e=>setTimeout(e,t)),as=/[\n\r\p{Z}\p{P}]+/u;var Ve;function qe(t){return{lang:t?.lang??Ve?.lang,message:t?.message,abortEarly:t?.abortEarly??Ve?.abortEarly,abortPipeEarly:t?.abortPipeEarly??Ve?.abortPipeEarly}}var cs;function fs(t){return cs?.get(t)}var ps;function hs(t){return ps?.get(t)}var ds;function ms(t,e){return ds?.get(t)?.get(e)}function de(t){let e=typeof t;return e==="string"?`"${t}"`:e==="number"||e==="bigint"||e==="boolean"?`${t}`:e==="object"||e==="function"?(t&&Object.getPrototypeOf(t)?.constructor?.name)??"null":e}function _(t,e,n,r,i){let s=i&&"input"in i?i.input:n.value,o=i?.expected??t.expects??null,u=i?.received??de(s),a={kind:t.kind,type:t.type,input:s,expected:o,received:u,message:`Invalid ${e}: ${o?`Expected ${o} but r`:"R"}eceived ${u}`,requirement:t.requirement,path:i?.path,issues:i?.issues,lang:r.lang,abortEarly:r.abortEarly,abortPipeEarly:r.abortPipeEarly},l=t.kind==="schema",f=i?.message??t.message??ms(t.reference,a.lang)??(l?hs(a.lang):null)??r.message??fs(a.lang);f!==void 0&&(a.message=typeof f=="function"?f(a):f),l&&(n.typed=!1),n.issues?n.issues.push(a):n.issues=[a]}function P(t){return{version:1,vendor:"valibot",validate(e){return t["~run"]({value:e},qe())}}}function ys(t,e){return Object.hasOwn(t,e)&&e!=="__proto__"&&e!=="prototype"&&e!=="constructor"}function Ge(t,e){let n=[...new Set(t)];return n.length>1?`(${n.join(` ${e} `)})`:n[0]??"never"}function Wt(t){if(t.path){let e="";for(let n of t.path)if(typeof n.key=="string"||typeof n.key=="number")e?e+=`.${n.key}`:e+=n.key;else return null;return e}return null}var me=class extends Error{constructor(t){super(t[0].message),this.name="ValiError",this.issues=t}};function te(t){return{kind:"transformation",type:"args",reference:te,async:!1,schema:t,"~run"(e,n){let r=e.value;return e.value=(...i)=>{let s=this.schema["~run"]({value:i},n);if(s.issues)throw new me(s.issues);return r(...s.value)},e}}}function Ke(t){return{kind:"validation",type:"non_empty",reference:Ke,async:!1,expects:"!0",message:t,"~run"(e,n){return e.typed&&e.value.length===0&&_(this,"length",e,n,{received:"0"}),e}}}function ye(t){return{kind:"transformation",type:"returns",reference:ye,async:!1,schema:t,"~run"(e,n){let r=e.value;return e.value=(...i)=>{let s=this.schema["~run"]({value:r(...i)},n);if(s.issues)throw new me(s.issues);return s.value},e}}}function He(t){return{kind:"transformation",type:"returns",reference:He,async:!1,schema:t,"~run"(e,n){let r=e.value;return e.value=async(...i)=>{let s=await this.schema["~run"]({value:await r(...i)},n);if(s.issues)throw new me(s.issues);return s.value},e}}}function Bt(t,e,n){return typeof t.fallback=="function"?t.fallback(e,n):t.fallback}function Ut(t){let e={};for(let n of t)if(n.path){let r=Wt(n);r?(e.nested||(e.nested={}),e.nested[r]?e.nested[r].push(n.message):e.nested[r]=[n.message]):e.other?e.other.push(n.message):e.other=[n.message]}else e.root?e.root.push(n.message):e.root=[n.message];return e}function ve(t,e,n){return typeof t.default=="function"?t.default(e,n):t.default}function R(t,e){return{kind:"schema",type:"array",reference:R,expects:"Array",async:!1,item:t,message:e,get"~standard"(){return P(this)},"~run"(n,r){let i=n.value;if(Array.isArray(i)){n.typed=!0,n.value=[];for(let s=0;sn.expects),"|"),async:!1,options:t,message:e,get"~standard"(){return P(this)},"~run"(n,r){let i,s,o;for(let u of this.options){let a=u["~run"]({value:n.value},r);if(a.typed)if(a.issues)s?s.push(a):s=[a];else{i=a;break}else o?o.push(a):o=[a]}if(i)return i;if(s){if(s.length===1)return s[0];_(this,"type",n,r,{issues:$t(s)}),n.typed=!0}else{if(o?.length===1)return o[0];_(this,"type",n,r,{issues:$t(o)})}return n}}}function nt(t){return{kind:"schema",type:"void",reference:nt,expects:"void",async:!1,message:t,get"~standard"(){return P(this)},"~run"(e,n){return e.value===void 0?e.typed=!0:_(this,"type",e,n),e}}}function rt(t,e,n){let r=t["~run"]({value:e},qe(n));if(r.issues)throw new me(r.issues);return r.value}function q(...t){return{...t[0],pipe:t,get"~standard"(){return P(this)},"~run"(e,n){for(let r of t)if(r.kind!=="metadata"){if(e.issues&&(r.kind==="schema"||r.kind==="transformation")){e.typed=!1;break}(!e.issues||!n.abortEarly&&!n.abortPipeEarly)&&(e=r["~run"](e,n))}return e}}}function G(t,e,n){let r=t["~run"]({value:e},qe(n));return{typed:r.typed,success:!r.issues,output:r.value,issues:r.issues}}function Vt(t){let e="";for(let n of t){e&&(e+=`
-`),e+=`\xD7 ${n.message}`;let r=Wt(n);r&&(e+=`
- \u2192 at ${r}`)}return e}var qt=100,ge=300,xe=(i=>(i.Disabled="disabled",i.Day="day",i.Week="week",i.Month="month",i))(xe||{}),vs='/[|\\t\\n\\r\\^"= -#%-*,.`\\/<>:;?@[-\\]_{}\\u00A0\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u09FD\\u0A76\\u0AF0\\u0C77\\u0C84\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166E\\u1680\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2000-\\u200A\\u2010-\\u2029\\u202F-\\u2043\\u2045-\\u2051\\u2053-\\u205F\\u207D\\u207E\\u208D\\u208E\\u2308-\\u230B\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E4F\\u3000-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA8FC\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]/'.slice(1,-1),Gt=new RegExp(`${vs}+`,"u"),Kt=/[|\[\]\(\)<>\{\} \t\n\r]/u;var Ht=!1,se=null,oe=Je(()=>tt([A(),I(),z(),Ze(),V(A(),oe),R(oe)])),Xt=j({content:b(I(),1),basename:b(I(),10),displayName:b(I(),10),directory:b(I(),7),aliases:b(I(),1),tags:b(I(),1)}),Jt=j({weights:b(Xt,rt(Xt,{})),weightCustomProperties:b(V(A(),I()),{}),recencyBoost:b(Xe(xe),"disabled"),downrankedFoldersFilters:b(R(A()),[]),ignoreDiacritics:b(z(),!0),ignoreArabicDiacritics:b(z(),!1),tokenizeUrls:b(z(),!0),splitCamelCase:b(z(),!0),fuzziness:b(re(["0","1","2"]),"1"),renderLineReturnInExcerpts:b(z(),!0),ignore:b(R(A()),[]),tokenizers:b(V(q(A(),Ke()),V(A(),oe)),{})});async function N(){if(se)return se;let t=await ae.getConfig("silversearch",{}),e=G(Jt,t);if(e.success)se=e.output;else{if(!Ht){let n=Object.entries(Ut(e.issues).nested??{}).map(([r,i])=>i?`${i.join(" & ")} in "${r}"`:"").filter(r=>r).join("; ");await C.flashNotification(`Silersearch - There was an error in your CONFIG: ${n}`),Ht=!0}se=rt(Jt,{})}return se}var gs=["\\u30FC","\\u309A","\\u3099"],xs=gs.join("|"),Qt=new RegExp(`(?!${xs})\\p{Diacritic}`,"gu");function Zt(t){return t.replace(/(\*|_)+(.+?)(\*|_)+/g,(e,n,r)=>r)}function st(t){return/[a-z][A-Z]/.test(t)?t.replace(/([a-z](?=[A-Z]))/g,"$1 ").split(" ").filter(n=>n):[]}function ot(t){return t.includes("-")?t.split("-").filter(e=>e):[]}function $(t,e=!1){if(t==null)return"";if(e){t=t.replace(/([^\u0621-\u063A\u0641-\u064A\u0660-\u0669a-zA-Z 0-9])/g,"").replace(/(آ|إ|أ)/g,"\u0627").replace(/(ة)/g,"\u0647").replace(/(ئ|ؤ)/g,"\u0621").replace(/(ى)/g,"\u064A");for(let n=0;n<10;n++)t.replace(String.fromCharCode(1632+n),String.fromCharCode(48+n))}return t=t.replaceAll("`","[__silversearch__backtick__]"),t=t.replaceAll("^","[__silversearch__caret__]"),t=t.normalize("NFD").replace(Qt,"").normalize("NFC"),t=t.replaceAll("[__silversearch__backtick__]","`"),t=t.replaceAll("[__silversearch__caret__]","^"),t}function Yt(t){return t.replace(new RegExp(`(?!\\^|\`)${Qt.source}`,"gu"),"")}function ut(t){let e=[],n=-1,r=0;for(;++r<100;){let i=ws(t,n,ge);if(!i.length)break;n=i.at(-1).offset,e.push(i)}return e}function ws(t,e,n){let r=t.find(i=>i.offset>e);return r?t.filter(i=>i.offset>e&&i.offset<=r.offset+n):[]}var bs=/\r\n?|\n/g,ks=/\0/g;function lt(t){let e=t;return e=e.replace(bs,`
-`),e=e.replace(ks,"\uFFFD"),e}var at=(t,e)=>{let n=0,r=0,i=0;for(;nt.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),_s=({link:t,startSyntax:e,endSyntax:n,links:r})=>{if(t.startsWith(e)){if(t.endsWith(n))return!1;if(t.includes(n)){let o=t.replace(/.?http/g,"http").substring(0,t.lastIndexOf(n)).replace(new RegExp(`\\${n}$`,"g"),"");return!r.some(u=>u.href===o)}}return!0},Es=t=>{let e=/mailto:([^\s[;]+)/g;return t.map(n=>n.href.match(e)||n.href.includes("@")?{...n,href:n.href.startsWith("mailto")?n.href:`mailto:${n.href}`,type:"email"}:n)},en=t=>t.startsWith("http")||t.startsWith("/")||t.includes("@"),ct=({line:t,url:e,lineLinks:n=[]})=>{var r;let i=((r=t?.match(new RegExp(tn(e),"g")))===null||r===void 0?void 0:r.length)||0,s=n?.reduce((o,u)=>o+(u.href.includes(e)?1:0)+(u.format==="[]()"&&u.text.includes(e)?1:0),0);return i>0&&s{var r,i;let s="",o=(((r=n?.match(/https:\/\/.*/g))===null||r===void 0?void 0:r[0])||"").replace("https://","");for(let u of e){let a=at(o,u.href);a.length>s.length&&(s=a)}if(s.length>0){let u=((i=t?.match(new RegExp(tn(s),"g")))===null||i===void 0?void 0:i.length)||0,a=e?.reduce((l,f)=>l+(f.href.includes(s)?1:0)+(f.format==="[]()"&&f.text.includes(s)?1:0),0);return u>0&&a{var e,n,r,i;let s=lt(t).split(`
-`),o=/.?\[([^\]]*)\]\(([^\][]*)\)/g,u=/<[^>]*>/g,a=/.?\(([^)]*)\)/g,l=/.?https?:\/\/[^\s[;]+/g,f=[];for(let p of s){let h=p.match(o)||[],d=p.match(u)||[],g=p.match(a)||[],v=p.match(l)||[],m=[];for(let y of h)if(!y.startsWith("!")){let x=y?.replace(/^.?\[/g,"[")||"",w=x.indexOf("["),k=x.indexOf("]",w+1),E=x;if(w!==-1&&k!==-1&&x[k+1]==="("){let Bn=k+1,Se=0,Te=-1;for(let le=Bn;le]*)>/))===null||i===void 0?void 0:i[1])||"").trim();if(en(x)){let w=x;if(ct({line:p,url:w,lineLinks:m})){let k={text:x,href:w,line:p,raw:y,type:"link",format:"<>"};f.push(k),m.push(k)}}}for(let y of g){let x=y.replace(/^.?\(/g,"("),w=x.match(/\(([^)]*)\)/),k=(w?.[1]||"").trim();if(!y.startsWith("]")&&en(k)){let E=k;if(ct({line:p,url:E,lineLinks:m})){let T={text:k,href:E,line:p,raw:x,type:"link",format:"()"};f.push(T),m.push(T)}}}for(let y of v)if([{start:"[",end:"]"},{start:"<",end:">"},{start:"(",end:")"}].every(w=>_s({link:y,startSyntax:w.start,endSyntax:w.end,links:m}))&&Ps({line:p,links:m,link:y})){let w=y.replace(/^.?http/g,"http"),k=w;if(ct({line:p,url:k,lineLinks:m})){let E={text:w,href:k,line:p,raw:w,type:"link",format:"raw"};f.push(E),m.push(E)}}}return Es(f)};var As=j({init:q(ne(),te(ie([oe])),He(nt())),isTokenizable:q(ne(),te(ie([A()])),ye(z())),tokenize:q(ne(),te(ie([A()])),ye(R(A())))}),we=class t{constructor(e){this.implementation=e}static async loadFromPath(e,n){let r;try{let s=await D.readDocument(e),o=new Blob([s],{type:"text/javascript"}),u=URL.createObjectURL(o);r=await import(u),URL.revokeObjectURL(u)}catch{return console.warn(`[Silversearch] Failed to load tokenizer at ${e}. Maybe the path is wrong?`),null}let i=G(As,r);if(i.success){let s=new t(i.output);return await s.implementation.init(n),s}else return console.warn(`[Silversearch] Failed to load tokenizer at ${e}.
- ${Vt(i.issues)}`),null}tryTokenization(e){try{if(this.implementation.isTokenizable(e))return this.implementation.tokenize(e)}catch(n){console.warn(`[Silversearch] Tokenizer failed with error ${n}. Ignoring!`)}return null}};function nn(t,e){try{let n=sn(t,e.tokenizers),r=[];return e.tokenizeUrls&&(r=ft(t).map(o=>o.href)),[...on(t).flatMap(o=>[o,...ot(o),...st(o)]),...n,...r]}catch(n){return console.error("[Silversearch] Error tokenizing text, skipping document",n),[]}}function rn(t,e){let n=ft(t).map(i=>i.href);t=n.reduce((i,s)=>i.replace(s,""),t);let r=[...on(t),...n];return{combineWith:"OR",queries:[{combineWith:"AND",queries:r},{combineWith:"AND",queries:sn(t,e.tokenizers)},{combineWith:"AND",queries:r.flatMap(ot)},{combineWith:"AND",queries:r.flatMap(st)}]}}function sn(t,e){return Ss(t,e)??t.split(Kt).filter(Boolean)}function on(t){return t.split(Gt).filter(Boolean)}function Ss(t,e){let n=e.flatMap(r=>r.tryTokenization(t)??[]);return n.length===0?null:n}function un(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Ts(t){if(!t.length)return/^$/g;t.sort((n,r)=>r.length-n.length);let e=`(${t.map(n=>`\\b${un(n)}\\b|${un(n)}`).join("|")})`;return new RegExp(`${e}`,"gui")}function ln(t,e){if(t=an(t),e.renderLineReturnInExcerpts){let n=new RegExp(/(?:\r\n|\r|\n)/g);t=t.split(n).filter(r=>r).join(`
-`),t=t.replaceAll(`
-`," ")}return t.trim()}async function pt(t,e,n){let r=await N();e=e.map(an);let i=Ts(e),s=t;r.ignoreDiacritics&&(t=$(t,r.ignoreArabicDiacritics));let o=new Date().getTime(),u=null,a=[],l=0;for(;(u=i.exec(t))!==null&&!(++l>=100||new Date().getTime()-o>50);){let f=u.index,p=f+u[0].length,h=s.substring(f,p).trim();h&&u.index>=0&&a.push({match:h,offset:u.index})}if(n&&(n.query.text.length>1||n.getExactTerms().length>0)){let f=t.indexOf(n.getBestStringForExcerpt());f>-1&&a.find(p=>p.offset===f)&&a.unshift({offset:f,match:n.getBestStringForExcerpt()})}return a.map(f=>({...f,match:ln(f.match,{renderLineReturnInExcerpts:r.renderLineReturnInExcerpts})}))}async function ht(t,e){let n=await N();try{let r=Math.max(0,e-qt),i=Math.min(t.length,e+ge);if(t=(r>0?"\u2026":"")+t.slice(r,i).trim()+(i0&&(t=t.slice(s+1))}return{excerpt:ln(t,{renderLineReturnInExcerpts:n.renderLineReturnInExcerpts}),offset:e}}catch(r){return console.error("[Silversearch] Error while creating excerpt",r),{excerpt:"",offset:0}}}function an(t){return t.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function Cs(t){return t!==""?t.split(".").pop().toLowerCase():"md"}function O(t){return Fs({path:t})}function K(t){return Cs(t)==="md"}function Fs(t){let e=t.path;return K(t.path)&&(e=e.slice(0,-3)),t.details?.type==="linecolumn"?(e+=`@L${t.details.line}`,t.details.column!==1&&(e+=`C${t.details.column}`)):t.details?.type==="position"?e+=`@${t.details.pos}`:t.details?.type==="header"&&(e+=`#${t.details.header}`),e}function cn(t){return t.split("/").slice(0,-1).join("/")}function fn(t){return t.split("/").pop()}var Ms=j({content:A(),cacheMode:et(re(["persistent","session"]),"session"),navigationMap:b(R(Ye({type:Qe("range"),from:I(),to:I(),tail:A()})))});async function pn(t,e){if(K(t))try{return{content:await D.readPage(O(t)),cacheMode:"session"}}catch{return null}else{let n;try{n=e??await D.getDocumentMeta(O(t))}catch{return null}let r;try{r=await ce.dispatchEvent("silversearch:index",{meta:n})}catch{return console.warn("[silversearch] Eventhandler threw error. Discarding it."),null}if(r.length===0)return null;r.length>1&&console.warn(`[silversearch] We got multiple responses while indexing ${n.name}. Can't handle that.`);let i=G(Ms,r[0]);return i.success?i.output:(console.warn("[silversearch] Eventhandler didn't return well-formed result. Discarding it."),null)}}var _n=Et(bn()),kn=4,J=class t{constructor(e,n){this.minisearch=new U(t.getOptions(e,n)),this.entryCache=new Map,this.tokenizers=n,this.ignore=(0,_n.default)().add(e.ignore)}static async create(e){let n=(await Promise.all(Object.entries(e.tokenizers).map(async([r,i])=>await we.loadFromPath(r,i)))).filter(r=>r!==null);return new t(e,n)}static async loadFromCache(e){if(await F.get("silversearch-cache-lock"))return await C.flashNotification("Silversearch - Last cache failed, resetting index"),t.deleteCache(),null;let n=await F.get("silversearch-cache");if(!n||n.version!==kn||typeof n.minisearch!="string"||typeof n.entries!="object")return console.log("[Silversearch] Couldn't find cache or cache version mismatched"),t.deleteCache(),null;let r=await this.create(e);return r.minisearch=await U.loadJSONAsync(n.minisearch,t.getOptions(e,r.tokenizers)),r.entryCache=new Map(n.entries),r}static async deleteCache(){await F.del("silversearch-cache"),await F.del("silversearch-cache-lock")}async writeToCache(){clearTimeout(this.cacheWritingTimer),await F.set("silversearch-cache-lock",!0),this.cacheWritingTimer=setTimeout(async()=>{await F.set("silversearch-cache",{version:kn,minisearch:JSON.stringify(this.minisearch),entries:Array.from(this.entryCache.entries()).filter(e=>e[1].cacheMode==="persistent")}),await F.set("silversearch-cache-lock",!1)},250)}async reindex(e=!1){e||await C.flashNotification("Silversearch - Reindexing, this can cause performance problems"),this.minisearch.removeAll();let n=(await D.listFiles()).map(r=>r.name).filter(this.isIndexedPath);console.log(`[Silversearch] Indexing ${n.length} pages`),e||await C.showProgress(0,"index"),await this.indexByPaths(n,(r,i)=>{e||C.showProgress(Math.round(r/i*100),"index"),F.set("silversearch-progress",{done:r,all:i})}),F.del("silversearch-progress"),e||(await C.showProgress(),await C.flashNotification("Silversearch - Full reindex done!"))}async indexByPath(e){await this.indexByPaths([e])}async indexByPaths(e,n){let r=0;for(let i of e)try{if(this.entryCache.delete(i),this.ignore.ignores(i))console.log(`[Silversearch] Ignoring ${O(i)}`),this.minisearch.discard(i);else{console.log(`[Silversearch] Indexing ${O(i)}`);let s=await this.getCacheEntry(i);if(!s)throw null;this.minisearch.has(s.path)?this.minisearch.replace(s):this.minisearch.add(s)}n&&n(++r,e.length)}catch{console.warn(`[Silversearch] Failed to index ${O(i)}. Skipping!`)}await this.writeToCache()}async deleteByPath(e){await this.deleteByPaths([e])}async deleteByPaths(e){for(let n of e)try{this.entryCache.delete(n),this.minisearch.discard(n)}catch{console.error("[Silversearch] Something went wrong. Failed to delete page")}await this.writeToCache()}async search(e,n){let r=await N();if(e.isEmpty())return[];let i={0:0,1:.1,2:.2}[r.fuzziness],s=rn(e.segmentsToStr(),{tokenizers:this.tokenizers}),o=this.minisearch.search(s,{prefix:p=>p.length>=n.prefixLength,fuzzy:p=>p.length<=3?0:p.length<=5?i/2:i,boost:{basename:r.weights.basename,aliases:r.weights.aliases,displayName:r.weights.displayName,directory:r.weights.directory,tags:r.weights.tags,unmarkedTags:r.weights.tags},tokenize:p=>[p],boostDocument(p,h,d){if(!d?.lastModified||r.recencyBoost==="disabled")return 1;let g=d?.lastModified,m=(new Date().getTime()-g)/(24*3600*1e3),y={day:-3,week:-.3,month:-.1};return 1+Math.exp(y[r.recencyBoost]*(m/1e3))}});if(e.query.ext?.length&&(o=o.filter(p=>{let h=p.id.includes(".")?"."+p.id.split(".").pop():".md";return e.query.ext?.some(d=>h.startsWith(d.startsWith(".")?d:"."+d))})),e.query.path&&(o=o.filter(p=>e.query.path?.some(h=>p.id.toLowerCase().includes(h.toLowerCase())))),e.query.exclude.path&&(o=o.filter(p=>!e.query.exclude.path?.some(h=>p.id.toLowerCase().includes(h.toLowerCase())))),!o.length)return[];if(n.singleFilePath)return o.filter(p=>p.id===n.singleFilePath);let u=await this.getCacheEntries(o.map(p=>p.id)),a=e.getTags();for(let p of o){let h=p.id,d=O(h);r.downrankedFoldersFilters.length>0&&(r.downrankedFoldersFilters.some(m=>d===m||d.startsWith(m+"/"))||d.split("/").some(m=>r.downrankedFoldersFilters.includes(m)))&&(p.score/=10);let g=u.find(v=>v.path===h);if(g)for(let[v,m]of Object.entries(r.weightCustomProperties)){let y=g.metadata[v];y&&p.terms.some(x=>y.includes(x))&&(p.score*=m)}for(let v of a)(g?.tags??[]).includes(v)&&(p.score*=100)}o=o.sort((p,h)=>h.score-p.score).slice(0,50);let l=e.getExactTerms();l.length&&(o=o.filter(p=>{let h=u.find(v=>v.path===p.id),d=h?.name.toLowerCase()??"",g=(h?.cleanedContent??"").toLowerCase();return l.every(v=>g.includes(v)||$(d,r.ignoreArabicDiacritics).includes(v))}));let f=e.query.exclude.text;return f.length&&(o=o.filter(p=>{let h=(u.find(d=>d.path===p.id)?.content??"").toLowerCase();return f.every(d=>!h.includes(d))})),o}async getSuggestions(e,n){let r=await this.search(e,{prefixLength:1,singleFilePath:n?.singleFilePath}),i=await this.getCacheEntries(r.map(o=>o.id));return(await Promise.all(r.map(async o=>{let u=i.find(d=>d.path===o.id);if(!u)return console.warn(`[Silversearch] Note "${o.name}" disappeared. Skipping`),null;let a=[...o.terms,...e.getExactTerms(),...e.getTags()],l=await pt(u.content,a,e),f=await pt(u.name,a),p;if(n?.singleFilePath){let d=ut(l),g=e.getExactTerms();g.length&&(d=d.filter(m=>g.every(y=>m.some(x=>x.match.includes(y)))));let v=d.map(m=>m[0].offset);p=await Promise.all(v.map(m=>ht(u?.content??"",m)))}else{let d=ut(l),g=0;d.length&&(g=d.reduce((m,y)=>y.length>m.length?y:m)[0].offset),p=[await ht(u.content,g)]}return{score:o.score,foundWords:a,matches:l,matchesName:f,excerpts:p,name:u.name,basename:u.basename,content:u.content,navigationMap:u.navigationMap}}))).filter(o=>o!==null)}async getCacheEntries(e){return(await Promise.all(e.map(r=>this.getCacheEntry(r)))).filter(r=>!!r)}async getCacheEntry(e){let n,r=this.entryCache.get(e);if(r)return r;try{let u=O(e);n=K(e)?await D.getPageMeta(u):await D.getDocumentMeta(u)}catch{return console.log("[Silversearch] Couldn't find the specified file:",e),null}let i=await t.pathToIndexableEntry(e,n);if(!i)return null;let s=Object.fromEntries(Object.entries(n).filter(([u,a])=>!["ref","tag","tags","itags","name","created","lastModified","perm","lastOpened","pageDecoration","aliases","extension","size","contentType"].includes(u))),o={...i.entry,...i.info,cleanedContent:Zt($(i.entry.content)),metadata:s,tags:n.tags?.map(u=>"#"+u)??[]};return this.entryCache.set(e,o),o}static async pathToIndexableEntry(e,n){let r=O(e),i;try{i=n??(K(e)?await D.getPageMeta(r):await D.getDocumentMeta(r))}catch{return null}let s=await pn(e,i);if(!s)return null;let o=Yt(s.content),u={path:e,name:r,content:o,basename:fn(r),directory:cn(r),aliases:i.aliases??[],displayName:i.displayName??"",unmarkedTags:i.tags??[],lastModified:new Date(i.lastModified).getTime()},{content:a,...l}=s;return{entry:u,info:l}}isIndexedPath(e){return!e.endsWith(".plug.js")}static getOptions(e,n){return{tokenize:r=>nn(r,{tokenizers:n,tokenizeUrls:e.tokenizeUrls}),processTerm:r=>(e.ignoreDiacritics?$(r,e.ignoreArabicDiacritics):r).toLowerCase(),idField:"path",fields:["content","basename","directory","aliases","displayName","unmarkedTags"],storeFields:["lastModified"]}}};var Sn=Et(An()),wt=["ext","path"],Pe=class{#e;constructor(e="",n){n.ignoreDiacritics&&(e=$(e,n.ignoreArabicDiacritics));let r=(0,Sn.parse)(e.toLowerCase(),{tokenize:!0,keywords:wt});r.text=r.text??[],r.exclude=r.exclude??{text:[]},r.exclude.text=r.exclude.text??[],Array.isArray(r.exclude.text)||(r.exclude.text=[r.exclude.text]),r.exclude.text=r.exclude.text.filter(s=>s.length);for(let s of wt){let o=r[s];o&&(r[s]=Array.isArray(o)?o:[o]);let u=r.exclude[s];u&&(r.exclude[s]=Array.isArray(u)?u:[u])}this.query=r;let i=this.query.text.filter(s=>s.startsWith(".")).map(s=>s.slice(1));this.query.ext=[...new Set([...i,...this.query.ext??[]])],this.query.text=this.query.text.filter(s=>!s.startsWith(".")),this.#e=e.match(/"([^"]+)"/g)?.map(s=>s.replace(/"/g,""))??[]}isEmpty(){for(let e of wt)if(this.query[e]?.length||this.query.text.length)return!1;return!0}segmentsToStr(){return this.query.text.join(" ")}getTags(){return this.query.text.filter(e=>e.startsWith("#"))}getTagsWithoutHashtag(){return this.getTags().map(e=>e.replace(/^#/,""))}getExactTerms(){return[...new Set([...this.query.text.filter(e=>e.split(" ").length>1),...this.#e].map(e=>e.toLowerCase()))]}getBestStringForExcerpt(){return this.#e.length?this.#e.sort((e,n)=>n.length-e.length)[0]??"":this.segmentsToStr()}};var Tn="1.2.5";var S=null,W=[],Ae=null;function bt(t=!0,e=!1){return Ae||(Ae=Zs(t,e).then(()=>{Ae=null})),Ae}async function Zs(t,e){if(S)return;let n=await N();S=await J.loadFromCache(n),!S&&t?(S=await J.create(n),await S.reindex(e)):S&&W.length&&(await S.indexByPaths(W.filter(r=>r.action==="index").map(r=>r.path)),await S.deleteByPaths(W.filter(r=>r.action==="delete").map(r=>r.path))),W=[]}async function Cn(t=""){await C.showPanel("modal",1,Tt,t?`globalThis.DEFAULT_QUERY = ${JSON.stringify(t)};`+ze:ze)}async function Fn(){await c("silversearch.openSearch")}async function Mn(){await bt(!1)}async function Dn({name:t}){let e=`${t}.md`;S?await S.indexByPath(e):W.push({action:"index",path:e})}async function In(t){let e=t;S?await S.indexByPath(e):W.push({action:"index",path:e})}async function zn(t){S?await S.deleteByPath(t):W.push({action:"delete",path:t})}async function On(t,e){await bt(!0,e.silent??!1);let n=await N(),r=new Pe(t,{ignoreDiacritics:n.ignoreDiacritics,ignoreArabicDiacritics:n.ignoreArabicDiacritics});return S.getSuggestions(r,{singleFilePath:e.singleFilePath})}async function Rn(t=!1){await J.deleteCache(),S=null,await bt(!0,t)}async function Ln(){await C.flashNotification(`Silversearch - Version ${Tn}`)}async function jn(){await C.flashNotification("Silversearch - Cleared History"),F.set("silversearch-history",[])}var Nn={startSearch:Fn,getVersion:Ln,clearHistory:jn,openSearch:Cn,reindex:Rn,init:Mn,indexPage:Dn,indexDocument:In,deleted:zn,search:On},$n={name:"silversearch",functions:{startSearch:{path:"./silversearch.ts:startSearch",command:{name:"Silversearch: Search",key:"Ctrl-s",mac:"Cmd-s"}},getVersion:{path:"./silversearch.ts:showVersion",command:{name:"Silversearch: Version"}},clearHistory:{path:"./silversearch.ts:clearHistory",command:{name:"Silversearch: Clear History"}},openSearch:{path:"./silversearch.ts:openSearch",syscall:"silversearch.openSearch"},reindex:{path:"./silversearch.ts:reindex",syscall:"silversearch.reindex",command:{name:"Silversearch: Reindex"}},init:{path:"./silversearch.ts:init",events:["editor:init"]},indexPage:{path:"./silversearch.ts:indexPage",events:["page:index"]},indexDocument:{path:"./silversearch.ts:indexDocument",events:["document:index"]},deleted:{path:"./silversearch.ts:deleted",events:["file:deleted"]},search:{path:"./silversearch.ts:search",syscall:"silversearch.search"}},assets:{}},ol={manifest:$n,functionMapping:Nn};St(Nn,$n,self.postMessage);export{ol as plug};
-/*! Bundled license information:
-
-search-query-parser/lib/search-query-parser.js:
- (*!
- * search-query-parser.js
- * Copyright(c) 2014-2019
- * MIT Licensed
- *)
-*/
-//# sourceMappingURL=silversearch.plug.js.map
diff --git a/configs/notes/space/_plug/treeview.plug.js b/configs/notes/space/_plug/treeview.plug.js
deleted file mode 100644
index e963797..0000000
--- a/configs/notes/space/_plug/treeview.plug.js
+++ /dev/null
@@ -1,39 +0,0 @@
-var Bt=Object.defineProperty;var te=(n,e)=>{for(var t in e)Bt(n,t,{get:e[t],enumerable:!0})};var le=n=>{throw new Error("Not initialized yet")},je=typeof window>"u"&&typeof globalThis.WebSocketPair>"u";typeof Deno>"u"&&(self.Deno={args:[],build:{arch:"x86_64"},env:{get(){}}});var Me=new Map,Ue=0;je&&(globalThis.syscall=async(n,...e)=>await new Promise((t,r)=>{Ue++,Me.set(Ue,{resolve:t,reject:r}),le({type:"sys",id:Ue,name:n,args:e})}));function tt(n,e,t){je&&(le=t,self.addEventListener("message",r=>{(async()=>{let s=r.data;switch(s.type){case"inv":{let a=n[s.name];if(!a)throw new Error(`Function not loaded: ${s.name}`);try{let o=await Promise.resolve(a(...s.args||[]));le({type:"invr",id:s.id,result:o})}catch(o){console.error("An exception was thrown as a result of invoking function",s.name,"error:",o.message),le({type:"invr",id:s.id,error:o.message})}}break;case"sysr":{let a=s.id,o=Me.get(a);if(!o)throw Error("Invalid request id");Me.delete(a),s.error?o.reject(new Error(s.error)):o.resolve(s.result)}break}})().catch(console.error)}),le({type:"manifest",manifest:e}))}function Vt(n){let e=atob(n),t=e.length,r=new Uint8Array(t);for(let s=0;s0?nt(t):void 0;e={method:n.method,headers:Object.fromEntries(n.headers.entries()),base64Body:r},n=n.url}return syscall("sandboxFetch.fetch",n,e)}globalThis.nativeFetch=globalThis.fetch;function _t(){globalThis.fetch=async function(n,e){let t=e&&e.body?nt(new Uint8Array(await new Response(e.body).arrayBuffer())):void 0,r=await Nt(n,e&&{method:e.method,headers:e.headers,base64Body:t});return new Response(r.base64Body?Vt(r.base64Body):null,{status:r.status,headers:r.headers})}}je&&_t();var O={};te(O,{confirm:()=>fn,copyToClipboard:()=>Rn,deleteLine:()=>Pn,dispatch:()=>mn,downloadFile:()=>nn,filterBox:()=>an,flashNotification:()=>sn,fold:()=>hn,foldAll:()=>Tn,getCurrentPage:()=>Lt,getCurrentPageMeta:()=>Ot,getCursor:()=>Et,getSelection:()=>Yt,getText:()=>Ht,getUiOption:()=>Zn,goHistory:()=>tn,hidePanel:()=>dn,insertAtCursor:()=>yn,insertAtPos:()=>un,moveCursor:()=>ln,moveCursorToLine:()=>pn,moveLineDown:()=>wn,moveLineUp:()=>Sn,navigate:()=>Mt,newWindow:()=>en,openCommandPalette:()=>Ft,openPageNavigator:()=>jt,openSearchPanel:()=>An,openUrl:()=>$t,prompt:()=>gn,rebuildEditorState:()=>qt,redo:()=>kn,reloadConfigAndCommands:()=>Qt,reloadPage:()=>Jt,reloadUI:()=>zt,replaceRange:()=>cn,save:()=>Ut,setSelection:()=>Dt,setText:()=>Xt,setUiOption:()=>In,showPanel:()=>on,toggleFold:()=>xn,undo:()=>vn,unfold:()=>bn,unfoldAll:()=>Cn,uploadFile:()=>rn,vimEx:()=>Gn});typeof self>"u"&&(self={syscall:()=>{throw new Error("Not implemented here")}});function d(n,...e){return globalThis.syscall(n,...e)}function Lt(){return d("editor.getCurrentPage")}function Ot(){return d("editor.getCurrentPageMeta")}function Ht(){return d("editor.getText")}function Xt(n,e=!1){return d("editor.setText",n,e)}function Et(){return d("editor.getCursor")}function Yt(){return d("editor.getSelection")}function Dt(n,e){return d("editor.setSelection",n,e)}function Ut(){return d("editor.save")}function Mt(n,e=!1,t=!1){return d("editor.navigate",n,e,t)}function jt(n="page"){return d("editor.openPageNavigator",n)}function Ft(){return d("editor.openCommandPalette")}function Jt(){return d("editor.reloadPage")}function zt(){return d("editor.reloadUI")}function qt(){return d("editor.rebuildEditorState")}function Qt(){return d("editor.reloadConfigAndCommands")}function $t(n,e=!1){return d("editor.openUrl",n,e)}function en(){return d("editor.newWindow")}function tn(n){return d("editor.goHistory",n)}function nn(n,e){return d("editor.downloadFile",n,e)}function rn(n,e){return d("editor.uploadFile",n,e)}function sn(n,e="info"){return d("editor.flashNotification",n,e)}function an(n,e,t="",r=""){return d("editor.filterBox",n,e,t,r)}function on(n,e,t,r=""){return d("editor.showPanel",n,e,t,r)}function dn(n){return d("editor.hidePanel",n)}function un(n,e){return d("editor.insertAtPos",n,e)}function cn(n,e,t){return d("editor.replaceRange",n,e,t)}function ln(n,e=!1){return d("editor.moveCursor",n,e)}function pn(n,e=1,t=!1){return d("editor.moveCursorToLine",n,e,t)}function yn(n,e=!1){return d("editor.insertAtCursor",n,e)}function mn(n){return d("editor.dispatch",n)}function gn(n,e=""){return d("editor.prompt",n,e)}function fn(n){return d("editor.confirm",n)}function Zn(n){return d("editor.getUiOption",n)}function In(n,e){return d("editor.setUiOption",n,e)}function hn(){return d("editor.fold")}function bn(){return d("editor.unfold")}function xn(){return d("editor.toggleFold")}function Tn(){return d("editor.foldAll")}function Cn(){return d("editor.unfoldAll")}function vn(){return d("editor.undo")}function kn(){return d("editor.redo")}function An(){return d("editor.openSearchPanel")}function Rn(n){return d("editor.copyToClipboard",n)}function Pn(){return d("editor.deleteLine")}function Sn(){return d("editor.moveLineUp")}function wn(){return d("editor.moveLineDown")}function Gn(n){return d("editor.vimEx",n)}var Se={};te(Se,{deleteAttachment:()=>Yn,deleteFile:()=>Fn,deletePage:()=>_n,fileExists:()=>Jn,getAttachmentMeta:()=>Hn,getFileMeta:()=>Mn,getPageMeta:()=>Bn,listAttachments:()=>On,listFiles:()=>Dn,listPages:()=>Wn,listPlugs:()=>Ln,readAttachment:()=>Xn,readFile:()=>Un,readPage:()=>Vn,writeAttachment:()=>En,writeFile:()=>jn,writePage:()=>Nn});function Wn(){return d("space.listPages")}function Bn(n){return d("space.getPageMeta",n)}function Vn(n){return d("space.readPage",n)}function Nn(n,e){return d("space.writePage",n,e)}function _n(n){return d("space.deletePage",n)}function Ln(){return d("space.listPlugs")}function On(){return d("space.listAttachments")}function Hn(n){return d("space.getAttachmentMeta",n)}function Xn(n){return d("space.readAttachment",n)}function En(n,e){return d("space.writeAttachment",n,e)}function Yn(n){return d("space.deleteAttachment",n)}function Dn(){return d("space.listFiles")}function Un(n){return d("space.readFile",n)}function Mn(n){return d("space.getFileMeta",n)}function jn(n,e){return d("space.writeFile",n,e)}function Fn(n){return d("space.deleteFile",n)}function Jn(n){return d("space.fileExists",n)}var _={};te(_,{applyAttributeExtractors:()=>tr,getEnv:()=>ar,getMode:()=>or,getSpaceConfig:()=>nr,getVersion:()=>ir,invokeCommand:()=>qn,invokeFunction:()=>zn,invokeSpaceFunction:()=>er,listCommands:()=>Qn,listSyscalls:()=>$n,reloadConfig:()=>sr,reloadPlugs:()=>rr});function zn(n,...e){return d("system.invokeFunction",n,...e)}function qn(n,e){return d("system.invokeCommand",n,e)}function Qn(){return d("system.listCommands")}function $n(){return d("system.listSyscalls")}function er(n,...e){return d("system.invokeSpaceFunction",n,...e)}function tr(n,e,t){return d("system.applyAttributeExtractors",n,e,t)}async function nr(n,e){return await d("system.getSpaceConfig",n)??e}function rr(){return d("system.reloadPlugs")}function sr(){return d("system.reloadConfig")}function ar(){return d("system.getEnv")}function or(){return d("system.getMode")}function ir(){return d("system.getVersion")}var E={};te(E,{del:()=>cr,get:()=>ur,set:()=>dr});function dr(n,e){return d("clientStore.set",n,e)}function ur(n){return d("clientStore.get",n)}function cr(n){return d("clientStore.delete",n)}var w={};te(w,{getFileMeta:()=>hr,listFiles:()=>Ir,readAsset:()=>Zr});function fr(n){let e=atob(n),t=e.length,r=new Uint8Array(t);for(let s=0;sjr,DIRTY:()=>Q,EMPTY_PATH:()=>Gr,INVALID:()=>f,NEVER:()=>Gs,OK:()=>P,ParseStatus:()=>R,Schema:()=>h,ZodAny:()=>ie,ZodArray:()=>ee,ZodBigInt:()=>me,ZodBoolean:()=>ge,ZodBranded:()=>Ae,ZodCatch:()=>ke,ZodDate:()=>fe,ZodDefault:()=>ve,ZodDiscriminatedUnion:()=>Be,ZodEffects:()=>z,ZodEnum:()=>de,ZodError:()=>G,ZodFirstPartyTypeKind:()=>pt,ZodFunction:()=>Le,ZodIntersection:()=>be,ZodIssueCode:()=>u,ZodLazy:()=>xe,ZodLiteral:()=>Te,ZodMap:()=>Ne,ZodNaN:()=>Oe,ZodNativeEnum:()=>Ce,ZodNever:()=>M,ZodNull:()=>Ie,ZodNullable:()=>q,ZodNumber:()=>ye,ZodObject:()=>F,ZodOptional:()=>H,ZodParsedType:()=>p,ZodPipeline:()=>Re,ZodPromise:()=>ue,ZodReadonly:()=>Pe,ZodRecord:()=>Ve,ZodSchema:()=>h,ZodSet:()=>_e,ZodString:()=>oe,ZodSymbol:()=>Ke,ZodTransformer:()=>z,ZodTuple:()=>J,ZodType:()=>h,ZodUndefined:()=>Ze,ZodUnion:()=>he,ZodUnknown:()=>$,ZodVoid:()=>We,addIssueToContext:()=>c,any:()=>ns,array:()=>os,bigint:()=>qr,boolean:()=>gt,coerce:()=>ws,custom:()=>lt,date:()=>Qr,datetimeRegex:()=>ut,defaultErrorMap:()=>D,discriminatedUnion:()=>cs,effect:()=>Ts,enum:()=>hs,function:()=>fs,getErrorMap:()=>ne,getParsedType:()=>Y,instanceof:()=>Jr,intersection:()=>ls,isAborted:()=>we,isAsync:()=>se,isDirty:()=>Ge,isValid:()=>re,late:()=>Fr,lazy:()=>Zs,literal:()=>Is,makeIssue:()=>pe,map:()=>ms,nan:()=>zr,nativeEnum:()=>bs,never:()=>ss,null:()=>ts,nullable:()=>vs,number:()=>mt,object:()=>is,objectUtil:()=>st,oboolean:()=>Ss,onumber:()=>Ps,optional:()=>Cs,ostring:()=>Rs,pipeline:()=>As,preprocess:()=>ks,promise:()=>xs,quotelessJson:()=>Pr,record:()=>ys,set:()=>gs,setErrorMap:()=>wr,strictObject:()=>ds,string:()=>yt,symbol:()=>$r,transformer:()=>Ts,tuple:()=>ps,undefined:()=>es,union:()=>us,unknown:()=>rs,util:()=>v,void:()=>as});var v;(k=>{k.assertEqual=x=>x;function e(x){}k.assertIs=e;function t(x){throw new Error}k.assertNever=t,k.arrayToEnum=x=>{let g={};for(let b of x)g[b]=b;return g},k.getValidEnumValues=x=>{let g=(0,k.objectKeys)(x).filter(A=>typeof x[x[A]]!="number"),b={};for(let A of g)b[A]=x[A];return(0,k.objectValues)(b)},k.objectValues=x=>(0,k.objectKeys)(x).map(function(g){return x[g]}),k.objectKeys=typeof Object.keys=="function"?x=>Object.keys(x):x=>{let g=[];for(let b in x)Object.prototype.hasOwnProperty.call(x,b)&&g.push(b);return g},k.find=(x,g)=>{for(let b of x)if(g(b))return b},k.isInteger=typeof Number.isInteger=="function"?x=>Number.isInteger(x):x=>typeof x=="number"&&isFinite(x)&&Math.floor(x)===x;function l(x,g=" | "){return x.map(b=>typeof b=="string"?`'${b}'`:b).join(g)}k.joinValues=l,k.jsonStringifyReplacer=(x,g)=>typeof g=="bigint"?g.toString():g})(v||={});var st;(e=>e.mergeShapes=(t,r)=>({...t,...r}))(st||={});var p=v.arrayToEnum(["string","nan","number","integer","float","boolean","date","bigint","symbol","function","undefined","null","array","object","unknown","promise","void","never","map","set"]),Y=n=>{switch(typeof n){case"undefined":return p.undefined;case"string":return p.string;case"number":return isNaN(n)?p.nan:p.number;case"boolean":return p.boolean;case"function":return p.function;case"bigint":return p.bigint;case"symbol":return p.symbol;case"object":return Array.isArray(n)?p.array:n===null?p.null:n.then&&typeof n.then=="function"&&n.catch&&typeof n.catch=="function"?p.promise:typeof Map<"u"&&n instanceof Map?p.map:typeof Set<"u"&&n instanceof Set?p.set:typeof Date<"u"&&n instanceof Date?p.date:p.object;default:return p.unknown}};var u=v.arrayToEnum(["invalid_type","invalid_literal","custom","invalid_union","invalid_union_discriminator","invalid_enum_value","unrecognized_keys","invalid_arguments","invalid_return_type","invalid_date","invalid_string","too_small","too_big","invalid_intersection_types","not_multiple_of","not_finite"]),Pr=n=>JSON.stringify(n,null,2).replace(/"([^"]+)":/g,"$1:"),G=class n extends Error{issues=[];get errors(){return this.issues}constructor(e){super();let t=new.target.prototype;Object.setPrototypeOf?Object.setPrototypeOf(this,t):this.__proto__=t,this.name="ZodError",this.issues=e}format(e){let t=e||function(a){return a.message},r={_errors:[]},s=a=>{for(let o of a.issues)if(o.code==="invalid_union")o.unionErrors.map(s);else if(o.code==="invalid_return_type")s(o.returnTypeError);else if(o.code==="invalid_arguments")s(o.argumentsError);else if(o.path.length===0)r._errors.push(t(o));else{let i=r,y=0;for(;ynew n(e);static assert(e){if(!(e instanceof n))throw new Error(`Not a ZodError: ${e}`)}toString(){return this.message}get message(){return JSON.stringify(this.issues,v.jsonStringifyReplacer,2)}get isEmpty(){return this.issues.length===0}addIssue=e=>{this.issues=[...this.issues,e]};addIssues=(e=[])=>{this.issues=[...this.issues,...e]};flatten(e=t=>t.message){let t={},r=[];for(let s of this.issues)s.path.length>0?(t[s.path[0]]=t[s.path[0]]||[],t[s.path[0]].push(e(s))):r.push(e(s));return{formErrors:r,fieldErrors:t}}get formErrors(){return this.flatten()}};var Sr=(n,e)=>{let t;switch(n.code){case u.invalid_type:n.received===p.undefined?t="Required":t=`Expected ${n.expected}, received ${n.received}`;break;case u.invalid_literal:t=`Invalid literal value, expected ${JSON.stringify(n.expected,v.jsonStringifyReplacer)}`;break;case u.unrecognized_keys:t=`Unrecognized key(s) in object: ${v.joinValues(n.keys,", ")}`;break;case u.invalid_union:t="Invalid input";break;case u.invalid_union_discriminator:t=`Invalid discriminator value. Expected ${v.joinValues(n.options)}`;break;case u.invalid_enum_value:t=`Invalid enum value. Expected ${v.joinValues(n.options)}, received '${n.received}'`;break;case u.invalid_arguments:t="Invalid function arguments";break;case u.invalid_return_type:t="Invalid function return type";break;case u.invalid_date:t="Invalid date";break;case u.invalid_string:typeof n.validation=="object"?"includes"in n.validation?(t=`Invalid input: must include "${n.validation.includes}"`,typeof n.validation.position=="number"&&(t=`${t} at one or more positions greater than or equal to ${n.validation.position}`)):"startsWith"in n.validation?t=`Invalid input: must start with "${n.validation.startsWith}"`:"endsWith"in n.validation?t=`Invalid input: must end with "${n.validation.endsWith}"`:v.assertNever(n.validation):n.validation!=="regex"?t=`Invalid ${n.validation}`:t="Invalid";break;case u.too_small:n.type==="array"?t=`Array must contain ${n.exact?"exactly":n.inclusive?"at least":"more than"} ${n.minimum} element(s)`:n.type==="string"?t=`String must contain ${n.exact?"exactly":n.inclusive?"at least":"over"} ${n.minimum} character(s)`:n.type==="number"?t=`Number must be ${n.exact?"exactly equal to ":n.inclusive?"greater than or equal to ":"greater than "}${n.minimum}`:n.type==="date"?t=`Date must be ${n.exact?"exactly equal to ":n.inclusive?"greater than or equal to ":"greater than "}${new Date(Number(n.minimum))}`:t="Invalid input";break;case u.too_big:n.type==="array"?t=`Array must contain ${n.exact?"exactly":n.inclusive?"at most":"less than"} ${n.maximum} element(s)`:n.type==="string"?t=`String must contain ${n.exact?"exactly":n.inclusive?"at most":"under"} ${n.maximum} character(s)`:n.type==="number"?t=`Number must be ${n.exact?"exactly":n.inclusive?"less than or equal to":"less than"} ${n.maximum}`:n.type==="bigint"?t=`BigInt must be ${n.exact?"exactly":n.inclusive?"less than or equal to":"less than"} ${n.maximum}`:n.type==="date"?t=`Date must be ${n.exact?"exactly":n.inclusive?"smaller than or equal to":"smaller than"} ${new Date(Number(n.maximum))}`:t="Invalid input";break;case u.custom:t="Invalid input";break;case u.invalid_intersection_types:t="Intersection results could not be merged";break;case u.not_multiple_of:t=`Number must be a multiple of ${n.multipleOf}`;break;case u.not_finite:t="Number must be finite";break;default:t=e.defaultError,v.assertNever(n)}return{message:t}},D=Sr;var at=D;function wr(n){at=n}function ne(){return at}var pe=n=>{let{data:e,path:t,errorMaps:r,issueData:s}=n,a=[...t,...s.path||[]],o={...s,path:a};if(s.message!==void 0)return{...s,path:a,message:s.message};let i="",y=r.filter(l=>!!l).slice().reverse();for(let l of y)i=l(o,{data:e,defaultError:i}).message;return{...s,path:a,message:i}},Gr=[];function c(n,e){let t=ne(),r=pe({issueData:e,data:n.data,path:n.path,errorMaps:[n.common.contextualErrorMap,n.schemaErrorMap,t,t===D?void 0:D].filter(s=>!!s)});n.common.issues.push(r)}var R=class n{value="valid";dirty(){this.value==="valid"&&(this.value="dirty")}abort(){this.value!=="aborted"&&(this.value="aborted")}static mergeArray(e,t){let r=[];for(let s of t){if(s.status==="aborted")return f;s.status==="dirty"&&e.dirty(),r.push(s.value)}return{status:e.value,value:r}}static async mergeObjectAsync(e,t){let r=[];for(let s of t){let a=await s.key,o=await s.value;r.push({key:a,value:o})}return n.mergeObjectSync(e,r)}static mergeObjectSync(e,t){let r={};for(let s of t){let{key:a,value:o}=s;if(a.status==="aborted"||o.status==="aborted")return f;a.status==="dirty"&&e.dirty(),o.status==="dirty"&&e.dirty(),a.value!=="__proto__"&&(typeof o.value<"u"||s.alwaysSet)&&(r[a.value]=o.value)}return{status:e.value,value:r}}},f=Object.freeze({status:"aborted"}),Q=n=>({status:"dirty",value:n}),P=n=>({status:"valid",value:n}),we=n=>n.status==="aborted",Ge=n=>n.status==="dirty",re=n=>n.status==="valid",se=n=>typeof Promise<"u"&&n instanceof Promise;var m;(t=>(t.errToObj=r=>typeof r=="string"?{message:r}:r||{},t.toString=r=>typeof r=="string"?r:r?.message))(m||={});var V=class{parent;data;_path;_key;_cachedPath=[];constructor(e,t,r,s){this.parent=e,this.data=t,this._path=r,this._key=s}get path(){return this._cachedPath.length||(this._key instanceof Array?this._cachedPath.push(...this._path,...this._key):this._cachedPath.push(...this._path,this._key)),this._cachedPath}},ot=(n,e)=>{if(re(e))return{success:!0,data:e.value};if(!n.common.issues.length)throw new Error("Validation failed but no issues detected.");return{success:!1,get error(){if(this._error)return this._error;let t=new G(n.common.issues);return this._error=t,this._error}}};function Z(n){if(!n)return{};let{errorMap:e,invalid_type_error:t,required_error:r,description:s}=n;if(e&&(t||r))throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`);return e?{errorMap:e,description:s}:{errorMap:(o,i)=>{let{message:y}=n;return o.code==="invalid_enum_value"?{message:y??i.defaultError}:typeof i.data>"u"?{message:y??r??i.defaultError}:o.code!=="invalid_type"?{message:i.defaultError}:{message:y??t??i.defaultError}},description:s}}var h=class{_type;_output;_input;_def;get description(){return this._def.description}_getType(e){return Y(e.data)}_getOrReturnCtx(e,t){return t||{common:e.parent.common,data:e.data,parsedType:Y(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}_processInputParams(e){return{status:new R,ctx:{common:e.parent.common,data:e.data,parsedType:Y(e.data),schemaErrorMap:this._def.errorMap,path:e.path,parent:e.parent}}}_parseSync(e){let t=this._parse(e);if(se(t))throw new Error("Synchronous parse encountered promise.");return t}_parseAsync(e){let t=this._parse(e);return Promise.resolve(t)}parse(e,t){let r=this.safeParse(e,t);if(r.success)return r.data;throw r.error}safeParse(e,t){let r={common:{issues:[],async:t?.async??!1,contextualErrorMap:t?.errorMap},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Y(e)},s=this._parseSync({data:e,path:r.path,parent:r});return ot(r,s)}async parseAsync(e,t){let r=await this.safeParseAsync(e,t);if(r.success)return r.data;throw r.error}async safeParseAsync(e,t){let r={common:{issues:[],contextualErrorMap:t?.errorMap,async:!0},path:t?.path||[],schemaErrorMap:this._def.errorMap,parent:null,data:e,parsedType:Y(e)},s=this._parse({data:e,path:r.path,parent:r}),a=await(se(s)?s:Promise.resolve(s));return ot(r,a)}spa=this.safeParseAsync;refine(e,t){let r=s=>typeof t=="string"||typeof t>"u"?{message:t}:typeof t=="function"?t(s):t;return this._refinement((s,a)=>{let o=e(s),i=()=>a.addIssue({code:u.custom,...r(s)});return typeof Promise<"u"&&o instanceof Promise?o.then(y=>y?!0:(i(),!1)):o?!0:(i(),!1)})}refinement(e,t){return this._refinement((r,s)=>e(r)?!0:(s.addIssue(typeof t=="function"?t(r,s):t),!1))}_refinement(e){return new z({schema:this,typeName:"ZodEffects",effect:{type:"refinement",refinement:e}})}superRefine(e){return this._refinement(e)}constructor(e){this._def=e,this.parse=this.parse.bind(this),this.safeParse=this.safeParse.bind(this),this.parseAsync=this.parseAsync.bind(this),this.safeParseAsync=this.safeParseAsync.bind(this),this.spa=this.spa.bind(this),this.refine=this.refine.bind(this),this.refinement=this.refinement.bind(this),this.superRefine=this.superRefine.bind(this),this.optional=this.optional.bind(this),this.nullable=this.nullable.bind(this),this.nullish=this.nullish.bind(this),this.array=this.array.bind(this),this.promise=this.promise.bind(this),this.or=this.or.bind(this),this.and=this.and.bind(this),this.transform=this.transform.bind(this),this.brand=this.brand.bind(this),this.default=this.default.bind(this),this.catch=this.catch.bind(this),this.describe=this.describe.bind(this),this.pipe=this.pipe.bind(this),this.readonly=this.readonly.bind(this),this.isNullable=this.isNullable.bind(this),this.isOptional=this.isOptional.bind(this)}optional(){return H.create(this,this._def)}nullable(){return q.create(this,this._def)}nullish(){return this.nullable().optional()}array(){return ee.create(this,this._def)}promise(){return ue.create(this,this._def)}or(e){return he.create([this,e],this._def)}and(e){return be.create(this,e,this._def)}transform(e){return new z({...Z(this._def),schema:this,typeName:"ZodEffects",effect:{type:"transform",transform:e}})}default(e){let t=typeof e=="function"?e:()=>e;return new ve({...Z(this._def),innerType:this,defaultValue:t,typeName:"ZodDefault"})}brand(){return new Ae({typeName:"ZodBranded",type:this,...Z(this._def)})}catch(e){let t=typeof e=="function"?e:()=>e;return new ke({...Z(this._def),innerType:this,catchValue:t,typeName:"ZodCatch"})}describe(e){let t=this.constructor;return new t({...this._def,description:e})}pipe(e){return Re.create(this,e)}readonly(){return Pe.create(this)}isOptional(){return this.safeParse(void 0).success}isNullable(){return this.safeParse(null).success}},Kr=/^c[^\s-]{8,}$/i,Wr=/^[0-9a-z]+$/,Br=/^[0-9A-HJKMNP-TV-Z]{26}$/,Vr=/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i,Nr=/^[a-z0-9_-]{21}$/i,_r=/^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/,Lr=/^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i,Or="^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$",Fe,Hr=/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,Xr=/^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/,Er=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,it="((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))",Yr=new RegExp(`^${it}$`);function dt(n){let e="([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d";return n.precision?e=`${e}\\.\\d{${n.precision}}`:n.precision==null&&(e=`${e}(\\.\\d+)?`),e}function Dr(n){return new RegExp(`^${dt(n)}$`)}function ut(n){let e=`${it}T${dt(n)}`,t=[];return t.push(n.local?"Z?":"Z"),n.offset&&t.push("([+-]\\d{2}:?\\d{2})"),e=`${e}(${t.join("|")})`,new RegExp(`^${e}$`)}function Ur(n,e){return!!((e==="v4"||!e)&&Hr.test(n)||(e==="v6"||!e)&&Xr.test(n))}var oe=class n extends h{_parse(e){if(this._def.coerce&&(e.data=String(e.data)),this._getType(e)!==p.string){let a=this._getOrReturnCtx(e);return c(a,{code:u.invalid_type,expected:p.string,received:a.parsedType}),f}let r=new R,s;for(let a of this._def.checks)if(a.kind==="min")e.data.lengtha.value&&(s=this._getOrReturnCtx(e,s),c(s,{code:u.too_big,maximum:a.value,type:"string",inclusive:!0,exact:!1,message:a.message}),r.dirty());else if(a.kind==="length"){let o=e.data.length>a.value,i=e.data.lengthe.test(s),{validation:t,code:u.invalid_string,...m.errToObj(r)})}_addCheck(e){return new n({...this._def,checks:[...this._def.checks,e]})}email(e){return this._addCheck({kind:"email",...m.errToObj(e)})}url(e){return this._addCheck({kind:"url",...m.errToObj(e)})}emoji(e){return this._addCheck({kind:"emoji",...m.errToObj(e)})}uuid(e){return this._addCheck({kind:"uuid",...m.errToObj(e)})}nanoid(e){return this._addCheck({kind:"nanoid",...m.errToObj(e)})}cuid(e){return this._addCheck({kind:"cuid",...m.errToObj(e)})}cuid2(e){return this._addCheck({kind:"cuid2",...m.errToObj(e)})}ulid(e){return this._addCheck({kind:"ulid",...m.errToObj(e)})}base64(e){return this._addCheck({kind:"base64",...m.errToObj(e)})}ip(e){return this._addCheck({kind:"ip",...m.errToObj(e)})}datetime(e){return typeof e=="string"?this._addCheck({kind:"datetime",precision:null,offset:!1,local:!1,message:e}):this._addCheck({kind:"datetime",precision:typeof e?.precision>"u"?null:e?.precision,offset:e?.offset??!1,local:e?.local??!1,...m.errToObj(e?.message)})}date(e){return this._addCheck({kind:"date",message:e})}time(e){return typeof e=="string"?this._addCheck({kind:"time",precision:null,message:e}):this._addCheck({kind:"time",precision:typeof e?.precision>"u"?null:e?.precision,...m.errToObj(e?.message)})}duration(e){return this._addCheck({kind:"duration",...m.errToObj(e)})}regex(e,t){return this._addCheck({kind:"regex",regex:e,...m.errToObj(t)})}includes(e,t){return this._addCheck({kind:"includes",value:e,position:t?.position,...m.errToObj(t?.message)})}startsWith(e,t){return this._addCheck({kind:"startsWith",value:e,...m.errToObj(t)})}endsWith(e,t){return this._addCheck({kind:"endsWith",value:e,...m.errToObj(t)})}min(e,t){return this._addCheck({kind:"min",value:e,...m.errToObj(t)})}max(e,t){return this._addCheck({kind:"max",value:e,...m.errToObj(t)})}length(e,t){return this._addCheck({kind:"length",value:e,...m.errToObj(t)})}nonempty(e){return this.min(1,m.errToObj(e))}trim(){return new n({...this._def,checks:[...this._def.checks,{kind:"trim"}]})}toLowerCase(){return new n({...this._def,checks:[...this._def.checks,{kind:"toLowerCase"}]})}toUpperCase(){return new n({...this._def,checks:[...this._def.checks,{kind:"toUpperCase"}]})}get isDatetime(){return!!this._def.checks.find(e=>e.kind==="datetime")}get isDate(){return!!this._def.checks.find(e=>e.kind==="date")}get isTime(){return!!this._def.checks.find(e=>e.kind==="time")}get isDuration(){return!!this._def.checks.find(e=>e.kind==="duration")}get isEmail(){return!!this._def.checks.find(e=>e.kind==="email")}get isURL(){return!!this._def.checks.find(e=>e.kind==="url")}get isEmoji(){return!!this._def.checks.find(e=>e.kind==="emoji")}get isUUID(){return!!this._def.checks.find(e=>e.kind==="uuid")}get isNANOID(){return!!this._def.checks.find(e=>e.kind==="nanoid")}get isCUID(){return!!this._def.checks.find(e=>e.kind==="cuid")}get isCUID2(){return!!this._def.checks.find(e=>e.kind==="cuid2")}get isULID(){return!!this._def.checks.find(e=>e.kind==="ulid")}get isIP(){return!!this._def.checks.find(e=>e.kind==="ip")}get isBase64(){return!!this._def.checks.find(e=>e.kind==="base64")}get minLength(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxLength(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuenew n({checks:[],typeName:"ZodString",coerce:e?.coerce??!1,...Z(e)})};function Mr(n,e){let t=(n.toString().split(".")[1]||"").length,r=(e.toString().split(".")[1]||"").length,s=t>r?t:r,a=parseInt(n.toFixed(s).replace(".","")),o=parseInt(e.toFixed(s).replace(".",""));return a%o/Math.pow(10,s)}var ye=class n extends h{_parse(e){if(this._def.coerce&&(e.data=Number(e.data)),this._getType(e)!==p.number){let a=this._getOrReturnCtx(e);return c(a,{code:u.invalid_type,expected:p.number,received:a.parsedType}),f}let r,s=new R;for(let a of this._def.checks)a.kind==="int"?v.isInteger(e.data)||(r=this._getOrReturnCtx(e,r),c(r,{code:u.invalid_type,expected:"integer",received:"float",message:a.message}),s.dirty()):a.kind==="min"?(a.inclusive?e.dataa.value:e.data>=a.value)&&(r=this._getOrReturnCtx(e,r),c(r,{code:u.too_big,maximum:a.value,type:"number",inclusive:a.inclusive,exact:!1,message:a.message}),s.dirty()):a.kind==="multipleOf"?Mr(e.data,a.value)!==0&&(r=this._getOrReturnCtx(e,r),c(r,{code:u.not_multiple_of,multipleOf:a.value,message:a.message}),s.dirty()):a.kind==="finite"?Number.isFinite(e.data)||(r=this._getOrReturnCtx(e,r),c(r,{code:u.not_finite,message:a.message}),s.dirty()):v.assertNever(a);return{status:s.value,value:e.data}}static create=e=>new n({checks:[],typeName:"ZodNumber",coerce:e?.coerce||!1,...Z(e)});gte(e,t){return this.setLimit("min",e,!0,m.toString(t))}min=this.gte;gt(e,t){return this.setLimit("min",e,!1,m.toString(t))}lte(e,t){return this.setLimit("max",e,!0,m.toString(t))}max=this.lte;lt(e,t){return this.setLimit("max",e,!1,m.toString(t))}setLimit(e,t,r,s){return new n({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:r,message:m.toString(s)}]})}_addCheck(e){return new n({...this._def,checks:[...this._def.checks,e]})}int(e){return this._addCheck({kind:"int",message:m.toString(e)})}positive(e){return this._addCheck({kind:"min",value:0,inclusive:!1,message:m.toString(e)})}negative(e){return this._addCheck({kind:"max",value:0,inclusive:!1,message:m.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:0,inclusive:!0,message:m.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:0,inclusive:!0,message:m.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:m.toString(t)})}step=this.multipleOf;finite(e){return this._addCheck({kind:"finite",message:m.toString(e)})}safe(e){return this._addCheck({kind:"min",inclusive:!0,value:Number.MIN_SAFE_INTEGER,message:m.toString(e)})._addCheck({kind:"max",inclusive:!0,value:Number.MAX_SAFE_INTEGER,message:m.toString(e)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuee.kind==="int"||e.kind==="multipleOf"&&v.isInteger(e.value))}get isFinite(){let e=null,t=null;for(let r of this._def.checks){if(r.kind==="finite"||r.kind==="int"||r.kind==="multipleOf")return!0;r.kind==="min"?(t===null||r.value>t)&&(t=r.value):r.kind==="max"&&(e===null||r.valuea.value:e.data>=a.value)&&(r=this._getOrReturnCtx(e,r),c(r,{code:u.too_big,type:"bigint",maximum:a.value,inclusive:a.inclusive,message:a.message}),s.dirty()):a.kind==="multipleOf"?e.data%a.value!==BigInt(0)&&(r=this._getOrReturnCtx(e,r),c(r,{code:u.not_multiple_of,multipleOf:a.value,message:a.message}),s.dirty()):v.assertNever(a);return{status:s.value,value:e.data}}static create=e=>new n({checks:[],typeName:"ZodBigInt",coerce:e?.coerce??!1,...Z(e)});gte(e,t){return this.setLimit("min",e,!0,m.toString(t))}min=this.gte;gt(e,t){return this.setLimit("min",e,!1,m.toString(t))}lte(e,t){return this.setLimit("max",e,!0,m.toString(t))}max=this.lte;lt(e,t){return this.setLimit("max",e,!1,m.toString(t))}setLimit(e,t,r,s){return new n({...this._def,checks:[...this._def.checks,{kind:e,value:t,inclusive:r,message:m.toString(s)}]})}_addCheck(e){return new n({...this._def,checks:[...this._def.checks,e]})}positive(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!1,message:m.toString(e)})}negative(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!1,message:m.toString(e)})}nonpositive(e){return this._addCheck({kind:"max",value:BigInt(0),inclusive:!0,message:m.toString(e)})}nonnegative(e){return this._addCheck({kind:"min",value:BigInt(0),inclusive:!0,message:m.toString(e)})}multipleOf(e,t){return this._addCheck({kind:"multipleOf",value:e,message:m.toString(t)})}get minValue(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e}get maxValue(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuenew n({typeName:"ZodBoolean",coerce:e?.coerce||!1,...Z(e)})},fe=class n extends h{_parse(e){if(this._def.coerce&&(e.data=new Date(e.data)),this._getType(e)!==p.date){let a=this._getOrReturnCtx(e);return c(a,{code:u.invalid_type,expected:p.date,received:a.parsedType}),f}if(isNaN(e.data.getTime())){let a=this._getOrReturnCtx(e);return c(a,{code:u.invalid_date}),f}let r=new R,s;for(let a of this._def.checks)a.kind==="min"?e.data.getTime()a.value&&(s=this._getOrReturnCtx(e,s),c(s,{code:u.too_big,message:a.message,inclusive:!0,exact:!1,maximum:a.value,type:"date"}),r.dirty()):v.assertNever(a);return{status:r.value,value:new Date(e.data.getTime())}}_addCheck(e){return new n({...this._def,checks:[...this._def.checks,e]})}min(e,t){return this._addCheck({kind:"min",value:e.getTime(),message:m.toString(t)})}max(e,t){return this._addCheck({kind:"max",value:e.getTime(),message:m.toString(t)})}get minDate(){let e=null;for(let t of this._def.checks)t.kind==="min"&&(e===null||t.value>e)&&(e=t.value);return e!=null?new Date(e):null}get maxDate(){let e=null;for(let t of this._def.checks)t.kind==="max"&&(e===null||t.valuenew n({checks:[],coerce:e?.coerce||!1,typeName:"ZodDate",...Z(e)})},Ke=class n extends h{_parse(e){if(this._getType(e)!==p.symbol){let r=this._getOrReturnCtx(e);return c(r,{code:u.invalid_type,expected:p.symbol,received:r.parsedType}),f}return P(e.data)}static create=e=>new n({typeName:"ZodSymbol",...Z(e)})},Ze=class n extends h{_parse(e){if(this._getType(e)!==p.undefined){let r=this._getOrReturnCtx(e);return c(r,{code:u.invalid_type,expected:p.undefined,received:r.parsedType}),f}return P(e.data)}params;static create=e=>new n({typeName:"ZodUndefined",...Z(e)})},Ie=class n extends h{_parse(e){if(this._getType(e)!==p.null){let r=this._getOrReturnCtx(e);return c(r,{code:u.invalid_type,expected:p.null,received:r.parsedType}),f}return P(e.data)}static create=e=>new n({typeName:"ZodNull",...Z(e)})},ie=class n extends h{_any=!0;_parse(e){return P(e.data)}static create=e=>new n({typeName:"ZodAny",...Z(e)})},$=class n extends h{_unknown=!0;_parse(e){return P(e.data)}static create=e=>new n({typeName:"ZodUnknown",...Z(e)})},M=class n extends h{_parse(e){let t=this._getOrReturnCtx(e);return c(t,{code:u.invalid_type,expected:p.never,received:t.parsedType}),f}static create=e=>new n({typeName:"ZodNever",...Z(e)})},We=class n extends h{_parse(e){if(this._getType(e)!==p.undefined){let r=this._getOrReturnCtx(e);return c(r,{code:u.invalid_type,expected:p.void,received:r.parsedType}),f}return P(e.data)}static create=e=>new n({typeName:"ZodVoid",...Z(e)})},ee=class n extends h{_parse(e){let{ctx:t,status:r}=this._processInputParams(e),s=this._def;if(t.parsedType!==p.array)return c(t,{code:u.invalid_type,expected:p.array,received:t.parsedType}),f;if(s.exactLength!==null){let o=t.data.length>s.exactLength.value,i=t.data.lengths.maxLength.value&&(c(t,{code:u.too_big,maximum:s.maxLength.value,type:"array",inclusive:!0,exact:!1,message:s.maxLength.message}),r.dirty()),t.common.async)return Promise.all([...t.data].map((o,i)=>s.type._parseAsync(new V(t,o,t.path,i)))).then(o=>R.mergeArray(r,o));let a=[...t.data].map((o,i)=>s.type._parseSync(new V(t,o,t.path,i)));return R.mergeArray(r,a)}get element(){return this._def.type}min(e,t){return new n({...this._def,minLength:{value:e,message:m.toString(t)}})}max(e,t){return new n({...this._def,maxLength:{value:e,message:m.toString(t)}})}length(e,t){return new n({...this._def,exactLength:{value:e,message:m.toString(t)}})}nonempty(e){return this.min(1,e)}static create=(e,t)=>new n({type:e,minLength:null,maxLength:null,exactLength:null,typeName:"ZodArray",...Z(t)})};function ae(n){if(n instanceof F){let e={};for(let t in n.shape){let r=n.shape[t];e[t]=H.create(ae(r))}return new F({...n._def,shape:()=>e})}else return n instanceof ee?new ee({...n._def,type:ae(n.element)}):n instanceof H?H.create(ae(n.unwrap())):n instanceof q?q.create(ae(n.unwrap())):n instanceof J?J.create(n.items.map(e=>ae(e))):n}var F=class n extends h{_cached=null;_getCached(){if(this._cached!==null)return this._cached;let e=this._def.shape(),t=v.objectKeys(e);return this._cached={shape:e,keys:t}}_parse(e){if(this._getType(e)!==p.object){let l=this._getOrReturnCtx(e);return c(l,{code:u.invalid_type,expected:p.object,received:l.parsedType}),f}let{status:r,ctx:s}=this._processInputParams(e),{shape:a,keys:o}=this._getCached(),i=[];if(!(this._def.catchall instanceof M&&this._def.unknownKeys==="strip"))for(let l in s.data)o.includes(l)||i.push(l);let y=[];for(let l of o){let I=a[l],k=s.data[l];y.push({key:{status:"valid",value:l},value:I._parse(new V(s,k,s.path,l)),alwaysSet:l in s.data})}if(this._def.catchall instanceof M){let l=this._def.unknownKeys;if(l==="passthrough")for(let I of i)y.push({key:{status:"valid",value:I},value:{status:"valid",value:s.data[I]}});else if(l==="strict")i.length>0&&(c(s,{code:u.unrecognized_keys,keys:i}),r.dirty());else if(l!=="strip")throw new Error("Internal ZodObject error: invalid unknownKeys value.")}else{let l=this._def.catchall;for(let I of i){let k=s.data[I];y.push({key:{status:"valid",value:I},value:l._parse(new V(s,k,s.path,I)),alwaysSet:I in s.data})}}return s.common.async?Promise.resolve().then(async()=>{let l=[];for(let I of y){let k=await I.key,x=await I.value;l.push({key:k,value:x,alwaysSet:I.alwaysSet})}return l}).then(l=>R.mergeObjectSync(r,l)):R.mergeObjectSync(r,y)}get shape(){return this._def.shape()}strict(e){return m.errToObj,new n({...this._def,unknownKeys:"strict",...e!==void 0?{errorMap:(t,r)=>{let s=this._def.errorMap?.(t,r).message??r.defaultError;return t.code==="unrecognized_keys"?{message:m.errToObj(e).message??s}:{message:s}}}:{}})}strip(){return new n({...this._def,unknownKeys:"strip"})}passthrough(){return new n({...this._def,unknownKeys:"passthrough"})}nonstrict=this.passthrough;extend(e){return new n({...this._def,shape:()=>({...this._def.shape(),...e})})}augment=this.extend;merge(e){return new n({unknownKeys:e._def.unknownKeys,catchall:e._def.catchall,shape:()=>({...this._def.shape(),...e._def.shape()}),typeName:"ZodObject"})}setKey(e,t){return this.augment({[e]:t})}catchall(e){return new n({...this._def,catchall:e})}pick(e){let t={};return v.objectKeys(e).forEach(r=>{e[r]&&this.shape[r]&&(t[r]=this.shape[r])}),new n({...this._def,shape:()=>t})}omit(e){let t={};return v.objectKeys(this.shape).forEach(r=>{e[r]||(t[r]=this.shape[r])}),new n({...this._def,shape:()=>t})}deepPartial(){return ae(this)}partial(e){let t={};return v.objectKeys(this.shape).forEach(r=>{let s=this.shape[r];e&&!e[r]?t[r]=s:t[r]=s.optional()}),new n({...this._def,shape:()=>t})}required(e){let t={};return v.objectKeys(this.shape).forEach(r=>{if(e&&!e[r])t[r]=this.shape[r];else{let a=this.shape[r];for(;a instanceof H;)a=a._def.innerType;t[r]=a}}),new n({...this._def,shape:()=>t})}keyof(){return ct(v.objectKeys(this.shape))}static create=(e,t)=>new n({shape:()=>e,unknownKeys:"strip",catchall:M.create(),typeName:"ZodObject",...Z(t)});static strictCreate=(e,t)=>new n({shape:()=>e,unknownKeys:"strict",catchall:M.create(),typeName:"ZodObject",...Z(t)});static lazycreate=(e,t)=>new n({shape:e,unknownKeys:"strip",catchall:M.create(),typeName:"ZodObject",...Z(t)})},he=class n extends h{_parse(e){let{ctx:t}=this._processInputParams(e),r=this._def.options;function s(a){for(let i of a)if(i.result.status==="valid")return i.result;for(let i of a)if(i.result.status==="dirty")return t.common.issues.push(...i.ctx.common.issues),i.result;let o=a.map(i=>new G(i.ctx.common.issues));return c(t,{code:u.invalid_union,unionErrors:o}),f}if(t.common.async)return Promise.all(r.map(async a=>{let o={...t,common:{...t.common,issues:[]},parent:null};return{result:await a._parseAsync({data:t.data,path:t.path,parent:o}),ctx:o}})).then(s);{let a,o=[];for(let y of r){let l={...t,common:{...t.common,issues:[]},parent:null},I=y._parseSync({data:t.data,path:t.path,parent:l});if(I.status==="valid")return I;I.status==="dirty"&&!a&&(a={result:I,ctx:l}),l.common.issues.length&&o.push(l.common.issues)}if(a)return t.common.issues.push(...a.ctx.common.issues),a.result;let i=o.map(y=>new G(y));return c(t,{code:u.invalid_union,unionErrors:i}),f}}get options(){return this._def.options}static create=(e,t)=>new n({options:e,typeName:"ZodUnion",...Z(t)})},U=n=>n instanceof xe?U(n.schema):n instanceof z?U(n.innerType()):n instanceof Te?[n.value]:n instanceof de?n.options:n instanceof Ce?v.objectValues(n.enum):n instanceof ve?U(n._def.innerType):n instanceof Ze?[void 0]:n instanceof Ie?[null]:n instanceof H?[void 0,...U(n.unwrap())]:n instanceof q?[null,...U(n.unwrap())]:n instanceof Ae||n instanceof Pe?U(n.unwrap()):n instanceof ke?U(n._def.innerType):[],Be=class n extends h{_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==p.object)return c(t,{code:u.invalid_type,expected:p.object,received:t.parsedType}),f;let r=this.discriminator,s=t.data[r],a=this.optionsMap.get(s);return a?t.common.async?a._parseAsync({data:t.data,path:t.path,parent:t}):a._parseSync({data:t.data,path:t.path,parent:t}):(c(t,{code:u.invalid_union_discriminator,options:Array.from(this.optionsMap.keys()),path:[r]}),f)}get discriminator(){return this._def.discriminator}get options(){return this._def.options}get optionsMap(){return this._def.optionsMap}static create(e,t,r){let s=new Map;for(let a of t){let o=U(a.shape[e]);if(!o.length)throw new Error(`A discriminator value for key \`${e}\` could not be extracted from all schema options`);for(let i of o){if(s.has(i))throw new Error(`Discriminator property ${String(e)} has duplicate value ${String(i)}`);s.set(i,a)}}return new n({typeName:"ZodDiscriminatedUnion",discriminator:e,options:t,optionsMap:s,...Z(r)})}};function Je(n,e){let t=Y(n),r=Y(e);if(n===e)return{valid:!0,data:n};if(t===p.object&&r===p.object){let s=v.objectKeys(e),a=v.objectKeys(n).filter(i=>s.indexOf(i)!==-1),o={...n,...e};for(let i of a){let y=Je(n[i],e[i]);if(!y.valid)return{valid:!1};o[i]=y.data}return{valid:!0,data:o}}else if(t===p.array&&r===p.array){if(n.length!==e.length)return{valid:!1};let s=[];for(let a=0;a{if(we(a)||we(o))return f;let i=Je(a.value,o.value);return i.valid?((Ge(a)||Ge(o))&&t.dirty(),{status:t.value,value:i.data}):(c(r,{code:u.invalid_intersection_types}),f)};return r.common.async?Promise.all([this._def.left._parseAsync({data:r.data,path:r.path,parent:r}),this._def.right._parseAsync({data:r.data,path:r.path,parent:r})]).then(([a,o])=>s(a,o)):s(this._def.left._parseSync({data:r.data,path:r.path,parent:r}),this._def.right._parseSync({data:r.data,path:r.path,parent:r}))}static create=(e,t,r)=>new n({left:e,right:t,typeName:"ZodIntersection",...Z(r)})},J=class n extends h{_parse(e){let{status:t,ctx:r}=this._processInputParams(e);if(r.parsedType!==p.array)return c(r,{code:u.invalid_type,expected:p.array,received:r.parsedType}),f;if(r.data.lengththis._def.items.length&&(c(r,{code:u.too_big,maximum:this._def.items.length,inclusive:!0,exact:!1,type:"array"}),t.dirty());let a=[...r.data].map((o,i)=>{let y=this._def.items[i]||this._def.rest;return y?y._parse(new V(r,o,r.path,i)):null}).filter(o=>!!o);return r.common.async?Promise.all(a).then(o=>R.mergeArray(t,o)):R.mergeArray(t,a)}get items(){return this._def.items}rest(e){return new n({...this._def,rest:e})}static create=(e,t)=>{if(!Array.isArray(e))throw new Error("You must pass an array of schemas to z.tuple([ ... ])");return new n({items:e,typeName:"ZodTuple",rest:null,...Z(t)})}},Ve=class n extends h{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:r}=this._processInputParams(e);if(r.parsedType!==p.object)return c(r,{code:u.invalid_type,expected:p.object,received:r.parsedType}),f;let s=[],a=this._def.keyType,o=this._def.valueType;for(let i in r.data)s.push({key:a._parse(new V(r,i,r.path,i)),value:o._parse(new V(r,r.data[i],r.path,i)),alwaysSet:i in r.data});return r.common.async?R.mergeObjectAsync(t,s):R.mergeObjectSync(t,s)}get element(){return this._def.valueType}static create(e,t,r){return t instanceof h?new n({keyType:e,valueType:t,typeName:"ZodRecord",...Z(r)}):new n({keyType:oe.create(),valueType:e,typeName:"ZodRecord",...Z(t)})}},Ne=class n extends h{get keySchema(){return this._def.keyType}get valueSchema(){return this._def.valueType}_parse(e){let{status:t,ctx:r}=this._processInputParams(e);if(r.parsedType!==p.map)return c(r,{code:u.invalid_type,expected:p.map,received:r.parsedType}),f;let s=this._def.keyType,a=this._def.valueType,o=[...r.data.entries()].map(([i,y],l)=>({key:s._parse(new V(r,i,r.path,[l,"key"])),value:a._parse(new V(r,y,r.path,[l,"value"]))}));if(r.common.async){let i=new Map;return Promise.resolve().then(async()=>{for(let y of o){let l=await y.key,I=await y.value;if(l.status==="aborted"||I.status==="aborted")return f;(l.status==="dirty"||I.status==="dirty")&&t.dirty(),i.set(l.value,I.value)}return{status:t.value,value:i}})}else{let i=new Map;for(let y of o){let l=y.key,I=y.value;if(l.status==="aborted"||I.status==="aborted")return f;(l.status==="dirty"||I.status==="dirty")&&t.dirty(),i.set(l.value,I.value)}return{status:t.value,value:i}}}static create=(e,t,r)=>new n({valueType:t,keyType:e,typeName:"ZodMap",...Z(r)})},_e=class n extends h{_parse(e){let{status:t,ctx:r}=this._processInputParams(e);if(r.parsedType!==p.set)return c(r,{code:u.invalid_type,expected:p.set,received:r.parsedType}),f;let s=this._def;s.minSize!==null&&r.data.sizes.maxSize.value&&(c(r,{code:u.too_big,maximum:s.maxSize.value,type:"set",inclusive:!0,exact:!1,message:s.maxSize.message}),t.dirty());let a=this._def.valueType;function o(y){let l=new Set;for(let I of y){if(I.status==="aborted")return f;I.status==="dirty"&&t.dirty(),l.add(I.value)}return{status:t.value,value:l}}let i=[...r.data.values()].map((y,l)=>a._parse(new V(r,y,r.path,l)));return r.common.async?Promise.all(i).then(y=>o(y)):o(i)}min(e,t){return new n({...this._def,minSize:{value:e,message:m.toString(t)}})}max(e,t){return new n({...this._def,maxSize:{value:e,message:m.toString(t)}})}size(e,t){return this.min(e,t).max(e,t)}nonempty(e){return this.min(1,e)}static create=(e,t)=>new n({valueType:e,minSize:null,maxSize:null,typeName:"ZodSet",...Z(t)})},Le=class n extends h{_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==p.function)return c(t,{code:u.invalid_type,expected:p.function,received:t.parsedType}),f;function r(i,y){return pe({data:i,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,ne(),D].filter(l=>!!l),issueData:{code:u.invalid_arguments,argumentsError:y}})}function s(i,y){return pe({data:i,path:t.path,errorMaps:[t.common.contextualErrorMap,t.schemaErrorMap,ne(),D].filter(l=>!!l),issueData:{code:u.invalid_return_type,returnTypeError:y}})}let a={errorMap:t.common.contextualErrorMap},o=t.data;if(this._def.returns instanceof ue){let i=this;return P(async function(...y){let l=new G([]),I=await i._def.args.parseAsync(y,a).catch(g=>{throw l.addIssue(r(y,g)),l}),k=await Reflect.apply(o,this,I);return await i._def.returns._def.type.parseAsync(k,a).catch(g=>{throw l.addIssue(s(k,g)),l})})}else{let i=this;return P(function(...y){let l=i._def.args.safeParse(y,a);if(!l.success)throw new G([r(y,l.error)]);let I=Reflect.apply(o,this,l.data),k=i._def.returns.safeParse(I,a);if(!k.success)throw new G([s(I,k.error)]);return k.data})}}parameters(){return this._def.args}returnType(){return this._def.returns}args(...e){return new n({...this._def,args:J.create(e).rest($.create())})}returns(e){return new n({...this._def,returns:e})}implement(e){return this.parse(e)}strictImplement(e){return this.parse(e)}validate=this.implement;static create(e,t,r){return new n({args:e||J.create([]).rest($.create()),returns:t||$.create(),typeName:"ZodFunction",...Z(r)})}},xe=class n extends h{get schema(){return this._def.getter()}_parse(e){let{ctx:t}=this._processInputParams(e);return this._def.getter()._parse({data:t.data,path:t.path,parent:t})}static create=(e,t)=>new n({getter:e,typeName:"ZodLazy",...Z(t)})},Te=class n extends h{_parse(e){if(e.data!==this._def.value){let t=this._getOrReturnCtx(e);return c(t,{received:t.data,code:u.invalid_literal,expected:this._def.value}),f}return{status:"valid",value:e.data}}get value(){return this._def.value}static create=(e,t)=>new n({value:e,typeName:"ZodLiteral",...Z(t)})};function ct(n,e){return new de({values:n,typeName:"ZodEnum",...Z(e)})}var de=class n extends h{#e;_parse(e){if(typeof e.data!="string"){let t=this._getOrReturnCtx(e),r=this._def.values;return c(t,{expected:v.joinValues(r),received:t.parsedType,code:u.invalid_type}),f}if(this.#e||(this.#e=new Set(this._def.values)),!this.#e.has(e.data)){let t=this._getOrReturnCtx(e),r=this._def.values;return c(t,{received:t.data,code:u.invalid_enum_value,options:r}),f}return P(e.data)}get options(){return this._def.values}get enum(){let e={};for(let t of this._def.values)e[t]=t;return e}get Values(){let e={};for(let t of this._def.values)e[t]=t;return e}get Enum(){let e={};for(let t of this._def.values)e[t]=t;return e}extract(e,t=this._def){return n.create(e,{...this._def,...t})}exclude(e,t=this._def){return n.create(this.options.filter(r=>!e.includes(r)),{...this._def,...t})}static create=ct},Ce=class n extends h{#e;_parse(e){let t=v.getValidEnumValues(this._def.values),r=this._getOrReturnCtx(e);if(r.parsedType!==p.string&&r.parsedType!==p.number){let s=v.objectValues(t);return c(r,{expected:v.joinValues(s),received:r.parsedType,code:u.invalid_type}),f}if(this.#e||(this.#e=new Set(v.getValidEnumValues(this._def.values))),!this.#e.has(e.data)){let s=v.objectValues(t);return c(r,{received:r.data,code:u.invalid_enum_value,options:s}),f}return P(e.data)}get enum(){return this._def.values}static create=(e,t)=>new n({values:e,typeName:"ZodNativeEnum",...Z(t)})},ue=class n extends h{unwrap(){return this._def.type}_parse(e){let{ctx:t}=this._processInputParams(e);if(t.parsedType!==p.promise&&t.common.async===!1)return c(t,{code:u.invalid_type,expected:p.promise,received:t.parsedType}),f;let r=t.parsedType===p.promise?t.data:Promise.resolve(t.data);return P(r.then(s=>this._def.type.parseAsync(s,{path:t.path,errorMap:t.common.contextualErrorMap})))}static create=(e,t)=>new n({type:e,typeName:"ZodPromise",...Z(t)})},z=class n extends h{innerType(){return this._def.schema}sourceType(){return this._def.schema._def.typeName==="ZodEffects"?this._def.schema.sourceType():this._def.schema}_parse(e){let{status:t,ctx:r}=this._processInputParams(e),s=this._def.effect||null,a={addIssue:o=>{c(r,o),o.fatal?t.abort():t.dirty()},get path(){return r.path}};if(a.addIssue=a.addIssue.bind(a),s.type==="preprocess"){let o=s.transform(r.data,a);if(r.common.async)return Promise.resolve(o).then(async i=>{if(t.value==="aborted")return f;let y=await this._def.schema._parseAsync({data:i,path:r.path,parent:r});return y.status==="aborted"?f:y.status==="dirty"?Q(y.value):t.value==="dirty"?Q(y.value):y});{if(t.value==="aborted")return f;let i=this._def.schema._parseSync({data:o,path:r.path,parent:r});return i.status==="aborted"?f:i.status==="dirty"?Q(i.value):t.value==="dirty"?Q(i.value):i}}if(s.type==="refinement"){let o=i=>{let y=s.refinement(i,a);if(r.common.async)return Promise.resolve(y);if(y instanceof Promise)throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead.");return i};if(r.common.async===!1){let i=this._def.schema._parseSync({data:r.data,path:r.path,parent:r});return i.status==="aborted"?f:(i.status==="dirty"&&t.dirty(),o(i.value),{status:t.value,value:i.value})}else return this._def.schema._parseAsync({data:r.data,path:r.path,parent:r}).then(i=>i.status==="aborted"?f:(i.status==="dirty"&&t.dirty(),o(i.value).then(()=>({status:t.value,value:i.value}))))}if(s.type==="transform")if(r.common.async===!1){let o=this._def.schema._parseSync({data:r.data,path:r.path,parent:r});if(!re(o))return o;let i=s.transform(o.value,a);if(i instanceof Promise)throw new Error("Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.");return{status:t.value,value:i}}else return this._def.schema._parseAsync({data:r.data,path:r.path,parent:r}).then(o=>re(o)?Promise.resolve(s.transform(o.value,a)).then(i=>({status:t.value,value:i})):o);v.assertNever(s)}static create=(e,t,r)=>new n({schema:e,typeName:"ZodEffects",effect:t,...Z(r)});static createWithPreprocess=(e,t,r)=>new n({schema:t,effect:{type:"preprocess",transform:e},typeName:"ZodEffects",...Z(r)})};var H=class n extends h{_parse(e){return this._getType(e)===p.undefined?P(void 0):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}static create=(e,t)=>new n({innerType:e,typeName:"ZodOptional",...Z(t)})},q=class n extends h{_parse(e){return this._getType(e)===p.null?P(null):this._def.innerType._parse(e)}unwrap(){return this._def.innerType}static create=(e,t)=>new n({innerType:e,typeName:"ZodNullable",...Z(t)})},ve=class n extends h{_parse(e){let{ctx:t}=this._processInputParams(e),r=t.data;return t.parsedType===p.undefined&&(r=this._def.defaultValue()),this._def.innerType._parse({data:r,path:t.path,parent:t})}removeDefault(){return this._def.innerType}static create=(e,t)=>new n({innerType:e,typeName:"ZodDefault",defaultValue:typeof t.default=="function"?t.default:()=>t.default,...Z(t)})},ke=class n extends h{_parse(e){let{ctx:t}=this._processInputParams(e),r={...t,common:{...t.common,issues:[]}},s=this._def.innerType._parse({data:r.data,path:r.path,parent:{...r}});return se(s)?s.then(a=>({status:"valid",value:a.status==="valid"?a.value:this._def.catchValue({get error(){return new G(r.common.issues)},input:r.data})})):{status:"valid",value:s.status==="valid"?s.value:this._def.catchValue({get error(){return new G(r.common.issues)},input:r.data})}}removeCatch(){return this._def.innerType}static create=(e,t)=>new n({innerType:e,typeName:"ZodCatch",catchValue:typeof t.catch=="function"?t.catch:()=>t.catch,...Z(t)})},Oe=class n extends h{_parse(e){if(this._getType(e)!==p.nan){let r=this._getOrReturnCtx(e);return c(r,{code:u.invalid_type,expected:p.nan,received:r.parsedType}),f}return{status:"valid",value:e.data}}static create=e=>new n({typeName:"ZodNaN",...Z(e)})},jr=Symbol("zod_brand"),Ae=class extends h{_parse(e){let{ctx:t}=this._processInputParams(e),r=t.data;return this._def.type._parse({data:r,path:t.path,parent:t})}unwrap(){return this._def.type}},Re=class n extends h{_parse(e){let{status:t,ctx:r}=this._processInputParams(e);if(r.common.async)return(async()=>{let a=await this._def.in._parseAsync({data:r.data,path:r.path,parent:r});return a.status==="aborted"?f:a.status==="dirty"?(t.dirty(),Q(a.value)):this._def.out._parseAsync({data:a.value,path:r.path,parent:r})})();{let s=this._def.in._parseSync({data:r.data,path:r.path,parent:r});return s.status==="aborted"?f:s.status==="dirty"?(t.dirty(),{status:"dirty",value:s.value}):this._def.out._parseSync({data:s.value,path:r.path,parent:r})}}static create(e,t){return new n({in:e,out:t,typeName:"ZodPipeline"})}},Pe=class n extends h{_parse(e){let t=this._def.innerType._parse(e),r=s=>(re(s)&&(s.value=Object.freeze(s.value)),s);return se(t)?t.then(s=>r(s)):r(t)}static create=(e,t)=>new n({innerType:e,typeName:"ZodReadonly",...Z(t)});unwrap(){return this._def.innerType}};function lt(n,e={},t){return n?ie.create().superRefine((r,s)=>{if(!n(r)){let a=typeof e=="function"?e(r):typeof e=="string"?{message:e}:e,o=a.fatal??t??!0,i=typeof a=="string"?{message:a}:a;s.addIssue({code:"custom",...i,fatal:o})}}):ie.create()}var Fr={object:F.lazycreate},pt=(C=>(C.ZodString="ZodString",C.ZodNumber="ZodNumber",C.ZodNaN="ZodNaN",C.ZodBigInt="ZodBigInt",C.ZodBoolean="ZodBoolean",C.ZodDate="ZodDate",C.ZodSymbol="ZodSymbol",C.ZodUndefined="ZodUndefined",C.ZodNull="ZodNull",C.ZodAny="ZodAny",C.ZodUnknown="ZodUnknown",C.ZodNever="ZodNever",C.ZodVoid="ZodVoid",C.ZodArray="ZodArray",C.ZodObject="ZodObject",C.ZodUnion="ZodUnion",C.ZodDiscriminatedUnion="ZodDiscriminatedUnion",C.ZodIntersection="ZodIntersection",C.ZodTuple="ZodTuple",C.ZodRecord="ZodRecord",C.ZodMap="ZodMap",C.ZodSet="ZodSet",C.ZodFunction="ZodFunction",C.ZodLazy="ZodLazy",C.ZodLiteral="ZodLiteral",C.ZodEnum="ZodEnum",C.ZodEffects="ZodEffects",C.ZodNativeEnum="ZodNativeEnum",C.ZodOptional="ZodOptional",C.ZodNullable="ZodNullable",C.ZodDefault="ZodDefault",C.ZodCatch="ZodCatch",C.ZodPromise="ZodPromise",C.ZodBranded="ZodBranded",C.ZodPipeline="ZodPipeline",C.ZodReadonly="ZodReadonly",C))(pt||{});var Jr=(n,e={message:`Input not instance of ${n.name}`})=>lt(t=>t instanceof n,e),yt=oe.create,mt=ye.create,zr=Oe.create,qr=me.create,gt=ge.create,Qr=fe.create,$r=Ke.create,es=Ze.create,ts=Ie.create,ns=ie.create,rs=$.create,ss=M.create,as=We.create,os=ee.create,is=F.create,ds=F.strictCreate,us=he.create,cs=Be.create,ls=be.create,ps=J.create,ys=Ve.create,ms=Ne.create,gs=_e.create,fs=Le.create,Zs=xe.create,Is=Te.create,hs=de.create,bs=Ce.create,xs=ue.create,Ts=z.create,Cs=H.create,vs=q.create,ks=z.createWithPreprocess,As=Re.create,Rs=()=>yt().optional(),Ps=()=>mt().optional(),Ss=()=>gt().optional(),ws={string:n=>oe.create({...n,coerce:!0}),number:n=>ye.create({...n,coerce:!0}),boolean:n=>ge.create({...n,coerce:!0}),bigint:n=>me.create({...n,coerce:!0}),date:n=>fe.create({...n,coerce:!0})};var Gs=f;var B="treeview",K="TreeView Plug",Zt="enableTreeView",It="treeViewSizeOverride",ht="treeViewShowHidden",Ks=["rhs","lhs","bhs","modal"],Ws=T.object({type:T.literal("regex"),rule:T.string(),negate:T.boolean().optional().default(!1)}),Bs=T.object({type:T.literal("tags"),tags:T.array(T.string()),negate:T.boolean().optional().default(!1),attribute:T.enum(["tags","itags","tag"]).optional().default("tags")}),Vs=T.object({type:T.literal("space-function"),name:T.string(),negate:T.boolean().optional().default(!1)}),Ns=["asc","desc"],ft=T.object({position:T.enum(Ks).optional().default("lhs"),size:T.number().gt(0).optional().default(1),sort:T.object({order:T.enum(Ns).optional().default("asc"),positions:T.object({folders:T.number().optional().default(1),folderNotes:T.number().optional().default(1),pages:T.number().optional().default(2),attachments:T.number().optional().default(2)}).optional().default({})}).optional().default({}),dragAndDrop:T.object({enabled:T.boolean().optional().default(!0),confirmOnRename:T.boolean().optional().default(!0)}).optional().default({}),revealOnLoad:T.boolean().optional().default(!0),attachments:T.object({enabled:T.boolean().optional().default(!1),extensionExcludeRegex:T.string().optional().default("")}).optional().default({}),pageExcludeRegex:T.string().optional().default(""),exclusions:T.array(T.discriminatedUnion("type",[Ws,Bs,Vs])).optional()});async function _s(n){if(He)return;He=!0;let e=`${typeof n}: ${String(n)}`;if(n instanceof G){let{formErrors:t,fieldErrors:r}=n.flatten(),s=Object.keys(r).map(a=>`\`${a}\` - ${r[a].join(", ")}`);e=[...t,...s].join("; ")}await O.flashNotification(`There was an error with your ${B} configuration. Check your SETTINGS file: ${e}`,"error")}var He=!1;async function Xe(){let n=await _.getSpaceConfig("treeview",{});console.log(`${K}: raw config from space:`,JSON.stringify(n));try{let e=ft.parse(n||{});return console.log(`${K}: parsed config:`,JSON.stringify(e)),e}catch(e){return He||(_s(e),He=!0),ft.parse({})}}async function ze(){return!!await E.get(Zt)}async function qe(n){return await E.set(Zt,n)}async function Ee(){return await E.get(It)}async function Qe(n){return await E.set(It,n)}async function $e(){return!!await E.get(ht)}async function bt(n){return await E.set(ht,n)}async function xt(){return await O.getUiOption("customStyles")}var Tt=async(n,{name:e,negate:t})=>{try{let r=await Promise.all(n.map(s=>_.invokeSpaceFunction(e,s)));return n.filter((s,a)=>t?r[a]:!r[a])}catch(r){return console.error(`${K}: filtering pages by function "${e}" failed`,r),n}};var et=(n,{rule:e,negate:t})=>{try{let r=new RegExp(e);return n.filter(({name:s})=>r.test(s)===t)}catch(r){return console.error(`${K}: filtering pages by regex "${e}" failed`,r),n}};var Ct=(n,{attribute:e,negate:t,tags:r})=>n.filter(s=>{let a=s[e],o=a!==void 0&&(Array.isArray(a)?a.some(i=>r.includes(i)):r.includes(a));return t?o:!o});async function vt(n,e=!1){let t=await O.getCurrentPage(),r=await _.invokeFunction("index.queryLuaObjects","page",{}),s={nodes:[]};if(!e){if(n.pageExcludeRegex){let g=`${K}:
-\`pageExcludeRegex\` setting is deprecated. Please use \`exclusions\`:
-
-\`\`\`yaml
-treeview:
- exclusions:
- - type: regex
- rule: "${n.pageExcludeRegex}"
-\`\`\`
- `;console.warn(g),r=et(r,{rule:n.pageExcludeRegex,negate:!1})}if(console.log(`${K}: exclusions config:`,JSON.stringify(n.exclusions)),console.log(`${K}: pages before exclusions:`,r.map(g=>g.name)),n.exclusions)for(let g of n.exclusions){console.log(`${K}: applying exclusion:`,JSON.stringify(g));let b=r.length;switch(g.type){case"regex":{r=et(r,g),console.log(`${K}: regex "${g.rule}" filtered ${b} -> ${r.length}`);break}case"tags":{r=Ct(r,g);break}case"space-function":{r=await Tt(r,g);break}}}}r.sort((g,b)=>g.name.localeCompare(b.name));let a=n.sort?.order??"asc",o=n.sort?.positions??{},i=o.folders??1,y=o.folderNotes??1,l=o.pages??2,I=o.attachments??2,k=g=>g.data.nodeType==="folder"?i:g.data.nodeType==="attachment"?I:g.nodes.length>0?y:l,x=g=>{g.sort((b,A)=>{let S=k(b),X=k(A);if(S!==X)return S-X;let L=b.data.title.localeCompare(A.data.title);return a==="desc"?-L:L}),g.forEach(b=>x(b.nodes))};if(r.forEach(g=>{g.name.split("/").reduce((b,A,S,X)=>{let L=X.length-1===S,N=b.nodes.find(W=>W.data.title===A);if(N)return L&&!("created"in N.data)&&(N.data={...g,title:A,isCurrentPage:t===g.name,nodeType:"page"}),N;if(L)N={data:{...g,title:A,isCurrentPage:t===g.name,nodeType:"page"},nodes:[]};else{let W=X.slice(0,S+1).join("/");N={data:{title:A,isCurrentPage:t===W,name:W,nodeType:"folder"},nodes:[]}}return b.nodes.push(N),N},s)}),n.attachments?.enabled){let g=await Se.listAttachments();if(!e&&n.attachments.extensionExcludeRegex){let b=new RegExp(n.attachments.extensionExcludeRegex);g=g.filter(A=>!b.test(A.name))}if(!e&&n.exclusions){for(let b of n.exclusions)if(b.type==="regex"){let A=new RegExp(b.rule);g=g.filter(S=>A.test(S.name)===b.negate)}}g.forEach(b=>{b.name.split("/").reduce((A,S,X,L)=>{let N=L.length-1===X,W=A.nodes.find(De=>De.data.title===S);if(W)return N&&W.data.nodeType==="folder"&&(W.data={...b,title:S,isCurrentPage:!1,nodeType:"attachment"}),W;if(N)W={data:{...b,title:S,isCurrentPage:!1,nodeType:"attachment"},nodes:[]};else{let De=L.slice(0,X+1).join("/");W={data:{title:S,isCurrentPage:!1,name:De,nodeType:"folder"},nodes:[]}}return A.nodes.push(W),W},s)})}return x(s.nodes),{nodes:s.nodes,currentPage:t}}var ce;async function kt(){await ze()?await Ye():await j()}async function Ye(){ce&&(await O.hidePanel(ce),ce=void 0,await qe(!1))}async function At(){try{if(await _.getEnv()==="server")return;if(await ze())return await j()}catch(n){console.error(`${K}: showTreeIfEnabled failed`,n)}}async function j(){let n=await Xe();ce&&n.position!==ce&&await Ye();let e=await $e(),[t,r,s,a,o,i,y,l,I,k,x]=await Promise.all([w.readAsset(B,"assets/sortable-tree/sortable-tree.css"),w.readAsset(B,"assets/sortable-tree/sortable-tree.js"),w.readAsset(B,"assets/treeview.css"),w.readAsset(B,"assets/treeview.js"),w.readAsset(B,"assets/icons/folder-minus.svg"),w.readAsset(B,"assets/icons/folder-plus.svg"),w.readAsset(B,"assets/icons/navigation-2.svg"),w.readAsset(B,"assets/icons/refresh-cw.svg"),w.readAsset(B,"assets/icons/x-circle.svg"),w.readAsset(B,"assets/icons/eye.svg"),w.readAsset(B,"assets/icons/eye-off.svg")]),{currentPage:g,nodes:b}=await vt(n,e),A=await xt(),S={nodes:b,currentPage:g,treeElementId:"treeview-tree",dragAndDrop:{...n.dragAndDrop,enabled:n.dragAndDrop.enabled},revealOnLoad:n.revealOnLoad},L=await Ee()??n.size;await O.showPanel(n.position,L,`
-
-
- `,`
- ${r}
- ${a}
- initializeTreeViewPanel(${JSON.stringify(S)});
- `),await qe(!0),ce=n.position}async function Rt(){let n=await $e();await bt(!n),await j()}async function Pt(){await _.invokeCommand("Page: Delete")}var St=.1;async function wt(){let n=await Xe(),e=await Ee()??n.size;await Qe(e+St),await j()}async function Gt(){let n=await Xe(),e=await Ee()??n.size,t=Math.max(.1,e-St);await Qe(t),await j()}var Kt={hide:Ye,show:j,showIfEnabled:At,toggle:kt,reveal:j,deletePage:Pt,increaseWidth:wt,decreaseWidth:Gt,toggleHidden:Rt},Wt={name:"treeview",version:.15,assets:{"assets/treeview.css":{data:"data:text/css;base64,LyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKgogKiBQYWdlIHZhcmlhYmxlcwogKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiovCgogaHRtbCB7CiAgLS10cmVldmlldy1ub2RlLWJvcmRlci13aWR0aDogMnB4OwogIC0tdHJlZXZpZXctbm9kZS1ib3JkZXItcmFkaXVzOiA1cHg7CgogIC0tdHJlZXZpZXctcGFnZS1jb2xvcjogdmFyKC0tZWRpdG9yLXdpa2ktbGluay1wYWdlLWNvbG9yKTsKICAtLXRyZWV2aWV3LXBhZ2UtYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tZWRpdG9yLXdpa2ktbGluay1wYWdlLWJhY2tncm91bmQtY29sb3IpOwogIC0tdHJlZXZpZXctcGFnZS1ib3JkZXItY29sb3I6IHZhcigtLXRyZWV2aWV3LXBhZ2UtYmFja2dyb3VuZC1jb2xvcik7CgogIC0tdHJlZXZpZXctZm9sZGVyLWNvbG9yOiB2YXIoLS1lZGl0b3Itd2lraS1saW5rLXBhZ2UtbWlzc2luZy1jb2xvcik7CiAgLS10cmVldmlldy1mb2xkZXItYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tdHJlZXZpZXctcGFnZS1iYWNrZ3JvdW5kLWNvbG9yKTsKICAtLXRyZWV2aWV3LWZvbGRlci1ib3JkZXItY29sb3I6IHZhcigtLXRyZWV2aWV3LXBhZ2UtYmFja2dyb3VuZC1jb2xvcik7CgogIC0tdHJlZXZpZXctYXR0YWNobWVudC1jb2xvcjogdmFyKC0tc3VidGxlLWNvbG9yKTsKICAtLXRyZWV2aWV3LWF0dGFjaG1lbnQtYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tdHJlZXZpZXctcGFnZS1iYWNrZ3JvdW5kLWNvbG9yKTsKICAtLXRyZWV2aWV3LWF0dGFjaG1lbnQtYm9yZGVyLWNvbG9yOiB2YXIoLS10cmVldmlldy1wYWdlLWJhY2tncm91bmQtY29sb3IpOwoKICAtLXRyZWV2aWV3LWN1cnJlbnQtcGFnZS1jb2xvcjogI2VlZTsKCS0tdHJlZXZpZXctY3VycmVudC1wYWdlLWJhY2tncm91bmQtY29sb3I6IHZhcigtLXVpLWFjY2VudC1jb2xvcik7CiAgLS10cmVldmlldy1jdXJyZW50LXBhZ2UtYm9yZGVyLWNvbG9yOiB2YXIoLS10cmVldmlldy1wYWdlLWJvcmRlci1jb2xvcik7Cn0KCi50cmVldmlldy1yb290IHsKICAvKiBTb3J0YWJsZVRyZWUgdmFyaWFibGVzICovCiAgLS1zdC1sYWJlbC1oZWlnaHQ6IGF1dG87CiAgLS1zdC1zdWJub2Rlcy1wYWRkaW5nLWxlZnQ6IDEuMjVyZW07CiAgLS1zdC1jb2xsYXBzZS1pY29uLWhlaWdodDogMi4xcmVtOwogIC0tc3QtY29sbGFwc2UtaWNvbi13aWR0aDogMS4yNXJlbTsKICAtLXN0LWNvbGxhcHNlLWljb24tc2l6ZTogMXJlbTsKfQoKLyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKgogKiBQYWdlIHN0eWxlcwogKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiovCgpodG1sLCBib2R5IHsKICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1yb290LWJhY2tncm91bmQtY29sb3IpOwogIHBhZGRpbmc6IDA7CiAgbWFyZ2luOiAwOwogIG92ZXJmbG93OiBpbml0aWFsICFpbXBvcnRhbnQ7Cn0KCi50cmVldmlldy1yb290IHsKICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1yb290LWJhY2tncm91bmQtY29sb3IpOwogIGNvbG9yOiB2YXIoLS1yb290LWNvbG9yKTsKICBmb250LWZhbWlseTogdmFyKC0tdWktZm9udCk7Cn0KCi8qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioKICogSGVhZGVyIHN0eWxlcwogKgogKiBBY3Rpb24gc3R5bGVzIGFyZSBtZWFudCB0byBtaW1pYyB0aGUgVG9wIEJhciBpY29ucyAoYHNiLWFjdGlvbnNgKQogKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiovCgoudHJlZXZpZXctaGVhZGVyIHsKICBwb3NpdGlvbjogc3RpY2t5OwogIHRvcDogMDsKICB6LWluZGV4OiAyOwogIHBhZGRpbmc6IDAuNXJlbSAwLjVyZW0gMHJlbTsKICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1yb290LWJhY2tncm91bmQtY29sb3IpOwp9CgoudHJlZXZpZXctYWN0aW9ucyB7CiAgZGlzcGxheTogZmxleDsKICBqdXN0aWZ5LWNvbnRlbnQ6IHNwYWNlLWJldHdlZW47CiAgYWxpZ24taXRlbXM6IGZsZXgtc3RhcnQ7CiAgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tZWRpdG9yLXdpZGdldC1iYWNrZ3JvdW5kLWNvbG9yKTsKICBib3JkZXI6IHZhcigtLXRvcC1ib3JkZXItY29sb3IpIDFweCBzb2xpZDsKICBib3JkZXItcmFkaXVzOiA1cHg7CiAgY29sb3I6IHZhcigtLXRvcC1jb2xvcik7Cn0KCi50cmVldmlldy1hY3Rpb25zLWxlZnQsCi50cmVldmlldy1hY3Rpb25zLXJpZ2h0IHsKICBkaXNwbGF5OiBmbGV4Owp9CgoudHJlZXZpZXctYWN0aW9ucy1sZWZ0IHsKICBmbGV4OiAxOwogIGZsZXgtd3JhcDogd3JhcDsKfQoKLnRyZWV2aWV3LWFjdGlvbnMgYnV0dG9uIHsKICBkaXNwbGF5OiBmbGV4OwogIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgbWFyZ2luOiAzcHg7CiAgcGFkZGluZzogNXB4OwogIGJhY2tncm91bmQtY29sb3I6IHZhcigtLWFjdGlvbi1idXR0b24tYmFja2dyb3VuZC1jb2xvcik7CiAgYm9yZGVyOiAwOwogIGNvbG9yOiB2YXIoLS1hY3Rpb24tYnV0dG9uLWNvbG9yKTsKICBjdXJzb3I6IHBvaW50ZXI7Cn0KCi50cmVldmlldy1hY3Rpb25zIGJ1dHRvbjpob3ZlciB7CiAgY29sb3I6IHZhcigtLWFjdGlvbi1idXR0b24taG92ZXItY29sb3IpOwp9CgoudHJlZXZpZXctYWN0aW9ucyBidXR0b246aG92ZXIgewogIGNvbG9yOiB2YXIoLS1hY3Rpb24tYnV0dG9uLWFjdGl2ZS1jb2xvcik7Cn0KCi50cmVldmlldy1hY3Rpb25zIGJ1dHRvbiA+IHN2ZyB7CiAgaGVpZ2h0OiAxOHB4OwogIHdpZHRoOiAxOHB4Owp9CgovKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqCiAqIFNvcnRhYmxlVHJlZSBzdHlsZXMKICoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqLwoKLnRyZWUgewoJZGlzcGxheTogZmxleDsKCWZsZXgtZGlyZWN0aW9uOiBjb2x1bW47CiAgbWFyZ2luOiAwLjI1cmVtIDAuNXJlbSAxcmVtOwogIHBhZGRpbmctbGVmdDogdmFyKC0tc3Qtc3Vibm9kZXMtcGFkZGluZy1sZWZ0KTsKfQoKLnRyZWVfX2xhYmVsIHsKICBkaXNwbGF5OiBmbGV4OwogIHBhZGRpbmc6IDJweDsKfQoKLnRyZWVfX2xhYmVsOmFmdGVyIHsKICBjb250ZW50OiAnICc7CiAgcG9zaXRpb246IGFic29sdXRlOwogIHotaW5kZXg6IDQ7CiAgaW5zZXQ6IGNhbGModmFyKC0tdHJlZXZpZXctbm9kZS1ib3JkZXItd2lkdGgpICogLTEpIHZhcigtLXRyZWV2aWV3LW5vZGUtYm9yZGVyLXdpZHRoKTsKICBib3JkZXItdG9wOiBjYWxjKHZhcigtLXRyZWV2aWV3LW5vZGUtYm9yZGVyLXdpZHRoKSAqIDIpIHNvbGlkIHRyYW5zcGFyZW50OwogIGJvcmRlci1ib3R0b206IGNhbGModmFyKC0tdHJlZXZpZXctbm9kZS1ib3JkZXItd2lkdGgpICogMikgc29saWQgdHJhbnNwYXJlbnQ7CiAgdHJhbnNpdGlvbjogYm9yZGVyLWNvbG9yIDAuMnM7CiAgcG9pbnRlci1ldmVudHM6IG5vbmU7Cn0KCi50cmVlX19ub2RlLS1kcm9wLWJlZm9yZSA+IC50cmVlX19sYWJlbDphZnRlciB7CiAgYm9yZGVyLXRvcC1jb2xvcjogdmFyKC0tdWktYWNjZW50LWNvbG9yKTsKfQoKLnRyZWVfX25vZGUtLWRyb3AtYWZ0ZXIgPiAudHJlZV9fbGFiZWw6YWZ0ZXIgewogIGJvcmRlci1ib3R0b20tY29sb3I6IHZhcigtLXVpLWFjY2VudC1jb2xvcik7Cn0KCi50cmVlX19sYWJlbCA+IHNwYW4gewogIG92ZXJmbG93OiBoaWRkZW47CiAgcGFkZGluZzogMCA1cHg7CiAgYm9yZGVyLWNvbG9yOiB0cmFuc3BhcmVudDsKICBib3JkZXItd2lkdGg6IHZhcigtLXRyZWV2aWV3LW5vZGUtYm9yZGVyLXdpZHRoKTsKICBib3JkZXItc3R5bGU6IHNvbGlkOwogIGJvcmRlci1yYWRpdXM6IHZhcigtLXRyZWV2aWV3LW5vZGUtYm9yZGVyLXJhZGl1cyk7CiAgY3Vyc29yOiBwb2ludGVyOwogIGZvbnQtZmFtaWx5OiB2YXIoLS1lZGl0b3ItZm9udCk7CiAgZm9udC1zaXplOiAxOHB4OwogIGxpbmUtaGVpZ2h0OiAxLjQ7CiAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwogIHRleHQtb3ZlcmZsb3c6IGVsbGlwc2lzOwogIHdoaXRlLXNwYWNlOiBub3dyYXA7Cn0KCi50cmVlX19sYWJlbCA+IHNwYW5bZGF0YS1ub2RlLXR5cGU9InBhZ2UiXSB7CiAgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tdHJlZXZpZXctcGFnZS1iYWNrZ3JvdW5kLWNvbG9yKTsKICBib3JkZXItY29sb3I6IHZhcigtLXRyZWV2aWV3LXBhZ2UtYm9yZGVyLWNvbG9yKTsKICBjb2xvcjogdmFyKC0tdHJlZXZpZXctcGFnZS1jb2xvcik7Cn0KCi50cmVlX19sYWJlbCA+IHNwYW5bZGF0YS1ub2RlLXR5cGU9ImZvbGRlciJdIHsKICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS10cmVldmlldy1mb2xkZXItYmFja2dyb3VuZC1jb2xvcik7CiAgYm9yZGVyLWNvbG9yOiB2YXIoLS10cmVldmlldy1mb2xkZXItYm9yZGVyLWNvbG9yKTsKICBjb2xvcjogdmFyKC0tdHJlZXZpZXctZm9sZGVyLWNvbG9yKTsKfQoKLnRyZWVfX2xhYmVsID4gc3BhbltkYXRhLW5vZGUtdHlwZT0iYXR0YWNobWVudCJdIHsKICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS10cmVldmlldy1hdHRhY2htZW50LWJhY2tncm91bmQtY29sb3IpOwogIGJvcmRlci1jb2xvcjogdmFyKC0tdHJlZXZpZXctYXR0YWNobWVudC1ib3JkZXItY29sb3IpOwogIGNvbG9yOiB2YXIoLS10cmVldmlldy1hdHRhY2htZW50LWNvbG9yKTsKfQoKCi50cmVlX19sYWJlbCA+IHNwYW5bZGF0YS1jdXJyZW50LXBhZ2U9InRydWUiXSB7CgliYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS10cmVldmlldy1jdXJyZW50LXBhZ2UtYmFja2dyb3VuZC1jb2xvcik7CiAgYm9yZGVyLWNvbG9yOiB2YXIoLS10cmVldmlldy1jdXJyZW50LXBhZ2UtYm9yZGVyLWNvbG9yKTsKCWNvbG9yOiB2YXIoLS10cmVldmlldy1jdXJyZW50LXBhZ2UtY29sb3IpOwp9CgoudHJlZV9fY29sbGFwc2UgewogIGNvbG9yOiB2YXIoLS1lZGl0b3ItY29tbWFuZC1idXR0b24tY29sb3IpOwp9CgoudHJlZV9fbm9kZS0tZHJhZ2dpbmcgewogIG1hcmdpbjogY2FsYyh2YXIoLS10cmVldmlldy1ub2RlLWJvcmRlci13aWR0aCkgKiAtMSk7CiAgYm9yZGVyOiB2YXIoLS10cmVldmlldy1ub2RlLWJvcmRlci13aWR0aCkgZGFzaGVkIHZhcigtLXN1YnRsZS1jb2xvcik7CiAgYm9yZGVyLXJhZGl1czogdmFyKC0tdHJlZXZpZXctbm9kZS1ib3JkZXItcmFkaXVzKTsKICBjdXJzb3I6IG1vdmUgIWltcG9ydGFudDsKfQoKLnRyZWVfX25vZGUtLWRyYWdnaW5nIC50cmVlX19sYWJlbCA+IHNwYW4gewogIG9wYWNpdHk6IDAuNzU7Cn0KCi50cmVlX19ub2RlLS1kcm9wLWluc2lkZSA+IC50cmVlX19sYWJlbCA+IHNwYW4gewogIHotaW5kZXg6IDM7CiAgYmFja2dyb3VuZC1jb2xvcjogdmFyKC0tc3VidGxlLWJhY2tncm91bmQtY29sb3IpOwogIGJvcmRlci1jb2xvcjogdmFyKC0tdWktYWNjZW50LWNvbG9yKTsKfQoKLnRyZWVfX25vZGUtLWRyb3AtaW5zaWRlID4gLnRyZWVfX2xhYmVsID4gc3BhbltkYXRhLWN1cnJlbnQtcGFnZT0idHJ1ZSJdIHsKCWNvbG9yOiB2YXIoLS10cmVldmlldy1wYWdlLWNvbG9yKTsKfQ==",mtime:1770926754871},"assets/sortable-tree/sortable-tree.css":{data:"data:text/css;base64,LyohIFNvcnRhYmxlIFRyZWUgMC43LjIsIChjKSAyMDIzIE1hcmMgQW50b24gRGFobWVuLCBNSVQgbGljZW5zZSAqLwo6cm9vdCB7CiAgICAtLXN0LWxhYmVsLWhlaWdodDogMi41cmVtOwogICAgLS1zdC1zdWJub2Rlcy1wYWRkaW5nLWxlZnQ6IDEuNXJlbTsKICAgIC0tc3QtY29sbGFwc2UtaWNvbi1oZWlnaHQ6IHZhcigtLXN0LWxhYmVsLWhlaWdodCk7CiAgICAtLXN0LWNvbGxhcHNlLWljb24td2lkdGg6IDEuMjVyZW07CiAgICAtLXN0LWNvbGxhcHNlLWljb24tc2l6ZTogMC43NXJlbTsKICB9CiAgc29ydGFibGUtdHJlZS1ub2RlIHsKICAgIHBvc2l0aW9uOiByZWxhdGl2ZTsKICAgIHotaW5kZXg6IDE7CiAgICBkaXNwbGF5OiBmbGV4OwogICAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsKICB9CiAgc29ydGFibGUtdHJlZS1ub2RlICogewogICAgdXNlci1zZWxlY3Q6IG5vbmU7CiAgfQogIHNvcnRhYmxlLXRyZWUtbm9kZSA+IDpmaXJzdC1jaGlsZCB7CiAgICBkaXNwbGF5OiBmbGV4OwogICAgYWxpZ24taXRlbXM6IGNlbnRlcjsKICAgIGhlaWdodDogdmFyKC0tc3QtbGFiZWwtaGVpZ2h0KTsKICB9CiAgc29ydGFibGUtdHJlZS1ub2RlID4gOm50aC1jaGlsZCgyKSB7CiAgICBkaXNwbGF5OiBub25lOwogICAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsKICAgIHBhZGRpbmctbGVmdDogdmFyKC0tc3Qtc3Vibm9kZXMtcGFkZGluZy1sZWZ0KTsKICB9CiAgc29ydGFibGUtdHJlZS1ub2RlW29wZW5dID4gZGl2Om50aC1jaGlsZCgyKSB7CiAgICBkaXNwbGF5OiBmbGV4OwogIH0KICBzb3J0YWJsZS10cmVlLW5vZGUgPiA6bnRoLWNoaWxkKDMpIHsKICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTsKICAgIGRpc3BsYXk6IGZsZXg7CiAgICBhbGlnbi1pdGVtczogY2VudGVyOwogICAgdG9wOiAwOwogICAgbGVmdDogY2FsYyh2YXIoLS1zdC1jb2xsYXBzZS1pY29uLXdpZHRoKSAqIC0xKTsKICAgIGhlaWdodDogdmFyKC0tc3QtY29sbGFwc2UtaWNvbi1oZWlnaHQpOwogICAgZm9udC1zaXplOiB2YXIoLS1zdC1jb2xsYXBzZS1pY29uLXNpemUpOwogICAgY3Vyc29yOiBwb2ludGVyOwogIH0KICBzb3J0YWJsZS10cmVlLW5vZGUgPiA6bnRoLWNoaWxkKDIpOmVtcHR5ICsgc3BhbiB7CiAgICBkaXNwbGF5OiBub25lOwogIH0KICAKICA=",mtime:1770926754872},"assets/sortable-tree/sortable-tree.js":{data:"data:application/javascript;base64,LyohIFNvcnRhYmxlIFRyZWUgMC43LjIsIChjKSAyMDI0IE1hcmMgQW50b24gRGFobWVuLCBNSVQgbGljZW5zZSAqLwovKiBNb2RpZmllZCBieSBNck11Z2FtZSAoaHR0cHM6Ly9naXRodWIuY29tL21hcmNhbnRvbmRhaG1lbi9zb3J0YWJsZS10cmVlL3B1bGwvMTEpICovCiFmdW5jdGlvbiAoZSwgdCkgeyAib2JqZWN0IiA9PSB0eXBlb2YgZXhwb3J0cyAmJiAib2JqZWN0IiA9PSB0eXBlb2YgbW9kdWxlID8gbW9kdWxlLmV4cG9ydHMgPSB0KCkgOiAiZnVuY3Rpb24iID09IHR5cGVvZiBkZWZpbmUgJiYgZGVmaW5lLmFtZCA/IGRlZmluZShbXSwgdCkgOiAib2JqZWN0IiA9PSB0eXBlb2YgZXhwb3J0cyA/IGV4cG9ydHMuU29ydGFibGVUcmVlID0gdCgpIDogZS5Tb3J0YWJsZVRyZWUgPSB0KCkgfShzZWxmLCAoKCkgPT4gKCgpID0+IHsgInVzZSBzdHJpY3QiOyB2YXIgZSA9IHsgZDogKHQsIG4pID0+IHsgZm9yICh2YXIgbyBpbiBuKSBlLm8obiwgbykgJiYgIWUubyh0LCBvKSAmJiBPYmplY3QuZGVmaW5lUHJvcGVydHkodCwgbywgeyBlbnVtZXJhYmxlOiAhMCwgZ2V0OiBuW29dIH0pIH0sIG86IChlLCB0KSA9PiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoZSwgdCkgfSwgdCA9IHt9OyBlLmQodCwgeyBkZWZhdWx0OiAoKSA9PiBMIH0pOyB2YXIgbiA9IGZ1bmN0aW9uIChlLCB0LCBuLCBvKSB7IHJldHVybiBuZXcgKG4gfHwgKG4gPSBQcm9taXNlKSkoKGZ1bmN0aW9uIChyLCBzKSB7IGZ1bmN0aW9uIGkoZSkgeyB0cnkgeyBsKG8ubmV4dChlKSkgfSBjYXRjaCAoZSkgeyBzKGUpIH0gfSBmdW5jdGlvbiBhKGUpIHsgdHJ5IHsgbChvLnRocm93KGUpKSB9IGNhdGNoIChlKSB7IHMoZSkgfSB9IGZ1bmN0aW9uIGwoZSkgeyB2YXIgdDsgZS5kb25lID8gcihlLnZhbHVlKSA6ICh0ID0gZS52YWx1ZSwgdCBpbnN0YW5jZW9mIG4gPyB0IDogbmV3IG4oKGZ1bmN0aW9uIChlKSB7IGUodCkgfSkpKS50aGVuKGksIGEpIH0gbCgobyA9IG8uYXBwbHkoZSwgdCB8fCBbXSkpLm5leHQoKSkgfSkpIH07IGNvbnN0IG8gPSBlID0+IGA8c3Bhbj4ke2UudGl0bGV9PC9zcGFuPmAsIHIgPSB7IGNvbGxhcHNlZDogJ1xuXHRcdDxzdmdcblx0XHR4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIFxuXHRcdHdpZHRoPSIxZW0iIFxuXHRcdGhlaWdodD0iMWVtIiBcblx0XHRmaWxsPSJjdXJyZW50Q29sb3IiIFxuXHRcdGNsYXNzPSJiaSBiaS1jYXJldC1yaWdodC1maWxsIiBcblx0XHR2aWV3Qm94PSIwIDAgMTYgMTYiPlxuXHRcdFx0PHBhdGggZD0ibTEyLjE0IDguNzUzLTUuNDgyIDQuNzk2Yy0uNjQ2LjU2Ni0xLjY1OC4xMDYtMS42NTgtLjc1M1YzLjIwNGExIDEgMCAwIDEgMS42NTktLjc1M2w1LjQ4IDQuNzk2YTEgMSAwIDAgMSAwIDEuNTA2eiIvPlxuXHRcdDwvc3ZnPicsIG9wZW46ICdcblx0XHQ8c3ZnIFxuXHRcdHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgXG5cdFx0d2lkdGg9IjFlbSIgXG5cdFx0aGVpZ2h0PSIxZW0iIFxuXHRcdGZpbGw9ImN1cnJlbnRDb2xvciIgXG5cdFx0Y2xhc3M9ImJpIGJpLWNhcmV0LWRvd24tZmlsbCIgXG5cdFx0dmlld0JveD0iMCAwIDE2IDE2Ij5cblx0XHRcdDxwYXRoIGQ9Ik03LjI0NyAxMS4xNCAyLjQ1MSA1LjY1OEMxLjg4NSA1LjAxMyAyLjM0NSA0IDMuMjA0IDRoOS41OTJhMSAxIDAgMCAxIC43NTMgMS42NTlsLTQuNzk2IDUuNDhhMSAxIDAgMCAxLTEuNTA2IDB6Ii8+XG5cdFx0PC9zdmc+XG5cdCcgfSwgcyA9IHsgdHJlZTogInRyZWUiLCBub2RlOiAidHJlZV9fbm9kZSIsIG5vZGVIb3ZlcjogInRyZWVfX25vZGUtLWhvdmVyIiwgbm9kZURyYWdnaW5nOiAidHJlZV9fbm9kZS0tZHJhZ2dpbmciLCBub2RlRHJvcEJlZm9yZTogInRyZWVfX25vZGUtLWRyb3AtYmVmb3JlIiwgbm9kZURyb3BJbnNpZGU6ICJ0cmVlX19ub2RlLS1kcm9wLWluc2lkZSIsIG5vZGVEcm9wQWZ0ZXI6ICJ0cmVlX19ub2RlLS1kcm9wLWFmdGVyIiwgbGFiZWw6ICJ0cmVlX19sYWJlbCIsIHN1Ym5vZGVzOiAidHJlZV9fc3Vibm9kZXMiLCBjb2xsYXBzZTogInRyZWVfX2NvbGxhcHNlIiB9LCBpID0gKHsgbm9kZXM6IGUsIG1vdmVkTm9kZTogdCwgc3JjUGFyZW50Tm9kZTogbywgdGFyZ2V0UGFyZW50Tm9kZTogciB9KSA9PiBuKHZvaWQgMCwgdm9pZCAwLCB2b2lkIDAsIChmdW5jdGlvbiogKCkgeyB9KSksIGEgPSAoZSwgdCkgPT4gbih2b2lkIDAsIHZvaWQgMCwgdm9pZCAwLCAoZnVuY3Rpb24qICgpIHsgfSkpLCBsID0gKGUsIHQpID0+IG4odm9pZCAwLCB2b2lkIDAsIHZvaWQgMCwgKGZ1bmN0aW9uKiAoKSB7IHJldHVybiAhMCB9KSk7IGNvbnN0IGQgPSAoZSwgdCA9IFtdLCBuID0gbnVsbCkgPT4geyBjb25zdCBvID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudChlKTsgcmV0dXJuIHQuZm9yRWFjaCgoZSA9PiB7IG8uY2xhc3NMaXN0LmFkZChlKSB9KSksIG4gJiYgbi5hcHBlbmRDaGlsZChvKSwgbyB9LCBjID0gKCkgPT4gKChlID0gMjEpID0+IGNyeXB0by5nZXRSYW5kb21WYWx1ZXMobmV3IFVpbnQ4QXJyYXkoZSkpLnJlZHVjZSgoKGUsIHQpID0+IGUgKyAoKHQgJj0gNjMpIDwgMzYgPyB0LnRvU3RyaW5nKDM2KSA6IHQgPCA2MiA/ICh0IC0gMjYpLnRvU3RyaW5nKDM2KS50b1VwcGVyQ2FzZSgpIDogdCA+IDYyID8gIi0iIDogIl8iKSksICIiKSkoKTsgY2xhc3MgdSBleHRlbmRzIEhUTUxFbGVtZW50IHsgc3RhdGljIGNyZWF0ZSh7IGRhdGE6IGUsIHJlbmRlckxhYmVsOiB0LCBpY29uczogbiwgc3R5bGVzOiBvLCBwYXJlbnQ6IHIsIG9uQ2xpY2s6IHMsIGRyYWdnYWJsZTogaSB9KSB7IGNvbnN0IGEgPSBkKHUuVEFHX05BTUUsIFtvLm5vZGVdLCByKSwgbCA9IGQoImRpdiIsIFtvLmxhYmVsXSwgYSksIGMgPSBkKCJkaXYiLCBbby5zdWJub2Rlc10sIGEpLCBoID0gZCgic3BhbiIsIFtvLmNvbGxhcHNlXSwgYSk7IHJldHVybiBsLmlubmVySFRNTCA9IHQoZSksIGguaW5uZXJIVE1MID0gbi5jb2xsYXBzZWQsIGguYWRkRXZlbnRMaXN0ZW5lcigiY2xpY2siLCBhLnRvZ2dsZS5iaW5kKGEpKSwgbC5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsIChlID0+IHsgcyhlLCBhKSB9KSksIGkgJiYgYS5zZXRBdHRyaWJ1dGUoImRyYWdnYWJsZSIsICJ0cnVlIiksIGEuX2RhdGEgPSBlLCBhLl9pY29ucyA9IG4sIGEuX2xhYmVsID0gbCwgYS5fbm9kZXMgPSBjLCBhLl9jb2xsYXBzZUJ1dHRvbiA9IGgsIGEgfSBnZXQgZGF0YSgpIHsgcmV0dXJuIHRoaXMuX2RhdGEgfSBnZXQgbGFiZWwoKSB7IHJldHVybiB0aGlzLl9sYWJlbCB9IGdldCBzdWJub2RlcygpIHsgcmV0dXJuIHRoaXMuX25vZGVzIH0gZ2V0IHN1Ym5vZGVzRGF0YSgpIHsgY29uc3QgZSA9IFtdOyByZXR1cm4gQXJyYXkuZnJvbSh0aGlzLl9ub2Rlcy5jaGlsZHJlbikuZm9yRWFjaCgodCA9PiB7IGUucHVzaCh0LmRhdGEpIH0pKSwgZSB9IGdldCBpZCgpIHsgcmV0dXJuIHRoaXMuX2lkIH0gY29uc3RydWN0b3IoKSB7IHN1cGVyKCksIHRoaXMuX2lkID0gYygpIH0gY29sbGFwc2UoZSkgeyBlID8gKHRoaXMucmVtb3ZlQXR0cmlidXRlKCJvcGVuIiksIHRoaXMuX2NvbGxhcHNlQnV0dG9uLmlubmVySFRNTCA9IHRoaXMuX2ljb25zLmNvbGxhcHNlZCkgOiAodGhpcy5zZXRBdHRyaWJ1dGUoIm9wZW4iLCAidHJ1ZSIpLCB0aGlzLl9jb2xsYXBzZUJ1dHRvbi5pbm5lckhUTUwgPSB0aGlzLl9pY29ucy5vcGVuKSB9IHRvZ2dsZSgpIHsgdGhpcy5jb2xsYXBzZSh0aGlzLmhhc0F0dHJpYnV0ZSgib3BlbiIpKSB9IHJldmVhbCgpIHsgKChlLCB0KSA9PiB7IGNvbnN0IG4gPSBbXTsgbGV0IG8gPSB0LmNsb3Nlc3QoZSk7IGZvciAoOyBudWxsICE9PSBvOyluLnB1c2gobyksIG8gPSBvLnBhcmVudE5vZGUuY2xvc2VzdChlKTsgcmV0dXJuIG4gfSkodS5UQUdfTkFNRSwgdGhpcykuZm9yRWFjaCgoZSA9PiB7IGUuY29sbGFwc2UoITEpIH0pKSB9IH0gdS5UQUdfTkFNRSA9ICJzb3J0YWJsZS10cmVlLW5vZGUiOyB2YXIgaCA9IGZ1bmN0aW9uIChlLCB0LCBuLCBvKSB7IHJldHVybiBuZXcgKG4gfHwgKG4gPSBQcm9taXNlKSkoKGZ1bmN0aW9uIChyLCBzKSB7IGZ1bmN0aW9uIGkoZSkgeyB0cnkgeyBsKG8ubmV4dChlKSkgfSBjYXRjaCAoZSkgeyBzKGUpIH0gfSBmdW5jdGlvbiBhKGUpIHsgdHJ5IHsgbChvLnRocm93KGUpKSB9IGNhdGNoIChlKSB7IHMoZSkgfSB9IGZ1bmN0aW9uIGwoZSkgeyB2YXIgdDsgZS5kb25lID8gcihlLnZhbHVlKSA6ICh0ID0gZS52YWx1ZSwgdCBpbnN0YW5jZW9mIG4gPyB0IDogbmV3IG4oKGZ1bmN0aW9uIChlKSB7IGUodCkgfSkpKS50aGVuKGksIGEpIH0gbCgobyA9IG8uYXBwbHkoZSwgdCB8fCBbXSkpLm5leHQoKSkgfSkpIH07IGNvbnN0IHAgPSAoeyBub2RlOiBlLCBldmVudE5hbWU6IHQsIGhhbmRsZXI6IG4sIHRyZWU6IG8gfSkgPT4geyBlLmFkZEV2ZW50TGlzdGVuZXIodCwgKGUgPT4geyBuKGUsIG8pIH0pLCAhMSkgfTsgdmFyIGY7ICFmdW5jdGlvbiAoZSkgeyBlLkJFRk9SRSA9ICJCRUZPUkUiLCBlLklOU0lERSA9ICJJTlNJREUiLCBlLkFGVEVSID0gIkFGVEVSIiB9KGYgfHwgKGYgPSB7fSkpOyBjb25zdCBnID0gZSA9PiB7IGNvbnN0IHQgPSBlLmNsaWVudFksIG4gPSB2KGUpLmxhYmVsLmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpLCBvID0gTWF0aC5yb3VuZChuLmhlaWdodCAvIDQpOyByZXR1cm4gbi50b3AgKyBvID4gdCA/IGYuQkVGT1JFIDogbi5ib3R0b20gLSBvIDwgdCA/IGYuQUZURVIgOiBmLklOU0lERSB9LCB2ID0gZSA9PiBlLnRhcmdldC5jbG9zZXN0KHUuVEFHX05BTUUpLCBiID0gZSA9PiB7IGNvbnN0IHQgPSBudWxsID09IGUgPyB2b2lkIDAgOiBlLnBhcmVudEVsZW1lbnQucGFyZW50RWxlbWVudDsgcmV0dXJuIChudWxsID09IHQgPyB2b2lkIDAgOiB0LnRhZ05hbWUudG9Mb3dlckNhc2UoKSkgIT09IHUuVEFHX05BTUUgPyBudWxsIDogdCB9LCBtID0gKGUsIHQpID0+IHsgdihlKS5jbGFzc0xpc3QucmVtb3ZlKHQuc3R5bGVzLm5vZGVEcm9wQWZ0ZXIsIHQuc3R5bGVzLm5vZGVEcm9wQmVmb3JlLCB0LnN0eWxlcy5ub2RlRHJvcEluc2lkZSkgfSwgRSA9IChlLCB0KSA9PiB7IGUuc3RvcFByb3BhZ2F0aW9uKCk7IGNvbnN0IG4gPSBlLnRhcmdldDsgbi50YWdOYW1lLnRvTG93ZXJDYXNlKCkgPT09IHUuVEFHX05BTUUgJiYgKGUuZGF0YVRyYW5zZmVyLnNldERhdGEoInRleHQiLCBuLmlkKSwgZS5kYXRhVHJhbnNmZXIuZWZmZWN0QWxsb3dlZCA9ICJtb3ZlIiwgbi5jbGFzc0xpc3QuYWRkKHQuc3R5bGVzLm5vZGVEcmFnZ2luZykpIH0sIF8gPSAoZSwgdCkgPT4gaCh2b2lkIDAsIHZvaWQgMCwgdm9pZCAwLCAoZnVuY3Rpb24qICgpIHsgZS5zdG9wUHJvcGFnYXRpb24oKSwgZS5wcmV2ZW50RGVmYXVsdCgpLCBtKGUsIHQpOyBjb25zdCBuID0gdihlKTsgaWYgKCFuKSByZXR1cm4gITE7IGNvbnN0IG8gPSBiKG4pLCByID0gZS5kYXRhVHJhbnNmZXIuZ2V0RGF0YSgidGV4dCIpLCBzID0gZyhlKSwgaSA9IHQuZ2V0Tm9kZShyKTsgaWYgKCFpIHx8IChudWxsID09IGkgPyB2b2lkIDAgOiBpLmNvbnRhaW5zKG4pKSkgcmV0dXJuICExOyBjb25zdCBhID0gYihpKTsgaWYgKHQubG9ja1Jvb3RMZXZlbCAmJiAhKG51bGwgPT0gbiA/IHZvaWQgMCA6IG4ucGFyZW50RWxlbWVudC5jbG9zZXN0KHUuVEFHX05BTUUpKSAmJiAocyA9PT0gZi5CRUZPUkUgfHwgcyA9PT0gZi5BRlRFUikpIHJldHVybiAhMTsgaWYgKHMgPT09IGYuQkVGT1JFKSByZXR1cm4gISEoeWllbGQgdC5jb25maXJtKGksIG8pKSAmJiAobi5wYXJlbnROb2RlLmluc2VydEJlZm9yZShpLCBuKSwgdC5vbkRyb3AoaSwgYSwgbyksICExKTsgaWYgKHMgPT09IGYuQUZURVIpIHsgaWYgKCEoeWllbGQgdC5jb25maXJtKGksIG8pKSkgcmV0dXJuICExOyBjb25zdCBlID0gbi5uZXh0RWxlbWVudFNpYmxpbmc7IHJldHVybiBlID8gKG4ucGFyZW50Tm9kZS5pbnNlcnRCZWZvcmUoaSwgZSksIHQub25Ecm9wKGksIGEsIG8pKSA6IChuLnBhcmVudE5vZGUuYXBwZW5kQ2hpbGQoaSksIHQub25Ecm9wKGksIGEsIG8pKSwgITEgfSByZXR1cm4gISEoeWllbGQgdC5jb25maXJtKGksIG4pKSAmJiAobi5zdWJub2Rlcy5hcHBlbmRDaGlsZChpKSwgdC5vbkRyb3AoaSwgYSwgbiksICExKSB9KSksIHkgPSAoZSwgdCkgPT4geyBlLnByZXZlbnREZWZhdWx0KCksIGUuZGF0YVRyYW5zZmVyLmRyb3BFZmZlY3QgPSAibW92ZSIsICgoZSwgdCkgPT4geyBjb25zdCBuID0gdihlKSwgbyA9IGcoZSk7IG4uY2xhc3NMaXN0LnRvZ2dsZSh0LnN0eWxlcy5ub2RlRHJvcEJlZm9yZSwgbyA9PT0gZi5CRUZPUkUpLCBuLmNsYXNzTGlzdC50b2dnbGUodC5zdHlsZXMubm9kZURyb3BJbnNpZGUsIG8gPT09IGYuSU5TSURFKSwgbi5jbGFzc0xpc3QudG9nZ2xlKHQuc3R5bGVzLm5vZGVEcm9wQWZ0ZXIsIG8gPT09IGYuQUZURVIpIH0pKGUsIHQpIH0sIEEgPSAoZSwgdCkgPT4geyBtKGUsIHQpIH0sIFQgPSAoZSwgdCkgPT4geyBlLnRhcmdldC5jbGFzc0xpc3QucmVtb3ZlKHQuc3R5bGVzLm5vZGVEcmFnZ2luZyksIG0oZSwgdCkgfSwgTiA9IGUgPT4gYHNvcnRhYmxlVHJlZVN0YXRlLSR7ZS5yZXBsYWNlKC9bXlx3XSsvLCAiLSIpfWA7IGNvbnN0IEwgPSBjbGFzcyB7IGNvbnN0cnVjdG9yKHsgbm9kZXM6IGUsIGVsZW1lbnQ6IHQsIHJlbmRlckxhYmVsOiBuLCBpY29uczogZCwgc3R5bGVzOiBjLCBsb2NrUm9vdExldmVsOiB1LCBvbkNoYW5nZTogaCwgb25DbGljazogcCwgaW5pdENvbGxhcHNlTGV2ZWw6IGYsIGNvbmZpcm06IGcsIGRpc2FibGVTb3J0aW5nOiB2LCBzdGF0ZUlkOiBiIH0pIHsgdGhpcy5ub2RlQ29sbGVjdGlvbiA9IHt9LCBlICYmICh0ID8gKHRoaXMuZGVmaW5lRWxlbWVudHMoKSwgdGhpcy5yb290ID0gdCwgdGhpcy5pY29ucyA9IE9iamVjdC5hc3NpZ24oT2JqZWN0LmFzc2lnbih7fSwgciksIGQpLCB0aGlzLnN0eWxlcyA9IE9iamVjdC5hc3NpZ24oT2JqZWN0LmFzc2lnbih7fSwgcyksIGMpLCB0aGlzLnJlbmRlckxhYmVsID0gbiB8fCBvLCB0aGlzLmxvY2tSb290TGV2ZWwgPSB2b2lkIDAgPT09IHUgfHwgdSwgdGhpcy5vbkNoYW5nZSA9IGggfHwgaSwgdGhpcy5vbkNsaWNrID0gcCB8fCBhLCB0aGlzLmNvbmZpcm0gPSBnIHx8IGwsIHRoaXMuaW5pdENvbGxhcHNlTGV2ZWwgPSB2b2lkIDAgPT09IGYgPyAyIDogZiwgdGhpcy5kaXNhYmxlU29ydGluZyA9IHYgfHwgITEsIHQuY2xhc3NMaXN0LmFkZCh0aGlzLnN0eWxlcy50cmVlKSwgdGhpcy5yZW5kZXIoeyBub2RlczogZSwgZWxlbWVudDogdCB9KSwgYiAmJiAoKChlLCB0KSA9PiB7IGNvbnN0IG4gPSBzZXNzaW9uU3RvcmFnZS5nZXRJdGVtKE4oZSkpOyBpZiAoIW4pIHJldHVybjsgY29uc3QgbyA9IEpTT04ucGFyc2UobiksIHIgPSAoZSwgdCkgPT4geyBjb25zdCBbbiwgb10gPSB0OyBlLmVsZW1lbnQuY29sbGFwc2UoMCA9PT0gbiksIG8gJiYgZS5zdWJub2Rlcy5mb3JFYWNoKCgoZSwgdCkgPT4geyB2b2lkIDAgIT09IG9bdF0gJiYgcihlLCBvW3RdKSB9KSkgfTsgdC5mb3JFYWNoKCgoZSwgdCkgPT4geyB2b2lkIDAgIT09IG9bdF0gJiYgcihlLCBvW3RdKSB9KSkgfSkoYiwgdGhpcy5wYXJzZVRyZWUodGhpcy5yb290KSksIHRoaXMuaW5pdFN0YXRlT2JzZXJ2ZXIoYikpKSA6IGNvbnNvbGUuZXJyb3IoJ0Vycm9yOiAiZWxlbWVudCIgaXMgbm90IGEgdmFsaWQgSFRNTCBlbGVtZW50IScpKSB9IGdldE5vZGUoZSkgeyByZXR1cm4gdGhpcy5ub2RlQ29sbGVjdGlvbltlXSB9IGZpbmROb2RlKGUsIHQpIHsgY29uc3QgbiA9IE9iamVjdC52YWx1ZXModGhpcy5ub2RlQ29sbGVjdGlvbik7IGZvciAobGV0IG8gPSAwOyBvIDwgbi5sZW5ndGg7IG8rKykgeyBjb25zdCByID0gbltvXSwgcyA9IHIuZGF0YTsgaWYgKGUgaW4gcyAmJiBzW2VdID09IHQpIHJldHVybiByIH0gcmV0dXJuIG51bGwgfSBvbkRyb3AoZSwgdCwgbikgeyBjb25zdCBvID0geyBub2RlczogdGhpcy5wYXJzZVRyZWUodGhpcy5yb290KSwgbW92ZWROb2RlOiBlLCBzcmNQYXJlbnROb2RlOiB0LCB0YXJnZXRQYXJlbnROb2RlOiBuIH07IG51bGwgPT0gbiB8fCBuLmNvbGxhcHNlKCExKSwgdGhpcy5vbkNoYW5nZShvKSB9IGRlc3Ryb3koKSB7IHRoaXMub2JzZXJ2ZXIuZGlzY29ubmVjdCgpLCB0aGlzLm9ic2VydmVyID0gbnVsbCB9IGluaXRTdGF0ZU9ic2VydmVyKGUpIHsgdGhpcy5vYnNlcnZlciA9IG5ldyBNdXRhdGlvbk9ic2VydmVyKCgoKSA9PiB7ICgoZSwgdCkgPT4geyBjb25zdCBuID0gW10sIG8gPSBlID0+IHsgY29uc3QgdCA9IFtdLCBuID0gW2UuZWxlbWVudC5nZXRBdHRyaWJ1dGUoIm9wZW4iKSA/IDEgOiAwLCB0XTsgcmV0dXJuIGUuc3Vibm9kZXMuZm9yRWFjaCgoZSA9PiB7IHQucHVzaChvKGUpKSB9KSksIG4gfTsgdC5mb3JFYWNoKChlID0+IHsgbi5wdXNoKG8oZSkpIH0pKSwgc2Vzc2lvblN0b3JhZ2Uuc2V0SXRlbShOKGUpLCBKU09OLnN0cmluZ2lmeShuKSkgfSkoZSwgdGhpcy5wYXJzZVRyZWUodGhpcy5yb290KSkgfSkpLCB0aGlzLm9ic2VydmVyLm9ic2VydmUodGhpcy5yb290LCB7IGNoaWxkTGlzdDogITAsIGF0dHJpYnV0ZXM6ICEwLCBzdWJ0cmVlOiAhMCB9KSB9IGRlZmluZUVsZW1lbnRzKCkgeyB0cnkgeyBjdXN0b21FbGVtZW50cy5kZWZpbmUodS5UQUdfTkFNRSwgdSkgfSBjYXRjaCAoZSkgeyB9IH0gcmVuZGVyKHsgbm9kZXM6IGUsIGVsZW1lbnQ6IHQgfSwgbiA9IDApIHsgbisrLCBlLmZvckVhY2goKGUgPT4geyBjb25zdCBvID0gdS5jcmVhdGUoeyBpY29uczogdGhpcy5pY29ucywgc3R5bGVzOiB0aGlzLnN0eWxlcywgcGFyZW50OiB0LCByZW5kZXJMYWJlbDogdGhpcy5yZW5kZXJMYWJlbCwgZGF0YTogZS5kYXRhLCBvbkNsaWNrOiB0aGlzLm9uQ2xpY2ssIGRyYWdnYWJsZTogIXRoaXMuZGlzYWJsZVNvcnRpbmcgfSk7IHRoaXMuZGlzYWJsZVNvcnRpbmcgfHwgKChlLCB0KSA9PiB7IGNvbnN0IG4gPSB7IGRyYWdzdGFydDogRSwgZHJvcDogXywgZHJhZ292ZXI6IHksIGRyYWdlbmQ6IFQsIGRyYWdsZWF2ZTogQSB9OyBmb3IgKGNvbnN0IFtvLCByXSBvZiBPYmplY3QuZW50cmllcyhuKSkgcCh7IG5vZGU6IGUsIGV2ZW50TmFtZTogbywgaGFuZGxlcjogciwgdHJlZTogdCB9KSB9KShvLCB0aGlzKSwgby5jb2xsYXBzZShuID4gdGhpcy5pbml0Q29sbGFwc2VMZXZlbCksIHRoaXMubm9kZUNvbGxlY3Rpb25bby5pZF0gPSBvLCBlLm5vZGVzICYmIHRoaXMucmVuZGVyKHsgbm9kZXM6IGUubm9kZXMsIGVsZW1lbnQ6IG8uc3Vibm9kZXMgfSwgbikgfSkpIH0gcGFyc2VUcmVlKGUpIHsgY29uc3QgdCA9IEFycmF5LmZyb20oZS5xdWVyeVNlbGVjdG9yQWxsKGA6c2NvcGUgPiAke3UuVEFHX05BTUV9YCkpLCBuID0gW107IHJldHVybiB0LmZvckVhY2goKGUgPT4geyBuLnB1c2goeyBlbGVtZW50OiBlLCBpZDogZS5pZCwgc3Vibm9kZXM6IHRoaXMucGFyc2VUcmVlKGUuc3Vibm9kZXMpIH0pIH0pKSwgbiB9IH07IHJldHVybiB0ID0gdC5kZWZhdWx0IH0pKCkpKTs=",mtime:1770926754872},"assets/icons/navigation-2.svg":{data:"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIgogICAgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIgogICAgY2xhc3M9ImZlYXRoZXIgZmVhdGhlci1uYXZpZ2F0aW9uLTIiPgogICAgPHBvbHlnb24gcG9pbnRzPSIxMiAyIDE5IDIxIDEyIDE3IDUgMjEgMTIgMiI+PC9wb2x5Z29uPgo8L3N2Zz4=",mtime:1770926754873},"assets/icons/eye-off.svg":{data:"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNy45NCAxNy45NEExMC4wNyAxMC4wNyAwIDAgMSAxMiAyMGMtNyAwLTExLTgtMTEtOGExOC40NSAxOC40NSAwIDAgMSA1LjA2LTUuOTRNOS45IDQuMjRBOS4xMiA5LjEyIDAgMCAxIDEyIDRjNyAwIDExIDggMTEgOGExOC41IDE4LjUgMCAwIDEtMi4xNiAzLjE5bS02LjcyLTEuMDdhMyAzIDAgMSAxLTQuMjQtNC4yNCI+PC9wYXRoPjxsaW5lIHgxPSIxIiB5MT0iMSIgeDI9IjIzIiB5Mj0iMjMiPjwvbGluZT48L3N2Zz4K",mtime:1770926754873},"assets/icons/x-circle.svg":{data:"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXIteC1jaXJjbGUiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIj48L2NpcmNsZT48bGluZSB4MT0iMTUiIHkxPSI5IiB4Mj0iOSIgeTI9IjE1Ij48L2xpbmU+PGxpbmUgeDE9IjkiIHkxPSI5IiB4Mj0iMTUiIHkyPSIxNSI+PC9saW5lPjwvc3ZnPg==",mtime:1770926754873},"assets/icons/eye.svg":{data:"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xIDEyczQtOCAxMS04IDExIDggMTEgOC00IDgtMTEgOC0xMS04LTExLTh6Ij48L3BhdGg+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyI+PC9jaXJjbGU+PC9zdmc+Cg==",mtime:1770926754873},"assets/icons/folder-minus.svg":{data:"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZm9sZGVyLW1pbnVzIj48cGF0aCBkPSJNMjIgMTlhMiAyIDAgMCAxLTIgMkg0YTIgMiAwIDAgMS0yLTJWNWEyIDIgMCAwIDEgMi0yaDVsMiAzaDlhMiAyIDAgMCAxIDIgMnoiPjwvcGF0aD48bGluZSB4MT0iOSIgeTE9IjE0IiB4Mj0iMTUiIHkyPSIxNCI+PC9saW5lPjwvc3ZnPg==",mtime:1770926754873},"assets/icons/folder-plus.svg":{data:"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZm9sZGVyLXBsdXMiPjxwYXRoIGQ9Ik0yMiAxOWEyIDIgMCAwIDEtMiAySDRhMiAyIDAgMCAxLTItMlY1YTIgMiAwIDAgMSAyLTJoNWwyIDNoOWEyIDIgMCAwIDEgMiAyeiI+PC9wYXRoPjxsaW5lIHgxPSIxMiIgeTE9IjExIiB4Mj0iMTIiIHkyPSIxNyI+PC9saW5lPjxsaW5lIHgxPSI5IiB5MT0iMTQiIHgyPSIxNSIgeTI9IjE0Ij48L2xpbmU+PC9zdmc+",mtime:1770926754873},"assets/icons/refresh-cw.svg":{data:"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItcmVmcmVzaC1jdyI+PHBvbHlsaW5lIHBvaW50cz0iMjMgNCAyMyAxMCAxNyAxMCI+PC9wb2x5bGluZT48cG9seWxpbmUgcG9pbnRzPSIxIDIwIDEgMTQgNyAxNCI+PC9wb2x5bGluZT48cGF0aCBkPSJNMy41MSA5YTkgOSAwIDAgMSAxNC44NS0zLjM2TDIzIDEwTTEgMTRsNC42NCA0LjM2QTkgOSAwIDAgMCAyMC40OSAxNSI+PC9wYXRoPjwvc3ZnPg==",mtime:1770926754873},"assets/treeview.js":{data:"data:application/javascript;base64,LyoqCiAqIEB0eXBlZGVmIHtpbXBvcnQoIi4uL2FwaS50cyIpLlRyZWVOb2RlfSBUcmVlTm9kZQogKi8KCi8qKgogKiBAdHlwZWRlZiBTb3J0YWJsZVRyZWVOb2RlCiAqIEB0eXBlIHtPYmplY3R9CiAqIEBwcm9wZXJ0eSB7VHJlZU5vZGVbImRhdGEiXX0gZGF0YQogKi8KCi8qKgogKgogKiBAdHlwZWRlZiBUcmVlVmlld0NvbmZpZwogKiBAdHlwZSB7T2JqZWN0fQogKiBAcHJvcGVydHkge3N0cmluZ30gY3VycmVudFBhZ2UgLSB0aGUgY3VycmVudCBwYWdlIHNob3duIGluIFNpbHZlckJ1bGxldC4KICogQHByb3BlcnR5IHtUcmVlTm9kZVtdfSBub2RlcyAtIGEgdHJlZSBvZiBhbGwgcGFnZXMgaW4gdGhlIGN1cnJlbnQgc3BhY2UuCiAqIEBwcm9wZXJ0eSB7T2JqZWN0fSBkcmFnQW5kRHJvcCAtIGRyYWcgYW5kIGRyb3AgcmVsYXRlZCBjb25maWcKICogQHByb3BlcnR5IHtib29sZWFufSBkcmFnQW5kRHJvcC5lbmFibGVkIC0gdHJ1ZSBpZiBkcmFnIGFuZCBkcm9wIGlzIGVuYWJsZWQKICogQHByb3BlcnR5IHtib29sZWFufSBkcmFnQW5kRHJvcC5jb25maXJtT25SZW5hbWUgLSB0cnVlIGlmIGEgY29uZmlybWF0aW9uIHNob3VsZCBiZSBzaG93bgogKiAgd2hlbiBhIG5vZGUgaXMgZHJhZ2dlZCBhbmQgZHJvcHBlZC4KKi8KCgpjb25zdCBUUkVFX1NUQVRFX0lEID0gInRyZWV2aWV3IjsKCi8qKgogKiBJbml0aWFsaXplcyB0aGUgVHJlZVZpZXcncyBgU29ydGFibGVUcmVlYCBpbnN0YW5jZS4KICogQHBhcmFtIHtUcmVlVmlld0NvbmZpZ30gY29uZmlnCiAqIEByZXR1cm5zIHtTb3J0YWJsZVRyZWV9CiAqLwpmdW5jdGlvbiBjcmVhdGVUcmVlVmlldyhjb25maWcpIHsKICByZXR1cm4gbmV3IFNvcnRhYmxlVHJlZSh7CiAgICBub2RlczogY29uZmlnLm5vZGVzLAogICAgZGlzYWJsZVNvcnRpbmc6ICFjb25maWcuZHJhZ0FuZERyb3AuZW5hYmxlZCwKICAgIGVsZW1lbnQ6IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGNvbmZpZy50cmVlRWxlbWVudElkKSwKICAgIHN0YXRlSWQ6IFRSRUVfU1RBVEVfSUQsCiAgICBpbml0Q29sbGFwc2VMZXZlbDogMCwKICAgIGxvY2tSb290TGV2ZWw6IGZhbHNlLAoKICAgIC8qKgogICAgICogQHBhcmFtIHtTb3J0YWJsZVRyZWVOb2RlfSBtb3ZlZE5vZGUKICAgICAqIEBwYXJhbSB7U29ydGFibGVUcmVlTm9kZX0gdGFyZ2V0UGFyZW50Tm9kZQogICAgICovCiAgICBjb25maXJtOiBhc3luYyAobW92ZWROb2RlLCB0YXJnZXRQYXJlbnROb2RlKSA9PiB7CiAgICAgIGNvbnN0IG9sZFByZWZpeCA9IG1vdmVkTm9kZS5kYXRhLm5hbWU7CiAgICAgIGNvbnN0IG5ld1ByZWZpeCA9IHRhcmdldFBhcmVudE5vZGUgPyBgJHt0YXJnZXRQYXJlbnROb2RlLmRhdGEubmFtZX0vJHttb3ZlZE5vZGUuZGF0YS50aXRsZX1gIDogbW92ZWROb2RlLmRhdGEudGl0bGU7CgogICAgICBpZiAob2xkUHJlZml4ID09PSBuZXdQcmVmaXgpIHsKICAgICAgICByZXR1cm47CiAgICAgIH0KCiAgICAgIGNvbnN0IHN1Y2Nlc3MgPSBhd2FpdCBzeXNjYWxsKCJzeXN0ZW0uaW52b2tlRnVuY3Rpb24iLCAiaW5kZXgucmVuYW1lUHJlZml4Q29tbWFuZCIsIHsKICAgICAgICBvbGRQcmVmaXgsCiAgICAgICAgbmV3UHJlZml4LAogICAgICAgIGRpc2FibGVDb25maXJtYXRpb246ICFjb25maWcuZHJhZ0FuZERyb3AuY29uZmlybU9uUmVuYW1lLAogICAgICB9KTsKCiAgICAgIGlmIChzdWNjZXNzICYmIGNvbmZpZy5jdXJyZW50UGFnZS5pbmRleE9mKG9sZFByZWZpeCkgPT09IDApIHsKICAgICAgICAvLyBJZiB0aGlzIHJlbmFtZWQgdGhlIGN1cnJlbnQgcGFnZSwgbmF2aWdhdGUgdG8gaXQgYXQgaXQncyB1cGRhdGVkIG5hbWUuCiAgICAgICAgYXdhaXQgc3lzY2FsbCgiZWRpdG9yLm5hdmlnYXRlIiwgY29uZmlnLmN1cnJlbnRQYWdlLnJlcGxhY2Uob2xkUHJlZml4LCBuZXdQcmVmaXgpLCBmYWxzZSwgZmFsc2UpOwogICAgICB9CgogICAgICByZXR1cm4gc3VjY2VzczsKICAgIH0sCgogICAgb25DaGFuZ2U6IGFzeW5jICgpID0+IHsKICAgICAgYXdhaXQgc3lzY2FsbCgic3lzdGVtLmludm9rZUZ1bmN0aW9uIiwgInRyZWV2aWV3LnNob3ciKTsKICAgIH0sCgogICAgLyoqCiAgICAgKiBAcGFyYW0ge1NvcnRhYmxlVHJlZU5vZGV9IG5vZGUKICAgICAqLwogICAgb25DbGljazogYXN5bmMgKF9ldmVudCwgbm9kZSkgPT4gewogICAgICBpZiAobm9kZS5kYXRhLm5vZGVUeXBlID09PSAiYXR0YWNobWVudCIpIHsKICAgICAgICAvLyBPcGVuIGF0dGFjaG1lbnQgaW4gbmV3IHRhYgogICAgICAgIHdpbmRvdy5vcGVuKGAvJHtub2RlLmRhdGEubmFtZX1gLCAiX2JsYW5rIik7CiAgICAgIH0gZWxzZSB7CiAgICAgICAgYXdhaXQgc3lzY2FsbCgiZWRpdG9yLm5hdmlnYXRlIiwgbm9kZS5kYXRhLm5hbWUsIGZhbHNlLCBmYWxzZSk7CiAgICAgIH0KICAgIH0sCgogICAgLyoqCiAgICAgKiBAcGFyYW0ge1NvcnRhYmxlVHJlZU5vZGVbImRhdGEiXX0gZGF0YQogICAgICogQHJldHVybnMge3N0cmluZ30KICAgICAqLwogICAgcmVuZGVyTGFiZWw6IChkYXRhKSA9PiBgCiAgICAgIDxzcGFuCiAgICAgICAgZGF0YS1jdXJyZW50LXBhZ2U9IiR7SlNPTi5zdHJpbmdpZnkoZGF0YS5pc0N1cnJlbnRQYWdlIHx8IGZhbHNlKX0iCiAgICAgICAgZGF0YS1ub2RlLXR5cGU9IiR7ZGF0YS5ub2RlVHlwZX0iCiAgICAgICAgZGF0YS1wZXJtaXNzaW9uPSIke2RhdGEucGVybX0iCiAgICAgICAgdGl0bGU9IiR7ZGF0YS5uYW1lfSIgPgogICAgICAgICR7ZGF0YS50aXRsZX0KICAgICAgPC9zcGFuPmAKICAgICwKICB9KTsKfQoKLyoqCiAqIEluaXRpYWxpemVzIHRoZSB0cmVlIHZpZXcgYW5kIGl0J3MgYWN0aW9uIGJhci4KICogQHBhcmFtIHtUcmVlVmlld0NvbmZpZ30gY29uZmlnCiAqLwovLyBkZW5vLWxpbnQtaWdub3JlIG5vLXVudXNlZC12YXJzCmZ1bmN0aW9uIGluaXRpYWxpemVUcmVlVmlld1BhbmVsKGNvbmZpZykgewogIGNvbnN0IHRyZWUgPSBjcmVhdGVUcmVlVmlldyhjb25maWcpOwogIGNvbnN0IGhhbmRsZUFjdGlvbiA9IChhY3Rpb24pID0+IHsKICAgIHN3aXRjaCAoYWN0aW9uKSB7CiAgICAgIGNhc2UgImNvbGxhcHNlLWFsbCI6IHsKICAgICAgICBkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsKCJzb3J0YWJsZS10cmVlLW5vZGVbb3Blbj0ndHJ1ZSddIikuZm9yRWFjaCgobm9kZSkgPT4gbm9kZS5jb2xsYXBzZSh0cnVlKSk7CiAgICAgICAgcmV0dXJuIHRydWU7CiAgICAgIH0KICAgICAgY2FzZSAiZXhwYW5kLWFsbCI6IHsKICAgICAgICBkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsKCJzb3J0YWJsZS10cmVlLW5vZGU6bm90KFtvcGVuPSd0cnVlJ10pIikuZm9yRWFjaCgobm9kZSkgPT4gbm9kZS5jb2xsYXBzZShmYWxzZSkpOwogICAgICAgIHJldHVybiB0cnVlOwogICAgICB9CiAgICAgIGNhc2UgImNsb3NlLXBhbmVsIjogewogICAgICAgIHN5c2NhbGwoInN5c3RlbS5pbnZva2VGdW5jdGlvbiIsICJ0cmVldmlldy5oaWRlIik7CiAgICAgICAgcmV0dXJuIHRydWU7CiAgICAgIH0KICAgICAgY2FzZSAicmVmcmVzaCI6IHsKICAgICAgICBzeXNjYWxsKCJzeXN0ZW0uaW52b2tlRnVuY3Rpb24iLCAidHJlZXZpZXcuc2hvdyIpOwogICAgICAgIHJldHVybiB0cnVlOwogICAgICB9CiAgICAgIGNhc2UgInJldmVhbC1jdXJyZW50LXBhZ2UiOiB7CiAgICAgICAgY29uc3QgY3VycmVudE5vZGUgPSB0cmVlLmZpbmROb2RlKCJpc0N1cnJlbnRQYWdlIiwgdHJ1ZSk7CiAgICAgICAgaWYgKGN1cnJlbnROb2RlKSB7CiAgICAgICAgICBjdXJyZW50Tm9kZS5yZXZlYWwoKTsKICAgICAgICAgIGN1cnJlbnROb2RlLnNjcm9sbEludG9WaWV3KHsKICAgICAgICAgICAgYmVoYXZpb3I6ICJhdXRvIiwKICAgICAgICAgICAgYmxvY2s6ICJuZWFyZXN0IiwKICAgICAgICAgICAgaW5saW5lOiAibmVhcmVzdCIsCiAgICAgICAgICB9KTsKICAgICAgICAgIHJldHVybiB0cnVlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gZmFsc2U7CiAgICAgIH0KICAgICAgY2FzZSAiaW5jcmVhc2Utd2lkdGgiOiB7CiAgICAgICAgc3lzY2FsbCgic3lzdGVtLmludm9rZUZ1bmN0aW9uIiwgInRyZWV2aWV3LmluY3JlYXNlV2lkdGgiKTsKICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfQogICAgICBjYXNlICJkZWNyZWFzZS13aWR0aCI6IHsKICAgICAgICBzeXNjYWxsKCJzeXN0ZW0uaW52b2tlRnVuY3Rpb24iLCAidHJlZXZpZXcuZGVjcmVhc2VXaWR0aCIpOwogICAgICAgIHJldHVybiB0cnVlOwogICAgICB9CiAgICAgIGNhc2UgInRvZ2dsZS1oaWRkZW4iOiB7CiAgICAgICAgc3lzY2FsbCgic3lzdGVtLmludm9rZUZ1bmN0aW9uIiwgInRyZWV2aWV3LnRvZ2dsZUhpZGRlbiIpOwogICAgICAgIHJldHVybiB0cnVlOwogICAgICB9CiAgICB9CgogICAgcmV0dXJuIGZhbHNlOwogIH0KCiAgaWYgKGNvbmZpZy5yZXZlYWxPbkxvYWQpIHsKICAgIGhhbmRsZUFjdGlvbigicmV2ZWFsLWN1cnJlbnQtcGFnZSIpOwogIH0KCiAgZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgiW2RhdGEtdHJlZXZpZXctYWN0aW9uXSIpLmZvckVhY2goKGVsKSA9PiB7CiAgICBlbC5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsIChlKSA9PiB7CiAgICAgIGlmIChoYW5kbGVBY3Rpb24oZWwuZGF0YXNldFsidHJlZXZpZXdBY3Rpb24iXSkpIHsKICAgICAgICBlLnN0b3BQcm9wYWdhdGlvbigpOwogICAgICAgIGUucHJldmVudERlZmF1bHQoKTsKICAgICAgfQogICAgfSk7CiAgfSkKfQo=",mtime:1770926754873}},functions:{hide:{path:"./treeview.ts:hideTree"},show:{path:"./treeview.ts:showTree"},showIfEnabled:{path:"./treeview.ts:showTreeIfEnabled",events:["editor:init","editor:pageLoaded","editor:pageSaved","file:deleted"]},toggle:{path:"./treeview.ts:toggleTree",command:{name:"Tree View: Toggle",key:"Ctrl-alt-b",mac:"Cmd-alt-b"}},reveal:{path:"./treeview.ts:showTree",command:{name:"Tree View: Reveal Current Page"}},deletePage:{path:"./treeview.ts:deletePage",command:{name:"Page: Delete (shortcut)",key:"Ctrl-Shift-d",mac:"Cmd-Shift-d"}},increaseWidth:{path:"./treeview.ts:increaseWidth",command:{name:"Tree View: Increase Width"}},decreaseWidth:{path:"./treeview.ts:decreaseWidth",command:{name:"Tree View: Decrease Width"}},toggleHidden:{path:"./treeview.ts:toggleHidden",command:{name:"Tree View: Toggle Hidden Files"}}}},No={manifest:Wt,functionMapping:Kt};tt(Kt,Wt,self.postMessage);export{No as plug};
diff --git a/configs/notes/space/inbox.md b/configs/notes/space/inbox.md
deleted file mode 100644
index 16341df..0000000
--- a/configs/notes/space/inbox.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Inbox
-
-Apple **Quick Note** analogue. Dump here first; triage into [[TODO]] (Now / Next / Waiting / Someday) or a dedicated page.
-
-Rules:
-
-- Empty capture OK — fill the next `- [ ]` line and save.
-- Do not create a new folder just to capture.
-- After triage, clear or check off lines so Inbox stays short.
-
-## Capture
-
-- [ ]
diff --git a/configs/notes/space/index.md b/configs/notes/space/index.md
deleted file mode 100644
index bfa625c..0000000
--- a/configs/notes/space/index.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-# SilverBullet index page (SB_INDEX_PAGE=index)
-# Apple-Notes-like home: pins + capture path + search
----
-
-# Notes
-
-Cross-host notes (magikoopa + clementine) via koopa SilverBullet.
-Mental model = Apple Notes: **folders/pages · list · editor**, pin primary TODO, Quick Note → triage.
-
-| Page | Apple analogue | Role |
-|------|----------------|------|
-| [[TODO]] | Pinned primary note | ADHD SoT: Now≤3 / Next / Waiting / Someday / Stats |
-| [[inbox]] | Quick Note | Capture before triage |
-
-## Pins
-
-- [[TODO]]
-- [[inbox]]
-
-## How to use (Apple habits → here)
-
-1. **Capture** into [[inbox]] (or SB quick-note if enabled) — do not invent folders first.
-2. **Triage** into [[TODO]] sections (Now max 3). Same rules as former Apple Notes guide.
-3. **Find** with Page Picker / search (**Cmd/Ctrl-K**).
-4. **New page** when a topic is not a todo (meetings, dumps) — link from TODO by title, do not dump transcripts into TODO.
-5. Optional later: TreeView plug (folder sidebar), Space Lua query page = “smart folder” for `#tag` open tasks.
-
-## Shortcuts (target)
-
-| Action | Typical SB |
-|--------|------------|
-| Open page / filter | Cmd/Ctrl-K (Page Picker) |
-| Quick note | Ctrl-q (if client supports) → prefer landing in [[inbox]] |
-| Search body | built-in search / SilverSearch plug |
-
-## Hygiene (from Apple Notes ADHD pack)
-
-- One primary [[TODO]] — no competing systems.
-- Capture first; tags (`#taler` …) optional and light.
-- On “I’m back”: re-read **Now** only.
-- No passwords/tokens in notes.
-
-## Apps
-
-- [[PLUGS]] — TreeView + SilverSearch
diff --git a/configs/paivana/README.md b/configs/paivana/README.md
index 285cec8..643fdb4 100644
--- a/configs/paivana/README.md
+++ b/configs/paivana/README.md
@@ -2,32 +2,27 @@
GNU Taler **paivana-httpd** reverse-proxy paywall (DD 95 / DD 76 style), GOA payments.
-One process = one `BASE_URL` + one `DESTINATION_BASE_URL` + one `MERCHANT_BACKEND_URL`.
-Shared **image / secrets / entrypoint / conf template**; per-service env sets
-`PAIVANA_BASE_URL` + `PAIVANA_DESTINATION_BASE_URL`. Portal on **9025**; app
-paywalls on **9028–9031** (Caddy unchanged). App backends listen loopback only
-(`19128` SilverBullet, `19129` Galene HTTP, `19130` CryptPad edge, `19131` Meet).
-
| Item | Value |
|------|--------|
| Live | `/home/hernani/koopa-paivana/` |
-| Containers | `koopa-paivana`, `koopa-paivana-{silverbullet,galene,cryptpad,meet}`, `koopa-paivana-upstream` |
+| Containers | `koopa-paivana`, `koopa-paivana-upstream` |
| Image | `localhost/koopa-paivana:latest` (built from `Containerfile`); process **uid 1000** `paivana-httpd` |
-| Upstream | portal: `nginxinc/nginx-unprivileged` on **:8080**; apps via pasta host IP `172.17.0.1:19128–19131` (compose publish, not `127.0.0.1`) |
-| Host ports | **9025** portal; **9028/9029/9030/9031** app paywalls → Caddy |
-| Currency | **multi** via goa-shop template `paivana` choices (CHF / FLOODS / GOA / TESTPAYSAN); wallet still filters by held coins |
+| Upstream | `nginxinc/nginx-unprivileged` on **:8080** (not root nginx:80) |
+| Host port | **9025** → Caddy `paivana.hacktivism.ch` |
+| Currency | **GOA** |
| Merchant | `https://taler.hacktivism.ch/instances/goa-shop/` |
-| Template | `paivana` (`template_type: paivana`, amount **`GOA:42`**, `website_regex: .*`) |
-| Access | **`-g`** site-wide: pay once → that hostname free to browse |
+| Template | `paivana` (`template_type: paivana`, amount **`GOA:4200`**, `website_regex: .*`) |
+| Access | **`-g`** site-wide: pay once → example upstream free to browse |
+| Upstream | example site in `upstream/` (compose service) |
## Layout
| Path | Role |
|------|------|
| `Containerfile` | Multi-stage: build paivana-httpd from `git.taler.net/paivana` + Taler libs |
-| `compose.yml` | portal + 4 app paywalls + nginx upstream (YAML anchor `x-paivana-common`) |
-| `conf/paivana.conf.template` | Shared conf; `@BASE_URL@` / `@DESTINATION_BASE_URL@` / secrets at start |
-| `entrypoint.sh` | Env + secrets → run `paivana-httpd -g -f` |
+| `compose.yml` | `koopa-paivana` + nginx upstream |
+| `conf/paivana.conf.template` | Config; secrets substituted at start |
+| `entrypoint.sh` | Inject secrets → run `paivana-httpd -g -f` |
| `secrets/` | **not in git** — live only |
| `upstream/` | Protected demo content |
| `container-koopa-paivana.service` | systemd --user oneshot compose up |
@@ -95,10 +90,9 @@ extra_hosts:
| Check | Expect |
|-------|--------|
| `curl -si http://127.0.0.1:9025/` | **302** → `/.well-known/paivana/templates/paivana#…` |
-| `curl -si http://127.0.0.1:902{8,9,0}/` | **302** same template path (per-host BASE_URL) |
-| `https://{paivana,silverbullet,galene,cryptpad,meet}.hacktivism.ch/` | unpaid → **302** `…/templates/paivana#…` (or paywall HTML) |
+| `https://paivana.hacktivism.ch/` | **402** paywall HTML + `paivana: taler://pay-template/…` |
| monitoring `urls` | `www.paivana` OK |
-| e2e (GOA, lokal) | `e2e.paivana` Template-Pay **GOA:42** (wenn `E2E_PAIVANA=1`) |
+| e2e (GOA, lokal) | `e2e.paivana` Template-Pay **GOA:4200** (wenn `E2E_PAIVANA=1`) |
---
@@ -143,26 +137,22 @@ Flags `-g` (site-wide payment) and `-f` (X-Forwarded-For).
## Merchant template
-Create (or replace via PATCH / DELETE+POST — see live merchant API):
-
```bash
curl -X POST 'https://taler.hacktivism.ch/instances/goa-shop/private/templates' \
-H "Authorization: Bearer secret-token:…" \
-H 'Content-Type: application/json' \
-d '{
"template_id": "paivana",
- "template_description": "Paivana paywall portal (GOA)",
+ "template_description": "Paivana paywall (GOA)",
"template_contract": {
"template_type": "paivana",
- "summary": "Access to paivana.hacktivism.ch portal",
+ "summary": "Access to paivana.hacktivism.ch",
"website_regex": ".*",
- "choices": [{ "amount": "GOA:42", "description": "Unlock portal (cryptpad / galene / silverbullet links)" }]
+ "choices": [{ "amount": "GOA:4200", "description": "Unlock example site (then free to browse)" }]
}
}'
```
-goa-shop template `paivana` exposes multi-currency **choices**. Wallet/UI still only offers currencies the wallet holds. Live conf must use `instances/goa-shop/` + matching merchant token (not `goa-demo-*`).
-
## Docs
- https://docs.taler.net/taler-paivana-manual.html
diff --git a/configs/paivana/compose.yml b/configs/paivana/compose.yml
index 160cfb7..221ec41 100644
--- a/configs/paivana/compose.yml
+++ b/configs/paivana/compose.yml
@@ -1,45 +1,29 @@
-# koopa-paivana — shared image/secrets/entrypoint for portal + app paywalls
+# koopa-paivana — host :9025 → Caddy paivana.hacktivism.ch
# Live: /home/hernani/koopa-paivana/
-#
-# Public Caddy targets (unchanged):
-# 9025 → portal (paivana.hacktivism.ch)
-# 9028 → paywall silverbullet
-# 9029 → paywall galene
-# 9030 → paywall cryptpad
-# 9031 → paywall meet (Jitsi web)
-# App backends on pasta host IP (see app compose; not LAN 0.0.0.0):
-# 172.17.0.1:19128 / 19129 / 19130 / 19131
-
-x-paivana-common: &paivana-common
- image: localhost/koopa-paivana:latest
- restart: unless-stopped
- user: "1000:1000"
- userns_mode: keep-id
- extra_hosts:
- - "taler.hacktivism.ch:172.17.0.1"
- - "bank.hacktivism.ch:172.17.0.1"
- - "exchange.hacktivism.ch:172.17.0.1"
- volumes:
- - ./conf/paivana.conf.template:/etc/paivana/paivana.conf.template:ro
- - ./entrypoint.sh:/usr/local/bin/paivana-entrypoint:ro
- - ./secrets/merchant-access-token:/run/secrets/merchant-access-token:ro
- - ./secrets/paivana-secret:/run/secrets/paivana-secret:ro
- labels:
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.currency: multi
-
services:
paivana:
- <<: *paivana-common
build:
context: .
dockerfile: Containerfile
+ image: localhost/koopa-paivana:latest
container_name: koopa-paivana
+ restart: unless-stopped
+ # Image already USER paivana-httpd; pin numeric + keep-id for host secrets/volumes.
+ user: "1000:1000"
+ userns_mode: keep-id
ports:
- "9025:9967"
- environment:
- PAIVANA_BASE_URL: https://paivana.hacktivism.ch/
- PAIVANA_DESTINATION_BASE_URL: http://upstream:8080/
+ extra_hosts:
+ # pasta publishes merchant on host :9010. Prefer fixed IPv4 — host-gateway
+ # alone still loses to public AAAA and times out from the container.
+ - "taler.hacktivism.ch:172.17.0.1"
+ - "bank.hacktivism.ch:172.17.0.1"
+ - "exchange.hacktivism.ch:172.17.0.1"
+ volumes:
+ - ./conf/paivana.conf.template:/etc/paivana/paivana.conf.template:ro
+ - ./entrypoint.sh:/usr/local/bin/paivana-entrypoint:ro
+ - ./secrets/merchant-access-token:/run/secrets/merchant-access-token:ro
+ - ./secrets/paivana-secret:/run/secrets/paivana-secret:ro
depends_on:
- upstream
labels:
@@ -47,70 +31,10 @@ services:
org.hacktivism.host_port: "9025"
org.hacktivism.site: paivana.hacktivism.ch
org.hacktivism.managed_by: koopa-admin
- org.hacktivism.currency: multi
-
- # App paywalls: same image/secrets; BASE/DEST via env. Caddy still hits 9028–9031.
- paivana-silverbullet:
- <<: *paivana-common
- container_name: koopa-paivana-silverbullet
- ports:
- - "9028:9967"
- environment:
- PAIVANA_BASE_URL: https://silverbullet.hacktivism.ch/
- PAIVANA_DESTINATION_BASE_URL: http://172.17.0.1:19128/
- labels:
- org.hacktivism.service: paivana-silverbullet
- org.hacktivism.host_port: "9028"
- org.hacktivism.site: silverbullet.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.currency: multi
-
- paivana-galene:
- <<: *paivana-common
- container_name: koopa-paivana-galene
- ports:
- - "9029:9967"
- environment:
- PAIVANA_BASE_URL: https://galene.hacktivism.ch/
- PAIVANA_DESTINATION_BASE_URL: http://172.17.0.1:19129/
- labels:
- org.hacktivism.service: paivana-galene
- org.hacktivism.host_port: "9029"
- org.hacktivism.site: galene.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.currency: multi
-
- paivana-cryptpad:
- <<: *paivana-common
- container_name: koopa-paivana-cryptpad
- ports:
- - "9030:9967"
- environment:
- PAIVANA_BASE_URL: https://cryptpad.hacktivism.ch/
- PAIVANA_DESTINATION_BASE_URL: http://172.17.0.1:19130/
- labels:
- org.hacktivism.service: paivana-cryptpad
- org.hacktivism.host_port: "9030"
- org.hacktivism.site: cryptpad.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.currency: multi
-
- paivana-meet:
- <<: *paivana-common
- container_name: koopa-paivana-meet
- ports:
- - "9031:9967"
- environment:
- PAIVANA_BASE_URL: https://meet.hacktivism.ch/
- PAIVANA_DESTINATION_BASE_URL: http://172.17.0.1:19131/
- labels:
- org.hacktivism.service: paivana-meet
- org.hacktivism.host_port: "9031"
- org.hacktivism.site: meet.hacktivism.ch
- org.hacktivism.managed_by: koopa-admin
- org.hacktivism.currency: multi
+ org.hacktivism.currency: GOA
upstream:
+ # Non-root nginx (listens on 8080). See DESTINATION_BASE_URL in conf template.
image: docker.io/nginxinc/nginx-unprivileged:1.27-alpine
container_name: koopa-paivana-upstream
restart: unless-stopped
diff --git a/configs/paivana/conf/paivana.conf.template b/configs/paivana/conf/paivana.conf.template
index fe8996c..6dec110 100644
--- a/configs/paivana/conf/paivana.conf.template
+++ b/configs/paivana/conf/paivana.conf.template
@@ -1,14 +1,14 @@
-# Shared paivana-httpd conf — BASE/DEST injected by entrypoint (env).
-# Secrets injected from /run/secrets/
+# paivana.hacktivism.ch — paywall reverse proxy
+# Secrets injected by entrypoint from /run/secrets/
[paivana]
SERVE = tcp
PORT = 9967
BIND_TO = 0.0.0.0
-BASE_URL = @BASE_URL@
+BASE_URL = https://paivana.hacktivism.ch/
-DESTINATION_BASE_URL = @DESTINATION_BASE_URL@
+DESTINATION_BASE_URL = http://upstream:8080/
MERCHANT_BACKEND_URL = https://taler.hacktivism.ch/instances/goa-shop/
MERCHANT_ACCESS_TOKEN = @MERCHANT_ACCESS_TOKEN@
diff --git a/configs/paivana/entrypoint.sh b/configs/paivana/entrypoint.sh
index 56461e4..e751b47 100755
--- a/configs/paivana/entrypoint.sh
+++ b/configs/paivana/entrypoint.sh
@@ -5,10 +5,6 @@ SECRET_FILE="${PAIVANA_SECRET_FILE:-/run/secrets/paivana-secret}"
TEMPLATE="${PAIVANA_CONF_TEMPLATE:-/etc/paivana/paivana.conf.template}"
OUT="${PAIVANA_CONF:-/tmp/paivana.conf}"
-# Per-instance (portal + app paywalls share this entrypoint/image).
-BASE_URL="${PAIVANA_BASE_URL:-https://paivana.hacktivism.ch/}"
-DEST_URL="${PAIVANA_DESTINATION_BASE_URL:-http://upstream:8080/}"
-
if [ ! -r "$TOKEN_FILE" ]; then
echo "missing merchant token: $TOKEN_FILE" >&2
exit 1
@@ -21,12 +17,10 @@ fi
TOKEN=$(tr -d '\n\r' < "$TOKEN_FILE")
SECRET=$(tr -d '\n\r' < "$SECRET_FILE")
-awk -v t="$TOKEN" -v s="$SECRET" -v b="$BASE_URL" -v d="$DEST_URL" '
+awk -v t="$TOKEN" -v s="$SECRET" '
{
gsub(/@MERCHANT_ACCESS_TOKEN@/, t)
gsub(/@PAIVANA_SECRET@/, s)
- gsub(/@BASE_URL@/, b)
- gsub(/@DESTINATION_BASE_URL@/, d)
print
}
' "$TEMPLATE" > "$OUT"
diff --git a/configs/paivana/upstream/index.html b/configs/paivana/upstream/index.html
index fe9706b..064d3d7 100644
--- a/configs/paivana/upstream/index.html
+++ b/configs/paivana/upstream/index.html
@@ -3,7 +3,7 @@
- hacktivism tools — unlocked
+ Example site — free after GOA:4200
unlocked
- hacktivism tools
+ Example site
- You paid once via GNU Taler / Paivana
- (template paivana on goa-shop: CHF / FLOODS / GOA / TESTPAYSAN choices).
- Site-wide access cookie is set for paivana.hacktivism.ch.
+ You paid GOA:4200 once via GNU Taler / Paivana.
+ This whole example site is now free for you to browse (site-wide access cookie).
-
-
Notes
+
What this demonstrates
- One Paivana process = one DESTINATION_BASE_URL → this portal is the gated upstream.
- Merchant: goa-shop · template paivana (multi-currency choices).
- Host: https://paivana.hacktivism.ch/
+ Paywall: paivana-httpd in front of a normal upstream HTTP site
+ Currency: exploration money GOA on exchange.hacktivism.ch
+ After payment: no further charge for pages behind this proxy
+
Host: https://paivana.hacktivism.ch/
diff --git a/configs/ports.md b/configs/ports.md
index d8febbe..52ca815 100644
--- a/configs/ports.md
+++ b/configs/ports.md
@@ -1,4 +1,4 @@
-# Port map (koopa)
+# Port map (see also host/overview/services.md)
| Port | Role |
|------|------|
@@ -7,44 +7,31 @@
| 443 | systemd `https-proxy.socket` → Caddy **9001** |
| 9000 | Caddy HTTP + ACME webroot |
| 9001 | Caddy HTTPS (vhosts) |
-| **9010** | `goa-regio-ng` merchant API (HTTPS in CTR) |
-| **9011** | `goa-regio-ng` exchange |
-| **9012** | `goa-regio-ng` libeufin-bank |
-| **9013–9015** | bank / exchange / merchant public landings (host nginx) |
-| 9020 | `koopa-castopod` → `castopod.hacktivism.ch` |
-| 9021 | `koopa-bonfire` → `bonfire.hacktivism.ch` |
-| 9022 | `koopa-prime` (Jellyfin) → `prime.hacktivism.ch` |
-| 9023 | qBittorrent WebUI → `bt.hacktivism.ch` |
-| **6881** | qBittorrent BT listen (tcp+udp; firewalld + VeciGate DNAT) |
-| **9024** | `koopa-forgejo` (HTTP) → **`git.hacktivism.ch`** |
-| **9025** | `koopa-paivana` → `paivana.hacktivism.ch` |
-| **9026** | `koopa-lemmy-proxy` → `lemmy.hacktivism.ch` |
-| **9027** | `koopa-decidim` → `decidim.hacktivism.ch` |
-| **9028** | `koopa-paivana-silverbullet` → `silverbullet.hacktivism.ch` |
-| **9029** | `koopa-paivana-galene` → `galene.hacktivism.ch` |
-| **9030** | `koopa-paivana-cryptpad` → `cryptpad.hacktivism.ch` |
-| **9031** | `koopa-paivana-meet` → `meet.hacktivism.ch` |
-| **9032** | `koopa-bbb-front` → `bbb.hacktivism.ch` |
-| **9200** | Forgejo git-SSH (host-direct; not Caddy) |
-| 9090–9092 | `koopa-tops-ng1`…`ng3` → `tops.ng{1,2,3}.hacktivism.ch` |
-| **8080** | host `tor.service` ORPort |
-| **9051** | host Tor ControlPort (loopback) |
+| 9010 | podman `taler-hacktivism` (merchant) |
+| 9011 | podman **`taler-hacktivism-exchange-ansible`** (exchange) |
+| 9012 | podman `taler-hacktivism-bank` (libeufin-bank) |
+| 9013–9015 | bank / exchange / merchant public landings |
+| 9020 | podman `koopa-castopod` → Caddy `castopod.hacktivism.ch` |
+| 9021 | podman `koopa-bonfire` → Caddy `bonfire.hacktivism.ch` |
+| 9022 | podman `koopa-prime` (Jellyfin) → Caddy `prime.hacktivism.ch` |
+| 9023 | podman qBittorrent → Caddy `bt.hacktivism.ch` |
+| **9024** | podman **`koopa-forgejo`** (HTTP) → Caddy **`git.hacktivism.ch`** |
+| **9025** | podman **`koopa-paivana`** (paivana-httpd) → Caddy **`paivana.hacktivism.ch`** |
+| **9026** | podman **`koopa-lemmy-proxy`** → Caddy **`lemmy.hacktivism.ch`** |
+| **9027** | podman **`koopa-decidim`** → Caddy **`decidim.hacktivism.ch`** |
+| **9200** | podman **Forgejo git-SSH** (host-direct; not Caddy) |
+| 9090–9092 | podman `koopa-tops-ng1` … `ng3` → Caddy `tops.ng{1,2,3}.hacktivism.ch` |
+| 8080 | Tor ORPort |
VeciGate: WAN **80→9000**, WAN **443→9001**.
-Public apps: Caddy on **9001** → 127.0.0.1:{9010–9015, 9020–9032, 9090–9092}.
-Git SSH needs **9200/tcp** if exposed to WAN.
+Public apps: Caddy vhosts on **9001** → 127.0.0.1:{9010–9015, 9020–9027, 9090–9092}.
+Git SSH needs separate NAT/firewall **9200/tcp** if exposed to WAN.
-| UDP range | Role |
-|-----------|------|
-| **10000–10099/udp** | Galene WebRTC |
-
-| Extra | Role |
-|-------|------|
-| **1789** | `koopa-nym` mixnet |
-| **1790** | `koopa-nym` verloc |
-| **9080** | `koopa-nym` HTTP API |
-| **9180** | lab `goa-regio-ng` HTTP (loopback; `configs/lab-local/`) |
-| **9322** | lab `goa-regio-ng` SSH/ansible (loopback) |
-| **8288** | lab `mytops-local` HTTP (loopback) |
-
-login-health inventory: `scripts/login-health/koopa-login-health.sh`.
+| **1789** | podman **`koopa-nym`** mixnet (nym.com nym-node) |
+| **1790** | podman **`koopa-nym`** verloc |
+| **9080** | podman **`koopa-nym`** HTTP API (mapped from container 8080; Tor keeps **8080**) |
+| **19000** | optional nym entry WS (gateway mode) |
+| **51822/udp** | optional nym WireGuard (dVPN path) |
+| **9180** | lab **`goa-regio-ng`** HTTP (loopback only; see `configs/lab-local/`) |
+| **9322** | lab **`goa-regio-ng`** SSH/ansible (loopback only) |
+| **8288** | lab **`mytops-local`** HTTP (loopback only) |
diff --git a/configs/prime/README.md b/configs/prime/README.md
index 5a81f79..2b0c287 100644
--- a/configs/prime/README.md
+++ b/configs/prime/README.md
@@ -6,7 +6,6 @@
| Compose | `compose.yml` (this dir; **no secret values**) |
| Jellyfin | **9022** → Caddy `prime.hacktivism.ch` |
| qBittorrent | **9023** → Caddy `bt.hacktivism.ch` |
-| BT listen | **6881** tcp+udp (firewalld + VeciGate DNAT); apply `scripts/prime/apply-qbittorrent-bt-net.sh` |
| Topic | `2026/2026-07-09--koopa-prime-jellyfin.md` |
`WEBUI_PASSWORD` is `${WEBUI_PASSWORD}` in the mirror — live value only in
diff --git a/configs/shared/landing-stack-versions.json b/configs/shared/landing-stack-versions.json
deleted file mode 100644
index a453057..0000000
--- a/configs/shared/landing-stack-versions.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "updated_iso": "2026-09-16T23:19:33+02:00",
- "updated_human": "2026-09-16 23:19 CEST",
- "ctr": "goa-regio-ng",
- "ssh": "hernani@192.168.100.95",
- "packages": {
- "libeufin-bank": {
- "dpkg": "1.6.10-0+trixie",
- "display": "1.6.10"
- },
- "libeufin-bank-webui": {
- "dpkg": "1.6.38-0+trixie",
- "display": "1.6.38"
- },
- "libeufin-common": {
- "dpkg": "1.6.10-0+trixie",
- "display": "1.6.10"
- },
- "libtalerexchange": {
- "dpkg": "1.6.15-0+trixie",
- "display": "1.6.15"
- },
- "libtalermerchant": {
- "dpkg": "1.6.23-0+trixie",
- "display": "1.6.23"
- },
- "taler-exchange": {
- "dpkg": "1.6.15-0+trixie",
- "display": "1.6.15"
- },
- "taler-exchange-database": {
- "dpkg": "1.6.15-0+trixie",
- "display": "1.6.15"
- },
- "taler-exchange-offline": {
- "dpkg": "1.6.15-0+trixie",
- "display": "1.6.15"
- },
- "taler-merchant": {
- "dpkg": "1.6.23-0+trixie",
- "display": "1.6.23"
- },
- "taler-merchant-webui": {
- "dpkg": "1.6.38-0+trixie",
- "display": "1.6.38"
- },
- "taler-terms-generator": {
- "dpkg": "1.6.15-0+trixie",
- "display": "1.6.15"
- }
- },
- "sites": {
- "bank": {
- "packages": {
- "libeufin-bank": "1.6.10",
- "libeufin-common": "1.6.10",
- "libeufin-bank-webui": "1.6.38"
- }
- },
- "exchange": {
- "packages": {
- "taler-exchange": "1.6.15",
- "libtalerexchange": "1.6.15",
- "taler-exchange-database": "1.6.15",
- "taler-exchange-offline": "1.6.15",
- "taler-terms-generator": "1.6.15"
- }
- },
- "merchant": {
- "packages": {
- "taler-merchant": "1.6.23",
- "taler-merchant-webui": "1.6.38",
- "libtalermerchant": "1.6.23",
- "libtalerexchange": "1.6.15",
- "taler-terms-generator": "1.6.15"
- }
- }
- }
-}
diff --git a/configs/shared/landing-stack.json b/configs/shared/landing-stack.json
deleted file mode 100644
index 0ab1749..0000000
--- a/configs/shared/landing-stack.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "note": "Stack-bar package lists per landing. Versions filled by scripts/taler-landing/stamp-landing-stack.sh from live CTR dpkg. Only hard link = vanilla branch.",
- "vanilla_href": "https://git.taler.net/taler-deployment.git/tree/?h=dev/hernani/goa-regio-ng-vanilla",
- "vanilla_title": "taler-deployment branch dev/hernani/goa-regio-ng-vanilla",
- "sites": {
- "bank": {
- "title": "Taler software on this bank service (goa-regio-ng vanilla)",
- "packages": [
- "libeufin-bank",
- "libeufin-common",
- "libeufin-bank-webui"
- ]
- },
- "exchange": {
- "title": "Taler software on this exchange service (goa-regio-ng vanilla)",
- "packages": [
- "taler-exchange",
- "libtalerexchange",
- "taler-exchange-database",
- "taler-exchange-offline",
- "taler-terms-generator"
- ]
- },
- "merchant": {
- "title": "Taler software on this merchant service (goa-regio-ng vanilla)",
- "packages": [
- "taler-merchant",
- "taler-merchant-webui",
- "libtalermerchant",
- "libtalerexchange",
- "taler-terms-generator"
- ]
- }
- }
-}
diff --git a/configs/shared/landing-version.json b/configs/shared/landing-version.json
index 59af0fd..722ff4e 100644
--- a/configs/shared/landing-version.json
+++ b/configs/shared/landing-version.json
@@ -2,34 +2,34 @@
"note": "Per-site landing content versions (start 42). Bump: scripts/taler-landing/stamp-landing-version.sh --bump bank|exchange|merchant",
"sites": {
"bank": {
- "version": 45,
+ "version": 44,
"label": "Bank landing",
"path": "configs/bank-landing",
- "updated_iso": "2026-09-16T23:19:33+02:00",
- "updated_human": "2026-09-16 23:19 CEST",
- "commit": "0d881cd726da8d298c98114678253e3cbb7aa5e6",
- "commit_short": "0d881cd",
- "repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/0d881cd726da8d298c98114678253e3cbb7aa5e6"
+ "updated_iso": "2026-07-17T18:31:49+02:00",
+ "updated_human": "2026-07-17 18:31 CEST",
+ "commit": "689ee595e08f3880c1e4068aac8e972574e20d21",
+ "commit_short": "689ee59",
+ "repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/689ee595e08f3880c1e4068aac8e972574e20d21"
},
"exchange": {
- "version": 44,
+ "version": 43,
"label": "Exchange landing",
"path": "configs/exchange-landing",
- "updated_iso": "2026-09-16T23:19:33+02:00",
- "updated_human": "2026-09-16 23:19 CEST",
- "commit": "0d881cd726da8d298c98114678253e3cbb7aa5e6",
- "commit_short": "0d881cd",
- "repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/0d881cd726da8d298c98114678253e3cbb7aa5e6"
+ "updated_iso": "2026-07-17T18:33:13+02:00",
+ "updated_human": "2026-07-17 18:33 CEST",
+ "commit": "d0594cb44ffb1bec2e009f6812167cbbf216eda9",
+ "commit_short": "d0594cb",
+ "repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/d0594cb44ffb1bec2e009f6812167cbbf216eda9"
},
"merchant": {
- "version": 43,
+ "version": 42,
"label": "Merchant landing",
"path": "configs/merchant-landing",
- "updated_iso": "2026-09-16T23:19:33+02:00",
- "updated_human": "2026-09-16 23:19 CEST",
- "commit": "0d881cd726da8d298c98114678253e3cbb7aa5e6",
- "commit_short": "0d881cd",
- "repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/0d881cd726da8d298c98114678253e3cbb7aa5e6"
+ "updated_iso": "2026-07-17T18:27:48+02:00",
+ "updated_human": "2026-07-17 18:27 CEST",
+ "commit": "37ea4bb8396691e3ea69ae61374e6623bf6c5ea7",
+ "commit_short": "37ea4bb",
+ "repo_commit_url": "https://git.hacktivism.ch/hernani/koopa-admin-log/commit/37ea4bb8396691e3ea69ae61374e6623bf6c5ea7"
}
}
}
diff --git a/configs/systemd/user/container-koopa-paivana.service.d/order.conf b/configs/systemd/user/container-koopa-paivana.service.d/order.conf
index 92b7e6f..c0678cc 100644
--- a/configs/systemd/user/container-koopa-paivana.service.d/order.conf
+++ b/configs/systemd/user/container-koopa-paivana.service.d/order.conf
@@ -1,4 +1,4 @@
[Unit]
# Paivana needs merchant private API — start after merchant apps, not only container shell
-After=taler-merchant-apps.service goa-regio-ng.service
+After=taler-merchant-apps.service container-taler-hacktivism.service
Wants=taler-merchant-apps.service
diff --git a/configs/systemd/user/container-koopa-tor-relay.service b/configs/systemd/user/container-koopa-tor-relay.service
deleted file mode 100644
index b65e1ce..0000000
--- a/configs/systemd/user/container-koopa-tor-relay.service
+++ /dev/null
@@ -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
diff --git a/configs/systemd/user/dossiers-ngi-branch-status.service b/configs/systemd/user/dossiers-ngi-branch-status.service
deleted file mode 100644
index 971c93b..0000000
--- a/configs/systemd/user/dossiers-ngi-branch-status.service
+++ /dev/null
@@ -1,25 +0,0 @@
-[Unit]
-Description=dossiers.ngi branch-status JSON + on-demand edition rebuild
-Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md
-After=default.target
-
-[Service]
-Type=oneshot
-Nice=10
-RemainAfterExit=no
-# Full freezes per changed tip can take a while; skip overlap via flock
-TimeoutStartSec=1h
-Environment=TZ=Europe/Zurich
-Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log
-Environment=TDPD_ROOT=%h/git/src/ngi/tech-dossier-project-data
-Environment=OUT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/branch-status.json
-Environment=EDITIONS_ROOT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/editions
-# Rebuild only when current tip freeze_ok is not already true (never force from timer)
-Environment=REBUILD=1
-EnvironmentFile=-%h/.config/dossiers-ngi/branch-status.env
-# flock -n: second fire while busy exits 0 without stacking garbage
-ExecStart=/usr/bin/flock -n %t/dossiers-ngi-branch-status.lock /usr/bin/python3 %h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/branch-status-hourly.py --rebuild
-SuccessExitStatus=0 1
-
-[Install]
-WantedBy=default.target
diff --git a/configs/systemd/user/dossiers-ngi-branch-status.timer b/configs/systemd/user/dossiers-ngi-branch-status.timer
deleted file mode 100644
index ba283a6..0000000
--- a/configs/systemd/user/dossiers-ngi-branch-status.timer
+++ /dev/null
@@ -1,14 +0,0 @@
-[Unit]
-Description=Timer · dossiers.ngi branch-status JSON (every 10 min)
-Documentation=file:%h/src/koopa/koopa-admin-log/scripts/dossiers-ngi/README.md
-
-[Timer]
-OnBootSec=3min
-OnUnitActiveSec=10min
-AccuracySec=1min
-RandomizedDelaySec=30
-Persistent=true
-Unit=dossiers-ngi-branch-status.service
-
-[Install]
-WantedBy=timers.target
diff --git a/configs/systemd/user/goa-regio-ng.service b/configs/systemd/user/goa-regio-ng.service
deleted file mode 100644
index ace6f55..0000000
--- a/configs/systemd/user/goa-regio-ng.service
+++ /dev/null
@@ -1,15 +0,0 @@
-[Unit]
-Description=goa-regio-ng Taler AIO container (vanilla live 9010-9015)
-Documentation=file:%h/src/koopa/koopa-admin-log/scripts/taler-shared/goa-regio-ng/README.md
-After=network-online.target
-Wants=network-online.target
-
-[Service]
-Type=oneshot
-RemainAfterExit=yes
-TimeoutStartSec=120
-ExecStart=/usr/bin/podman start goa-regio-ng
-ExecStop=/usr/bin/podman stop -t 20 goa-regio-ng
-
-[Install]
-WantedBy=default.target
diff --git a/configs/systemd/user/goa-regio-ng.service.d/apps.conf b/configs/systemd/user/goa-regio-ng.service.d/apps.conf
deleted file mode 100644
index 332262d..0000000
--- a/configs/systemd/user/goa-regio-ng.service.d/apps.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-[Unit]
-# Pull in in-container bank/merchant apps after the AIO shell is up
-Wants=taler-bank-apps.service taler-merchant-apps.service
-Before=taler-bank-apps.service taler-merchant-apps.service
diff --git a/configs/systemd/user/koopa-amanita-probe.service b/configs/systemd/user/koopa-amanita-probe.service
deleted file mode 100644
index 508003f..0000000
--- a/configs/systemd/user/koopa-amanita-probe.service
+++ /dev/null
@@ -1,16 +0,0 @@
-[Unit]
-Description=External amanita freeze probe (ICMP+SSH last-alive)
-Documentation=file:%h/src/koopa/koopa-admin-log/scripts/amanita-probe/README.md
-After=default.target
-
-[Service]
-Type=oneshot
-ExecStart=%h/.local/bin/koopa-amanita-probe once
-# Probe outcomes are data in the log; never fail the timer
-SuccessExitStatus=0 1
-Nice=10
-IOSchedulingClass=best-effort
-IOSchedulingPriority=6
-
-[Install]
-WantedBy=default.target
diff --git a/configs/systemd/user/koopa-amanita-probe.timer b/configs/systemd/user/koopa-amanita-probe.timer
deleted file mode 100644
index 049fee7..0000000
--- a/configs/systemd/user/koopa-amanita-probe.timer
+++ /dev/null
@@ -1,13 +0,0 @@
-[Unit]
-Description=Timer · external amanita probe (~15s)
-Documentation=file:%h/src/koopa/koopa-admin-log/scripts/amanita-probe/README.md
-
-[Timer]
-OnBootSec=30s
-OnUnitActiveSec=15s
-AccuracySec=5s
-Persistent=false
-Unit=koopa-amanita-probe.service
-
-[Install]
-WantedBy=timers.target
diff --git a/configs/systemd/user/koopa-firecuda-probe.service b/configs/systemd/user/koopa-firecuda-probe.service
deleted file mode 100644
index 5a672e9..0000000
--- a/configs/systemd/user/koopa-firecuda-probe.service
+++ /dev/null
@@ -1,16 +0,0 @@
-[Unit]
-Description=External firecuda crash probe (ICMP+SSH last-alive)
-Documentation=file:%h/src/koopa/koopa-admin-log/scripts/firecuda-probe/README.md
-After=default.target
-
-[Service]
-Type=oneshot
-ExecStart=%h/.local/bin/koopa-firecuda-probe once
-# Probe outcomes are data in the log; never fail the timer
-SuccessExitStatus=0 1
-Nice=10
-IOSchedulingClass=best-effort
-IOSchedulingPriority=6
-
-[Install]
-WantedBy=default.target
diff --git a/configs/systemd/user/koopa-firecuda-probe.timer b/configs/systemd/user/koopa-firecuda-probe.timer
deleted file mode 100644
index 25f6712..0000000
--- a/configs/systemd/user/koopa-firecuda-probe.timer
+++ /dev/null
@@ -1,13 +0,0 @@
-[Unit]
-Description=Timer · external firecuda probe (~15s)
-Documentation=file:%h/src/koopa/koopa-admin-log/scripts/firecuda-probe/README.md
-
-[Timer]
-OnBootSec=30s
-OnUnitActiveSec=15s
-AccuracySec=5s
-Persistent=false
-Unit=koopa-firecuda-probe.service
-
-[Install]
-WantedBy=timers.target
diff --git a/configs/systemd/user/taler-bank-apps.service b/configs/systemd/user/taler-bank-apps.service
index 0dada1f..7eb3ef2 100644
--- a/configs/systemd/user/taler-bank-apps.service
+++ b/configs/systemd/user/taler-bank-apps.service
@@ -1,14 +1,13 @@
[Unit]
-Description=Start Taler bank apps inside goa-regio-ng (auto-confirm + demo-withdraw)
+Description=Start Taler bank apps inside taler-hacktivism-bank
Documentation=file:%h/src/koopa/koopa-admin-log/scripts/taler-shared/ensure-taler-apps.sh
-After=network-online.target goa-regio-ng.service
-Wants=network-online.target goa-regio-ng.service
+After=network-online.target container-taler-hacktivism-bank.service
+Wants=network-online.target container-taler-hacktivism-bank.service
[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=300
-EnvironmentFile=-%h/.config/taler-landing/stack.env
ExecStart=%h/.local/bin/ensure-taler-apps.sh bank
[Install]
diff --git a/configs/systemd/user/taler-landing-stats.service b/configs/systemd/user/taler-landing-stats.service
index f9e49b5..cc6bd88 100644
--- a/configs/systemd/user/taler-landing-stats.service
+++ b/configs/systemd/user/taler-landing-stats.service
@@ -3,8 +3,8 @@ Description=Taler landing stats (bank full scan + exchange/merchant)
Documentation=file:%h/src/koopa/koopa-admin-log/scripts/taler-landing/README.md
After=network-online.target
Wants=network-online.target
-# Prefer after AIO container + in-container apps are up
-After=goa-regio-ng.service
+# Prefer after containers are up (ignore if unit names differ)
+After=container-taler-hacktivism-bank.service container-taler-hacktivism.service
After=taler-bank-apps.service taler-merchant-apps.service
[Service]
diff --git a/configs/systemd/user/taler-landing-stats.timer b/configs/systemd/user/taler-landing-stats.timer
index 2fc1d21..7106c8a 100644
--- a/configs/systemd/user/taler-landing-stats.timer
+++ b/configs/systemd/user/taler-landing-stats.timer
@@ -1,11 +1,11 @@
[Unit]
-Description=Timer · Taler landing stats (every 4h)
+Description=Timer · Taler landing stats (every 2 min)
Documentation=file:%h/src/koopa/koopa-admin-log/scripts/taler-landing/README.md
[Timer]
OnBootSec=90s
-OnUnitActiveSec=4h
-AccuracySec=5min
+OnUnitActiveSec=2min
+AccuracySec=20s
Persistent=true
Unit=taler-landing-stats.service
diff --git a/configs/systemd/user/taler-merchant-apps.service b/configs/systemd/user/taler-merchant-apps.service
index e4d24bc..22deacd 100644
--- a/configs/systemd/user/taler-merchant-apps.service
+++ b/configs/systemd/user/taler-merchant-apps.service
@@ -1,15 +1,14 @@
[Unit]
-Description=Start Taler merchant apps inside goa-regio-ng
+Description=Start Taler merchant apps inside taler-hacktivism
Documentation=file:%h/src/koopa/koopa-admin-log/scripts/taler-shared/ensure-taler-apps.sh
-After=network-online.target goa-regio-ng.service
-Wants=network-online.target goa-regio-ng.service
+After=network-online.target container-taler-hacktivism.service
+Wants=network-online.target container-taler-hacktivism.service
[Service]
Type=oneshot
RemainAfterExit=yes
# Merchant start_base + helpers can take a while after cold boot
TimeoutStartSec=300
-EnvironmentFile=-%h/.config/taler-landing/stack.env
ExecStart=%h/.local/bin/ensure-taler-apps.sh merchant
[Install]
diff --git a/configs/tor/Containerfile b/configs/tor/Containerfile
new file mode 100644
index 0000000..0e1fb31
--- /dev/null
+++ b/configs/tor/Containerfile
@@ -0,0 +1,18 @@
+# koopa-tor-relay — Tor non-exit relay
+# Process is NOT root: debian-tor remapped to uid/gid 1000 for rootless
+# podman + host volumes (~/koopa-tor-relay/{data,log} owned by hernani).
+# Recreate container with: --user 1000:1000 --userns=keep-id (see README).
+FROM docker.io/library/debian:stable-slim
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends tor nyx ca-certificates tor-geoipdb \
+ && rm -rf /var/lib/apt/lists/* \
+ && mkdir -p /var/lib/tor /var/log/tor \
+ && groupmod --gid 1000 debian-tor \
+ && usermod --uid 1000 --gid 1000 debian-tor \
+ && chown -R debian-tor:debian-tor /var/lib/tor /var/log/tor \
+ && chmod 700 /var/lib/tor
+EXPOSE 8080 9051
+USER debian-tor:debian-tor
+ENTRYPOINT ["/usr/bin/tor"]
+CMD ["-f", "/etc/tor/torrc", "--runasdaemon", "0"]
diff --git a/configs/tor/README.md b/configs/tor/README.md
new file mode 100644
index 0000000..4f618f6
--- /dev/null
+++ b/configs/tor/README.md
@@ -0,0 +1,64 @@
+# Tor relay — koopa (`KoopaRelay`)
+
+Live: **podman** container **`koopa-tor-relay`** (rootless hernani, `--network host`).
+Host layout: `~/koopa-tor-relay/{torrc,data,log,build}`.
+Mirror in this repo: `configs/tor/`.
+
+| Setting | Value |
+|---------|--------|
+| Nickname | `KoopaRelay` |
+| Fingerprint | `02C7EECCDF7814FBA2DB1CBAED000D06E8A32EE8` |
+| ORPort | **8080** (IPv4 + IPv6; VeciGate WAN DNAT → koopa:8080) |
+| ControlPort | **127.0.0.1:9051** |
+| ExitRelay | **0** (non-exit) |
+| SocksPort | **0** |
+| MyFamily | `52BB94DDC1292F950CF728708AC48523E018A718` |
+| Bandwidth* | 2000 MBytes rate/burst |
+| Image | `localhost/koopa-tor-relay:latest` (Debian stable + tor + nyx) |
+| Process user | **non-root** `debian-tor` remapped to **uid/gid 1000** (`USER` in image) |
+| Rootless note | recreate with `--user 1000:1000 --userns=keep-id` so `~/koopa-tor-relay/data` (hernani) stays writable |
+
+## Files
+
+| File | Role |
+|------|------|
+| `torrc` | Active policy (mounted read-only into container) |
+| `Containerfile` | Image build (tor, nyx, tor-geoipdb); **not** root process |
+| `create-container.sh` | Recreate container non-root + host net + volume mounts |
+| `migrate-identity.sh` | One-shot copy of `/var/lib/tor` → container data (same identity) |
+| `container-koopa-tor-relay.service` | systemd --user unit template |
+| `torrc.minimal` / `torrc.sample` | Distro templates (reference) |
+
+## Ops
+
+```bash
+# build image + recreate non-root container (as hernani)
+cd ~/koopa-tor-relay/build # or configs/tor mirror
+podman build -t localhost/koopa-tor-relay:latest -f Containerfile .
+./create-container.sh # or: bash configs/tor/create-container.sh
+podman start koopa-tor-relay
+systemctl --user enable --now container-koopa-tor-relay.service
+
+# status
+podman ps --filter name=koopa-tor-relay
+podman exec koopa-tor-relay id # expect uid=1000(debian-tor)
+systemctl --user status container-koopa-tor-relay
+ss -lntp | grep -E '8080|9051'
+tail -f ~/koopa-tor-relay/log/notices.log
+
+# monitor
+podman exec -it koopa-tor-relay nyx
+```
+
+### Cutover (host package → container)
+
+1. `sudo systemctl stop tor && sudo systemctl disable tor` (prefer `mask` so it never returns)
+2. `sudo ~/koopa-tor-relay/migrate-identity.sh` (or copy of this script)
+3. `podman start koopa-tor-relay` / user unit enable
+4. Confirm fingerprint and ORPort reachability
+
+Do **not** run host `tor.service` and the container at the same time.
+
+Monitoring helpers: `scripts/monitoring/tor_*.sh`.
+Firewall: `configs/firewalld/public-ports.md` (**8080/tcp**).
+Router: `../vecigate-admin-log` (WAN :8080 → koopa).
diff --git a/configs/tor/build/Containerfile b/configs/tor/build/Containerfile
deleted file mode 100644
index c57a137..0000000
--- a/configs/tor/build/Containerfile
+++ /dev/null
@@ -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"]
diff --git a/configs/tor/container-koopa-tor-relay.service b/configs/tor/container-koopa-tor-relay.service
new file mode 100644
index 0000000..051b56e
--- /dev/null
+++ b/configs/tor/container-koopa-tor-relay.service
@@ -0,0 +1,28 @@
+# container-koopa-tor-relay.service
+# Install on koopa as hernani:
+# mkdir -p ~/.config/systemd/user
+# cp configs/tor/container-koopa-tor-relay.service ~/.config/systemd/user/
+# systemctl --user daemon-reload
+# systemctl --user enable --now container-koopa-tor-relay.service
+# Requires: loginctl enable-linger hernani (root, once)
+#
+# autogenerated by Podman 5.8.3 (paths are host-specific; regenerate if container id changes)
+
+[Unit]
+Description=Podman container-koopa-tor-relay.service
+Documentation=man:podman-generate-systemd(1)
+Wants=network-online.target
+After=network-online.target
+RequiresMountsFor=/run/user/1000/containers
+
+[Service]
+Environment=PODMAN_SYSTEMD_UNIT=%n
+Restart=always
+TimeoutStopSec=70
+ExecStart=/usr/bin/podman start koopa-tor-relay
+ExecStop=/usr/bin/podman stop -t 10 koopa-tor-relay
+ExecStopPost=/usr/bin/podman stop -t 10 koopa-tor-relay
+Type=forking
+
+[Install]
+WantedBy=default.target
diff --git a/configs/tor/create-container.sh b/configs/tor/create-container.sh
new file mode 100755
index 0000000..2f7156a
--- /dev/null
+++ b/configs/tor/create-container.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Recreate koopa-tor-relay as non-root (uid 1000) under rootless podman keep-id.
+# Run as hernani on koopa (not root). Stops/removes existing container first.
+set -euo pipefail
+
+NAME=koopa-tor-relay
+IMAGE="${TOR_IMAGE:-localhost/koopa-tor-relay:latest}"
+BASE="${KOOPA_TOR_BASE:-$HOME/koopa-tor-relay}"
+
+if [[ ! -f "$BASE/torrc" ]]; then
+ echo "ERROR: missing $BASE/torrc" >&2
+ exit 1
+fi
+mkdir -p "$BASE/data" "$BASE/log"
+# Host ownership: hernani (uid 1000) ↔ container debian-tor (uid 1000) via keep-id
+chmod 700 "$BASE/data" 2>/dev/null || true
+
+if podman ps -a --format '{{.Names}}' | grep -qx "$NAME"; then
+ podman stop "$NAME" 2>/dev/null || true
+ podman rm "$NAME"
+fi
+
+podman create \
+ --name "$NAME" \
+ --replace \
+ --network host \
+ --user 1000:1000 \
+ --userns keep-id \
+ --restart unless-stopped \
+ -v "$BASE/torrc:/etc/tor/torrc:ro" \
+ -v "$BASE/data:/var/lib/tor:Z" \
+ -v "$BASE/log:/var/log/tor:Z" \
+ --label org.hacktivism.service=tor \
+ --label org.hacktivism.container=koopa-tor-relay \
+ --label org.hacktivism.managed_by=koopa-admin \
+ "$IMAGE"
+
+echo "created $NAME (user 1000:1000, userns=keep-id, host net)"
+echo "start: podman start $NAME # or systemctl --user start container-koopa-tor-relay"
diff --git a/configs/tor/migrate-identity.sh b/configs/tor/migrate-identity.sh
new file mode 100755
index 0000000..344fddd
--- /dev/null
+++ b/configs/tor/migrate-identity.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Copy host Tor identity → container data. Works with: sudo ./migrate-identity.sh
+set -euo pipefail
+
+if [[ "$(id -u)" -ne 0 ]]; then
+ exec sudo -H "$0" "$@"
+fi
+
+OWNER="${SUDO_USER:-hernani}"
+if [[ "$OWNER" == root ]]; then OWNER=hernani; fi
+BASE="$(getent passwd "$OWNER" | cut -d: -f6)/koopa-tor-relay"
+SRC=/var/lib/tor
+DST="$BASE/data"
+
+if [[ ! -d "$BASE" ]]; then
+ echo "ERROR: missing $BASE" >&2
+ exit 1
+fi
+if systemctl is-active --quiet tor 2>/dev/null; then
+ echo "ERROR: stop host tor first: systemctl stop tor" >&2
+ exit 1
+fi
+if command -v podman >/dev/null 2>&1; then
+ if runuser -u "$OWNER" -- podman ps --format "{{.Names}}" 2>/dev/null | grep -qx koopa-tor-relay; then
+ echo "ERROR: stop container first: podman stop koopa-tor-relay" >&2
+ exit 1
+ fi
+fi
+
+echo "Copying $SRC → $DST (identity keys as $OWNER)…"
+mkdir -p "$DST"
+rsync -aHAX --delete "$SRC"/ "$DST"/
+chown -R "${OWNER}:${OWNER}" "$DST"
+chmod 700 "$DST"
+
+echo "--- fingerprint ---"
+[[ -f "$DST/fingerprint" ]] && cat "$DST/fingerprint" || echo "(no fingerprint file yet)"
+[[ -f "$DST/fingerprint-ed25519" ]] && cat "$DST/fingerprint-ed25519" || true
+echo "OK — $DST ready. Then: podman start koopa-tor-relay"
diff --git a/configs/tor/torrc b/configs/tor/torrc
index 9f1705f..63cd8cf 100644
--- a/configs/tor/torrc
+++ b/configs/tor/torrc
@@ -1,13 +1,11 @@
-# KoopaRelay — same policy as live /etc/tor/torrc + admin-log configs/tor/torrc
-# Identity: keys in DataDirectory (migrate from host /var/lib/tor)
+# KoopaRelay — active policy (podman koopa-tor-relay, host network)
+# Live paths: ~/koopa-tor-relay/{torrc,data,log} on koopa
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
+# dual-stack ORPort (IPv4 + IPv6)
ORPort 8080
ControlPort 127.0.0.1:9051
-CookieAuthentication 1
-CookieAuthFile /var/lib/tor/control_auth_cookie
ExitRelay 0
SocksPort 0
MyFamily 52BB94DDC1292F950CF728708AC48523E018A718
diff --git a/configs/tor/torrc.minimal b/configs/tor/torrc.minimal
new file mode 100644
index 0000000..a05f52c
--- /dev/null
+++ b/configs/tor/torrc.minimal
@@ -0,0 +1,192 @@
+## Configuration file for a typical Tor user
+## Last updated 9 October 2013 for Tor 0.2.5.2-alpha.
+## (may or may not work for much older or much newer versions of Tor.)
+##
+## Lines that begin with "## " try to explain what's going on. Lines
+## that begin with just "#" are disabled commands: you can enable them
+## by removing the "#" symbol.
+##
+## See 'man tor', or https://www.torproject.org/docs/tor-manual.html,
+## for more options you can use in this file.
+##
+## Tor will look for this file in various places based on your platform:
+## https://www.torproject.org/docs/faq#torrc
+
+## Tor opens a socks proxy on port 9050 by default -- even if you don't
+## configure one below. Set "SocksPort 0" if you plan to run Tor only
+## as a relay, and not make any local application connections yourself.
+#SocksPort 9050 # Default: Bind to localhost:9050 for local connections.
+#SocksPort 192.168.0.1:9100 # Bind to this address:port too.
+
+## Entry policies to allow/deny SOCKS requests based on IP address.
+## First entry that matches wins. If no SocksPolicy is set, we accept
+## all (and only) requests that reach a SocksPort. Untrusted users who
+## can access your SocksPort may be able to learn about the connections
+## you make.
+#SocksPolicy accept 192.168.0.0/16
+#SocksPolicy reject *
+
+## Logs go to stdout at level "notice" unless redirected by something
+## else, like one of the below lines. You can have as many Log lines as
+## you want.
+##
+## We advise using "notice" in most cases, since anything more verbose
+## may provide sensitive information to an attacker who obtains the logs.
+##
+## Send all messages of level 'notice' or higher to /var/log/tor/notices.log
+#Log notice file /var/log/tor/notices.log
+## Send every possible message to /var/log/tor/debug.log
+#Log debug file /var/log/tor/debug.log
+## Use the system log instead of Tor's logfiles
+#Log notice syslog
+## To send all messages to stderr:
+#Log debug stderr
+
+## Uncomment this to start the process in the background... or use
+## --runasdaemon 1 on the command line. This is ignored on Windows;
+## see the FAQ entry if you want Tor to run as an NT service.
+#RunAsDaemon 1
+
+## The directory for keeping all the keys/etc. By default, we store
+## things in $HOME/.tor on Unix, and in Application Data\tor on Windows.
+#DataDirectory /var/lib/tor
+
+## The port on which Tor will listen for local connections from Tor
+## controller applications, as documented in control-spec.txt.
+#ControlPort 9051
+## If you enable the controlport, be sure to enable one of these
+## authentication methods, to prevent attackers from accessing it.
+#HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
+#CookieAuthentication 1
+
+############### This section is just for location-hidden services ###
+
+## Once you have configured a hidden service, you can look at the
+## contents of the file ".../hidden_service/hostname" for the address
+## to tell people.
+##
+## HiddenServicePort x y:z says to redirect requests on port x to the
+## address y:z.
+
+#HiddenServiceDir /var/lib/tor/hidden_service/
+#HiddenServicePort 80 127.0.0.1:80
+
+#HiddenServiceDir /var/lib/tor/other_hidden_service/
+#HiddenServicePort 80 127.0.0.1:80
+#HiddenServicePort 22 127.0.0.1:22
+
+################ This section is just for relays #####################
+#
+## See https://www.torproject.org/docs/tor-doc-relay for details.
+
+## Required: what port to advertise for incoming Tor connections.
+#ORPort 9001
+## If you want to listen on a port other than the one advertised in
+## ORPort (e.g. to advertise 443 but bind to 9090), you can do it as
+## follows. You'll need to do ipchains or other port forwarding
+## yourself to make this work.
+#ORPort 443 NoListen
+#ORPort 127.0.0.1:9090 NoAdvertise
+
+## The IP address or full DNS name for incoming connections to your
+## relay. Leave commented out and Tor will guess.
+#Address noname.example.com
+
+## If you have multiple network interfaces, you can specify one for
+## outgoing traffic to use.
+# OutboundBindAddress 10.0.0.5
+
+## A handle for your relay, so people don't have to refer to it by key.
+#Nickname ididnteditheconfig
+
+## Define these to limit how much relayed traffic you will allow. Your
+## own traffic is still unthrottled. Note that RelayBandwidthRate must
+## be at least 20 KB.
+## Note that units for these config options are bytes per second, not bits
+## per second, and that prefixes are binary prefixes, i.e. 2^10, 2^20, etc.
+#RelayBandwidthRate 100 KB # Throttle traffic to 100KB/s (800Kbps)
+#RelayBandwidthBurst 200 KB # But allow bursts up to 200KB/s (1600Kbps)
+
+## Use these to restrict the maximum traffic per day, week, or month.
+## Note that this threshold applies separately to sent and received bytes,
+## not to their sum: setting "4 GB" may allow up to 8 GB total before
+## hibernating.
+##
+## Set a maximum of 4 gigabytes each way per period.
+#AccountingMax 4 GB
+## Each period starts daily at midnight (AccountingMax is per day)
+#AccountingStart day 00:00
+## Each period starts on the 3rd of the month at 15:00 (AccountingMax
+## is per month)
+#AccountingStart month 3 15:00
+
+## Administrative contact information for this relay or bridge. This line
+## can be used to contact you if your relay or bridge is misconfigured or
+## something else goes wrong. Note that we archive and publish all
+## descriptors containing these lines and that Google indexes them, so
+## spammers might also collect them. You may want to obscure the fact that
+## it's an email address and/or generate a new address for this purpose.
+#ContactInfo Random Person
+## You might also include your PGP or GPG fingerprint if you have one:
+#ContactInfo 0xFFFFFFFF Random Person
+
+## Uncomment this to mirror directory information for others. Please do
+## if you have enough bandwidth.
+#DirPort 9030 # what port to advertise for directory connections
+## If you want to listen on a port other than the one advertised in
+## DirPort (e.g. to advertise 80 but bind to 9091), you can do it as
+## follows. below too. You'll need to do ipchains or other port
+## forwarding yourself to make this work.
+#DirPort 80 NoListen
+#DirPort 127.0.0.1:9091 NoAdvertise
+## Uncomment to return an arbitrary blob of html on your DirPort. Now you
+## can explain what Tor is if anybody wonders why your IP address is
+## contacting them. See contrib/tor-exit-notice.html in Tor's source
+## distribution for a sample.
+#DirPortFrontPage /etc/tor/tor-exit-notice.html
+
+## Uncomment this if you run more than one Tor relay, and add the identity
+## key fingerprint of each Tor relay you control, even if they're on
+## different networks. You declare it here so Tor clients can avoid
+## using more than one of your relays in a single circuit. See
+## https://www.torproject.org/docs/faq#MultipleRelays
+## However, you should never include a bridge's fingerprint here, as it would
+## break its concealability and potentionally reveal its IP/TCP address.
+#MyFamily $keyid,$keyid,...
+
+## A comma-separated list of exit policies. They're considered first
+## to last, and the first match wins. If you want to _replace_
+## the default exit policy, end this with either a reject *:* or an
+## accept *:*. Otherwise, you're _augmenting_ (prepending to) the
+## default exit policy. Leave commented to just use the default, which is
+## described in the man page or at
+## https://www.torproject.org/documentation.html
+##
+## Look at https://www.torproject.org/faq-abuse.html#TypicalAbuses
+## for issues you might encounter if you use the default exit policy.
+##
+## If certain IPs and ports are blocked externally, e.g. by your firewall,
+## you should update your exit policy to reflect this -- otherwise Tor
+## users will be told that those destinations are down.
+##
+## For security, by default Tor rejects connections to private (local)
+## networks, including to your public IP address. See the man page entry
+## for ExitPolicyRejectPrivate if you want to allow "exit enclaving".
+##
+#ExitPolicy accept *:6660-6667,reject *:* # allow irc ports but no more
+#ExitPolicy accept *:119 # accept nntp as well as default exit policy
+#ExitPolicy reject *:* # no exits allowed
+
+## Bridge relays (or "bridges") are Tor relays that aren't listed in the
+## main directory. Since there is no complete public list of them, even an
+## ISP that filters connections to all the known Tor relays probably
+## won't be able to block all the bridges. Also, websites won't treat you
+## differently because they won't know you're running Tor. If you can
+## be a real relay, please do; but if not, be a bridge!
+#BridgeRelay 1
+## By default, Tor will advertise your bridge to users through various
+## mechanisms like https://bridges.torproject.org/. If you want to run
+## a private bridge, for example because you'll give out your bridge
+## address manually to your friends, uncomment this line:
+#PublishServerDescriptor 0
+
diff --git a/configs/tor/torrc.sample b/configs/tor/torrc.sample
new file mode 100644
index 0000000..7be6773
--- /dev/null
+++ b/configs/tor/torrc.sample
@@ -0,0 +1,257 @@
+## Configuration file for a typical Tor user
+## Last updated 28 February 2019 for Tor 0.3.5.1-alpha.
+## (may or may not work for much older or much newer versions of Tor.)
+##
+## Lines that begin with "## " try to explain what's going on. Lines
+## that begin with just "#" are disabled commands: you can enable them
+## by removing the "#" symbol.
+##
+## See 'man tor', or https://www.torproject.org/docs/tor-manual.html,
+## for more options you can use in this file.
+##
+## Tor will look for this file in various places based on your platform:
+## https://support.torproject.org/tbb/tbb-editing-torrc/
+
+## Tor opens a SOCKS proxy on port 9050 by default -- even if you don't
+## configure one below. Set "SOCKSPort 0" if you plan to run Tor only
+## as a relay, and not make any local application connections yourself.
+#SOCKSPort 9050 # Default: Bind to localhost:9050 for local connections.
+#SOCKSPort 192.168.0.1:9100 # Bind to this address:port too.
+
+## Entry policies to allow/deny SOCKS requests based on IP address.
+## First entry that matches wins. If no SOCKSPolicy is set, we accept
+## all (and only) requests that reach a SOCKSPort. Untrusted users who
+## can access your SOCKSPort may be able to learn about the connections
+## you make.
+#SOCKSPolicy accept 192.168.0.0/16
+#SOCKSPolicy accept6 FC00::/7
+#SOCKSPolicy reject *
+
+## Logs go to stdout at level "notice" unless redirected by something
+## else, like one of the below lines. You can have as many Log lines as
+## you want.
+##
+## We advise using "notice" in most cases, since anything more verbose
+## may provide sensitive information to an attacker who obtains the logs.
+##
+## Send all messages of level 'notice' or higher to /var/log/tor/notices.log
+#Log notice file /var/log/tor/notices.log
+## Send every possible message to /var/log/tor/debug.log
+#Log debug file /var/log/tor/debug.log
+## Use the system log instead of Tor's logfiles
+#Log notice syslog
+## To send all messages to stderr:
+#Log debug stderr
+
+## Uncomment this to start the process in the background... or use
+## --runasdaemon 1 on the command line. This is ignored on Windows;
+## see the FAQ entry if you want Tor to run as an NT service.
+#RunAsDaemon 1
+
+## The directory for keeping all the keys/etc. By default, we store
+## things in $HOME/.tor on Unix, and in Application Data\tor on Windows.
+#DataDirectory /var/lib/tor
+
+## The port on which Tor will listen for local connections from Tor
+## controller applications, as documented in control-spec.txt.
+#ControlPort 9051
+## If you enable the controlport, be sure to enable one of these
+## authentication methods, to prevent attackers from accessing it.
+#HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
+#CookieAuthentication 1
+
+############### This section is just for location-hidden services ###
+
+## Once you have configured a hidden service, you can look at the
+## contents of the file ".../hidden_service/hostname" for the address
+## to tell people.
+##
+## HiddenServicePort x y:z says to redirect requests on port x to the
+## address y:z.
+
+#HiddenServiceDir /var/lib/tor/hidden_service/
+#HiddenServicePort 80 127.0.0.1:80
+
+#HiddenServiceDir /var/lib/tor/other_hidden_service/
+#HiddenServicePort 80 127.0.0.1:80
+#HiddenServicePort 22 127.0.0.1:22
+
+################ This section is just for relays #####################
+#
+## See https://community.torproject.org/relay for details.
+
+## Required: what port to advertise for incoming Tor connections.
+#ORPort 9001
+## If you want to listen on a port other than the one advertised in
+## ORPort (e.g. to advertise 443 but bind to 9090), you can do it as
+## follows. You'll need to do ipchains or other port forwarding
+## yourself to make this work.
+#ORPort 443 NoListen
+#ORPort 127.0.0.1:9090 NoAdvertise
+## If you want to listen on IPv6 your numeric address must be explicitly
+## between square brackets as follows. You must also listen on IPv4.
+#ORPort [2001:DB8::1]:9050
+
+## The IP address or full DNS name for incoming connections to your
+## relay. Leave commented out and Tor will guess.
+#Address noname.example.com
+
+## If you have multiple network interfaces, you can specify one for
+## outgoing traffic to use.
+## OutboundBindAddressExit will be used for all exit traffic, while
+## OutboundBindAddressOR will be used for all OR and Dir connections
+## (DNS connections ignore OutboundBindAddress).
+## If you do not wish to differentiate, use OutboundBindAddress to
+## specify the same address for both in a single line.
+#OutboundBindAddressExit 10.0.0.4
+#OutboundBindAddressOR 10.0.0.5
+
+## A handle for your relay, so people don't have to refer to it by key.
+## Nicknames must be between 1 and 19 characters inclusive, and must
+## contain only the characters [a-zA-Z0-9].
+## If not set, "Unnamed" will be used.
+#Nickname ididnteditheconfig
+
+## Define these to limit how much relayed traffic you will allow. Your
+## own traffic is still unthrottled. Note that RelayBandwidthRate must
+## be at least 75 kilobytes per second.
+## Note that units for these config options are bytes (per second), not
+## bits (per second), and that prefixes are binary prefixes, i.e. 2^10,
+## 2^20, etc.
+#RelayBandwidthRate 100 KBytes # Throttle traffic to 100KB/s (800Kbps)
+#RelayBandwidthBurst 200 KBytes # But allow bursts up to 200KB (1600Kb)
+
+## Use these to restrict the maximum traffic per day, week, or month.
+## Note that this threshold applies separately to sent and received bytes,
+## not to their sum: setting "40 GB" may allow up to 80 GB total before
+## hibernating.
+##
+## Set a maximum of 40 gigabytes each way per period.
+#AccountingMax 40 GBytes
+## Each period starts daily at midnight (AccountingMax is per day)
+#AccountingStart day 00:00
+## Each period starts on the 3rd of the month at 15:00 (AccountingMax
+## is per month)
+#AccountingStart month 3 15:00
+
+## Administrative contact information for this relay or bridge. This line
+## can be used to contact you if your relay or bridge is misconfigured or
+## something else goes wrong. Note that we archive and publish all
+## descriptors containing these lines and that Google indexes them, so
+## spammers might also collect them. You may want to obscure the fact that
+## it's an email address and/or generate a new address for this purpose.
+##
+## If you are running multiple relays, you MUST set this option.
+##
+#ContactInfo Random Person
+## You might also include your PGP or GPG fingerprint if you have one:
+#ContactInfo 0xFFFFFFFF Random Person
+
+## Uncomment this to mirror directory information for others. Please do
+## if you have enough bandwidth.
+#DirPort 9030 # what port to advertise for directory connections
+## If you want to listen on a port other than the one advertised in
+## DirPort (e.g. to advertise 80 but bind to 9091), you can do it as
+## follows. below too. You'll need to do ipchains or other port
+## forwarding yourself to make this work.
+#DirPort 80 NoListen
+#DirPort 127.0.0.1:9091 NoAdvertise
+## Uncomment to return an arbitrary blob of html on your DirPort. Now you
+## can explain what Tor is if anybody wonders why your IP address is
+## contacting them. See contrib/tor-exit-notice.html in Tor's source
+## distribution for a sample.
+#DirPortFrontPage /etc/tor/tor-exit-notice.html
+
+## Uncomment this if you run more than one Tor relay, and add the identity
+## key fingerprint of each Tor relay you control, even if they're on
+## different networks. You declare it here so Tor clients can avoid
+## using more than one of your relays in a single circuit. See
+## https://support.torproject.org/relay-operators/multiple-relays/
+## However, you should never include a bridge's fingerprint here, as it would
+## break its concealability and potentially reveal its IP/TCP address.
+##
+## If you are running multiple relays, you MUST set this option.
+##
+## Note: do not use MyFamily on bridge relays.
+#MyFamily $keyid,$keyid,...
+
+## Uncomment this if you want your relay to be an exit, with the default
+## exit policy (or whatever exit policy you set below).
+## (If ReducedExitPolicy, ExitPolicy, or IPv6Exit are set, relays are exits.
+## If none of these options are set, relays are non-exits.)
+#ExitRelay 1
+
+## Uncomment this if you want your relay to allow IPv6 exit traffic.
+## (Relays do not allow any exit traffic by default.)
+#IPv6Exit 1
+
+## Uncomment this if you want your relay to be an exit, with a reduced set
+## of exit ports.
+#ReducedExitPolicy 1
+
+## Uncomment these lines if you want your relay to be an exit, with the
+## specified set of exit IPs and ports.
+##
+## A comma-separated list of exit policies. They're considered first
+## to last, and the first match wins.
+##
+## If you want to allow the same ports on IPv4 and IPv6, write your rules
+## using accept/reject *. If you want to allow different ports on IPv4 and
+## IPv6, write your IPv6 rules using accept6/reject6 *6, and your IPv4 rules
+## using accept/reject *4.
+##
+## If you want to _replace_ the default exit policy, end this with either a
+## reject *:* or an accept *:*. Otherwise, you're _augmenting_ (prepending to)
+## the default exit policy. Leave commented to just use the default, which is
+## described in the man page or at
+## https://support.torproject.org/relay-operators
+##
+## Look at https://support.torproject.org/abuse/exit-relay-expectations/
+## for issues you might encounter if you use the default exit policy.
+##
+## If certain IPs and ports are blocked externally, e.g. by your firewall,
+## you should update your exit policy to reflect this -- otherwise Tor
+## users will be told that those destinations are down.
+##
+## For security, by default Tor rejects connections to private (local)
+## networks, including to the configured primary public IPv4 and IPv6 addresses,
+## and any public IPv4 and IPv6 addresses on any interface on the relay.
+## See the man page entry for ExitPolicyRejectPrivate if you want to allow
+## "exit enclaving".
+##
+#ExitPolicy accept *:6660-6667,reject *:* # allow irc ports on IPv4 and IPv6 but no more
+#ExitPolicy accept *:119 # accept nntp ports on IPv4 and IPv6 as well as default exit policy
+#ExitPolicy accept *4:119 # accept nntp ports on IPv4 only as well as default exit policy
+#ExitPolicy accept6 *6:119 # accept nntp ports on IPv6 only as well as default exit policy
+#ExitPolicy reject *:* # no exits allowed
+
+## Uncomment this if you want your exit relay to reevaluate its exit policy on
+## existing connections when the exit policy is modified.
+#ReevaluateExitPolicy 1
+
+## Bridge relays (or "bridges") are Tor relays that aren't listed in the
+## main directory. Since there is no complete public list of them, even an
+## ISP that filters connections to all the known Tor relays probably
+## won't be able to block all the bridges. Also, websites won't treat you
+## differently because they won't know you're running Tor. If you can
+## be a real relay, please do; but if not, be a bridge!
+##
+## Warning: when running your Tor as a bridge, make sure than MyFamily is
+## NOT configured.
+#BridgeRelay 1
+## By default, Tor will advertise your bridge to users through various
+## mechanisms like https://bridges.torproject.org/. If you want to run
+## a private bridge, for example because you'll give out your bridge
+## address manually to your friends, uncomment this line:
+#BridgeDistribution none
+
+## Configuration options can be imported from files or folders using the %include
+## option with the value being a path. This path can have wildcards. Wildcards are
+## expanded first, using lexical order. Then, for each matching file or folder, the following
+## rules are followed: if the path is a file, the options from the file will be parsed as if
+## they were written where the %include option is. If the path is a folder, all files on that
+## folder will be parsed following lexical order. Files starting with a dot are ignored. Files
+## on subfolders are ignored.
+## The %include option can be used recursively.
+#%include /etc/torrc.d/*.conf
+
diff --git a/host/caddy/Caddyfile b/host/caddy/Caddyfile
index 28800ea..7b960b9 100644
--- a/host/caddy/Caddyfile
+++ b/host/caddy/Caddyfile
@@ -1,7 +1,7 @@
# Internal only (not in the browser URL):
# 9010 merchant API | 9011 exchange API | 9012 bank API
# 9013 bank landing | 9014 exchange landing | 9015 merchant landing
-# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9029 galene | 9030 cryptpad | 9031 meet | 9032 bbb | 9200 forgejo-ssh
+# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9200 forgejo-ssh
# 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3
{
@@ -37,85 +37,22 @@ taler.hacktivism.ch {
}
}
- # surface + aptdeploy HTML (taler.hacktivism.ch only)
- handle /taler-monitoring-surface_err {
- redir /taler-monitoring-surface_err/ 302
- }
- handle /taler-monitoring-surface_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-surface {
- redir /taler-monitoring-surface/ 302
- }
- handle /taler-monitoring-surface* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy_err {
- redir /taler-monitoring-aptdeploy_err/ 302
- }
- handle /taler-monitoring-aptdeploy_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy {
- redir /taler-monitoring-aptdeploy/ 302
- }
- handle /taler-monitoring-aptdeploy* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mattermost_err {
- redir /taler-monitoring-mattermost_err/ 302
- }
- handle /taler-monitoring-mattermost_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mattermost {
- redir /taler-monitoring-mattermost/ 302
- }
- handle /taler-monitoring-mattermost* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mail_err {
- redir /taler-monitoring-mail_err/ 302
- }
- handle /taler-monitoring-mail_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mail {
- redir /taler-monitoring-mail/ 302
- }
- handle /taler-monitoring-mail* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
# taler-monitoring console HTML (host static — not in app containers)
handle /monitoring_err {
redir /monitoring_err/ 302
}
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
+ handle /monitoring_err/ {
+ root * /var/www/monitoring-sites/{host}/monitoring_err
file_server
}
handle /monitoring {
redir /monitoring/ 302
}
- handle /monitoring* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
+ handle /monitoring/ {
+ root * /var/www/monitoring-sites/{host}/monitoring
file_server
}
-
-
-
# SPA: /webui → /webui/
redir /webui /webui/ 302
@@ -145,19 +82,18 @@ exchange.hacktivism.ch {
handle /monitoring_err {
redir /monitoring_err/ 302
}
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
+ handle /monitoring_err/ {
+ root * /var/www/monitoring-sites/{host}/monitoring_err
file_server
}
handle /monitoring {
redir /monitoring/ 302
}
- handle /monitoring* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
+ handle /monitoring/ {
+ root * /var/www/monitoring-sites/{host}/monitoring
file_server
}
-
reverse_proxy 127.0.0.1:9011 {
import proxy_public
}
@@ -179,19 +115,18 @@ bank.hacktivism.ch {
handle /monitoring_err {
redir /monitoring_err/ 302
}
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
+ handle /monitoring_err/ {
+ root * /var/www/monitoring-sites/{host}/monitoring_err
file_server
}
handle /monitoring {
redir /monitoring/ 302
}
- handle /monitoring* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
+ handle /monitoring/ {
+ root * /var/www/monitoring-sites/{host}/monitoring
file_server
}
-
# Static terms/privacy on landing nginx :9013
handle /terms* {
reverse_proxy 127.0.0.1:9013 {
@@ -204,19 +139,6 @@ bank.hacktivism.ch {
}
}
- # FP pattern (francpaysan-admin-log files/caddy/Caddyfile @wdGet):
- # GET withdrawal-operation → landing :9013 → demo-api :19096
- # (injects suggested_exchange / required_exchange for Android/iOS)
- @wdGet {
- method GET
- path /taler-integration/withdrawal-operation/*
- }
- handle @wdGet {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
reverse_proxy 127.0.0.1:9012 {
import proxy_public
}
@@ -313,114 +235,7 @@ paivana.hacktivism.ch {
}
}
-# 9026 lemmy (+ ui/pictrs via internal nginx)
-lemmy.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9026 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9027 decidim (rails)
-decidim.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9027 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9029 galene — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site galene.hacktivism.ch --port 9029 --label galene
-galene.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9029 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9030 cryptpad — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site cryptpad.hacktivism.ch --port 9030 --label cryptpad
-# CryptPad also needs sandbox subdomain (cryptpad-ui / sandbox) — wire after first boot.
-cryptpad.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9030 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9031 meet (Jitsi) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site meet.hacktivism.ch --port 9031 --label meet
-# WebRTC media (UDP/JVB) is separate from this HTTPS vhost — VeciGate/firewall later.
-meet.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9031 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9032 bbb (BigBlueButton) — allocated 2026-09-08
-# Merge: scripts/caddy/caddy-apply.sh --site bbb.hacktivism.ch --port 9032 --label bbb
-# Stack image base: Ubuntu (not CachyOS). Media/UDP ports TBD with install.
-bbb.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9032 {
- header_up Host {host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Host {host}
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch, http://galene.hacktivism.ch, http://cryptpad.hacktivism.ch, http://meet.hacktivism.ch, http://bbb.hacktivism.ch {
+http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch {
handle /.well-known/acme-challenge/* {
root * /var/www/acme
file_server
diff --git a/host/hernani-home.md b/host/hernani-home.md
deleted file mode 100644
index 77e88a7..0000000
--- a/host/hernani-home.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# hernani@koopa — home layout (SoT paths)
-
-Terse map after GOA single-CTR. Live moves on koopa need Freigabe.
-
-## GOA
-
-| Path | Role |
-|------|------|
-| `~/scripts/taler-shared/goa-regio-ng/` | restore / all-in-one helpers (admin-log SoT) |
-| `~/goa-regio-ng-scripts/` | optional live copy of helpers |
-| `~/backups/goa/current` | CTR bak for restore |
-| `~/goa-landing-deploy/` | host landings → `/var/www/*-landing/` |
-| CTR `goa-regio-ng` | pasta **9010–9015**; landings+Caddy outside CTR |
-
-## Monitoring state (not a CTR)
-
-| Path | Role |
-|------|------|
-| `~/src/elelmon` | monitoring suite SoT (Forgejo `hernani/elelmon`) |
-| `~/.local/state/taler-hacktivism-monitoring/` | elelmon host-agent state dir name (legacy string OK) |
-| `~/.config/taler-monitoring/env` | suite secrets/config (product name kept) |
-
-## Tor
-
-Host `tor.service` — ORPort **8080**, ControlPort **9051**. No `~/…/tor-relay` CTR.
-
-## Other CTRs (unchanged)
-
-Forgejo / social / media: see `configs/ports.md`. Inventory: `koopa-login-health`.
-
-## Magikoopa laptop (ops junk)
-
-Prefer under `$HOME/git/` + `$HOME/Archives/`; do not leave `/tmp/commit-*`, `/tmp/goa-*`, or loose `~/*.asc` / screenshots as SoT.
diff --git a/host/overview/LIVE.md b/host/overview/LIVE.md
new file mode 100644
index 0000000..46ee303
--- /dev/null
+++ b/host/overview/LIVE.md
@@ -0,0 +1,113 @@
+# Live inventory — koopa (2026-07-13)
+
+Snapshot of **what runs on host koopa** (openSUSE Tumbleweed). No secrets.
+
+Refresh command ideas:
+
+```bash
+hostname; date -R
+podman ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
+systemctl is-active caddy
+systemctl --user is-active container-koopa-tor-relay
+systemctl --user is-active container-koopa-nym
+ss -lntp | grep -E '90[0-9]{2}|9200|8080|9080|1789'
+```
+
+## Host
+
+| Item | Value |
+|------|--------|
+| Hostname | `koopa` |
+| Role | App host behind VeciGate (WAN :80→9000, :443→9001) |
+| Edge | Caddy **9000/9001**, systemd socket proxies **80/443** |
+| Tor | **podman `koopa-tor-relay`** (host net) ORPort **8080** dual-stack, ControlPort **127.0.0.1:9051** — not host `tor.service` |
+
+## Running podman containers
+
+| Name | Image | Ports (host) | Public site |
+|------|--------|--------------|-------------|
+| `taler-hacktivism` | `localhost/taler-hacktivism-live:landing` | **9010**, **9015** | `taler.hacktivism.ch` |
+| **`taler-hacktivism-exchange-ansible`** | `localhost/taler-hacktivism-exchange-ansible:landing` | **9011**, **9014** | `exchange.hacktivism.ch` |
+| `taler-hacktivism-bank` | **`localhost/taler-hacktivism-banking:live`** | **9012**, **9013** | `bank.hacktivism.ch` |
+| `koopa-castopod` (+ mariadb, redis) | `castopod/castopod:1` | **9020** | `castopod.hacktivism.ch` |
+| `koopa-bonfire` (+ postgres) | `bonfire:1.0.5-social-amd64` | **9021** | `bonfire.hacktivism.ch` |
+| `koopa-prime-jellyfin` | `linuxserver/jellyfin:10.10.7` | **9022** | `prime.hacktivism.ch` |
+| `koopa-prime-qbittorrent` | `linuxserver/qbittorrent:5.0.4` | **9023**, **6881** | `bt.hacktivism.ch` |
+| `koopa-forgejo` (+ postgres) | `forgejo:11-rootless` | **9024**, **9200** | `git.hacktivism.ch` |
+| **`koopa-paivana`** (+ upstream) | `localhost/koopa-paivana:latest` | **9025** | `paivana.hacktivism.ch` |
+| `koopa-lemmy-proxy` (+ lemmy, ui, pictrs, db) | `lemmy:0.19.20` | **9026** | `lemmy.hacktivism.ch` |
+| **`koopa-tor-relay`** | `localhost/koopa-tor-relay:latest` | **8080**, **9051** (host net) | Tor OR (non-exit) |
+| **`koopa-nym`** | `localhost/koopa-nym:latest` | **9080**, **1789**, **1790** (+ opt. WG) | Nym mixnet node (nym.com) |
+| `koopa-tops-ng1` | `nginx` | **9090** | `tops.ng1.hacktivism.ch` |
+| `koopa-tops-ng2` | `nginx` | **9091** | `tops.ng2.hacktivism.ch` |
+| `koopa-tops-ng3` | `nginx` | **9092** | `tops.ng3.hacktivism.ch` |
+
+### Naming note (exchange)
+
+Live exchange container is **`taler-hacktivism-exchange-ansible`** (not `taler-hacktivism-exchange-ansible` / not without `-ansible`).
+Managed from **`/home/hernani/ansible-taler-exchange/`** (see `configs/taler-exchange-ansible/`).
+
+## Caddy vhosts → backends
+
+| Host | Backend |
+|------|---------|
+| `taler.hacktivism.ch` | 9010 (API) + 9015 (`/intro`) |
+| `exchange.hacktivism.ch` | 9011 + 9014 (`/intro`) |
+| `bank.hacktivism.ch` | 9012 + 9013 (`/intro`, terms, privacy) |
+| `castopod.hacktivism.ch` | 9020 |
+| `bonfire.hacktivism.ch` | 9021 |
+| `prime.hacktivism.ch` | 9022 |
+| `bt.hacktivism.ch` | 9023 |
+| `git.hacktivism.ch` | 9024 (HTTP); git-SSH **9200** host-direct |
+| `paivana.hacktivism.ch` | 9025 (paivana-httpd GOA paywall) |
+| `lemmy.hacktivism.ch` | 9026 |
+| `tops.ng1.hacktivism.ch` | 9090 |
+| `tops.ng2.hacktivism.ch` | 9091 |
+| `tops.ng3.hacktivism.ch` | 9092 |
+
+Config: `/etc/caddy/Caddyfile` (mirror `configs/caddy/Caddyfile`).
+
+## Paths on host (`hernani`)
+
+| Path | Role |
+|------|------|
+| `~/ansible-taler-exchange/` | Ansible + scripts for exchange-ansible container |
+| `~/koopa-castopod/` | Castopod compose |
+| `~/koopa-lemmy/` | Lemmy compose |
+| `~/koopa-bonfire/` | Bonfire compose + gitbot |
+| `~/koopa-prime/` | Jellyfin + qBittorrent |
+| `~/koopa-forgejo/` | Forgejo rootless |
+| `~/koopa-paivana/` | Paivana paywall (`koopa-paivana`) |
+| `~/koopa-tops/` | tops.ng1–ng3 (`koopa-tops-ng*`) |
+| `~/koopa-caddy/` | Caddyfile working tree on host |
+| `~/koopa-tor-relay/` | Tor relay container (torrc, data/identity, log) |
+| `~/koopa-nym/` | Nym nym-node (nym.com mixnet / NymVPN network) |
+
+## Start models
+
+| Stack | How |
+|-------|-----|
+| Taler merchant/bank | root `start_base_services_*` → service user → `/usr/local/bin/start_*.sh` |
+| Exchange (GOA) | `~/ansible-taler-exchange/run-container-koopa.sh` then `deploy-hacktivism-goa.sh` |
+| User apps | `cd ~/koopa-* && podman-compose up -d` |
+| tops (ng1–ng3) | user unit `container-koopa-tops.service` (linger) |
+
+## Config mirrors in this repo (`configs/`)
+
+| Live container | Git path |
+|----------------|----------|
+| `taler-hacktivism` | `configs/taler-hacktivism/` |
+| `taler-hacktivism-bank` (image **…-banking**) | `configs/taler-hacktivism-bank/` |
+| `taler-hacktivism-exchange-ansible` | `configs/taler-exchange-ansible/` + conf in `configs/taler-exchange/` |
+| `koopa-tops-ng1` … `ng3` | `configs/tops/` |
+
+## Related docs in this repo
+
+| Topic | Doc |
+|-------|-----|
+| Ports | `configs/ports.md` |
+| Diagram | `host/overview/services.md` |
+| Exchange Ansible (koopa) | `configs/taler-exchange-ansible/` |
+| Forgejo | `2026/2026-07-10--forgejo-rootless.md` |
+| Bonfire public feeds | `configs/bonfire/public-feeds.md` |
+| Castopod content | `2026/2026-07-09--castopod-content.md` |
diff --git a/host/overview/services.md b/host/overview/services.md
new file mode 100644
index 0000000..0c9daf2
--- /dev/null
+++ b/host/overview/services.md
@@ -0,0 +1,107 @@
+# Service overview — koopa
+
+**Live container/port inventory:** [`LIVE.md`](LIVE.md) (state as observed on host).
+
+Diagram
+
+```mermaid
+flowchart TB
+ subgraph Internet
+ Client[Client IPv4/IPv6]
+ end
+
+ subgraph VeciGate["VeciGate (MikroTik)"]
+ DNAT80["DNAT :80 → koopa:9000"]
+ DNAT443["DNAT :443 → koopa:9001"]
+ DNATSSH["DNAT :23235 → koopa:22"]
+ DNATTOR["DNAT :8080 → koopa:8080"]
+ end
+
+ subgraph Host["openSUSE host koopa"]
+ subgraph Edge["Edge on host"]
+ S80["systemd http-proxy.socket :80"]
+ S443["systemd https-proxy.socket :443"]
+ CaddyHTTP["Caddy :9000 HTTP\nACME + redirect"]
+ CaddyHTTPS["Caddy :9001 HTTPS"]
+ FW["firewalld public\n9000,9001,80,8080,ssh"]
+ end
+
+ subgraph Podman["podman rootless (hernani)"]
+ Merch["taler-hacktivism\n:9010"]
+ Exch["taler-hacktivism-exchange-ansible\n:9011"]
+ Bank["taler-bank-hacktivism\n:9012"]
+ Castopod["koopa-castopod\n:9020"]
+ Bonfire["koopa-bonfire\n:9021"]
+ Prime["koopa-prime jellyfin\n:9022"]
+ BT["qbittorrent\n:9023"]
+ Forgejo["koopa-forgejo ROOTLESS\n:9024 HTTP / :9200 SSH"]
+ Paivana["koopa-paivana\n:9025"]
+ Lemmy["koopa-lemmy-proxy\n:9026"]
+ end
+
+ Tor["tor ORPort :8080"]
+ SSH["sshd :22"]
+ end
+
+ Client --> DNAT80 --> CaddyHTTP
+ Client --> DNAT443 --> CaddyHTTPS
+ Client --> DNATSSH --> SSH
+ Client --> DNATTOR --> Tor
+
+ S80 --> CaddyHTTP
+ S443 --> CaddyHTTPS
+
+ CaddyHTTP -->|"/.well-known"| ACME["/var/www/acme"]
+ CaddyHTTP -->|other| Redir[301 HTTPS]
+ CaddyHTTPS -->|taler.hacktivism.ch| Merch
+ CaddyHTTPS -->|exchange.hacktivism.ch| Exch
+ CaddyHTTPS -->|bank.hacktivism.ch| Bank
+ CaddyHTTPS -->|castopod.hacktivism.ch| Castopod
+ CaddyHTTPS -->|bonfire.hacktivism.ch| Bonfire
+ CaddyHTTPS -->|prime.hacktivism.ch| Prime
+ CaddyHTTPS -->|bt.hacktivism.ch| BT
+ CaddyHTTPS -->|git.hacktivism.ch| Forgejo
+ CaddyHTTPS -->|paivana.hacktivism.ch| Paivana
+ CaddyHTTPS -->|lemmy.hacktivism.ch| Lemmy
+```
+
+## Port table
+
+| Port | Listener | Backend / notes |
+|------|----------|-----------------|
+| 22 | sshd | LAN; WAN via 23235 |
+| 80 | systemd socket | → 127.0.0.1:**9000** |
+| 443 | systemd socket | → 127.0.0.1:**9001** |
+| 9000 | caddy | HTTP + ACME |
+| 9001 | caddy | HTTPS vhosts |
+| 9010 | podman | **merchant** nginx |
+| 9011 | podman | **exchange** httpd |
+| 9012 | podman | **bank** libeufin |
+| 9020 | podman | **castopod** |
+| 9021 | podman | **bonfire** |
+| 9022 | podman | **jellyfin** (prime) |
+| 9023 | podman | **qbittorrent** (bt) |
+| **9024** | podman | **forgejo** HTTP → `git.hacktivism.ch` |
+| **9025** | podman | **paivana** → `paivana.hacktivism.ch` |
+| **9026** | podman | **lemmy** → `lemmy.hacktivism.ch` |
+| **9200** | podman | **forgejo** git-SSH (host-direct) |
+| 9090–9092 | podman | **tops** `koopa-tops-ng1`…`ng3` |
+| 8080 | tor | ORPort |
+
+Full port notes: `configs/ports.md`.
+
+## Service index (details elsewhere)
+
+| Service | Detail docs |
+|---------|-------------|
+| Caddy | `host/caddy/`, `configs/caddy/` |
+| firewalld | `host/firewalld/` |
+| systemd proxies | `host/systemd/` |
+| Merchant container | `configs/taler-hacktivism/`, `scripts/taler-merchant/` |
+| Exchange container | `configs/taler-exchange/`, `scripts/taler-exchange/` |
+| **Forgejo rootless** | `configs/forgejo/`, `2026/2026-07-10--forgejo-rootless.md` |
+| Paivana paywall | `configs/paivana/` |
+| tops (`koopa-tops-ng*`) | `configs/tops/` |
+| Tor relay (`koopa-tor-relay` podman) | `configs/tor/`, `host/tor/` |
+| Tor monitoring scripts | `scripts/monitoring/` |
+| VeciGate NAT | `../vecigate-admin-log/ip/firewall/nat/` |
diff --git a/host/overview/services.txt b/host/overview/services.txt
new file mode 100644
index 0000000..a37748b
--- /dev/null
+++ b/host/overview/services.txt
@@ -0,0 +1,24 @@
+ Internet
+ |
+ +-------------+-------------+
+ | VeciGate |
+ | :80→9000 :443→9001 |
+ | :23235→22 :8080→8080 |
+ +-------------+-------------+
+ |
+ koopa 192.168.100.95
+ |
+ +----------------------+----------------------+
+ | | |
+ systemd Caddy other
+ :80 → 9000 :9000 HTTP sshd :22
+ :443 → 9001 :9001 HTTPS tor :8080
+ | |
+ +----------+-----------+
+ |
+ +-------+--------+
+ | |
+ :9010 merchant :9011 exchange
+ (nginx TLS) (httpd TCP)
+ | |
+ unix sock postgres/secmods
diff --git a/host/tor/README.md b/host/tor/README.md
index fa68a9c..18643b5 100644
--- a/host/tor/README.md
+++ b/host/tor/README.md
@@ -1,30 +1,28 @@
# 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`
+**Runtime:** podman **`koopa-tor-relay`** (not host `tor.service`).
+**Config mirror:** `configs/tor/` (`torrc`, Containerfile, migrate script, `create-container.sh`, user unit).
+**Data/identity:** `~/koopa-tor-relay/data` (same keys as former `/var/lib/tor`).
+**Process:** non-root **uid 1000** (`debian-tor`); recreate via `configs/tor/create-container.sh` after image rebuild.
-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`.
+| Port | Bind | Role |
+|------|------|------|
+| **8080** | `0.0.0.0` + `[::]` | ORPort (dual-stack) |
+| **9051** | `127.0.0.1` | ControlPort (nyx / stem) |
+
+Non-exit relay (`ExitRelay 0`, `SocksPort 0`).
+
+### Root checklist
```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
+sudo systemctl disable --now tor
+sudo systemctl mask tor
+# linger already required for rootless user units:
+# sudo loginctl enable-linger hernani
```
-Apply / recreate (after host tor off):
+### User unit
```bash
-sudo APPLY=1 /tmp/tor.sh
-# later upgrades:
-~/upgrade-containers.sh --apply --rebuild-local --only koopa-tor-relay
+systemctl --user enable --now container-koopa-tor-relay.service
```
-
-login-health: expect user unit active + CTR Up; host `tor.service` inactive. Parent TODO=704.
diff --git a/koopa-setup b/koopa-setup
index 7d1832e..0a18930 100644
--- a/koopa-setup
+++ b/koopa-setup
@@ -14,18 +14,14 @@ configs/ports.md
2026/2026-09-04--mytops-local-podman.md
# outward: ~/git/mytops-admin-log → git switch mytops-local
-## goa-regio-ng (live GOA on koopa)
+## goa-regio-ng
-# single CTR goa-regio-ng · pasta 9010–9015 · landings host-side
-# SoT: scripts/taler-shared/goa-regio-ng/ · bak ~/backups/goa/current
2026/2026-09-04--goa-backup-regio-ng.md
-configs/ports.md
-scripts/login-health/
+# outward: ~/src/regio-local/
## host map
-configs/ports.md
+host/overview/
configs/caddy/
-host/tor/README.md
SECRETS.md
ISSUES.md
diff --git a/ops/local-branches-sync.md b/ops/local-branches-sync.md
index 0af0ea4..60be734 100644
--- a/ops/local-branches-sync.md
+++ b/ops/local-branches-sync.md
@@ -1,6 +1,6 @@
# Local-only copy — /home/hernani/koopa-admin-log
#
-# master → Forgejo origin (ok to push)
+# main → Forgejo origin (ok to push)
# local/* → NEVER push to origin; sync only via home/bundle between kisten
#
# Branches:
@@ -15,7 +15,7 @@
# cd /home/hernani/koopa-admin-log && git fetch /tmp/koopa-local.bundle "+refs/heads/local/*:refs/heads/local/*"
#
# Pull main:
-# git fetch origin && git checkout master && git merge --ff-only origin/master
+# git fetch origin && git checkout main && git merge --ff-only origin/main
## 2026-07-17 clone merge
diff --git a/scripts/README.md b/scripts/README.md
index 18f1b66..daa0bfa 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -8,7 +8,7 @@
| `taler-bank/` | bank host helpers + **`maintenance/`** (debit limits, dry-run) |
| `taler-shared/` | host **ensure-taler-apps** (post-container app start + auto-confirm) |
| `taler-sanity/` | host root checks (stack, settlement, helpers) |
-| `taler-monitoring/` | **moved** → [hernani/elelmon](https://git.hacktivism.ch/hernani/elelmon) (see `scripts/taler-monitoring/README.md`; archived `taler-monitoring` not SoT) |
+| `taler-monitoring/` | **moved** → [hernani/taler-monitoring](https://git.hacktivism.ch/hernani/taler-monitoring) (see `scripts/taler-monitoring/README.md`) |
| `taler-shared/` | shared helpers (`upgrade-goa-debs.sh` = apt upgrade **inside** bank/exchange/merchant containers) |
| `taler-landing/` | **hernani user systemd**: central landing `stats.json` (full bank scan + alt units) |
| `monitoring/` | host `/home/hernani/scripts` (tor relay stats) |
@@ -17,7 +17,6 @@
| `taler-wallet-cli/` | thin wrappers; **benchmarks live in** `../benchmarks/` |
| `castopod/` | host `hernani` podman-compose `~/koopa-castopod` — see `castopod/README.md` |
| `lemmy/` | host `hernani` podman-compose `~/koopa-lemmy` — see `configs/lemmy/README.md` |
-| `prime/` | qBittorrent BT net apply (`apply-qbittorrent-bt-net.sh`; wrapper `koopa-bt-fix.sh`) — incomplete dir, UPnP/PF, firewalld **6881** |
**Secrets:** never in this tree — sibling **`../koopa-admin-secrets`** (`koopa/host-root//` ↔ `/root/` on host; `containers/…/secrets/` for in-container).
diff --git a/scripts/amanita-probe/README.md b/scripts/amanita-probe/README.md
deleted file mode 100644
index bbd1fd9..0000000
--- a/scripts/amanita-probe/README.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# amanita-probe — external last-alive from koopa
-
-**Why:** amanita hard hangs leave no panic; internal HB stops with the box.
-External ICMP+SSH from **koopa** distinguishes:
-
-| Pattern | Likely meaning |
-|---------|----------------|
-| ICMP fail + SSH fail, long gap | hang, power cut, or **PSU** (shared wall outlet with rabbithole ≠ whole-circuit outage) |
-| ICMP ok + SSH fail | kernel/userspace hang, sshd stuck, or vx lock |
-| both ok, HB stamp stale vs wall | rare clock skew / forced-cmd path oddity |
-| both ok, HB fresh | alive |
-
-Log is **hernani-readable** on koopa (`~/.local/state/amanita-probe/`).
-SSH key is **vx-only** on amanita, restricted to koopa LAN IP + forced `cat` of HB mirror.
-
-## Pieces
-
-| Piece | Path |
-|-------|------|
-| Script | `scripts/amanita-probe/koopa-amanita-probe.sh` → `~/.local/bin/koopa-amanita-probe` |
-| Timer | `configs/systemd/user/koopa-amanita-probe.{service,timer}` (~15 s) |
-| Key | `~/.ssh/id_ed25519_amanita_probe` (hernani on koopa) |
-| Log | `~/.local/state/amanita-probe/amanita-probe.log` (+ `.last` / `.meta`) |
-| SSH Host | `amanita-probe` → `vx@192.168.100.7` with that IdentityFile |
-
-## Install (koopa as hernani)
-
-Laptop SoT: `$HOME/git/admin-logs/local/koopa-admin-log`. Live mirror: `~/src/koopa/koopa-admin-log` (no git commit on koopa).
-
-```bash
-# on koopa after syncing the tree:
-cd ~/src/koopa/koopa-admin-log
-./scripts/amanita-probe/install-amanita-probe.sh --keygen
-# copy the printed authorized_keys line → amanita vx (see below)
-./scripts/amanita-probe/install-amanita-probe.sh # units + first probe
-```
-
-Linger already yes for hernani → timer survives logout.
-
-## amanita vx authorized_keys (one line)
-
-```text
-from="192.168.100.95",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,command="/bin/cat /var/lib/amanita/heartbeat.last" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBeMenvp0D9EarfAEUtu49Q7bCbGhw+R/E1nJPQSSmZc koopa-amanita-probe@hernani-koopa
-```
-
-- **User:** `vx` only (not root).
-- **from=:** only koopa LAN `192.168.100.95`.
-- **forced command:** read-only HB mirror (world-readable on amanita).
-- **Fingerprint:** `SHA256:R2i2nZfI1svR6Ts3DaSw5eZj2fvnGgm1R9PoMFS4Jj0`
-- hernani on koopa owns the private key and the probe log (live since 2026-09-18).
-- Daylog: `2026/2026-09-18--amanita-external-probe.md`.
-
-## Verify
-
-```bash
-# koopa
-systemctl --user list-timers koopa-amanita-probe.timer
-koopa-amanita-probe status
-koopa-amanita-probe show
-ssh amanita-probe # should print one HB line, no shell
-
-# gaps after a freeze: look for icmp=fail/ssh=fail with large gap_s=
-# also: ref= (rabbithole ICMP) and edge= on icmp/ssh pair change
-```
-
-## Uninstall
-
-```bash
-systemctl --user disable --now koopa-amanita-probe.timer
-rm -f ~/.local/bin/koopa-amanita-probe \
- ~/.config/systemd/user/koopa-amanita-probe.{service,timer}
-systemctl --user daemon-reload
-# optional: remove key + Host block + vx authorized_keys line
-```
-
-## Do not
-
-- Put this key on **root** authorized_keys.
-- `modprobe ramoops` without reserved mem (known hard reset 2026-09-16).
-- Expect this to replace internal HB — both are needed.
diff --git a/scripts/amanita-probe/install-amanita-probe.sh b/scripts/amanita-probe/install-amanita-probe.sh
deleted file mode 100644
index f418ab4..0000000
--- a/scripts/amanita-probe/install-amanita-probe.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env bash
-# Install koopa-amanita-probe (hernani on koopa):
-# - ~/.local/bin/koopa-amanita-probe
-# - user systemd timer ~15s → ICMP+SSH last-alive log
-# - optional: generate ~/.ssh/id_ed25519_amanita_probe + Host amanita-probe
-# - optional: print restricted pubkey line for amanita vx authorized_keys
-#
-# On koopa as hernani (after laptop push + pull into ~/src/koopa/koopa-admin-log):
-# ./scripts/amanita-probe/install-amanita-probe.sh
-# ./scripts/amanita-probe/install-amanita-probe.sh --keygen # if key missing
-# ./scripts/amanita-probe/install-amanita-probe.sh --print-pubkey
-#
-# Key install on amanita is separate (vx authorized_keys) — see README.
-
-set -euo pipefail
-
-ROOT=$(cd "$(dirname "$0")" && pwd)
-REPO_ROOT=$(cd "$ROOT/../.." && pwd)
-SRC="$ROOT/koopa-amanita-probe.sh"
-UNIT_SRC_DIR="$REPO_ROOT/configs/systemd/user"
-BIN_DIR="${HOME}/.local/bin"
-DEST="$BIN_DIR/koopa-amanita-probe"
-UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
-KEY="${AMANITA_PROBE_KEY:-${HOME}/.ssh/id_ed25519_amanita_probe}"
-SSH_CONFIG="${HOME}/.ssh/config"
-HOST_MARKER_BEGIN="# >>> amanita-probe (koopa-admin-log) >>>"
-HOST_MARKER_END="# <<< amanita-probe (koopa-admin-log) <<<"
-DO_KEYGEN=0
-DO_PRINT=0
-DO_UNITS=1
-
-for arg in "$@"; do
- case "$arg" in
- --keygen) DO_KEYGEN=1 ;;
- --print-pubkey) DO_PRINT=1 ;;
- --no-units) DO_UNITS=0 ;;
- -h|--help)
- sed -n '2,16p' "$0" | sed 's/^# \?//'
- exit 0
- ;;
- *)
- echo "unknown arg: $arg" >&2
- exit 2
- ;;
- esac
-done
-
-if [ ! -f "$SRC" ]; then
- echo "missing $SRC" >&2
- exit 1
-fi
-
-mkdir -p "$BIN_DIR" "$UNIT_DIR" "${HOME}/.ssh"
-chmod 700 "${HOME}/.ssh" 2>/dev/null || true
-
-install -m 0755 "$SRC" "$DEST"
-echo "installed $DEST"
-
-if [ "$DO_KEYGEN" -eq 1 ]; then
- if [ -f "$KEY" ]; then
- echo "key exists: $KEY (skip --keygen)"
- else
- ssh-keygen -t ed25519 -a 64 -f "$KEY" -N "" \
- -C "koopa-amanita-probe@hernani-$(hostname -s 2>/dev/null || echo koopa)"
- chmod 600 "$KEY" "${KEY}.pub"
- echo "generated $KEY"
- fi
-fi
-
-if [ -f "$KEY" ]; then
- # Ensure Host block for manual/debug ssh (timer uses -i directly)
- touch "$SSH_CONFIG"
- chmod 600 "$SSH_CONFIG" 2>/dev/null || true
- if grep -qF "$HOST_MARKER_BEGIN" "$SSH_CONFIG" 2>/dev/null; then
- tmp=$(mktemp)
- awk -v b="$HOST_MARKER_BEGIN" -v e="$HOST_MARKER_END" '
- $0 == b {skip=1; next}
- $0 == e {skip=0; next}
- !skip {print}
- ' "$SSH_CONFIG" >"$tmp"
- mv "$tmp" "$SSH_CONFIG"
- fi
- {
- echo ""
- echo "$HOST_MARKER_BEGIN"
- echo "Host amanita-probe"
- echo " Hostname 192.168.100.7"
- echo " User vx"
- echo " IdentityFile $KEY"
- echo " IdentitiesOnly yes"
- echo " BatchMode yes"
- echo " PreferredAuthentications publickey"
- echo " ConnectTimeout 5"
- echo "$HOST_MARKER_END"
- } >>"$SSH_CONFIG"
- echo "updated $SSH_CONFIG Host amanita-probe"
-fi
-
-if [ "$DO_PRINT" -eq 1 ] || [ "$DO_KEYGEN" -eq 1 ]; then
- if [ ! -f "${KEY}.pub" ]; then
- echo "missing ${KEY}.pub — run with --keygen first" >&2
- exit 1
- fi
- pub="$(awk '{print $1, $2}' "${KEY}.pub")"
- comment="$(awk '{print $3}' "${KEY}.pub")"
- echo ""
- echo "=== paste ONE line into amanita:~vx/.ssh/authorized_keys ==="
- printf 'from="192.168.100.95",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/bin/cat /var/lib/amanita/heartbeat.last" %s %s\n' \
- "$pub" "${comment:-koopa-amanita-probe}"
- echo "=== end ==="
-fi
-
-if [ "$DO_UNITS" -eq 1 ]; then
- for u in koopa-amanita-probe.service koopa-amanita-probe.timer; do
- if [ ! -f "$UNIT_SRC_DIR/$u" ]; then
- echo "missing unit $UNIT_SRC_DIR/$u" >&2
- exit 1
- fi
- install -m 0644 "$UNIT_SRC_DIR/$u" "$UNIT_DIR/$u"
- echo "installed $UNIT_DIR/$u"
- done
- systemctl --user daemon-reload
- systemctl --user enable --now koopa-amanita-probe.timer
- systemctl --user start koopa-amanita-probe.service || true
- echo "timer: $(systemctl --user is-active koopa-amanita-probe.timer 2>/dev/null || echo unknown)"
- echo "probe once: $DEST once"
- "$DEST" once || true
- "$DEST" status || true
-fi
-
-echo "done. log: ~/.local/state/amanita-probe/amanita-probe.log"
diff --git a/scripts/amanita-probe/koopa-amanita-probe.sh b/scripts/amanita-probe/koopa-amanita-probe.sh
deleted file mode 100644
index 3d90bbc..0000000
--- a/scripts/amanita-probe/koopa-amanita-probe.sh
+++ /dev/null
@@ -1,162 +0,0 @@
-#!/usr/bin/env bash
-# koopa-amanita-probe — external ICMP+SSH last-alive for amanita freezes.
-# Runs as hernani on koopa (~15s user timer). Log is hernani-readable on koopa.
-# SSH uses dedicated ed25519 → vx only (forced command: cat heartbeat.last).
-#
-# Usage:
-# koopa-amanita-probe # one probe cycle (default; timer path)
-# koopa-amanita-probe once # same
-# koopa-amanita-probe show # tail last lines
-# koopa-amanita-probe status # last line + timer hint
-#
-# Env:
-# AMANITA_PROBE_HOST default 192.168.100.7
-# AMANITA_PROBE_USER default vx
-# AMANITA_PROBE_KEY default ~/.ssh/id_ed25519_amanita_probe
-# AMANITA_PROBE_STATE default ~/.local/state/amanita-probe
-# AMANITA_PROBE_REF second ICMP target (default 192.168.100.3, rabbithole)
-set +e
-set +u
-
-CMD="${1:-once}"
-HOST="${AMANITA_PROBE_HOST:-192.168.100.7}"
-USER_R="${AMANITA_PROBE_USER:-vx}"
-KEY="${AMANITA_PROBE_KEY:-${HOME}/.ssh/id_ed25519_amanita_probe}"
-STATE="${AMANITA_PROBE_STATE:-${XDG_STATE_HOME:-${HOME}/.local/state}/amanita-probe}"
-LOG="${STATE}/amanita-probe.log"
-LAST="${STATE}/amanita-probe.last"
-META="${STATE}/amanita-probe.meta"
-SSH_TO="${AMANITA_PROBE_SSH_TIMEOUT:-5}"
-PING_TO="${AMANITA_PROBE_PING_TIMEOUT:-2}"
-REF="${AMANITA_PROBE_REF:-192.168.100.3}"
-
-mkdir -p "${STATE}" 2>/dev/null || true
-
-do_show() {
- if [ ! -f "${LOG}" ]; then
- echo "amanita-probe: no log yet (${LOG})"
- echo " start: systemctl --user start koopa-amanita-probe.service"
- return 0
- fi
- tail -n "${1:-20}" "${LOG}"
-}
-
-do_status() {
- if [ -f "${LAST}" ]; then
- echo "last: $(cat "${LAST}")"
- else
- echo "last: (none)"
- fi
- if [ -f "${META}" ]; then
- cat "${META}"
- fi
- systemctl --user is-active koopa-amanita-probe.timer 2>/dev/null \
- | awk '{print "timer=" $0}'
-}
-
-do_once() {
- local ts icmp ssh hb gap prev_ts prev_epoch now_epoch
- local old_icmp old_ssh old_pair new_pair edge ref
- ts="$(date -Iseconds)"
- now_epoch="$(date +%s)"
- icmp=fail
- ssh=fail
- hb="-"
- old_icmp=""
- old_ssh=""
- old_pair=""
- if [ -f "${LAST}" ]; then
- old_icmp="$(awk '{for(i=1;i<=NF;i++) if($i ~ /^icmp=/){sub(/^icmp=/,"",$i); print $i; exit}}' "${LAST}" 2>/dev/null)"
- old_ssh="$(awk '{for(i=1;i<=NF;i++) if($i ~ /^ssh=/){sub(/^ssh=/,"",$i); print $i; exit}}' "${LAST}" 2>/dev/null)"
- if [ -n "${old_icmp}" ] && [ -n "${old_ssh}" ]; then
- old_pair="${old_icmp}/${old_ssh}"
- fi
- fi
-
- if ping -c1 -W "${PING_TO}" "${HOST}" >/dev/null 2>&1; then
- icmp=ok
- fi
-
- if [ ! -f "${KEY}" ]; then
- ssh=nokey
- else
- # Forced command on amanita returns heartbeat.last; ignore remote argv.
- hb="$(
- ssh -i "${KEY}" -o IdentitiesOnly=yes -o BatchMode=yes \
- -o StrictHostKeyChecking=accept-new \
- -o ConnectTimeout="${SSH_TO}" -o ConnectionAttempts=1 \
- -o PreferredAuthentications=publickey \
- "${USER_R}@${HOST}" true 2>/dev/null \
- | tr '\n' ' ' | tr -s ' ' | head -c 800
- )"
- if [ -n "${hb}" ]; then
- ssh=ok
- else
- # Distinguish timeout/refuse when ICMP ok
- if [ "${icmp}" = "ok" ]; then
- ssh=fail
- else
- ssh=down
- fi
- hb="-"
- fi
- fi
-
- gap="?"
- if [ -f "${LAST}" ]; then
- prev_ts="$(awk '{print $1}' "${LAST}" 2>/dev/null)"
- if prev_epoch="$(date -d "${prev_ts}" +%s 2>/dev/null)"; then
- gap=$((now_epoch - prev_epoch))
- fi
- fi
-
- ref=fail
- if ping -c1 -W "${PING_TO}" "${REF}" >/dev/null 2>&1; then
- ref=ok
- fi
- new_pair="${icmp}/${ssh}"
- edge=""
- if [ -n "${old_pair}" ] && [ "${old_pair}" != "${new_pair}" ]; then
- edge=" edge=${old_pair}>${new_pair}"
- fi
-
- local line
- line="$(printf '%s icmp=%s ssh=%s gap_s=%s host=%s user=%s ref=%s%s hb=%s' \
- "${ts}" "${icmp}" "${ssh}" "${gap}" "${HOST}" "${USER_R}" "${ref}" "${edge}" "${hb}")"
-
- printf '%s\n' "${line}" >>"${LOG}"
- printf '%s\n' "${line}" >"${LAST}"
- {
- echo "updated=${ts}"
- echo "log=${LOG}"
- echo "icmp=${icmp}"
- echo "ssh=${ssh}"
- echo "gap_s=${gap}"
- echo "ref=${ref}"
- } >"${META}"
-
- # Keep log bounded (~14d at 15s ≈ 80k lines; rotate soft at 200k lines)
- if [ -f "${LOG}" ]; then
- local lines
- lines="$(wc -l <"${LOG}" 2>/dev/null | tr -d ' ')"
- if [ "${lines:-0}" -gt 200000 ] 2>/dev/null; then
- tail -n 100000 "${LOG}" >"${LOG}.tmp" && mv "${LOG}.tmp" "${LOG}"
- fi
- fi
-
- # Exit 0 always for timer; FAIL is data, not unit failure
- return 0
-}
-
-case "${CMD}" in
- once|probe|"") do_once ;;
- show) do_show "${2:-20}" ;;
- status) do_status ;;
- -h|--help)
- sed -n '2,20p' "$0" | sed 's/^# \?//'
- ;;
- *)
- echo "unknown: ${CMD} (once|show|status)" >&2
- exit 2
- ;;
-esac
diff --git a/scripts/bbb/build.sh b/scripts/bbb/build.sh
deleted file mode 100755
index cf0a4c0..0000000
--- a/scripts/bbb/build.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-# Build BBB front stub on Ubuntu (run on koopa).
-set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CTX="${ROOT}/configs/bbb"
-TAG="${IMAGE_TAG:-localhost/koopa-bbb-front:ubuntu}"
-cd "$CTX"
-[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
-echo "build $TAG ctx=$CTX"
-podman build -t "$TAG" -f Containerfile .
-echo "OK: $TAG"
diff --git a/scripts/caddy/Caddyfile.koopa-live-wdGet b/scripts/caddy/Caddyfile.koopa-live-wdGet
deleted file mode 100644
index aebd16c..0000000
--- a/scripts/caddy/Caddyfile.koopa-live-wdGet
+++ /dev/null
@@ -1,354 +0,0 @@
-# Internal only (not in the browser URL):
-# 9010 merchant API | 9011 exchange API | 9012 bank API
-# 9013 bank landing | 9014 exchange landing | 9015 merchant landing
-# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9200 forgejo-ssh
-# 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3
-{
- email info+koopa@hacktivism.ch
- http_port 9000
- https_port 9001
- auto_https disable_redirects
- # Caddy listens on 9001 behind VeciGate/https-proxy :443.
- # Default HTTP/3 would send Alt-Svc: h3=":9001" — break public HTTPS on :443.
- servers {
- protocols h1 h2
- }
-}
-
-(proxy_public) {
- header_up Host {host}
- header_up X-Forwarded-Port 443
- header_down Location "^https?://[^/]+:90[0-9]{2}(.*)$" "https://{host}$1"
-}
-
-taler.hacktivism.ch {
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9015 {
- import proxy_public
- }
- }
-
- # surface + aptdeploy HTML (taler.hacktivism.ch only)
- handle /taler-monitoring-surface_err {
- redir /taler-monitoring-surface_err/ 302
- }
- handle /taler-monitoring-surface_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-surface {
- redir /taler-monitoring-surface/ 302
- }
- handle /taler-monitoring-surface* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy_err {
- redir /taler-monitoring-aptdeploy_err/ 302
- }
- handle /taler-monitoring-aptdeploy_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-aptdeploy {
- redir /taler-monitoring-aptdeploy/ 302
- }
- handle /taler-monitoring-aptdeploy* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mattermost_err {
- redir /taler-monitoring-mattermost_err/ 302
- }
- handle /taler-monitoring-mattermost_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mattermost {
- redir /taler-monitoring-mattermost/ 302
- }
- handle /taler-monitoring-mattermost* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- handle /taler-monitoring-mail_err {
- redir /taler-monitoring-mail_err/ 302
- }
- handle /taler-monitoring-mail_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /taler-monitoring-mail {
- redir /taler-monitoring-mail/ 302
- }
- handle /taler-monitoring-mail* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/taler.hacktivism.ch
- file_server
- }
-
- # SPA: /webui → /webui/
- redir /webui /webui/ 302
-
- # Merchant API + WebUI (nginx :9010 → unix socket)
- reverse_proxy https://127.0.0.1:9010 {
- transport http {
- tls_insecure_skip_verify
- }
- import proxy_public
- }
-}
-
-exchange.hacktivism.ch {
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9014 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/exchange.hacktivism.ch
- file_server
- }
-
- reverse_proxy 127.0.0.1:9011 {
- import proxy_public
- }
-}
-
-bank.hacktivism.ch {
- header Alt-Svc "clear"
-
- # Public landing first
- redir / /intro/ 302
-
- handle /intro* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- # taler-monitoring console HTML (host static — not in app containers)
- handle /monitoring_err {
- redir /monitoring_err/ 302
- }
- handle /monitoring_err* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
- handle /monitoring {
- redir /monitoring/ 302
- }
- handle /monitoring* {
- root * /var/www/monitoring-sites/bank.hacktivism.ch
- file_server
- }
-
- # Static terms/privacy on landing nginx :9013
- handle /terms* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
- handle /privacy* {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- # FP pattern (francpaysan-admin-log files/caddy/Caddyfile @wdGet):
- # GET withdrawal-operation → landing :9013 → demo-api :19096
- # (injects suggested_exchange / required_exchange for Android/iOS)
- @wdGet {
- method GET
- path /taler-integration/withdrawal-operation/*
- }
- handle @wdGet {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
-
- reverse_proxy 127.0.0.1:9012 {
- import proxy_public
- }
-}
-
-castopod.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9020 {
- header_up Host {host}
- }
-}
-
-bonfire.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9021 {
- header_up Host {host}
- flush_interval -1
- }
-}
-
-prime.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9022 {
- header_up Host {host}
- flush_interval -1
- }
-}
-
-bt.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9023 {
- header_up Host {host}
- }
-}
-
-# 9024 forgejo HTTP (SSH :9200 host-direct, not via Caddy)
-git.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9024 {
- header_up Host {host}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# Taler Operations design previews (static nginx)
-tops.ng1.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9090 {
- header_up Host {host}
- }
-}
-
-tops.ng2.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9091 {
- header_up Host {host}
- }
-}
-
-tops.ng3.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9092 {
- header_up Host {host}
- }
-}
-
-# 9025 paivana-httpd (GOA paywall)
-paivana.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9025 {
- header_up Host {host}
- header_up X-Forwarded-Port 443
- }
-}
-
-# 9026 lemmy (+ ui/pictrs via internal nginx)
-lemmy.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9026 {
- header_up Host {host}
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-# 9027 decidim (rails)
-decidim.hacktivism.ch {
- header Alt-Svc "clear"
- reverse_proxy 127.0.0.1:9027 {
- header_up Host {host}
- header_up X-Forwarded-Port 443
- flush_interval -1
- transport http {
- read_timeout 3600s
- write_timeout 3600s
- }
- }
-}
-
-http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch {
- handle /.well-known/acme-challenge/* {
- root * /var/www/acme
- file_server
- }
- handle {
- redir https://{host}{uri} permanent
- }
-}
-
-# --- LLM Lightweight Monitoring (applied 2026-09-05T10:08:03+02:00) ---
-monitoring.hacktivism.ch {
- header Alt-Svc "clear"
- root * /var/www/monitoring-sites/monitoring.hacktivism.ch
- file_server
- try_files {path} /index.html
-}
-
-# ngi0-landing dossiers test sites (static) — numbering like tops.ng*
-dossiers.ngi.hacktivism.ch {
- header Alt-Svc "clear"
- root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
- file_server
- try_files {path} {path}/ {path}/index.html
-}
-
-dossiers.2.ngi.hacktivism.ch {
- header Alt-Svc "clear"
- root * /var/www/monitoring-sites/dossiers.2.ngi.hacktivism.ch
- file_server
- try_files {path} {path}/ {path}/index.html
-}
-
-dossiers.3.ngi.hacktivism.ch {
- header Alt-Svc "clear"
- root * /var/www/monitoring-sites/dossiers.3.ngi.hacktivism.ch
- file_server
- try_files {path} {path}/ {path}/index.html
-}
diff --git a/scripts/caddy/README.md b/scripts/caddy/README.md
index ae4921d..a9f5139 100644
--- a/scripts/caddy/README.md
+++ b/scripts/caddy/README.md
@@ -10,79 +10,17 @@ sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
-Preferred live path: edit `~/koopa-caddy/Caddyfile`, then `sudo ~/bin/caddy-apply`
-(copies → `/etc/caddy/Caddyfile`, validate, `systemctl reload caddy`).
-
-## GOA bank @wdGet (FP Android withdraw) — full replace
-
-Live snapshot + `@wdGet` (GET Integration → `:9013` → demo-api):
-
-- `Caddyfile.koopa-live-wdGet` — whole-file replacement (based on live koopa, not admin-log mirror)
-- `apply-goa-wdGet-full-replace.sh` — replaces `~/koopa-caddy/Caddyfile` **and** `/etc/caddy/Caddyfile`, reload, smoke
-
-```bash
-# on koopa (root) — scp both files first if no admin-log checkout:
-sudo bash /tmp/goa-fp-port/caddy/apply-goa-wdGet-full-replace.sh \
- /tmp/goa-fp-port/caddy/Caddyfile.koopa-live-wdGet
-```
-
-FP SoT: `$HOME/git/admin-logs/taler/francpaysan-admin-log` `files/caddy/Caddyfile` `@wdGet`.
-
## dossiers.ngi* — basic_auth (htaccess-in-front)
Caddy **ignores** Apache `.htaccess`. Use:
```bash
-# clone koopa-admin-log from Forgejo when needed (no permanent ~/ clone)
-sudo bash /path/to/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
+sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
```
-Host runtime: `/etc/caddy/secrets/dossiers-ngi-basicauth.txt` (root-only).
-SoT: `koopa-admin-secrets-enc` (git-crypt) → `koopa/home-hernani/caddy/`.
+Password file (not git): `/home/hernani/koopa-secrets/dossiers-ngi-basicauth.txt`
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`.
+## New HTTPS vhost
-## silverbullet.hacktivism.ch — merge + basic_auth
-
-After podman cutover (`scripts/notes/cutover-to-silverbullet.sh --apply`) and DNS:
-
-```bash
-# laptop: SoT → /tmp seed (no secret echo)
-bash /path/to/koopa-admin-log/scripts/notes/prepare-basicauth-seed.sh
-scp /tmp/silverbullet-basicauth-seed.txt hernani@koopa:/tmp/
-
-# koopa root:
-sudo bash /path/to/koopa-admin-log/scripts/caddy/merge-notes-site.sh
-sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt \
- bash /path/to/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh
-```
-
-`merge-notes-site.sh` renames leftover `freenotes`/`notes` site blocks → `silverbullet.hacktivism.ch`.
-Host runtime: `/etc/caddy/secrets/silverbullet-basicauth.txt` (root-only).
-SoT: `koopa-admin-secrets-enc` (git-crypt) → `koopa/home-hernani/caddy/silverbullet-basicauth.txt`.
-
-## New HTTPS vhost — `caddy-apply.sh`
-
-Generic merge (site snippet + ACME `http://` list + ports comment):
-
-```bash
-# laptop mirror (no reload):
-bash scripts/caddy/caddy-apply.sh \
- --config configs/caddy/Caddyfile \
- --snippet configs/caddy/galene.hacktivism.ch.caddy \
- --site galene.hacktivism.ch --port 9029 --label galene \
- --no-reload
-
-# koopa live (root):
-sudo bash …/scripts/caddy/caddy-apply.sh \
- --snippet …/configs/caddy/SITE.caddy \
- --site SITE.hacktivism.ch --port NNNN --label NAME
-```
-
-Allocated collab vhosts (2026-09-08): `galene` **9029**, `cryptpad` **9030**, `meet` **9031**, `bbb` **9032**.
-Snippets under `configs/caddy/*.hacktivism.ch.caddy`. Image bases: CachyOS except **bbb=Ubuntu**.
-
-Still valid: hand-edit live + mirror, then validate/reload.
+Add a site block to the Caddyfile (live + mirror), then validate and reload. Include the host in the shared HTTP site list (ACME + redirect) at the bottom of the file.
diff --git a/scripts/caddy/apply-dossiers-ngi-basicauth.sh b/scripts/caddy/apply-dossiers-ngi-basicauth.sh
index 9582170..4587f6a 100755
--- a/scripts/caddy/apply-dossiers-ngi-basicauth.sh
+++ b/scripts/caddy/apply-dossiers-ngi-basicauth.sh
@@ -4,12 +4,12 @@
#
# Caddy file_server does NOT honor Apache .htaccess — this edits the Caddyfile.
#
-# On koopa (clone admin-log from Forgejo when needed; no home git dump):
-# sudo bash /path/to/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
+# On koopa (password for sudo):
+# sudo -n true 2>/dev/null || sudo -v
+# sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
#
-# Host runtime file (root-only, not under ~):
-# /etc/caddy/secrets/dossiers-ngi-basicauth.txt
-# SoT (git-crypt): koopa-admin-secrets-enc → koopa/home-hernani/caddy/
+# Idempotent. Password file is NOT in git:
+# /home/hernani/koopa-secrets/dossiers-ngi-basicauth.txt
set -euo pipefail
if [[ "$(id -u)" -ne 0 ]]; then
@@ -19,9 +19,8 @@ fi
CADDY=/etc/caddy/Caddyfile
USER_NAME="${DOSSIERS_AUTH_USER:-hernani}"
-SECRETS_DIR=/etc/caddy/secrets
+SECRETS_DIR=/home/hernani/koopa-secrets
PASS_FILE="${SECRETS_DIR}/dossiers-ngi-basicauth.txt"
-OLD_PASS_FILE=/home/hernani/koopa-secrets/dossiers-ngi-basicauth.txt
SITES=(
dossiers.ngi.hacktivism.ch
dossiers.2.ngi.hacktivism.ch
@@ -35,14 +34,9 @@ fi
command -v caddy >/dev/null || { echo "ERROR: caddy not in PATH" >&2; exit 1; }
mkdir -p "$SECRETS_DIR"
+chown hernani:hernani "$SECRETS_DIR"
chmod 700 "$SECRETS_DIR"
-if [[ ! -f "$PASS_FILE" && -f "$OLD_PASS_FILE" ]]; then
- cp -a "$OLD_PASS_FILE" "$PASS_FILE"
- chmod 600 "$PASS_FILE"
- echo "OK: migrated $OLD_PASS_FILE → $PASS_FILE"
-fi
-
if [[ -f "$PASS_FILE" ]]; then
# shellcheck disable=SC1090
# file format: USER=... PASS=... HASH=...
diff --git a/scripts/caddy/apply-goa-wdGet-full-replace.sh b/scripts/caddy/apply-goa-wdGet-full-replace.sh
deleted file mode 100755
index b61bb4e..0000000
--- a/scripts/caddy/apply-goa-wdGet-full-replace.sh
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/env bash
-# Full-replace koopa Caddyfile with live snapshot + FP @wdGet (GOA Android withdraw).
-#
-# Replaces BOTH:
-# /home/hernani/koopa-caddy/Caddyfile (runtime SoT for ~/bin/caddy-apply)
-# /etc/caddy/Caddyfile (active config)
-# then validates + systemctl reload caddy.
-#
-# On koopa (root):
-# sudo bash /path/to/apply-goa-wdGet-full-replace.sh
-# # or with explicit file:
-# sudo bash …/apply-goa-wdGet-full-replace.sh /path/to/Caddyfile.koopa-live-wdGet
-#
-# FP pattern: francpaysan-admin-log files/caddy/Caddyfile @wdGet
-# Ship file: scripts/caddy/Caddyfile.koopa-live-wdGet (live koopa + @wdGet only)
-set -euo pipefail
-
-HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-SRC="${1:-$HERE/Caddyfile.koopa-live-wdGet}"
-USER_CADDY="/home/hernani/koopa-caddy/Caddyfile"
-ETC_CADDY="/etc/caddy/Caddyfile"
-BACKUP_DIR="/etc/caddy"
-
-if [[ "$(id -u)" -ne 0 ]]; then
- echo "ERROR: run as root: sudo bash $0${1:+ $1}" >&2
- exit 1
-fi
-
-[[ -f "$SRC" ]] || { echo "ERROR: missing source: $SRC" >&2; exit 1; }
-[[ -d "$(dirname "$USER_CADDY")" ]] || { echo "ERROR: missing dir: $(dirname "$USER_CADDY")" >&2; exit 1; }
-[[ -f "$ETC_CADDY" ]] || { echo "ERROR: missing $ETC_CADDY" >&2; exit 1; }
-command -v caddy >/dev/null || { echo "ERROR: caddy not in PATH" >&2; exit 1; }
-
-if ! grep -q '@wdGet' "$SRC"; then
- echo "ERROR: source lacks @wdGet — refusing replace: $SRC" >&2
- exit 1
-fi
-if ! grep -q 'path /taler-integration/withdrawal-operation/\*' "$SRC"; then
- echo "ERROR: source lacks Integration @wdGet path — refusing: $SRC" >&2
- exit 1
-fi
-
-ts=$(date +%Y%m%d-%H%M%S)
-bak_etc="${BACKUP_DIR}/Caddyfile.bak-goa-wdGet-${ts}"
-bak_user="${USER_CADDY}.bak-goa-wdGet-${ts}"
-
-cp -a "$ETC_CADDY" "$bak_etc"
-echo "backup etc: $bak_etc"
-if [[ -f "$USER_CADDY" ]]; then
- cp -a "$USER_CADDY" "$bak_user"
- echo "backup user: $bak_user"
-fi
-
-install -o hernani -g hernani -m 644 "$SRC" "$USER_CADDY"
-echo "wrote: $USER_CADDY"
-
-install -o root -g caddy -m 644 "$SRC" "$ETC_CADDY" 2>/dev/null \
- || install -o root -g root -m 644 "$SRC" "$ETC_CADDY"
-echo "wrote: $ETC_CADDY"
-
-echo "validate..."
-caddy validate --config "$ETC_CADDY"
-
-echo "reload..."
-systemctl reload caddy
-systemctl is-active caddy
-
-echo "spot-check @wdGet:"
-grep -n '@wdGet\|withdrawal-operation\|reverse_proxy 127.0.0.1:9013\|reverse_proxy 127.0.0.1:9012' "$ETC_CADDY" | head -20
-
-echo "smoke public Integration (expect suggested_exchange + required_exchange):"
-smoke="$(curl -sS --max-time 10 \
- 'https://bank.hacktivism.ch/taler-integration/withdrawal-operation/00000000-0000-0000-0000-000000000001' || true)"
-echo "$smoke"
-if echo "$smoke" | grep -q 'suggested_exchange' && echo "$smoke" | grep -q 'required_exchange'; then
- echo "OK: public Integration injects exchange fields"
-else
- echo "WARN: public smoke missing exchange fields — check nginx Integration + demo-api :19096" >&2
- exit 1
-fi
diff --git a/scripts/caddy/apply-notes-basicauth.sh b/scripts/caddy/apply-notes-basicauth.sh
deleted file mode 100755
index e45db3f..0000000
--- a/scripts/caddy/apply-notes-basicauth.sh
+++ /dev/null
@@ -1,179 +0,0 @@
-#!/usr/bin/env bash
-# Apply HTTP basic_auth to silverbullet.hacktivism.ch (dossiers pattern).
-#
-# On koopa (clone admin-log from Forgejo when needed; no home git dump):
-# sudo bash /path/to/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh
-#
-# Host runtime file (root-only, not under ~):
-# /etc/caddy/secrets/silverbullet-basicauth.txt
-# SoT (git-crypt): koopa-admin-secrets-enc → koopa/home-hernani/caddy/
-set -euo pipefail
-
-if [[ "$(id -u)" -ne 0 ]]; then
- echo "ERROR: run as root: sudo bash $0" >&2
- exit 1
-fi
-
-CADDY=/etc/caddy/Caddyfile
-USER_NAME="${NOTES_AUTH_USER:-hernani}"
-SECRETS_DIR=/etc/caddy/secrets
-PASS_FILE="${SECRETS_DIR}/silverbullet-basicauth.txt"
-# legacy home dumps (migrate once, then delete)
-OLD_PASS_FILE=/home/hernani/koopa-secrets/silverbullet-basicauth.txt
-OLD_NOTES_FILE=/home/hernani/koopa-secrets/notes-basicauth.txt
-OLD_FREENOTES_FILE=/home/hernani/koopa-secrets/freenotes-basicauth.txt
-SITE=silverbullet.hacktivism.ch
-
-if [[ ! -f "$CADDY" ]]; then
- echo "ERROR: missing $CADDY" >&2
- exit 1
-fi
-command -v caddy >/dev/null || { echo "ERROR: caddy not in PATH" >&2; exit 1; }
-
-mkdir -p "$SECRETS_DIR"
-chmod 700 "$SECRETS_DIR"
-
-# Prefer explicit seed (SoT from secrets-enc), then legacy home dumps once.
-SEED_FILE="${NOTES_AUTH_SEED:-}"
-if [[ ! -f "$PASS_FILE" && -n "$SEED_FILE" && -f "$SEED_FILE" ]]; then
- cp -a "$SEED_FILE" "$PASS_FILE"
- chmod 600 "$PASS_FILE"
- echo "OK: seeded $SEED_FILE → $PASS_FILE"
-fi
-if [[ ! -f "$PASS_FILE" && -f "$OLD_PASS_FILE" ]]; then
- cp -a "$OLD_PASS_FILE" "$PASS_FILE"
- chmod 600 "$PASS_FILE"
- echo "OK: migrated $OLD_PASS_FILE → $PASS_FILE"
-fi
-if [[ ! -f "$PASS_FILE" && -f "$OLD_NOTES_FILE" ]]; then
- cp -a "$OLD_NOTES_FILE" "$PASS_FILE"
- chmod 600 "$PASS_FILE"
- echo "OK: migrated $OLD_NOTES_FILE → $PASS_FILE"
-fi
-if [[ ! -f "$PASS_FILE" && -f "$OLD_FREENOTES_FILE" ]]; then
- cp -a "$OLD_FREENOTES_FILE" "$PASS_FILE"
- chmod 600 "$PASS_FILE"
- echo "OK: migrated $OLD_FREENOTES_FILE → $PASS_FILE"
-fi
-
-if [[ -f "$PASS_FILE" ]]; then
- USER_NAME="$(awk -F= '/^USER=/{print substr($0,6); exit}' "$PASS_FILE")"
- PASS="$(awk -F= '/^PASS=/{print substr($0,6); exit}' "$PASS_FILE")"
- HASH="$(awk -F= '/^HASH=/{print substr($0,6); exit}' "$PASS_FILE")"
- if [[ -z "$USER_NAME" || -z "$HASH" ]]; then
- echo "ERROR: $PASS_FILE incomplete — move it aside and re-run" >&2
- exit 1
- fi
- echo "OK: reusing credentials in $PASS_FILE"
-else
- PASS="$(openssl rand -base64 18 | tr -d '/+=' | head -c 20)"
- HASH="$(caddy hash-password --plaintext "$PASS")"
- umask 077
- cat >"$PASS_FILE" < tuple[int, int]:
- marker = f"{site_name} {{"
- i = src.find(marker)
- if i < 0:
- raise SystemExit(
- f"ERROR: site block not found: {site_name} — merge silverbullet.hacktivism.ch.caddy first"
- )
- depth = 0
- for k in range(i + len(marker) - 1, len(src)):
- ch = src[k]
- if ch == "{":
- depth += 1
- elif ch == "}":
- depth -= 1
- if depth == 0:
- return i, k + 1
- raise SystemExit(f"ERROR: unclosed block: {site_name}")
-
-
-def has_basic_auth_directive(block: str) -> bool:
- # Ignore comments; only real directives count.
- stripped = "\n".join(
- ln for ln in block.splitlines() if not ln.lstrip().startswith("#")
- )
- return bool(re.search(r"(?m)^\s*basic_?auth\b", stripped))
-
-
-i, j = site_span(text, site)
-block = text[i:j]
-auth_lines = (
- "\tbasic_auth {\n"
- f"\t\t{user} {hash_caddy}\n"
- "\t}\n"
-)
-# Replace existing basic_auth block (also fixes legacy "$$" hashes that never matched).
-block_new, n = re.subn(
- r"(?m)^[ \t]*basic_auth\s*\{.*?\n[ \t]*\}\n?",
- auth_lines,
- block,
- count=1,
- flags=re.S,
-)
-if n:
- text = text[:i] + block_new + text[j:]
- print(f"OK: replaced basic_auth in {site}")
-elif has_basic_auth_directive(block):
- print(f"OK: {site} already has basic_auth")
-else:
- m = re.search(r'(?m)^[ \t]*header Alt-Svc "clear"[ \t]*\n', block)
- if m:
- insert_at = m.end()
- new_block = block[:insert_at] + auth_lines + block[insert_at:]
- else:
- nl = block.find("\n")
- new_block = block[: nl + 1] + auth_lines + block[nl + 1 :]
- text = text[:i] + new_block + text[j:]
- print(f"OK: injected basic_auth in {site}")
-
-if text != orig:
- path.write_text(text)
- print(f"wrote {path}")
-else:
- print("no Caddyfile change")
-PY
-
-echo "== validate =="
-caddy validate --config "$CADDY"
-
-echo "== reload =="
-if systemctl is-active --quiet caddy; then
- systemctl reload caddy
-else
- echo "WARN: caddy unit not active — start it yourself" >&2
-fi
-
-echo
-echo "Done. Browser: https://${SITE}/"
-echo "User/pass: $PASS_FILE"
diff --git a/scripts/caddy/caddy-apply.sh b/scripts/caddy/caddy-apply.sh
deleted file mode 100755
index f43e4e5..0000000
--- a/scripts/caddy/caddy-apply.sh
+++ /dev/null
@@ -1,217 +0,0 @@
-#!/usr/bin/env bash
-# Generic: merge a site snippet into a Caddyfile + ACME http:// list + ports comment.
-#
-# Laptop (mirror only, no reload):
-# bash scripts/caddy/caddy-apply.sh \
-# --config configs/caddy/Caddyfile \
-# --snippet configs/caddy/galene.hacktivism.ch.caddy \
-# --site galene.hacktivism.ch --port 9029 --label galene \
-# --no-reload
-#
-# Koopa live (root, Freigabe):
-# sudo bash …/caddy-apply.sh \
-# --snippet /home/hernani/koopa-admin-log/configs/caddy/galene.hacktivism.ch.caddy \
-# --site galene.hacktivism.ch --port 9029 --label galene
-set -euo pipefail
-
-CADDY=/etc/caddy/Caddyfile
-SNIPPET=""
-SITE=""
-PORT=""
-LABEL=""
-INSERT_AFTER="decidim.hacktivism.ch"
-ACME_ANCHOR="http://decidim.hacktivism.ch"
-NO_RELOAD=0
-DRY=0
-OLD_SITES=()
-
-usage() {
- sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
- echo "Options: --config PATH --snippet PATH --site HOST --port N --label NAME"
- echo " --insert-after HOST --acme-anchor http://HOST --old-site HOST"
- echo " --no-reload --dry-run"
-}
-
-while [[ $# -gt 0 ]]; do
- case "$1" in
- --config) CADDY=$2; shift 2 ;;
- --snippet) SNIPPET=$2; shift 2 ;;
- --site) SITE=$2; shift 2 ;;
- --port) PORT=$2; shift 2 ;;
- --label) LABEL=$2; shift 2 ;;
- --insert-after) INSERT_AFTER=$2; shift 2 ;;
- --acme-anchor) ACME_ANCHOR=$2; shift 2 ;;
- --old-site) OLD_SITES+=("$2"); shift 2 ;;
- --no-reload) NO_RELOAD=1; shift ;;
- --dry-run) DRY=1; shift ;;
- -h|--help) usage; exit 0 ;;
- *) echo "ERROR: unknown arg: $1" >&2; usage >&2; exit 2 ;;
- esac
-done
-
-[[ -n "$SNIPPET" && -n "$SITE" ]] || { echo "ERROR: --snippet and --site required" >&2; exit 2; }
-[[ -f "$CADDY" ]] || { echo "ERROR: missing $CADDY" >&2; exit 1; }
-[[ -f "$SNIPPET" ]] || { echo "ERROR: missing $SNIPPET" >&2; exit 1; }
-LABEL="${LABEL:-$SITE}"
-
-if [[ "$CADDY" == /etc/caddy/Caddyfile && "$(id -u)" -ne 0 && "$DRY" -eq 0 ]]; then
- echo "ERROR: live Caddyfile needs root (or --dry-run / --config mirror)" >&2
- exit 1
-fi
-
-ts=$(date +%Y%m%d-%H%M%S)
-if [[ "$DRY" -eq 0 ]]; then
- cp -a "$CADDY" "${CADDY}.bak-caddy-apply-${ts}"
- echo "backup ${CADDY}.bak-caddy-apply-${ts}"
-fi
-
-export CADDY SNIPPET SITE PORT LABEL INSERT_AFTER ACME_ANCHOR DRY
-export OLD_SITES_CSV
-OLD_SITES_CSV=$(IFS=,; echo "${OLD_SITES[*]-}")
-
-python3 - <<'PY'
-import os, re, sys
-from pathlib import Path
-
-caddy = Path(os.environ["CADDY"])
-snip = Path(os.environ["SNIPPET"]).read_text().rstrip() + "\n\n"
-site = os.environ["SITE"]
-port = os.environ.get("PORT") or ""
-label = os.environ.get("LABEL") or site
-insert_after = os.environ["INSERT_AFTER"]
-acme_anchor = os.environ["ACME_ANCHOR"]
-dry = os.environ.get("DRY") == "1"
-old_sites = [s for s in os.environ.get("OLD_SITES_CSV", "").split(",") if s]
-
-text = caddy.read_text()
-orig = text
-
-def drop_site_block(text: str, old_site: str) -> str:
- marker_old = f"{old_site} {{"
- i = text.find(marker_old)
- if i < 0:
- return text
- j = text.find("\n}", i)
- if j < 0:
- return text
- end = j + 2
- while end < len(text) and text[end] == "\n":
- end += 1
- text = text[:i] + text[end:]
- text = text.replace(f", http://{old_site}", "").replace(f"http://{old_site}, ", "")
- text = text.replace(f"http://{old_site}", "")
- print(f"OK: removed duplicate old block {old_site}")
- return text
-
-for old_site in old_sites:
- if old_site in text and site not in text:
- text = text.replace(old_site, site)
- print(f"OK: renamed {old_site} → {site}")
- elif old_site in text and site in text:
- text = drop_site_block(text, old_site)
-
-marker = f"{site} {{"
-if marker in text:
- print(f"OK: site block already present: {site}")
-else:
- insert_at = None
- after = text.find(f"{insert_after} {{")
- if after >= 0:
- j = text.find("\n}", after)
- if j >= 0:
- insert_at = j + 2
- while insert_at < len(text) and text[insert_at] == "\n":
- insert_at += 1
- if insert_at is None:
- acme = text.find("http://taler.hacktivism.ch")
- insert_at = acme if acme >= 0 else len(text)
- text = text[:insert_at] + snip + text[insert_at:]
- print(f"OK: inserted site block {site}")
-
-http_tok = f"http://{site}"
-if http_tok in text:
- print(f"OK: ACME http list already has {http_tok}")
-else:
- # Prefer exact "anchor {" form; else append before " {" of the long http list
- old = f"{acme_anchor} {{"
- new = f"{acme_anchor}, {http_tok} {{"
- if old in text:
- text = text.replace(old, new, 1)
- print(f"OK: added {http_tok} to ACME http list (anchor)")
- else:
- # Find the shared ACME line (starts with http://taler…)
- m = re.search(r"(http://taler\.hacktivism\.ch[^\n]*?)(\s*\{)", text)
- if not m:
- raise SystemExit("ERROR: ACME http list not found")
- line = m.group(1)
- if http_tok in line:
- print(f"OK: ACME http list already has {http_tok}")
- else:
- text = text[: m.start(1)] + line.rstrip() + f", {http_tok}" + text[m.end(1) :]
- print(f"OK: added {http_tok} to ACME http list (append)")
-
-# Ports comment header (first matching line starting with "# 9020 castopod")
-if port:
- token = f"{port} {label}"
- def upd_hdr(line: str) -> str:
- if token in line or f"| {port} " in line or f"| {port}|" in line:
- return line
- # insert before " | 9200 forgejo-ssh" if present, else before end
- if "9200 forgejo-ssh" in line:
- return line.replace(" | 9200 forgejo-ssh", f" | {token} | 9200 forgejo-ssh", 1)
- if line.rstrip().endswith("forgejo-ssh"):
- return line.rstrip() + f" | {token}\n"
- return line.rstrip() + f" | {token}\n"
-
- lines = text.splitlines(keepends=True)
- changed = False
- for i, line in enumerate(lines):
- if line.startswith("# 9020 castopod"):
- new_line = upd_hdr(line)
- if new_line != line:
- lines[i] = new_line if new_line.endswith("\n") else new_line + "\n"
- print(f"OK: updated ports comment (+{token})")
- changed = True
- else:
- print(f"OK: ports comment already has {token}")
- break
- if changed:
- text = "".join(lines)
-
-if text != orig:
- if dry:
- print(f"DRY: would write {caddy} ({len(text) - len(orig):+d} bytes)")
- else:
- caddy.write_text(text)
- print(f"wrote {caddy}")
-else:
- print("no Caddyfile change")
-PY
-
-if [[ "$DRY" -eq 1 ]]; then
- echo "dry-run done (no validate/reload)"
- exit 0
-fi
-
-if command -v caddy >/dev/null 2>&1; then
- echo "== validate =="
- caddy validate --config "$CADDY"
-else
- echo "WARN: caddy binary missing — skip validate" >&2
-fi
-
-if [[ "$NO_RELOAD" -eq 1 ]]; then
- echo "skip reload (--no-reload)"
- exit 0
-fi
-
-if [[ "$CADDY" == /etc/caddy/Caddyfile ]] && command -v systemctl >/dev/null 2>&1; then
- echo "== reload =="
- if systemctl is-active --quiet caddy; then
- systemctl reload caddy
- else
- echo "WARN: caddy unit not active — start it yourself" >&2
- fi
-else
- echo "skip reload (not live path or no systemctl)"
-fi
diff --git a/scripts/caddy/merge-notes-site.sh b/scripts/caddy/merge-notes-site.sh
deleted file mode 100755
index 402d644..0000000
--- a/scripts/caddy/merge-notes-site.sh
+++ /dev/null
@@ -1,133 +0,0 @@
-#!/usr/bin/env bash
-# Merge silverbullet.hacktivism.ch into live /etc/caddy/Caddyfile + ACME http list.
-# Run on koopa as root AFTER podman is up on :9028:
-# sudo bash /home/hernani/koopa-admin-log/scripts/caddy/merge-notes-site.sh
-# Then:
-# sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh
-set -euo pipefail
-
-if [[ "$(id -u)" -ne 0 ]]; then
- echo "ERROR: run as root: sudo bash $0" >&2
- exit 1
-fi
-
-CADDY=/etc/caddy/Caddyfile
-SNIPPET=/home/hernani/koopa-admin-log/configs/caddy/silverbullet.hacktivism.ch.caddy
-SITE=silverbullet.hacktivism.ch
-# Prior hostnames to rename into SITE (order: newest leftovers first)
-OLD_SITES="freenotes.hacktivism.ch notes.hacktivism.ch"
-
-[[ -f "$CADDY" ]] || { echo "ERROR: missing $CADDY" >&2; exit 1; }
-[[ -f "$SNIPPET" ]] || { echo "ERROR: missing $SNIPPET" >&2; exit 1; }
-command -v caddy >/dev/null || { echo "ERROR: caddy not in PATH" >&2; exit 1; }
-
-ts=$(date +%Y%m%d-%H%M%S)
-cp -a "$CADDY" "${CADDY}.bak-silverbullet-merge-${ts}"
-echo "backup ${CADDY}.bak-silverbullet-merge-${ts}"
-
-python3 - "$CADDY" "$SNIPPET" "$SITE" $OLD_SITES <<'PY'
-import sys
-from pathlib import Path
-
-caddy = Path(sys.argv[1])
-snippet_path = Path(sys.argv[2])
-site = sys.argv[3]
-old_sites = sys.argv[4:]
-text = caddy.read_text()
-orig = text
-snip = snippet_path.read_text().rstrip() + "\n\n"
-
-def drop_site_block(text: str, old_site: str) -> str:
- marker_old = f"{old_site} {{"
- i = text.find(marker_old)
- if i < 0:
- return text
- j = text.find("\n}", i)
- if j < 0:
- return text
- end = j + 2
- while end < len(text) and text[end] == "\n":
- end += 1
- text = text[:i] + text[end:]
- text = text.replace(f", http://{old_site}", "").replace(f"http://{old_site}, ", "")
- text = text.replace(f"http://{old_site}", "")
- print(f"OK: removed duplicate old block {old_site}")
- return text
-
-# Rename leftover freenotes/notes.hacktivism.ch → silverbullet.hacktivism.ch
-for old_site in old_sites:
- if old_site in text and site not in text:
- text = text.replace(old_site, site)
- text = text.replace("9028 freenotes", "9028 silverbullet")
- text = text.replace("9028 notes", "9028 silverbullet")
- print(f"OK: renamed {old_site} → {site}")
- elif old_site in text and site in text:
- text = drop_site_block(text, old_site)
-
-marker = f"{site} {{"
-if marker in text:
- print(f"OK: site block already present: {site}")
-else:
- # Insert after decidim block if present, else before ACME http list
- insert_at = None
- dec = text.find("decidim.hacktivism.ch {")
- if dec >= 0:
- j = text.find("\n}", dec)
- if j >= 0:
- insert_at = j + 2 # after closing brace newline
- while insert_at < len(text) and text[insert_at] == "\n":
- insert_at += 1
- if insert_at is None:
- acme = text.find("http://taler.hacktivism.ch")
- insert_at = acme if acme >= 0 else len(text)
- text = text[:insert_at] + snip + text[insert_at:]
- print(f"OK: inserted site block {site}")
-
-http_tok = f"http://{site}"
-if http_tok in text:
- print(f"OK: ACME http list already has {http_tok}")
-else:
- old = "http://decidim.hacktivism.ch {"
- new = f"http://decidim.hacktivism.ch, {http_tok} {{"
- if old not in text:
- # maybe already on a long list without trailing brace-only form
- raise SystemExit("ERROR: ACME http list marker not found (decidim)")
- text = text.replace(old, new, 1)
- print(f"OK: added {http_tok} to ACME http list")
-
-# Comment header line near top ports comment
-for old_hdr, new_hdr in (
- (
- "# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9200 forgejo-ssh",
- "# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9028 silverbullet | 9200 forgejo-ssh",
- ),
- (
- "# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9028 notes | 9200 forgejo-ssh",
- "# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9028 silverbullet | 9200 forgejo-ssh",
- ),
-):
- if old_hdr in text:
- text = text.replace(old_hdr, new_hdr, 1)
- print("OK: updated ports comment header")
- break
-
-if text != orig:
- caddy.write_text(text)
- print(f"wrote {caddy}")
-else:
- print("no Caddyfile change")
-PY
-
-echo "== validate =="
-caddy validate --config "$CADDY"
-
-echo "== reload =="
-if systemctl is-active --quiet caddy; then
- systemctl reload caddy
-else
- echo "WARN: caddy unit not active — start it yourself" >&2
-fi
-
-echo
-echo "Next: sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh"
-echo "DNS/VeciGate for ${SITE} still separate."
diff --git a/scripts/caddy/wdGet-bank-snippet.caddy b/scripts/caddy/wdGet-bank-snippet.caddy
deleted file mode 100644
index 93a1635..0000000
--- a/scripts/caddy/wdGet-bank-snippet.caddy
+++ /dev/null
@@ -1,18 +0,0 @@
-# Paste into live ~/koopa-caddy/Caddyfile inside bank.hacktivism.ch { ... }
-# BEFORE the catch-all: reverse_proxy 127.0.0.1:9012
-# Then on koopa: sudo ~/bin/caddy-apply
-#
-# FP pattern: francpaysan-admin-log files/caddy/Caddyfile @wdGet
-# GET Integration → landing :9013 → demo-api :19096
-# (injects suggested_exchange / required_exchange for Android/iOS)
-# SoT mirrors: configs/caddy/Caddyfile + host/caddy/Caddyfile
-
- @wdGet {
- method GET
- path /taler-integration/withdrawal-operation/*
- }
- handle @wdGet {
- reverse_proxy 127.0.0.1:9013 {
- import proxy_public
- }
- }
diff --git a/scripts/cryptpad/build.sh b/scripts/cryptpad/build.sh
deleted file mode 100755
index ae8f6dd..0000000
--- a/scripts/cryptpad/build.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-# Build CryptPad image on CachyOS base (run on koopa).
-set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CTX="${ROOT}/configs/cryptpad"
-TAG="${IMAGE_TAG:-localhost/koopa-cryptpad:cachyos}"
-CRYPTPAD_VERSION="${CRYPTPAD_VERSION:-2025.9.0}"
-cd "$CTX"
-[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
-echo "build $TAG (CRYPTPAD_VERSION=$CRYPTPAD_VERSION) ctx=$CTX"
-podman build \
- --build-arg "CRYPTPAD_VERSION=${CRYPTPAD_VERSION}" \
- -t "$TAG" \
- -f Containerfile \
- .
-echo "OK: $TAG"
-podman image inspect "$TAG" --format '{{.Id}} {{.Size}} {{.Architecture}}' 2>/dev/null || true
diff --git a/scripts/decidim/apply-branding.sh b/scripts/decidim/apply-branding.sh
deleted file mode 100755
index 6327fe3..0000000
--- a/scripts/decidim/apply-branding.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-# Apply hacktivism colors + header CSS on the live organization.
-set -euo pipefail
-cd "${HOME}/koopa-decidim"
-set -a
-# shellcheck disable=SC1091
-source .env
-# shellcheck disable=SC1091
-source users.env
-set +a
-podman exec \
- -e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \
- -e DECIDIM_SYSTEM_EMAIL \
- -e DECIDIM_SYSTEM_PASSWORD \
- -e DECIDIM_ADMIN_EMAIL \
- -e DECIDIM_ADMIN_PASSWORD \
- -e DECIDIM_ADMIN_NAME \
- -e DECIDIM_APPLICATION_NAME \
- -e DECIDIM_DEFAULT_LOCALE \
- -e DECIDIM_AVAILABLE_LOCALES \
- koopa-decidim bundle exec rails runner /code/lib/koopa_bootstrap.rb
-echo "branding applied. public: https://decidim.hacktivism.ch/"
diff --git a/scripts/decidim/install-systemd.sh b/scripts/decidim/install-systemd.sh
deleted file mode 100755
index 01934d3..0000000
--- a/scripts/decidim/install-systemd.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/env bash
-# Install Decidim user systemd units on koopa (boot via linger).
-set -euo pipefail
-HERE="$(cd "$(dirname "$0")" && pwd)"
-LIVE="${HOME}/koopa-decidim"
-# admin-log tree (scripts/decidim → ../..) or live ~/koopa-decidim/bin
-if [ -f "${HERE}/../../configs/decidim/container-koopa-decidim.service" ]; then
- UNIT_SRC="${HERE}/../../configs/decidim"
-elif [ -f "${LIVE}/container-koopa-decidim.service" ]; then
- UNIT_SRC="${LIVE}"
-else
- echo "ERROR: decidim unit files not found" >&2
- exit 1
-fi
-UNIT_DIR="${HOME}/.config/systemd/user"
-
-mkdir -p "${UNIT_DIR}"
-cp "${UNIT_SRC}/container-koopa-decidim-db.service" "${UNIT_DIR}/"
-cp "${UNIT_SRC}/container-koopa-decidim.service" "${UNIT_DIR}/"
-
-systemctl --user daemon-reload
-systemctl --user enable \
- container-koopa-decidim-db.service \
- container-koopa-decidim.service
-
-if ! podman ps --format '{{.Names}}' | grep -qx koopa-decidim; then
- (cd "${HOME}/koopa-decidim" && set -a && source .env && set +a && podman-compose up -d)
-fi
-
-systemctl --user start \
- container-koopa-decidim-db.service \
- container-koopa-decidim.service
-
-systemctl --user --no-pager --lines=0 status \
- container-koopa-decidim-db.service \
- container-koopa-decidim.service || true
-
-if command -v loginctl >/dev/null 2>&1; then
- if [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)" != "yes" ]; then
- echo "NOTE: enable linger so units start after reboot without login:"
- echo " sudo loginctl enable-linger $(id -un)"
- else
- echo "linger already enabled for $(id -un)"
- fi
-fi
-
-echo "OK. Public: https://decidim.hacktivism.ch/"
diff --git a/scripts/decidim/seed-example.sh b/scripts/decidim/seed-example.sh
deleted file mode 100755
index 51f1dba..0000000
--- a/scripts/decidim/seed-example.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env bash
-# Publish public example process/meeting + optional isoc user from users.env.
-set -euo pipefail
-cd "${HOME}/koopa-decidim"
-set -a
-# shellcheck disable=SC1091
-source .env
-# shellcheck disable=SC1091
-source users.env
-set +a
-
-src=""
-if [ -f "${HOME}/src/koopa/koopa-admin-log/configs/decidim/seed_example.rb" ]; then
- src="${HOME}/src/koopa/koopa-admin-log/configs/decidim/seed_example.rb"
-elif [ -f ./seed_example.rb ]; then
- src="./seed_example.rb"
-else
- echo "ERROR: seed_example.rb not found" >&2
- exit 1
-fi
-podman cp "${src}" koopa-decidim:/code/lib/koopa_seed_example.rb
-
-avatar_host="${DECIDIM_ISOC_AVATAR:-${HOME}/koopa-decidim/assets/img/isoc-gold.png}"
-avatar_ctr=""
-if [ -f "${avatar_host}" ]; then
- avatar_ctr="/tmp/isoc-gold.png"
- podman cp "${avatar_host}" "koopa-decidim:${avatar_ctr}"
-fi
-
-podman exec \
- -e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \
- -e DECIDIM_ADMIN_EMAIL \
- -e DECIDIM_ISOC_EMAIL \
- -e DECIDIM_ISOC_PASSWORD \
- -e DECIDIM_ISOC_NAME \
- -e DECIDIM_ISOC_NICKNAME \
- -e DECIDIM_ISOC_AVATAR="${avatar_ctr}" \
- koopa-decidim bundle exec rails runner /code/lib/koopa_seed_example.rb
-echo "seed done. public: https://decidim.hacktivism.ch/processes/example"
diff --git a/scripts/decidim/up.sh b/scripts/decidim/up.sh
deleted file mode 100755
index 21a6a9d..0000000
--- a/scripts/decidim/up.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-DIR="${HOME}/koopa-decidim"
-cd "${DIR}"
-set -a
-# shellcheck disable=SC1091
-source .env
-set +a
-podman-compose up -d
-echo "== wait for :9027 =="
-ok=0
-for i in $(seq 1 80); do
- code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:9027/" || true)
- echo "try $i: $code"
- case "$code" in
- 2*|3*) ok=1; break ;;
- esac
- sleep 5
-done
-[[ "$ok" -eq 1 ]] || { echo "ERROR: no answer on 9027"; podman-compose ps; exit 1; }
-echo "local ok. public: https://decidim.hacktivism.ch/"
diff --git a/scripts/dossiers-ngi/README.md b/scripts/dossiers-ngi/README.md
deleted file mode 100644
index 6b9b65c..0000000
--- a/scripts/dossiers-ngi/README.md
+++ /dev/null
@@ -1,165 +0,0 @@
-# dossiers-ngi — dynamic edition bar + 10‑min rebuild
-
-SoT for the sticky edition bar on **dossiers.ngi.hacktivism.ch**. Live freezes
-sit under `editions//` on koopa; cookie `tdpd_edition` selects the build.
-The bar renders a **semi-graphic parent→children tree** from
-`/branch-status.json` (all `origin/*` tips, not a hard-coded ladder).
-
-| Path | Role |
-|------|------|
-| `sites/…/_cascade/` | Sticky bar shell + JS tree renderer |
-| `editions/` on koopa www | Full freezes switched by the bar |
-| `sites/…/cascade/` | Legacy tip stubs (optional) |
-| `scripts/dossiers-ngi/branch-status-hourly.py` | Fetch remotes → tree JSON → on-demand rebuild |
-| `scripts/dossiers-ngi/smoke-branch-tree.sh` | Temp-branch smoke (creates/deletes local origin only) |
-| `configs/systemd/user/dossiers-ngi-branch-status.{timer,service}` | Every **10 min** oneshot on koopa (`flock` against overlap) |
-
-## Env
-
-| Var | Default | Meaning |
-|-----|---------|---------|
-| `TDPD_ROOT` | `$HOME/git/src/ngi/tech-dossier-project-data` | tdpd git checkout (may be git-dir only) |
-| `TDPD_VENV` | `(TDPD_ROOT)/.venv` | Frozen-Flask venv; symlinked into worktrees |
-| `OUT` | kal `sites/…/branch-status.json` | write path (live www on koopa) |
-| `EDITIONS_ROOT` | (none) | `…/editions` — required for `--rebuild` |
-| `REBUILD` / `--rebuild` | off | rebuild when `.edition-tip` ≠ tip |
-| `FORCE_REBUILD` / `--force` | off | rebuild every branch (timing) |
-| `FETCH` / `--no-fetch` | fetch on | `git fetch --prune origin` |
-| `FREEZE_CMD` | (make/venv freeze) | stub for smoke tests; `{root}` `{branch}` `{slug}` |
-| `DEFAULT_BRANCH` | `feat/website-views` | behind cookie `flask` |
-| `HOST` | `https://dossiers.ngi.hacktivism.ch` | public host in JSON |
-
-On koopa: `%h/src/koopa/koopa-admin-log` +
-`~/.config/dossiers-ngi/branch-status.env` for path overrides.
-NGI clone token lives in **koopa-admin-secrets-enc** (not this tree).
-
-## Manual run
-
-```bash
-# JSON only
-python3 scripts/dossiers-ngi/branch-status-hourly.py
-
-# rebuild stale freezes + timing block in JSON
-EDITIONS_ROOT=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/editions \
- python3 scripts/dossiers-ngi/branch-status-hourly.py --rebuild
-
-# force all branches (wall clock → rebuild_timing)
-… --rebuild --force
-```
-
-`rebuild_timing` fields: `wall_seconds`, `built`, `skipped`, `errors`,
-`built_seconds_sum`, `built_seconds_max`. Per-branch `rebuild[name].seconds`.
-
-Per branch in JSON / sticky bar:
-
-| Field | Meaning |
-|-------|---------|
-| `tip` | short SHA |
-| `tip_time` | committer time of tip (`%cI`) |
-| `built_at` | when this edition freeze was written |
-| `build_seconds` | wall time of that freeze |
-| `freeze_ok` | `true` when the current origin tip freeze succeeded (sticky bar green ✓); `false` when it failed, is a placeholder, or the recorded tip is stale (red ✗) |
-| `freeze_error` | last freeze error text (only when not ok) |
-| `vs_parent_plus` / `vs_parent_minus` | `git diff --numstat` tip vs **parent** tip (sticky bar +grün/−rot; omitted when `parent` is empty or not in the tree, e.g. `main` and `feature/nl-net-injestion-injestor-update`) |
-| `loc` | text-file line count of the tip vs the empty tree (gray `N LOC` in the plus/minus columns; only when there is no parent) |
-
-Rebuild runs **only when** the current origin tip is not already a successful freeze (`freeze_ok`; timer never forces). Cookie alias `editions/flask` copies from `DEFAULT_BRANCH` unless that alias is already `freeze_ok` for the same tip **and** `.edition-built-at` matches the source freeze.
-Each run also: `git fetch --prune`, `worktree prune`, drop stale
-`editions//` (gone remotes), purge leftover `tdpd-*` tmp dirs.
-
-## Smoke
-
-```bash
-bash scripts/dossiers-ngi/smoke-branch-tree.sh
-# → SMOKE_OK (temp origin + smoke/temp-sibling deleted)
-```
-
-## Edition freeze missing the bar (cookie trap)
-
-`feat/website-views` freeze can omit `{% include "_cascade/bar.html" %}`.
-Cookie `tdpd_edition` then has no switcher. **No zypper / Caddy plugin.**
-Way back: `https://dossiers.ngi.hacktivism.ch/_cascade/use/flask`
-or `/_cascade/` (`index.html` switcher).
-
-On koopa (hernani, no root):
-
-```bash
-bash scripts/dossiers-ngi/inject-editions-bar.sh
-```
-
-Hourly rebuild injects on skip **and** freeze-error (keep existing HTML + include).
-
-The bar is `position: fixed` (not sticky); `100vw` overflow is avoided.
-
-## Deploy landing (Freigabe)
-
-Do **not** `--delete` the whole www root. Sync:
-
-```bash
-SITE="$HOME/git/admin-logs/local/koopa-admin-log/sites/dossiers.ngi.hacktivism.ch"
-WWW=hernani@koopa:/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
-rsync -avz --exclude .DS_Store "$SITE/_cascade/" "$WWW/_cascade/"
-rsync -avz --exclude .DS_Store "$SITE/branch-status.json" "$WWW/branch-status.json"
-# editions/ from a --rebuild run
-```
-
-## Sticky cascade bar
-
-`bar.js` fetches `/branch-status.json` and draws:
-
-```
-feature/nl-net-injestion-injestor-update … N LOC
-main … N LOC
-├── feat/sm-content … +N −M # vs main
-│ └── feat/website-features … +N −M # vs sm-content
-└── project_template … +N −M # vs main
-```
-
-Visible cells: freeze mark (green ✓ / red ✗) · name (tree prefix + branch) · tip `monDD HH:MM` · `Ns monDD HH:MM` (build seconds + freeze time) · plus column · minus column. `+N −M` vs the **parent** tip, in two right-aligned columns so `+898` and `+1200` share an edge. Roots with no parent show gray `N` in the plus column and `LOC` in the minus column. No `@sha`, no `default` label (`data-default` stays for flask/cookie). Inner tree `max-width: 45rem` (same as site `#navigation` / `#content` 45em at root 16px), so names line up with About and the minus column with the page edge; below 45rem the tree is full width with a small pad. Rear cells are right-aligned `tabular-nums`. Poll every 30s **updates mark/when/built/plus/minus in place** when the branch structure is unchanged (no full tree remount).
-
-Each name links to `/_cascade/use/` (cookie + 302 `/`). Caddy map:
-
-```
-~^[A-Za-z0-9_-]+$ {http.request.cookie.tdpd_edition}
-```
-
-Also serve `/cascade/*` from the site www root (path stubs). Live Caddy must not use a trailing `~.* flask` catch-all.
-
-Edition purity: responses send `Vary: Cookie` and `Cache-Control: private, no-cache` so `/static/css/style.css` (same path, different bodies per edition) cannot stick the green design CSS onto `main` after a views visit.
-
-so new branches need no Caddy edit — only an `editions//` tree.
-
-```bash
-bash ~/bin/apply-dossiers-ngi-edition-switch.sh
-sudo ~/bin/caddy-apply
-```
-
-Stanza SoT: `scripts/dossiers-ngi/caddy-dossiers-ngi.snippet`.
-
-## HTTP → HTTPS 308
-
-Shared Caddy HTTP list already `redir https://{host}{uri} permanent` (308).
-`dossiers.ngi.hacktivism.ch` was missing there → HTTP `200` empty body /
-Firefox `NS_ERROR_NET_EMPTY_RESPONSE`. HTTPS site body stays untouched.
-
-On koopa (hernani, then root apply):
-
-```bash
-# SoT: scripts/dossiers-ngi/apply-dossiers-ngi-http-308.sh
-cp /path/to/apply-dossiers-ngi-http-308.sh ~/bin/
-chmod +x ~/bin/apply-dossiers-ngi-http-308.sh
-bash ~/bin/apply-dossiers-ngi-http-308.sh
-sudo ~/bin/caddy-apply
-```
-
-Smoke after apply:
-
-```bash
-curl -sSI http://dossiers.ngi.hacktivism.ch/ | grep -Ei 'HTTP|location'
-# 308 Location: https://dossiers.ngi.hacktivism.ch/
-```
-
-`.2` / `.3` stay off this list unless `SITE=` is set.
-
-
-Caddy edition map must list each `editions/` explicitly. A map output of `{http.request.cookie.tdpd_edition}` did not expand on Caddy v2.11.4 (only `flask` worked → 404 for `main`/views). After new tips appear, refresh the snippet map (or regenerate from `ls editions`) before `apply-dossiers-ngi-edition-switch` + `caddy-apply`.
diff --git a/scripts/dossiers-ngi/apply-dossiers-ngi-edition-switch.sh b/scripts/dossiers-ngi/apply-dossiers-ngi-edition-switch.sh
deleted file mode 100755
index ea043c3..0000000
--- a/scripts/dossiers-ngi/apply-dossiers-ngi-edition-switch.sh
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env bash
-# Host-local on koopa. Replaces dossiers.ngi.hacktivism.ch site-block body
-# with cookie→editions sticky-bar switch. Leaves .2/.3 alone.
-# Next: sudo ~/bin/caddy-apply
-set -euo pipefail
-
-CADDY="${CADDY:-$HOME/koopa-caddy/Caddyfile}"
-SITE="dossiers.ngi.hacktivism.ch"
-SNIPPET="${SNIPPET:-$HOME/bin/caddy-dossiers-ngi.snippet}"
-
-if [[ ! -f "$CADDY" ]]; then
- echo "FAIL: missing $CADDY" >&2
- exit 1
-fi
-if [[ ! -f "$SNIPPET" ]]; then
- echo "FAIL: missing snippet $SNIPPET" >&2
- exit 1
-fi
-
-ts="$(date +%Y%m%d-%H%M%S)"
-bak="${CADDY}.bak-dossiers-edition-${ts}"
-cp -a "$CADDY" "$bak"
-echo "bak: $bak"
-
-python3 - "$CADDY" "$SITE" "$SNIPPET" <<'PY'
-import sys
-from pathlib import Path
-
-caddy_path, site, snippet_path = Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3])
-text = caddy_path.read_text()
-raw_snip = snippet_path.read_text().splitlines()
-body_lines = []
-for line in raw_snip:
- if line.startswith("#") or not line.strip():
- continue
- body_lines.append(line if line.startswith("\t") else "\t" + line.lstrip())
-body = "\n".join(body_lines).rstrip() + "\n"
-if "edition_dir" not in body or "tdpd_edition" not in body:
- print("FAIL: snippet body looks empty/wrong", file=sys.stderr)
- sys.exit(1)
-
-needle = site + " {"
-start = text.find(needle)
-if start < 0:
- # allow no space
- needle = site + "{"
- start = text.find(needle)
-if start < 0:
- print(f"FAIL: site block not found: {site}", file=sys.stderr)
- sys.exit(1)
-
-brace_open = text.find("{", start)
-if brace_open < 0:
- print("FAIL: no opening brace", file=sys.stderr)
- sys.exit(1)
-
-depth = 0
-end = None
-for i, ch in enumerate(text[brace_open:], start=brace_open):
- if ch == "{":
- depth += 1
- elif ch == "}":
- depth -= 1
- if depth == 0:
- end = i
- break
-if end is None:
- print("FAIL: unclosed site block", file=sys.stderr)
- sys.exit(1)
-
-# keep optional newline after closing brace
-close_end = end + 1
-if close_end < len(text) and text[close_end] == "\n":
- close_end += 1
-
-new_text = text[: brace_open + 1] + "\n" + body + "}" + ("\n" if text[end+1:end+2] == "\n" or True else "")
-# normalize: site {\n BODY }\n
-new_text = text[: brace_open + 1] + "\n" + body + "}\n" + text[close_end:]
-caddy_path.write_text(new_text)
-print(f"OK: replaced body of {site} ({len(body_lines)} lines)")
-print("Next: sudo ~/bin/caddy-apply")
-PY
diff --git a/scripts/dossiers-ngi/branch-status-hourly.py b/scripts/dossiers-ngi/branch-status-hourly.py
deleted file mode 100755
index 8cf6433..0000000
--- a/scripts/dossiers-ngi/branch-status-hourly.py
+++ /dev/null
@@ -1,764 +0,0 @@
-#!/usr/bin/env python3
-"""Discover all origin branches, emit branch-status.json, optional edition rebuild.
-
-Dynamic cascade for dossiers.ngi.hacktivism.ch:
-
- • every origin/* tip (not a hard-coded ladder)
- • parent = nearest other tip that is a git ancestor
- • sticky bar reads this JSON and draws a semi-graphic tree
- • optional --rebuild / REBUILD=1: worktree + make html into editions//
- only when .edition-tip differs (on demand; timer ~10min)
-
-Env:
-
- TDPD_ROOT tech-dossier-project-data checkout (may be git-dir only)
- OUT branch-status.json path (live www on koopa)
- EDITIONS_ROOT editions/ dir on www (required for rebuild)
- REBUILD 1 → rebuild stale/missing freezes
- FETCH 0 → skip git fetch (default 1)
- FREEZE_CMD override build (default: make html); `{root}` `{branch}` `{slug}`
- DEFAULT_BRANCH branch behind cookie flask (default feat/website-views)
- HOST public host URL
-"""
-from __future__ import annotations
-
-import argparse
-import json
-import os
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
-import time
-from datetime import datetime, timezone
-from pathlib import Path
-
-ADMIN_LOG = Path(__file__).resolve().parents[2]
-_DEFAULT_OUT = ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "branch-status.json"
-_PLACEHOLDER = (
- ADMIN_LOG / "sites" / "dossiers.ngi.hacktivism.ch" / "_edition-placeholder" / "index.html"
-)
-HOST_DEFAULT = "https://dossiers.ngi.hacktivism.ch"
-DEFAULT_TDPD = Path.home() / "git" / "src" / "ngi" / "tech-dossier-project-data"
-INJECT = Path(__file__).resolve().parent / "inject-cascade-bar.py"
-SLUG_RE = re.compile(r"[^A-Za-z0-9_-]+")
-
-
-def env_path(name: str, default: Path | None = None) -> Path | None:
- raw = os.environ.get(name, "").strip()
- if raw:
- return Path(raw).expanduser()
- return default
-
-
-def out_path() -> Path:
- return env_path("OUT", _DEFAULT_OUT) # type: ignore[return-value]
-
-
-def tdpd_root() -> Path:
- return env_path("TDPD_ROOT", DEFAULT_TDPD) # type: ignore[return-value]
-
-
-def editions_root() -> Path | None:
- return env_path("EDITIONS_ROOT", None)
-
-
-def host() -> str:
- return os.environ.get("HOST", "").strip() or HOST_DEFAULT
-
-
-def default_branch() -> str:
- return os.environ.get("DEFAULT_BRANCH", "").strip() or "feat/website-views"
-
-
-def edition_slug(branch: str) -> str:
- return SLUG_RE.sub("-", branch.replace("/", "-")).strip("-") or "branch"
-
-
-def git(repo: Path, *args: str, check: bool = True, text: bool = True) -> subprocess.CompletedProcess:
- return subprocess.run(
- ["git", "-C", str(repo), *args],
- check=check,
- text=text,
- capture_output=True,
- )
-
-
-def fetch_origin(repo: Path) -> None:
- if os.environ.get("FETCH", "1").strip() == "0":
- return
- subprocess.run(
- ["git", "-C", str(repo), "fetch", "--prune", "origin"],
- check=False,
- capture_output=True,
- text=True,
- )
-
-
-def list_origin_branches(repo: Path) -> dict[str, str]:
- """branch name → full SHA for refs/remotes/origin/* (no HEAD)."""
- cp = git(repo, "for-each-ref", "--format=%(refname:short)|%(objectname)", "refs/remotes/origin")
- out: dict[str, str] = {}
- for line in cp.stdout.splitlines():
- if "|" not in line:
- continue
- short, sha = line.split("|", 1)
- if short in ("origin", "origin/HEAD") or "->" in short:
- continue
- if not short.startswith("origin/"):
- continue
- name = short[len("origin/") :]
- if name == "HEAD":
- continue
- out[name] = sha
- return out
-
-
-def is_ancestor(repo: Path, anc: str, desc: str) -> bool:
- cp = git(repo, "merge-base", "--is-ancestor", anc, desc, check=False)
- return cp.returncode == 0
-
-
-def commits_between(repo: Path, a: str, b: str) -> int:
- cp = git(repo, "rev-list", "--count", f"{a}..{b}", check=False)
- if cp.returncode != 0:
- return 10**9
- try:
- return int(cp.stdout.strip() or "0")
- except ValueError:
- return 10**9
-
-
-def short_sha(sha: str) -> str:
- return sha[:7] if sha else ""
-
-
-def nearest_parents(repo: Path, tips: dict[str, str]) -> dict[str, str]:
- """For each branch, nearest other tip that is a strict ancestor."""
- parents: dict[str, str] = {}
- names = list(tips)
- for b in names:
- tip_b = tips[b]
- cands: list[tuple[int, str]] = []
- for a in names:
- if a == b:
- continue
- tip_a = tips[a]
- if tip_a == tip_b:
- continue
- if not is_ancestor(repo, tip_a, tip_b):
- continue
- cands.append((commits_between(repo, tip_a, tip_b), a))
- parents[b] = min(cands)[1] if cands else ""
- return parents
-
-
-def children_map(parents: dict[str, str]) -> dict[str, list[str]]:
- kids: dict[str, list[str]] = {b: [] for b in parents}
- orphans: list[str] = []
- for b, p in parents.items():
- if p and p in kids:
- kids[p].append(b)
- else:
- orphans.append(b)
- for p in kids:
- kids[p].sort()
- orphans.sort()
- # roots = orphans (parent missing or empty)
- kids[""] = orphans
- return kids
-
-
-def walk_tree(
- parents: dict[str, str],
- tips: dict[str, str],
-) -> list[dict]:
- """Flat rows in DFS order with semi-graphic prefix fields."""
- kids = children_map(parents)
- rows: list[dict] = []
-
- def walk(name: str, prefix: str, is_last: bool, is_root: bool) -> None:
- tip = tips[name]
- if is_root:
- branch_prefix = ""
- child_prefix = ""
- else:
- branch_prefix = prefix + ("└── " if is_last else "├── ")
- child_prefix = prefix + (" " if is_last else "│ ")
- rows.append(
- {
- "branch": name,
- "edition": edition_slug(name),
- "tip": short_sha(tip),
- "tip_full": tip,
- "parent": parents.get(name, ""),
- "prefix": branch_prefix,
- "url": f"/_cascade/use/{edition_slug(name)}",
- }
- )
- ch = kids.get(name, [])
- for i, c in enumerate(ch):
- walk(c, child_prefix, i == len(ch) - 1, False)
-
- roots = kids.get("", [])
- for i, r in enumerate(roots):
- walk(r, "", i == len(roots) - 1, True)
- return rows
-
-
-def ensure_placeholder(editions: Path, slug: str, tip: str) -> None:
- dest = editions / slug
- dest.mkdir(parents=True, exist_ok=True)
- idx = dest / "index.html"
- if not idx.exists():
- src = _PLACEHOLDER
- if src.is_file():
- idx.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
- else:
- idx.write_text(
- ""
- '{% include "_cascade/bar.html" . %}'
- "edition pending \n",
- encoding="utf-8",
- )
- tip_path = dest / ".edition-tip"
- if not tip_path.exists():
- tip_path.write_text(tip + "\n", encoding="utf-8")
-
-
-def tip_recorded(editions: Path, slug: str) -> str:
- p = editions / slug / ".edition-tip"
- if not p.is_file():
- return ""
- return p.read_text(encoding="utf-8").strip()
-
-
-
-def diff_numstat(repo: Path, a: str, b: str) -> tuple[int, int]:
- """Insertions/deletions of b vs a (git diff --numstat)."""
- if not a or not b or a == b:
- return 0, 0
- cp = git(repo, "diff", "--numstat", a, b, check=False)
- if cp.returncode != 0:
- return 0, 0
- plus = minus = 0
- for line in (cp.stdout or "").splitlines():
- parts = line.split(" ")
- if len(parts) < 3:
- continue
- a, d = parts[0], parts[1]
- if a == "-" or d == "-":
- continue
- try:
- plus += int(a)
- minus += int(d)
- except ValueError:
- continue
- return plus, minus
-
-
-def empty_tree_oid(repo: Path) -> str:
- cp = git(repo, "hash-object", "-t", "tree", "/dev/null")
- return (cp.stdout or "").strip()
-
-
-def loc_at_tip(repo: Path, tip: str, empty: str) -> int:
- """Text-file line count of tip vs the empty tree."""
- plus, _ = diff_numstat(repo, empty, tip)
- return plus
-
-
-def tip_commit_time(repo: Path, tip: str) -> str:
- """Committer ISO time of tip (empty on failure)."""
- cp = git(repo, "log", "-1", "--format=%cI", tip, check=False)
- if cp.returncode != 0:
- return ""
- return (cp.stdout or "").strip()
-
-
-def read_build_meta(editions: Path, slug: str) -> dict:
- dest = editions / slug
- out: dict = {}
- at = dest / ".edition-built-at"
- sec = dest / ".edition-build-seconds"
- if at.is_file():
- out["built_at"] = at.read_text(encoding="utf-8").strip()
- if sec.is_file():
- raw = sec.read_text(encoding="utf-8").strip()
- try:
- out["build_seconds"] = float(raw)
- except ValueError:
- pass
- return out
-
-
-def write_build_meta(editions: Path, slug: str, seconds: float) -> None:
- dest = editions / slug
- dest.mkdir(parents=True, exist_ok=True)
- now = datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
- (dest / ".edition-built-at").write_text(now + "\n", encoding="utf-8")
- (dest / ".edition-build-seconds").write_text(f"{seconds:.3f}\n", encoding="utf-8")
-
-
-def write_freeze_status(editions: Path, slug: str, ok: bool, error: str = "") -> None:
- dest = editions / slug
- dest.mkdir(parents=True, exist_ok=True)
- (dest / ".edition-freeze-ok").write_text("1\n" if ok else "0\n", encoding="utf-8")
- errp = dest / ".edition-freeze-error"
- if ok or not error:
- if errp.exists():
- errp.unlink()
- return
- errp.write_text(error[:500] + "\n", encoding="utf-8")
-
-
-def freeze_ok_for(editions: Path | None, slug: str, tip_full: str) -> tuple[bool, str]:
- """Current origin tip freeze succeeded (green ✓) or not (red ✗)."""
- if editions is None or not tip_full:
- return False, ""
- dest = editions / slug
- err = ""
- errp = dest / ".edition-freeze-error"
- if errp.is_file():
- err = errp.read_text(encoding="utf-8").strip()
- recorded = tip_recorded(editions, slug)
- okp = dest / ".edition-freeze-ok"
- if okp.is_file():
- file_ok = okp.read_text(encoding="utf-8").strip().lower() in (
- "1",
- "true",
- "ok",
- "yes",
- )
- else:
- file_ok = (dest / ".edition-built-at").is_file()
- ok = bool(file_ok and recorded == tip_full and (dest / "index.html").is_file())
- return ok, err
-
-
-def cleanup_stale_editions(editions: Path, tips: dict[str, str]) -> list[str]:
- """Drop edition dirs whose branch tip is gone (keep flask alias)."""
- keep = {edition_slug(b) for b in tips} | {"flask"}
- removed: list[str] = []
- if not editions.is_dir():
- return removed
- for child in list(editions.iterdir()):
- if not child.is_dir():
- continue
- if child.name in keep:
- continue
- shutil.rmtree(child, ignore_errors=True)
- removed.append(child.name)
- return removed
-
-
-def cleanup_orphan_tmps(max_age_sec: int = 3600) -> int:
- """Remove leftover tdpd-* workdirs older than max_age_sec."""
- bases: list[Path] = []
- home_tmp = Path.home() / "tmp"
- if home_tmp.is_dir():
- bases.append(home_tmp)
- bases.append(Path(tempfile.gettempdir()))
- cutoff = time.time() - max_age_sec
- n = 0
- for base in bases:
- try:
- kids = list(base.glob("tdpd-*"))
- except OSError:
- continue
- for path in kids:
- if not path.is_dir():
- continue
- try:
- if path.stat().st_mtime >= cutoff:
- continue
- except OSError:
- continue
- shutil.rmtree(path, ignore_errors=True)
- n += 1
- return n
-
-
-def cleanup_repo(repo: Path) -> None:
- git(repo, "worktree", "prune", check=False)
-
-
-def _attach_venv(work: Path) -> None:
- """Worktrees lack .venv; reuse TDPD_VENV or sibling checkout .venv via symlink."""
- if (work / ".venv" / "bin" / "python").exists():
- return
- candidates: list[Path] = []
- raw = os.environ.get("TDPD_VENV", "").strip()
- if raw:
- candidates.append(Path(raw).expanduser())
- root = tdpd_root()
- candidates.append(root / ".venv")
- # laptop SoT checkout even when TDPD_ROOT is git-dir-only on koopa
- candidates.append(DEFAULT_TDPD / ".venv")
- for venv in candidates:
- py = venv / "bin" / "python"
- if py.is_file():
- dest = work / ".venv"
- if dest.exists() or dest.is_symlink():
- return
- dest.symlink_to(venv.resolve())
- return
-
-
-def run_freeze(work: Path, branch: str, slug: str) -> None:
- tmpl = os.environ.get("FREEZE_CMD", "").strip()
- env = os.environ.copy()
- env["TDPD_BRANCH"] = branch
- env["TDPD_SLUG"] = slug
- if tmpl:
- cmd = tmpl.format(root=str(work), branch=branch, slug=slug)
- subprocess.run(cmd, shell=True, check=True, cwd=str(work), env=env)
- return
- _attach_venv(work)
- # Older branches hardcode `python3 -m website.freeze` without .venv;
- # prefer the attached venv interpreter directly.
- py = work / ".venv" / "bin" / "python"
- freeze_mod = work / "website" / "freeze.py"
- if py.is_file() and freeze_mod.is_file():
- build = work / "website" / "build"
- if build.exists():
- shutil.rmtree(build)
- subprocess.run(
- [str(py), "-m", "website.freeze"],
- check=True,
- cwd=str(work),
- env=env,
- )
- return
- makefile = work / "Makefile"
- if not makefile.is_file():
- raise FileNotFoundError(f"no Makefile in {work}")
- subprocess.run(["make", "html"], check=True, cwd=str(work), env=env)
-
-
-def inject_bar(edition_dir: Path) -> None:
- if not INJECT.is_file():
- print(f"WARN: inject script missing: {INJECT}", file=sys.stderr)
- return
- cp = subprocess.run(
- [sys.executable, str(INJECT), str(edition_dir)],
- check=False,
- capture_output=True,
- text=True,
- )
- if cp.stdout:
- print(cp.stdout, end="" if str(cp.stdout).endswith("\n") else "\n", file=sys.stderr)
- if cp.returncode != 0:
- err = (cp.stderr or "").strip()[:300]
- print(f"WARN: inject_bar {edition_dir}: {err}", file=sys.stderr)
-
-
-def rebuild_edition(repo: Path, branch: str, tip: str, editions: Path) -> dict:
- """Return {status, seconds} — status skipped|built|placeholder|error:…"""
- slug = edition_slug(branch)
- dest = editions / slug
- t0 = time.perf_counter()
- force = os.environ.get("FORCE_REBUILD", "").strip() in ("1", "true", "yes")
- if not force and freeze_ok_for(editions, slug, tip)[0]:
- inject_bar(dest)
- if not (dest / ".edition-freeze-ok").is_file():
- write_freeze_status(editions, slug, True)
- return {"status": "skipped", "seconds": round(time.perf_counter() - t0, 3)}
-
- wt: Path | None = None
- try:
- tmp_base = Path.home() / "tmp" if (Path.home() / "tmp").is_dir() else Path(tempfile.gettempdir())
- wt = Path(tempfile.mkdtemp(prefix=f"tdpd-{slug}-", dir=str(tmp_base)))
- cp = git(repo, "worktree", "add", "--detach", str(wt), tip, check=False)
- if cp.returncode != 0:
- git(repo, "worktree", "prune", check=False)
- cp = git(repo, "worktree", "add", "--detach", str(wt), tip, check=False)
- if cp.returncode != 0:
- st = f"error:worktree:{cp.stderr.strip()[:200]}"
- ensure_placeholder(editions, slug, tip)
- write_freeze_status(editions, slug, False, st)
- inject_bar(dest)
- return {
- "status": st,
- "seconds": round(time.perf_counter() - t0, 3),
- }
-
- run_freeze(wt, branch, slug)
- build = wt / "website" / "build"
- if not build.is_dir():
- ensure_placeholder(editions, slug, tip)
- write_freeze_status(editions, slug, False, "error:no-build")
- inject_bar(dest)
- return {"status": "error:no-build", "seconds": round(time.perf_counter() - t0, 3)}
-
- dest.mkdir(parents=True, exist_ok=True)
- keep_meta = {
- ".edition-tip",
- ".edition-built-at",
- ".edition-build-seconds",
- ".edition-freeze-ok",
- ".edition-freeze-error",
- }
- for child in list(dest.iterdir()):
- if child.name in keep_meta:
- continue
- if child.is_dir():
- shutil.rmtree(child)
- else:
- child.unlink()
- for child in build.iterdir():
- target = dest / child.name
- if child.is_dir():
- shutil.copytree(child, target)
- else:
- shutil.copy2(child, target)
- secs = round(time.perf_counter() - t0, 3)
- (dest / ".edition-tip").write_text(tip + "\n", encoding="utf-8")
- write_build_meta(editions, slug, secs)
- write_freeze_status(editions, slug, True)
- inject_bar(dest)
- return {"status": "built", "seconds": secs}
- except Exception as exc: # noqa: BLE001 — hourly must continue
- st = f"error:{type(exc).__name__}:{exc}"
- ensure_placeholder(editions, slug, tip)
- write_freeze_status(editions, slug, False, st)
- inject_bar(dest)
- return {
- "status": st,
- "seconds": round(time.perf_counter() - t0, 3),
- }
- finally:
- if wt is not None:
- git(repo, "worktree", "remove", "--force", str(wt), check=False)
- shutil.rmtree(wt, ignore_errors=True)
-
-
-def _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
- 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:
- return
- if not (src / "index.html").is_file():
- ensure_placeholder(editions, "flask", tip)
- return
- if dst.exists():
- shutil.rmtree(dst)
- shutil.copytree(src, dst)
- (dst / ".edition-tip").write_text(tip + "\n", encoding="utf-8")
- # reuse src build meta if present
- for name in (
- ".edition-built-at",
- ".edition-build-seconds",
- ".edition-freeze-ok",
- ".edition-freeze-error",
- ):
- s = src / name
- if s.is_file():
- shutil.copy2(s, dst / name)
- else:
- p = dst / name
- if p.exists():
- p.unlink()
- inject_bar(dst)
-
-
-def build_payload(repo: Path, tips: dict[str, str], rebuild: bool) -> dict:
- parents = nearest_parents(repo, tips)
- rows = walk_tree(parents, tips)
- rebuild_status: dict[str, dict] = {}
- rebuild_timing: dict | None = None
- ed_root = editions_root()
- cleanup_repo(repo)
- n_tmp = cleanup_orphan_tmps()
- if n_tmp:
- print(f"cleanup_tmp removed={n_tmp}", file=sys.stderr)
- if ed_root is not None:
- ed_root.mkdir(parents=True, exist_ok=True)
- removed = cleanup_stale_editions(ed_root, tips)
- if removed:
- print(f"cleanup_editions removed={removed}", file=sys.stderr)
- if rebuild and ed_root is not None:
- wall0 = time.perf_counter()
- for name, tip in sorted(tips.items()):
- # rebuild_edition itself skips when .edition-tip matches tip
- rebuild_status[name] = rebuild_edition(repo, name, tip, ed_root)
- sync_flask_alias(ed_root, tips)
- cleanup_repo(repo)
- cleanup_orphan_tmps(max_age_sec=600)
- wall = round(time.perf_counter() - wall0, 3)
- built_secs = [
- v["seconds"]
- for v in rebuild_status.values()
- if v.get("status") == "built"
- ]
- rebuild_timing = {
- "wall_seconds": wall,
- "branches": len(tips),
- "built": sum(1 for v in rebuild_status.values() if v.get("status") == "built"),
- "skipped": sum(1 for v in rebuild_status.values() if v.get("status") == "skipped"),
- "errors": sum(
- 1 for v in rebuild_status.values() if str(v.get("status", "")).startswith("error")
- ),
- "built_seconds_sum": round(sum(built_secs), 3) if built_secs else 0.0,
- "built_seconds_max": round(max(built_secs), 3) if built_secs else 0.0,
- }
- print(
- f"REBUILD_TIMING wall_seconds={wall} built={rebuild_timing['built']} "
- f"skipped={rebuild_timing['skipped']} errors={rebuild_timing['errors']} "
- f"built_seconds_sum={rebuild_timing['built_seconds_sum']} "
- f"built_seconds_max={rebuild_timing['built_seconds_max']}",
- file=sys.stderr,
- )
- elif ed_root is not None:
- ed_root.mkdir(parents=True, exist_ok=True)
- for name, tip in tips.items():
- slug = edition_slug(name)
- if not (ed_root / slug / "index.html").is_file():
- ensure_placeholder(ed_root, slug, tip)
- write_freeze_status(ed_root, slug, False, "placeholder")
- rebuild_status[name] = {"status": "placeholder", "seconds": 0.0}
- if not (ed_root / "flask" / "index.html").is_file():
- sync_flask_alias(ed_root, tips)
-
- tip_times = {name: tip_commit_time(repo, tip) for name, tip in tips.items()}
- empty = empty_tree_oid(repo)
- branches_out = []
- for row in rows:
- name = row["branch"]
- slug = row["edition"]
- meta = read_build_meta(ed_root, slug) if ed_root is not None else {}
- # prefer this-run built seconds when present
- rb = rebuild_status.get(name) or {}
- if rb.get("status") == "built" and "seconds" in rb:
- meta = {
- **meta,
- "build_seconds": rb["seconds"],
- }
- entry = {
- "branch": name,
- "edition": slug,
- "tip": row["tip"],
- "tip_time": tip_times.get(name, ""),
- "parent": row["parent"],
- "prefix": row["prefix"],
- "url": row["url"],
- }
- if meta.get("built_at"):
- entry["built_at"] = meta["built_at"]
- if "build_seconds" in meta:
- entry["build_seconds"] = meta["build_seconds"]
- freeze_ok, freeze_err = freeze_ok_for(ed_root, slug, tips[name])
- st = str(rb.get("status") or "")
- if st == "built":
- freeze_ok, freeze_err = True, ""
- elif st.startswith("error") or st == "placeholder":
- freeze_ok, freeze_err = False, st
- entry["freeze_ok"] = freeze_ok
- if freeze_err:
- entry["freeze_error"] = freeze_err
- # sticky bar: +grün/−rot vs parent tip; gray loc when no parent in tree
- parent = row["parent"]
- if parent and parent in tips:
- plus, minus = diff_numstat(repo, tips[parent], row["tip"])
- entry["vs_parent_plus"] = plus
- entry["vs_parent_minus"] = minus
- else:
- entry["loc"] = loc_at_tip(repo, row["tip"], empty)
- branches_out.append(entry)
-
- payload = {
- "generated": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
- "host": host(),
- "default_branch": default_branch(),
- "default_edition": "flask",
- "branches": branches_out,
- }
- if rebuild_status:
- payload["rebuild"] = rebuild_status
- if rebuild_timing is not None:
- payload["rebuild_timing"] = rebuild_timing
- return payload
-
-
-def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
- p = argparse.ArgumentParser(description=__doc__)
- p.add_argument(
- "--rebuild",
- action="store_true",
- help="rebuild stale editions into EDITIONS_ROOT",
- )
- p.add_argument(
- "--force",
- action="store_true",
- help="rebuild every edition even when .edition-tip matches (timing)",
- )
- p.add_argument(
- "--no-fetch",
- action="store_true",
- help="skip git fetch --prune origin",
- )
- return p.parse_args(argv)
-
-
-def main(argv: list[str] | None = None) -> int:
- args = parse_args(argv)
- repo = tdpd_root()
- if not (repo / ".git").exists() and not (repo / "HEAD").exists():
- # allow bare / gitfile
- gitfile = repo / ".git"
- if not repo.is_dir():
- print(f"TDPD_ROOT missing or not a dir: {repo}", file=sys.stderr)
- return 1
- if not gitfile.exists() and not (repo / "objects").exists():
- print(f"TDPD_ROOT has no git metadata: {repo}", file=sys.stderr)
- return 1
-
- if args.no_fetch:
- os.environ["FETCH"] = "0"
- if args.force:
- os.environ["FORCE_REBUILD"] = "1"
- fetch_origin(repo)
-
- tips = list_origin_branches(repo)
- if not tips:
- print("no origin/* branches found", file=sys.stderr)
- return 1
-
- rebuild = (
- args.rebuild
- or args.force
- or os.environ.get("REBUILD", "").strip() in ("1", "true", "yes")
- )
- if rebuild and editions_root() is None:
- print("REBUILD set but EDITIONS_ROOT missing", file=sys.stderr)
- return 1
-
- payload = build_payload(repo, tips, rebuild)
- dest = out_path()
- dest.parent.mkdir(parents=True, exist_ok=True)
- dest.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
- print(f"wrote {dest} branches={len(payload['branches'])}", file=sys.stderr)
- if "rebuild" in payload:
- print(f"rebuild={payload['rebuild']}", file=sys.stderr)
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet b/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet
deleted file mode 100644
index 7e02a41..0000000
--- a/scripts/dossiers-ngi/caddy-dossiers-ngi.snippet
+++ /dev/null
@@ -1,68 +0,0 @@
-# Stanza body for dossiers.ngi.hacktivism.ch only (hand-check / re-seed).
-# Apply on koopa (host-local):
-# bash ~/bin/apply-dossiers-ngi-edition-switch.sh
-# sudo ~/bin/caddy-apply
-#
-# Sticky bar switches build via cookie tdpd_edition → editions//.
-# Stock Caddy only (templates + map + handle). No replace-response plugin.
-# Keep .2 / .3 blocks untouched.
-#
-# Site-block body (no outer braces):
-
- header Alt-Svc "clear"
- # Cookie selects editions//; same paths (/static/css/style.css) must not
- # share one browser/CDN cache entry across editions (main has no green CSS).
- header Vary "Cookie"
- header Cache-Control "private, no-cache"
-
- @use path_regexp use ^/_cascade/use/([A-Za-z0-9_-]+)/?$
- handle @use {
- header Set-Cookie "tdpd_edition={re.use.1}; Path=/; Max-Age=31536000; SameSite=Lax"
- 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
- }
-
- handle /_cascade/* {
- root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
- header Cache-Control "private, no-cache"
- file_server
- }
- # Path-root cascade stubs live under sites/…/cascade/
- handle /cascade/* {
- root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
- header Cache-Control "private, no-cache"
- file_server
- try_files {path} {path}/ {path}/index.html
- }
- handle /branch-status.json {
- root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
- header Cache-Control "private, no-cache"
- file_server
- }
-
- # Explicit edition_dir map (placeholder map output broke on Caddy v2.11.4 —
- # non-flask cookies 404'd). Refresh this list when new origin tips appear
- # (branch-status rebuild creates editions//).
- map {http.request.cookie.tdpd_edition} {edition_dir} {
- default flask
- flask flask
- feat-sm-content feat-sm-content
- feat-website-design feat-website-design
- feat-website-features feat-website-features
- feat-website-views feat-website-views
- feature-nl-net-injestion-injestor-update feature-nl-net-injestion-injestor-update
- main main
- project_template project_template
- }
-
- root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch/editions/{edition_dir}
- templates {
- mime text/html
- between "{%" "%}"
- root /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
- }
- file_server
- try_files {path} {path}/ {path}/index.html
diff --git a/scripts/dossiers-ngi/inject-cascade-bar.py b/scripts/dossiers-ngi/inject-cascade-bar.py
deleted file mode 100755
index 9659dde..0000000
--- a/scripts/dossiers-ngi/inject-cascade-bar.py
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/env python3
-"""Insert {% include "_cascade/bar.html" . %} after in freeze HTML.
-
-Idempotent. Skips files that already contain the include or live under
-_cascade/. Usage:
-
- python3 inject-cascade-bar.py /path/to/edition/root
-"""
-from __future__ import annotations
-
-import re
-import sys
-from pathlib import Path
-
-INCLUDE = '{% include "_cascade/bar.html" . %}'
-BODY_RE = re.compile(r"(]*>)", re.IGNORECASE)
-
-
-def inject(text: str) -> str | None:
- if INCLUDE in text:
- return None
- if "_cascade/bar" in text:
- return None
- m = BODY_RE.search(text)
- if not m:
- return None
- i = m.end()
- return text[:i] + "\n" + INCLUDE + "\n" + text[i:]
-
-
-def main() -> int:
- if len(sys.argv) != 2:
- print(f"usage: {sys.argv[0]} EDITION_ROOT", file=sys.stderr)
- return 2
- root = Path(sys.argv[1]).resolve()
- if not root.is_dir():
- print(f"not a dir: {root}", file=sys.stderr)
- return 2
- changed = 0
- scanned = 0
- for path in root.rglob("*.html"):
- if "/_cascade/" in path.as_posix() + "/":
- continue
- scanned += 1
- raw = path.read_text(encoding="utf-8", errors="ignore")
- out = inject(raw)
- if out is None:
- continue
- path.write_text(out, encoding="utf-8")
- changed += 1
- print(f"OK: scanned={scanned} injected={changed} root={root}")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/scripts/dossiers-ngi/inject-editions-bar.sh b/scripts/dossiers-ngi/inject-editions-bar.sh
deleted file mode 100755
index f30790c..0000000
--- a/scripts/dossiers-ngi/inject-editions-bar.sh
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/env bash
-# Inject cascade-bar include into every editions/ freeze on koopa.
-# No zypper, no Caddy plugin, no root — hernani must own $WWW.
-#
-# bash scripts/dossiers-ngi/inject-editions-bar.sh
-# WWW=/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch bash …
-#
-# Way back without the bar: https://dossiers.ngi.hacktivism.ch/_cascade/use/flask
-set -euo pipefail
-
-WWW="${WWW:-/var/www/monitoring-sites/dossiers.ngi.hacktivism.ch}"
-ED="${ED:-$WWW/editions}"
-
-if [[ ! -d "$ED" ]]; then
- echo "FAIL: not a dir: $ED" >&2
- exit 2
-fi
-
-python3 - "$ED" <<'PY'
-from __future__ import annotations
-
-import re
-import sys
-from pathlib import Path
-
-INCLUDE = '{% include "_cascade/bar.html" . %}'
-BODY_RE = re.compile(r"(]*>)", re.IGNORECASE)
-
-
-def inject(text: str) -> str | None:
- if INCLUDE in text or "_cascade/bar" in text:
- return None
- m = BODY_RE.search(text)
- if not m:
- return None
- i = m.end()
- return text[:i] + "\n" + INCLUDE + "\n" + text[i:]
-
-
-root = Path(sys.argv[1]).resolve()
-scanned = changed = skipped = nobody = 0
-print(f"ED={root}")
-for slug_dir in sorted(p for p in root.iterdir() if p.is_dir()):
- for path in slug_dir.rglob("*.html"):
- posix = path.as_posix()
- if "/_cascade/" in posix + "/":
- continue
- scanned += 1
- raw = path.read_text(encoding="utf-8", errors="ignore")
- if INCLUDE in raw or "_cascade/bar" in raw:
- skipped += 1
- continue
- out = inject(raw)
- if out is None:
- nobody += 1
- print(f"NOBODY {path.relative_to(root)}")
- continue
- path.write_text(out, encoding="utf-8")
- changed += 1
- print(f"INJECT {path.relative_to(root)}")
-
-print(f"OK scanned={scanned} injected={changed} already={skipped} nobody={nobody}")
-if nobody:
- sys.exit(1)
-PY
-
-echo "----- index.html bar? -----"
-python3 - "$ED" <<'PY'
-from pathlib import Path
-import sys
-root = Path(sys.argv[1])
-for d in sorted(p for p in root.iterdir() if p.is_dir()):
- idx = d / "index.html"
- if not idx.is_file():
- print(f"MISSING {d.name}/index.html")
- continue
- t = idx.read_text(encoding="utf-8", errors="ignore")
- flag = "BAR" if ("cascade-bar" in t or "_cascade/bar" in t) else "NOBAR"
- print(f"{flag} {d.name}")
-PY
diff --git a/scripts/dossiers-ngi/smoke-branch-tree.sh b/scripts/dossiers-ngi/smoke-branch-tree.sh
deleted file mode 100755
index e180854..0000000
--- a/scripts/dossiers-ngi/smoke-branch-tree.sh
+++ /dev/null
@@ -1,138 +0,0 @@
-#!/usr/bin/env bash
-# Local smoke: temp origin with extra branches → tree JSON + stub rebuild → cleanup.
-# Does not touch origin on git.ngi-0.eu.
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-SCRIPT="$ROOT/scripts/dossiers-ngi/branch-status-hourly.py"
-TMP="$(mktemp -d "${TMPDIR:-/tmp}/tdpd-smoke-XXXXXX")"
-cleanup() { rm -rf "$TMP"; }
-trap cleanup EXIT
-
-echo "TMP=$TMP"
-
-BARE="$TMP/origin.git"
-CLONE="$TMP/tdpd"
-EDITIONS="$TMP/editions"
-OUT="$TMP/branch-status.json"
-
-git init --bare "$BARE" >/dev/null
-git clone "$BARE" "$CLONE" >/dev/null 2>&1
-cd "$CLONE"
-git config user.email "smoke@example.test"
-git config user.name "smoke"
-echo "root" > README
-git add README
-git commit -m "main root" >/dev/null
-git branch -M main
-git push -u origin main >/dev/null 2>&1
-
-# ladder: main → feat/a → feat/b
-git checkout -b feat/a >/dev/null 2>&1
-echo a > a.txt && git add a.txt && git commit -m "feat a" >/dev/null
-git push -u origin feat/a >/dev/null 2>&1
-git checkout -b feat/b >/dev/null 2>&1
-echo b > b.txt && git add b.txt && git commit -m "feat b" >/dev/null
-git push -u origin feat/b >/dev/null 2>&1
-
-# sibling off main
-git checkout main >/dev/null 2>&1
-git checkout -b smoke/temp-sibling >/dev/null 2>&1
-echo s > s.txt && git add s.txt && git commit -m "smoke sibling" >/dev/null
-git push -u origin smoke/temp-sibling >/dev/null 2>&1
-
-# stub freeze: write website/build/index.html
-FREEZE_CMD='mkdir -p "{root}/website/build" && printf "%s\n" "{branch} " > "{root}/website/build/index.html"'
-
-mkdir -p "$EDITIONS"
-# seed a Makefile so non-stub path would find one; stub overrides via FREEZE_CMD
-FETCH=0 TDPD_ROOT="$CLONE" OUT="$OUT" EDITIONS_ROOT="$EDITIONS" \
- FREEZE_CMD="$FREEZE_CMD" REBUILD=1 \
- python3 "$SCRIPT" --rebuild --no-fetch
-
-python3 - "$OUT" <<'PY'
-import json, sys
-from pathlib import Path
-data = json.loads(Path(sys.argv[1]).read_text())
-branches = {b["branch"]: b for b in data["branches"]}
-assert "main" in branches
-assert "feat/a" in branches
-assert "feat/b" in branches
-assert "smoke/temp-sibling" in branches
-assert branches["feat/a"]["parent"] == "main", branches["feat/a"]
-assert branches["feat/b"]["parent"] == "feat/a", branches["feat/b"]
-assert branches["smoke/temp-sibling"]["parent"] == "main", branches["smoke/temp-sibling"]
-for b in data["branches"]:
- assert b.get("freeze_ok") is True, b
-# prefixes semi-graphic
-prefs = {b["branch"]: b["prefix"] for b in data["branches"]}
-assert any("├──" in p or "└──" in p for p in prefs.values()), prefs
-# editions built
-print("OK tree:")
-for b in data["branches"]:
- print(f" {b['prefix']}{b['branch']} @ {b['tip']} -> {b['edition']} parent={b['parent']!r}")
-print("rebuild", data.get("rebuild"))
-PY
-
-# editions exist with stub content
-test -f "$EDITIONS/feat-a/index.html"
-test -f "$EDITIONS/smoke-temp-sibling/index.html"
-grep -q "feat/a" "$EDITIONS/feat-a/index.html"
-grep -q "smoke/temp-sibling" "$EDITIONS/smoke-temp-sibling/index.html"
-test "$(cat "$EDITIONS/feat-a/.edition-freeze-ok")" = "1"
-
-# red ✗ when last freeze of this tip failed
-printf '0\n' > "$EDITIONS/feat-a/.edition-freeze-ok"
-printf 'error:smoke\n' > "$EDITIONS/feat-a/.edition-freeze-error"
-FETCH=0 TDPD_ROOT="$CLONE" OUT="$OUT" EDITIONS_ROOT="$EDITIONS" \
- FREEZE_CMD="$FREEZE_CMD" \
- python3 "$SCRIPT" --no-fetch
-python3 - "$OUT" <<'PY'
-import json, sys
-from pathlib import Path
-data = json.loads(Path(sys.argv[1]).read_text())
-rows = {b["branch"]: b for b in data["branches"]}
-assert rows["feat/a"]["freeze_ok"] is False, rows["feat/a"]
-assert rows["feat/a"].get("freeze_error") == "error:smoke", rows["feat/a"]
-assert rows["main"]["freeze_ok"] is True, rows["main"]
-print("OK freeze_ok false after failed mark")
-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' 'stale-flask' > "$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
-git branch -D smoke/temp-sibling >/dev/null 2>&1 || true
-TDPD_ROOT="$CLONE" OUT="$OUT" EDITIONS_ROOT="$EDITIONS" \
- FREEZE_CMD="$FREEZE_CMD" \
- python3 "$SCRIPT"
-python3 - "$OUT" <<'PY'
-import json, sys
-from pathlib import Path
-data = json.loads(Path(sys.argv[1]).read_text())
-names = {b["branch"] for b in data["branches"]}
-assert "smoke/temp-sibling" not in names, names
-assert names == {"main", "feat/a", "feat/b"}, names
-print("OK smoke branch removed from tree")
-PY
-
-echo "SMOKE_OK"
diff --git a/scripts/firecuda-probe/README.md b/scripts/firecuda-probe/README.md
deleted file mode 100644
index 1b0e217..0000000
--- a/scripts/firecuda-probe/README.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# firecuda-probe — external last-alive from koopa
-
-**Why:** firecuda panics (e.g. 1GbE PCIe completion timeout 2026-09-20) and hard hangs leave the internal HB dead with the box. External ICMP+SSH from **koopa** distinguishes:
-
-| Pattern | Likely meaning |
-|---------|----------------|
-| ICMP fail + SSH fail, long gap | hang, power cut, or NIC/kernel down |
-| ICMP ok + SSH fail | userspace/sshd stuck, or max lock |
-| both ok, HB stamp stale vs wall | rare clock skew / forced-cmd path oddity / HB LaunchDaemon stopped |
-| both ok, HB fresh | alive |
-
-Log is **hernani-readable** on koopa (`~/.local/state/firecuda-probe/`).
-SSH key is **max-only** on firecuda, restricted to koopa LAN IP + forced `cat` of HB mirror.
-
-Requires firecuda crash-debug HB live (`/usr/local/var/lib/firecuda/heartbeat.last`) — see `firecuda-admin-log/configs/crash-debug/`.
-
-## Pieces
-
-| Piece | Path |
-|-------|------|
-| Script | `scripts/firecuda-probe/koopa-firecuda-probe.sh` → `~/.local/bin/koopa-firecuda-probe` |
-| Timer | `configs/systemd/user/koopa-firecuda-probe.{service,timer}` (~15 s) |
-| Key | `~/.ssh/id_ed25519_firecuda_probe` (hernani on koopa) |
-| Log | `~/.local/state/firecuda-probe/firecuda-probe.log` (+ `.last` / `.meta`) |
-| SSH Host | `firecuda-probe` → `max@192.168.100.84` with that IdentityFile |
-
-## Install (koopa as hernani) — Freigabe
-
-Laptop SoT: `$HOME/git/admin-logs/local/koopa-admin-log`. Live mirror: `~/src/koopa/koopa-admin-log` (no git commit on koopa).
-
-```bash
-# on koopa after syncing the tree:
-cd ~/src/koopa/koopa-admin-log
-./scripts/firecuda-probe/install-firecuda-probe.sh --keygen
-# copy the printed authorized_keys line → firecuda max (see below)
-./scripts/firecuda-probe/install-firecuda-probe.sh # units + first probe
-```
-
-Linger already yes for hernani → timer survives logout.
-
-## firecuda max authorized_keys (one line)
-
-After `--keygen`, install prints the exact line. Template:
-
-```text
-from="192.168.100.95",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,command="/bin/cat /usr/local/var/lib/firecuda/heartbeat.last" ssh-ed25519 koopa-firecuda-probe@hernani-koopa
-```
-
-- **User:** `max` only (not root).
-- **from=:** only koopa LAN `192.168.100.95`.
-- **forced command:** read-only HB mirror (world-readable after crash-debug install).
-- Daylog: `firecuda-admin-log/2026/2026-09-20--pcie-lan-panic+crash-debug.md`.
-
-## Verify
-
-```bash
-# koopa
-systemctl --user list-timers koopa-firecuda-probe.timer
-koopa-firecuda-probe status
-koopa-firecuda-probe show
-ssh firecuda-probe # should print one HB line, no shell
-
-# gaps after a crash: look for icmp=fail/ssh=fail with large gap_s=
-```
-
-## Uninstall
-
-```bash
-systemctl --user disable --now koopa-firecuda-probe.timer
-rm -f ~/.local/bin/koopa-firecuda-probe \
- ~/.config/systemd/user/koopa-firecuda-probe.{service,timer}
-systemctl --user daemon-reload
-# optional: remove key + Host block + max authorized_keys line
-```
-
-## Do not
-
-- Put this key on **root** authorized_keys.
-- Enable timer / write authorized_keys without Freigabe.
-- Expect this to fix PCIe/Ethernet panics — it only timestamps last-alive.
diff --git a/scripts/firecuda-probe/install-firecuda-probe.sh b/scripts/firecuda-probe/install-firecuda-probe.sh
deleted file mode 100755
index e21accf..0000000
--- a/scripts/firecuda-probe/install-firecuda-probe.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env bash
-# Install koopa-firecuda-probe (hernani on koopa):
-# - ~/.local/bin/koopa-firecuda-probe
-# - user systemd timer ~15s → ICMP+SSH last-alive log
-# - optional: generate ~/.ssh/id_ed25519_firecuda_probe + Host firecuda-probe
-# - optional: print restricted pubkey line for firecuda max authorized_keys
-#
-# On koopa as hernani (after laptop push + pull into ~/src/koopa/koopa-admin-log):
-# ./scripts/firecuda-probe/install-firecuda-probe.sh
-# ./scripts/firecuda-probe/install-firecuda-probe.sh --keygen # if key missing
-# ./scripts/firecuda-probe/install-firecuda-probe.sh --print-pubkey
-#
-# Key install on firecuda is separate (max authorized_keys) — see README.
-# Live install = Freigabe.
-
-set -euo pipefail
-
-ROOT=$(cd "$(dirname "$0")" && pwd)
-REPO_ROOT=$(cd "$ROOT/../.." && pwd)
-SRC="$ROOT/koopa-firecuda-probe.sh"
-UNIT_SRC_DIR="$REPO_ROOT/configs/systemd/user"
-BIN_DIR="${HOME}/.local/bin"
-DEST="$BIN_DIR/koopa-firecuda-probe"
-UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
-KEY="${FIRECUDA_PROBE_KEY:-${HOME}/.ssh/id_ed25519_firecuda_probe}"
-SSH_CONFIG="${HOME}/.ssh/config"
-HOST_MARKER_BEGIN="# >>> firecuda-probe (koopa-admin-log) >>>"
-HOST_MARKER_END="# <<< firecuda-probe (koopa-admin-log) <<<"
-DO_KEYGEN=0
-DO_PRINT=0
-DO_UNITS=1
-
-for arg in "$@"; do
- case "$arg" in
- --keygen) DO_KEYGEN=1 ;;
- --print-pubkey) DO_PRINT=1 ;;
- --no-units) DO_UNITS=0 ;;
- -h|--help)
- sed -n '2,16p' "$0" | sed 's/^# \?//'
- exit 0
- ;;
- *)
- echo "unknown arg: $arg" >&2
- exit 2
- ;;
- esac
-done
-
-if [ ! -f "$SRC" ]; then
- echo "missing $SRC" >&2
- exit 1
-fi
-
-mkdir -p "$BIN_DIR" "$UNIT_DIR" "${HOME}/.ssh"
-chmod 700 "${HOME}/.ssh" 2>/dev/null || true
-
-install -m 0755 "$SRC" "$DEST"
-echo "installed $DEST"
-
-if [ "$DO_KEYGEN" -eq 1 ]; then
- if [ -f "$KEY" ]; then
- echo "key exists: $KEY (skip --keygen)"
- else
- ssh-keygen -t ed25519 -a 64 -f "$KEY" -N "" \
- -C "koopa-firecuda-probe@hernani-$(hostname -s 2>/dev/null || echo koopa)"
- chmod 600 "$KEY" "${KEY}.pub"
- echo "generated $KEY"
- fi
-fi
-
-if [ -f "$KEY" ]; then
- touch "$SSH_CONFIG"
- chmod 600 "$SSH_CONFIG" 2>/dev/null || true
- if grep -qF "$HOST_MARKER_BEGIN" "$SSH_CONFIG" 2>/dev/null; then
- tmp=$(mktemp)
- awk -v b="$HOST_MARKER_BEGIN" -v e="$HOST_MARKER_END" '
- $0 == b {skip=1; next}
- $0 == e {skip=0; next}
- !skip {print}
- ' "$SSH_CONFIG" >"$tmp"
- mv "$tmp" "$SSH_CONFIG"
- fi
- {
- echo ""
- echo "$HOST_MARKER_BEGIN"
- echo "Host firecuda-probe"
- echo " Hostname 192.168.100.84"
- echo " User max"
- echo " IdentityFile $KEY"
- echo " IdentitiesOnly yes"
- echo " BatchMode yes"
- echo " PreferredAuthentications publickey"
- echo " ConnectTimeout 5"
- echo "$HOST_MARKER_END"
- } >>"$SSH_CONFIG"
- echo "updated $SSH_CONFIG Host firecuda-probe"
-fi
-
-if [ "$DO_PRINT" -eq 1 ] || [ "$DO_KEYGEN" -eq 1 ]; then
- if [ ! -f "${KEY}.pub" ]; then
- echo "missing ${KEY}.pub — run with --keygen first" >&2
- exit 1
- fi
- pub="$(awk '{print $1, $2}' "${KEY}.pub")"
- comment="$(awk '{print $3}' "${KEY}.pub")"
- echo ""
- echo "=== paste ONE line into firecuda:~max/.ssh/authorized_keys ==="
- printf 'from="192.168.100.95",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/bin/cat /usr/local/var/lib/firecuda/heartbeat.last" %s %s\n' \
- "$pub" "${comment:-koopa-firecuda-probe}"
- echo "=== end ==="
-fi
-
-if [ "$DO_UNITS" -eq 1 ]; then
- for u in koopa-firecuda-probe.service koopa-firecuda-probe.timer; do
- if [ ! -f "$UNIT_SRC_DIR/$u" ]; then
- echo "missing unit $UNIT_SRC_DIR/$u" >&2
- exit 1
- fi
- install -m 0644 "$UNIT_SRC_DIR/$u" "$UNIT_DIR/$u"
- echo "installed $UNIT_DIR/$u"
- done
- systemctl --user daemon-reload
- systemctl --user enable --now koopa-firecuda-probe.timer
- systemctl --user start koopa-firecuda-probe.service || true
- echo "timer: $(systemctl --user is-active koopa-firecuda-probe.timer 2>/dev/null || echo unknown)"
- echo "probe once: $DEST once"
- "$DEST" once || true
- "$DEST" status || true
-fi
-
-echo "done. log: ~/.local/state/firecuda-probe/firecuda-probe.log"
diff --git a/scripts/firecuda-probe/koopa-firecuda-probe.sh b/scripts/firecuda-probe/koopa-firecuda-probe.sh
deleted file mode 100755
index 5d35fab..0000000
--- a/scripts/firecuda-probe/koopa-firecuda-probe.sh
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/env bash
-# koopa-firecuda-probe — external ICMP+SSH last-alive for firecuda crashes/hangs.
-# Runs as hernani on koopa (~15s user timer). Log is hernani-readable on koopa.
-# SSH uses dedicated ed25519 → max only (forced command: cat heartbeat.last).
-#
-# Usage:
-# koopa-firecuda-probe # one probe cycle (default; timer path)
-# koopa-firecuda-probe once # same
-# koopa-firecuda-probe show # tail last lines
-# koopa-firecuda-probe status # last line + timer hint
-#
-# Env:
-# FIRECUDA_PROBE_HOST default 192.168.100.84
-# FIRECUDA_PROBE_USER default max
-# FIRECUDA_PROBE_KEY default ~/.ssh/id_ed25519_firecuda_probe
-# FIRECUDA_PROBE_STATE default ~/.local/state/firecuda-probe
-set +e
-set +u
-
-CMD="${1:-once}"
-HOST="${FIRECUDA_PROBE_HOST:-192.168.100.84}"
-USER_R="${FIRECUDA_PROBE_USER:-max}"
-KEY="${FIRECUDA_PROBE_KEY:-${HOME}/.ssh/id_ed25519_firecuda_probe}"
-STATE="${FIRECUDA_PROBE_STATE:-${XDG_STATE_HOME:-${HOME}/.local/state}/firecuda-probe}"
-LOG="${STATE}/firecuda-probe.log"
-LAST="${STATE}/firecuda-probe.last"
-META="${STATE}/firecuda-probe.meta"
-SSH_TO="${FIRECUDA_PROBE_SSH_TIMEOUT:-5}"
-PING_TO="${FIRECUDA_PROBE_PING_TIMEOUT:-2}"
-
-mkdir -p "${STATE}" 2>/dev/null || true
-
-do_show() {
- if [ ! -f "${LOG}" ]; then
- echo "firecuda-probe: no log yet (${LOG})"
- echo " start: systemctl --user start koopa-firecuda-probe.service"
- return 0
- fi
- tail -n "${1:-20}" "${LOG}"
-}
-
-do_status() {
- if [ -f "${LAST}" ]; then
- echo "last: $(cat "${LAST}")"
- else
- echo "last: (none)"
- fi
- if [ -f "${META}" ]; then
- cat "${META}"
- fi
- systemctl --user is-active koopa-firecuda-probe.timer 2>/dev/null \
- | awk '{print "timer=" $0}'
-}
-
-do_once() {
- local ts icmp ssh hb gap prev_ts prev_epoch now_epoch
- ts="$(date -Iseconds)"
- now_epoch="$(date +%s)"
- icmp=fail
- ssh=fail
- hb="-"
-
- if ping -c1 -W "${PING_TO}" "${HOST}" >/dev/null 2>&1; then
- icmp=ok
- fi
-
- if [ ! -f "${KEY}" ]; then
- ssh=nokey
- else
- # Forced command on firecuda returns heartbeat.last; ignore remote argv.
- hb="$(
- ssh -i "${KEY}" -o IdentitiesOnly=yes -o BatchMode=yes \
- -o StrictHostKeyChecking=accept-new \
- -o ConnectTimeout="${SSH_TO}" -o ConnectionAttempts=1 \
- -o PreferredAuthentications=publickey \
- "${USER_R}@${HOST}" true 2>/dev/null \
- | tr '\n' ' ' | tr -s ' ' | head -c 400
- )"
- if [ -n "${hb}" ]; then
- ssh=ok
- else
- if [ "${icmp}" = "ok" ]; then
- ssh=fail
- else
- ssh=down
- fi
- hb="-"
- fi
- fi
-
- gap="?"
- if [ -f "${LAST}" ]; then
- prev_ts="$(awk '{print $1}' "${LAST}" 2>/dev/null)"
- if prev_epoch="$(date -d "${prev_ts}" +%s 2>/dev/null)"; then
- gap=$((now_epoch - prev_epoch))
- fi
- fi
-
- local line
- line="$(printf '%s icmp=%s ssh=%s gap_s=%s host=%s user=%s hb=%s' \
- "${ts}" "${icmp}" "${ssh}" "${gap}" "${HOST}" "${USER_R}" "${hb}")"
-
- printf '%s\n' "${line}" >>"${LOG}"
- printf '%s\n' "${line}" >"${LAST}"
- {
- echo "updated=${ts}"
- echo "log=${LOG}"
- echo "icmp=${icmp}"
- echo "ssh=${ssh}"
- echo "gap_s=${gap}"
- } >"${META}"
-
- # Keep log bounded (~14d at 15s ≈ 80k lines; rotate soft at 200k lines)
- if [ -f "${LOG}" ]; then
- local lines
- lines="$(wc -l <"${LOG}" 2>/dev/null | tr -d ' ')"
- if [ "${lines:-0}" -gt 200000 ] 2>/dev/null; then
- tail -n 100000 "${LOG}" >"${LOG}.tmp" && mv "${LOG}.tmp" "${LOG}"
- fi
- fi
-
- return 0
-}
-
-case "${CMD}" in
- once|probe|"") do_once ;;
- show) do_show "${2:-20}" ;;
- status) do_status ;;
- -h|--help)
- sed -n '2,20p' "$0" | sed 's/^# \?//'
- ;;
- *)
- echo "unknown: ${CMD} (once|show|status)" >&2
- exit 2
- ;;
-esac
diff --git a/scripts/galene/build.sh b/scripts/galene/build.sh
deleted file mode 100755
index 0b1aa55..0000000
--- a/scripts/galene/build.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-# Build Galene image on CachyOS base (run on koopa).
-set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CTX="${ROOT}/configs/galene"
-TAG="${IMAGE_TAG:-localhost/koopa-galene:cachyos}"
-GALENE_VERSION="${GALENE_VERSION:-0.9.1}"
-cd "$CTX"
-[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
-echo "build $TAG (GALENE_VERSION=$GALENE_VERSION) ctx=$CTX"
-podman build \
- --build-arg "GALENE_VERSION=${GALENE_VERSION}" \
- -t "$TAG" \
- -f Containerfile \
- .
-echo "OK: $TAG"
-podman image inspect "$TAG" --format '{{.Id}} {{.Size}} {{.Architecture}}' 2>/dev/null || true
diff --git a/scripts/lemmy/install-systemd.sh b/scripts/lemmy/install-systemd.sh
index f064f9f..626dc3b 100755
--- a/scripts/lemmy/install-systemd.sh
+++ b/scripts/lemmy/install-systemd.sh
@@ -1,21 +1,12 @@
#!/usr/bin/env bash
# Install Lemmy user systemd units on koopa (boot via linger).
set -euo pipefail
-HERE="$(cd "$(dirname "$0")" && pwd)"
-LIVE="${HOME}/koopa-lemmy"
-if [ -f "${HERE}/../../configs/lemmy/container-koopa-lemmy.service" ]; then
- UNIT_SRC="${HERE}/../../configs/lemmy"
-elif [ -f "${LIVE}/container-koopa-lemmy.service" ]; then
- UNIT_SRC="${LIVE}"
-else
- echo "ERROR: lemmy unit files not found" >&2
- exit 1
-fi
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
UNIT_DIR="${HOME}/.config/systemd/user"
mkdir -p "${UNIT_DIR}"
-cp "${UNIT_SRC}/container-koopa-lemmy-db.service" "${UNIT_DIR}/"
-cp "${UNIT_SRC}/container-koopa-lemmy.service" "${UNIT_DIR}/"
+cp "${ROOT}/configs/lemmy/container-koopa-lemmy-db.service" "${UNIT_DIR}/"
+cp "${ROOT}/configs/lemmy/container-koopa-lemmy.service" "${UNIT_DIR}/"
systemctl --user daemon-reload
systemctl --user enable \
@@ -34,13 +25,4 @@ systemctl --user --no-pager --lines=0 status \
container-koopa-lemmy-db.service \
container-koopa-lemmy.service || true
-if command -v loginctl >/dev/null 2>&1; then
- if [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)" != "yes" ]; then
- echo "NOTE: enable linger so units start after reboot without login:"
- echo " sudo loginctl enable-linger $(id -un)"
- else
- echo "linger already enabled for $(id -un)"
- fi
-fi
-
echo "OK. Public: https://lemmy.hacktivism.ch/"
diff --git a/scripts/login-health/README.md b/scripts/login-health/README.md
index 1f48387..04b1a6b 100644
--- a/scripts/login-health/README.md
+++ b/scripts/login-health/README.md
@@ -1,81 +1,113 @@
# login-health — status on SSH login (koopa)
-SSH login shows a short **pre-collected** snapshot: Forgejo, GOA (`goa-regio-ng`), Caddy edge, host Tor, rest of inventory.
+When you **SSH into koopa**, show a short **pre-collected** health snapshot so you see whether **git.hacktivism.ch** (Forgejo), Taler GOA, Caddy edge, and the rest of the LIVE inventory are up.
-Inventory: `configs/ports.md` + this script’s arrays (not `host/overview/`).
+Inventory source: `host/overview/LIVE.md`, `configs/ports.md`.
-## New / changed services
+## New / changed services (mandatory)
-Same package:
+Whenever a **new service** is added on koopa (or ports/vhosts change), update **in the same work package**:
1. `koopa-login-health.sh` — `EXPECT_CONTAINERS`, `PROBES`, critical vs optional
-2. `configs/ports.md`
-3. Deploy → `~/.local/bin/koopa-login-health` on koopa (no git commit on koopa)
-4. `koopa-login-health collect`
+2. `host/overview/LIVE.md` + `configs/ports.md` (and `services.md` if the diagram changes)
+3. Deploy script to `~/.local/bin/koopa-login-health` on koopa (laptop commits/pushes; **no git commit on koopa**)
+4. `koopa-login-health collect` so the next SSH login is not stale
-Workflow: `koopa-services-health`.
+Grok workflows: `koopa-services-health` (inventory drift) + `commit-rules` (planning rule 10).
## Design: no probes at login
-| Path | What |
-|------|------|
-| **user timer** (5 min + after boot) | `collect` — podman, Caddy, Tor, loopback HTTP → snapshot |
-| **SSH / bashrc** | `show` only — cat snapshot |
+| Path | What runs |
+|------|-----------|
+| **systemd user timer** (every **5 min** + soon after boot) | `koopa-login-health collect` — podman, Caddy, loopback HTTP, write snapshot |
+| **SSH / bashrc** | `koopa-login-health show` only — **cat** last snapshot (milliseconds, no curl/podman) |
-State: `~/.local/state/koopa-login-health/` (`status.compact` / `.verbose` / `.meta`).
-Stale if older than `KOOPA_HEALTH_STALE_SEC` (default 900).
+Snapshot files (`XDG_STATE_HOME`, default `~/.local/state/koopa-login-health/`):
+
+| File | Content |
+|------|---------|
+| `status.compact` | login view (WARN/FAIL + overall) |
+| `status.verbose` | full line list |
+| `status.meta` | `overall=`, `collected_at=` (unix), RFC date |
+
+If the snapshot is older than **`KOOPA_HEALTH_STALE_SEC`** (default **900** = 15 min), `show` prints a **stale** warning (timer likely down).
## Login must never break
-`KOOPA_HEALTH_FROM_LOGIN=1` → exit 0 · `timeout 2s` · `|| true` · `bash -n` on install.
+| Guard | What it does |
+|-------|----------------|
+| **show only** | bashrc never runs `collect` |
+| **`KOOPA_HEALTH_FROM_LOGIN=1`** | always exit 0 |
+| **`timeout 2s`** | wall cap to cat a few KB |
+| **`\|\| true` / `set +e`** | hook cannot abort the shell |
+| **`bash -n`** on install | refuse a broken bashrc hook |
+
Skip: `KOOPA_SKIP_HEALTH=1 ssh koopa`
## What you see
```text
-koopa health koopa Sat, 19 Sep 2026 …
+koopa health koopa Wed, …
git.hacktivism.ch = Forgejo :9024/:9200 · edge Caddy :9000/:9001
- OK all critical checks passed (… ok)
-overall: OK ok=… warn=0 fail=0
+ OK all critical checks passed (42 ok)
+overall: OK ok=42 warn=0 fail=0
+ checked: Wed, … (3m ago) [timer snapshot; not live at login]
```
-## Checks (collect only)
+## Checks (on collect only)
| Area | What |
|------|------|
| Edge | `caddy.service`, proxy sockets |
-| Tor | host `tor.service` (ORPort 8080 / ControlPort 9051) — WARN if down |
| Listen | critical + optional ports |
-| Podman | `goa-regio-ng`, forgejo, optional apps |
-| HTTP | loopback backends (2s curl) |
+| Podman | LIVE container names |
+| HTTP | loopback backends (2s curl, parallel) |
| Git SSH | TCP `:9200` |
-**Critical** (FAIL): `goa-regio-ng`, `koopa-forgejo`, edge/ports **9010–9012**, **9024**, **9200**.
-**Optional** (WARN): castopod, bonfire, lemmy, decidim, prime, bt, paivana, tops, nym.
-
-GOA = single CTR `goa-regio-ng` (pasta **9010–9015**). Landings **9013–9015** host-side (`~/goa-landing-deploy`).
-Legacy `taler-hacktivism*` / `koopa-tor-relay` are not expected.
+**Critical** (→ overall FAIL): merchant, exchange, bank, forgejo, edge/critical ports.
+**Optional** (→ WARN): castopod, bonfire, lemmy, prime, bt, paivana, tops, tor, nym.
Read-only. No restarts, no secrets.
-## Install (koopa as `hernani`)
+## Install (on koopa as `hernani`)
```bash
cd ~/src/koopa/koopa-admin-log
./scripts/login-health/install-login-health.sh
+```
+
+Installs binary, user units, bashrc hook, enables timer, runs one collect.
+
+Linger (so the timer runs after reboot without an open SSH session):
+
+```bash
sudo loginctl enable-linger hernani
```
## Manual
```bash
-koopa-login-health
-koopa-login-health show -v
-koopa-login-health collect
+koopa-login-health # show compact snapshot
+koopa-login-health show -v # verbose snapshot
+koopa-login-health show -q # overall (+ stale note)
+koopa-login-health collect # refresh now (same as the timer unit)
+systemctl --user start koopa-login-health.service
systemctl --user status koopa-login-health.timer
```
+## Uninstall
+
+```bash
+systemctl --user disable --now koopa-login-health.timer
+rm -f ~/.config/systemd/user/koopa-login-health.{service,timer}
+systemctl --user daemon-reload
+rm -f ~/.local/bin/koopa-login-health
+# remove marked block in ~/.bashrc
+rm -rf ~/.local/state/koopa-login-health
+```
+
## Related
-- `configs/ports.md` · `host/tor/README.md` (host Tor)
+- Grok workflow `koopa-services-health` (laptop public HTTPS + generated mon pages)
+- `scripts/taler-sanity/check_stack-health.sh` (deeper Taler, often root)
- units: `configs/systemd/user/koopa-login-health.{service,timer}`
diff --git a/scripts/login-health/koopa-login-health.sh b/scripts/login-health/koopa-login-health.sh
index 80ebdbc..aa9c2db 100755
--- a/scripts/login-health/koopa-login-health.sh
+++ b/scripts/login-health/koopa-login-health.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# koopa-login-health — read-only GOA/koopa service health.
-# Inventory: configs/ports.md + arrays below (goa-regio-ng single CTR).
+# Inventory: koopa-admin-log host/overview/LIVE.md (+ ports.md).
#
# Architecture:
# collect — run probes, write snapshot (systemd user timer; NOT on login)
@@ -138,20 +138,24 @@ do_show() {
# ---------- COLLECT: probes → write snapshot ----------
CRITICAL_CONTAINERS=(
- goa-regio-ng
+ taler-hacktivism
+ taler-hacktivism-exchange-ansible
+ taler-hacktivism-bank
koopa-forgejo
)
EXPECT_CONTAINERS=(
- goa-regio-ng
+ taler-hacktivism
+ taler-hacktivism-exchange-ansible
+ taler-hacktivism-bank
koopa-castopod
koopa-bonfire
koopa-lemmy-proxy
- koopa-decidim
koopa-prime-jellyfin
koopa-prime-qbittorrent
koopa-forgejo
koopa-paivana
+ koopa-tor-relay
koopa-nym
koopa-tops-ng1
koopa-tops-ng2
@@ -162,13 +166,13 @@ OPTIONAL_CONTAINERS=(
koopa-prime-jellyfin
koopa-prime-qbittorrent
koopa-nym
+ koopa-tor-relay
koopa-tops-ng1
koopa-tops-ng2
koopa-tops-ng3
koopa-castopod
koopa-bonfire
koopa-lemmy-proxy
- koopa-decidim
koopa-paivana
)
@@ -181,7 +185,6 @@ PROBES=(
"castopod|http://127.0.0.1:9020/|200,302,303,307,308"
"bonfire|http://127.0.0.1:9021/|200,302,303,307,308"
"lemmy|http://127.0.0.1:9026/|200,302,303,307,308"
- "decidim|http://127.0.0.1:9027/|200,301,302,303,307,308"
"prime|http://127.0.0.1:9022/|200,302,303,307,308"
"bt|http://127.0.0.1:9023/|200,302,303,307,308,401"
"paivana|http://127.0.0.1:9025/|200,302,303,307,308"
@@ -190,7 +193,7 @@ PROBES=(
"tops-ng3|http://127.0.0.1:9092/|200,302,303"
)
-OPTIONAL_PROBES=(castopod bonfire lemmy decidim prime bt paivana tops-ng1 tops-ng2 tops-ng3)
+OPTIONAL_PROBES=(castopod bonfire lemmy prime bt paivana tops-ng1 tops-ng2 tops-ng3)
C_OK=; C_WARN=; C_FAIL=; C_DIM=; C_RST=; C_BOLD=
# colors only when writing for a tty later; snapshots are plain + optional ANSI if COLLECT_COLOR=1
@@ -262,12 +265,6 @@ do_collect() {
else
record FAIL "caddy.service is ${caddy_st} (edge HTTPS)"
fi
- tor_st=$(systemctl is-active tor 2>/dev/null || echo unknown)
- if [ "${tor_st}" = "active" ]; then
- record OK "tor.service active"
- else
- record WARN "tor.service is ${tor_st} (host ORPort/ControlPort)"
- fi
for sock in http-proxy.socket https-proxy.socket; do
st=$(systemctl is-active "${sock}" 2>/dev/null || echo unknown)
if [ "${st}" = "active" ] || [ "${st}" = "listening" ]; then
@@ -280,34 +277,6 @@ do_collect() {
record WARN "systemctl not available"
fi
- # Reboot survival: linger + enabled user units (public stacks).
- if command -v loginctl >/dev/null 2>&1; then
- linger_st=$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)
- if [ "${linger_st}" = "yes" ]; then
- record OK "linger=yes ($(id -un))"
- else
- record FAIL "linger=${linger_st} (user units will not start after reboot without login)"
- fi
- else
- record WARN "loginctl not available (cannot check linger)"
- fi
- if command -v systemctl >/dev/null 2>&1 && [ "$(id -u 2>/dev/null)" != "0" ]; then
- local u en
- for u in \
- container-koopa-lemmy-db.service \
- container-koopa-lemmy.service \
- container-koopa-decidim-db.service \
- container-koopa-decidim.service
- do
- en=$(systemctl --user is-enabled "${u}" 2>/dev/null || echo unknown)
- if [ "${en}" = "enabled" ]; then
- record OK "unit ${u} enabled"
- else
- record WARN "unit ${u} is ${en} (will not start on reboot)"
- fi
- done
- fi
-
if command -v ss >/dev/null 2>&1; then
listen_blob=$(ss -lnt 2>/dev/null | awk '{print $4}' || true)
for port in 9000 9001 9010 9011 9012 9024 9200; do
@@ -317,7 +286,7 @@ do_collect() {
record FAIL "nothing listening on :${port}"
fi
done
- for port in 9020 9021 9022 9023 9025 9026 9027 9090 9091 9092 8080 9080; do
+ for port in 9020 9021 9022 9023 9025 9026 9090 9091 9092 8080 9080; do
if printf '%s\n' "${listen_blob}" | grep -qE "[:.]${port}\$"; then
record OK "listen :${port} (optional)"
else
@@ -354,7 +323,7 @@ do_collect() {
fi
fi
done
- for dep in koopa-forgejo-db koopa-castopod-mariadb koopa-castopod-redis koopa-bonfire-db koopa-lemmy-db koopa-decidim-db koopa-decidim-redis; do
+ for dep in koopa-forgejo-db koopa-castopod-mariadb koopa-castopod-redis koopa-bonfire-db koopa-lemmy-db; do
if printf '%s\n' "${running_names}" | grep -qxF "${dep}"; then
record INFO "dep ${dep} running"
else
diff --git a/scripts/meet/build.sh b/scripts/meet/build.sh
deleted file mode 100755
index 377b0ec..0000000
--- a/scripts/meet/build.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-# Build Meet web stub on CachyOS (run on koopa).
-set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CTX="${ROOT}/configs/meet"
-TAG="${IMAGE_TAG:-localhost/koopa-meet-web:cachyos}"
-cd "$CTX"
-[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
-echo "build $TAG ctx=$CTX"
-podman build -t "$TAG" -f Containerfile .
-echo "OK: $TAG"
diff --git a/scripts/notes/README.md b/scripts/notes/README.md
deleted file mode 100644
index 4d6bbf9..0000000
--- a/scripts/notes/README.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# SilverBullet — scripts (koopa)
-
-Product **FreeNotes** is separate (native enc). These scripts operate the parked **SilverBullet** web stack on port **9028**.
-
-| Script | Role |
-|--------|------|
-| `status.sh` | Read-only dirs / containers / `:9028` |
-| `build.sh` | `podman build` → `localhost/koopa-silverbullet:cachyos` |
-| `up.sh` | `podman-compose up` in `~/koopa-silverbullet` + health |
-| `cutover-to-silverbullet.sh` | Live freenotes → silverbullet (default **dry-run**; `--apply` mutates) |
-| `prepare-basicauth-seed.sh` | SoT → `/tmp/silverbullet-basicauth-seed.txt` (no sudo) |
-
-Caddy (sudo, separate): `scripts/caddy/merge-notes-site.sh`, `apply-notes-basicauth.sh`.
-
-## Cutover order (needs Freigabe + DNS)
-
-On koopa as `hernani` (admin-log clone from Forgejo when needed):
-
-```bash
-bash …/scripts/notes/status.sh
-bash …/scripts/notes/cutover-to-silverbullet.sh # dry-run
-bash …/scripts/notes/cutover-to-silverbullet.sh --apply # Freigabe
-```
-
-Then Caddy (root) + seed from laptop SoT:
-
-```bash
-# laptop (unlocked secrets-enc):
-bash …/scripts/notes/prepare-basicauth-seed.sh
-scp /tmp/silverbullet-basicauth-seed.txt hernani@koopa:/tmp/
-
-# koopa root:
-sudo bash …/scripts/caddy/merge-notes-site.sh
-sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt \
- bash …/scripts/caddy/apply-notes-basicauth.sh
-```
-
-DNS: `silverbullet.hacktivism.ch` → koopa (human). Public: `https://silverbullet.hacktivism.ch/`.
-
-Keeps on cutover: `space/`, `.env`. Refreshes compose/Containerfile/entrypoint/unit from `configs/notes/`.
diff --git a/scripts/notes/build.sh b/scripts/notes/build.sh
deleted file mode 100755
index 80b4bc3..0000000
--- a/scripts/notes/build.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env bash
-# Build SilverBullet image on CachyOS base (run on koopa).
-# bash ~/koopa-admin-log/scripts/notes/build.sh
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CTX="${ROOT}/configs/notes"
-TAG="${IMAGE_TAG:-localhost/koopa-silverbullet:cachyos}"
-SB_VERSION="${SB_VERSION:-2.10.0}"
-
-cd "$CTX"
-[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
-[[ -f entrypoint.sh ]] || { echo "ERROR: missing $CTX/entrypoint.sh" >&2; exit 1; }
-
-echo "build $TAG (SB_VERSION=$SB_VERSION) ctx=$CTX"
-podman build \
- --build-arg "SB_VERSION=${SB_VERSION}" \
- -t "$TAG" \
- -f Containerfile \
- .
-
-echo "OK: $TAG"
-podman image inspect "$TAG" --format '{{.Id}} {{.Size}} {{.Architecture}}' 2>/dev/null || true
diff --git a/scripts/notes/cutover-to-silverbullet.sh b/scripts/notes/cutover-to-silverbullet.sh
deleted file mode 100755
index af3f37b..0000000
--- a/scripts/notes/cutover-to-silverbullet.sh
+++ /dev/null
@@ -1,162 +0,0 @@
-#!/usr/bin/env bash
-# Cut over live stack freenotes → silverbullet on koopa (hernani).
-# Default: dry-run. Mutate only with --apply.
-#
-# bash /path/to/koopa-admin-log/scripts/notes/cutover-to-silverbullet.sh
-# bash /path/to/koopa-admin-log/scripts/notes/cutover-to-silverbullet.sh --apply
-#
-# Keeps: space/, .env. Refreshes: compose.yml, Containerfile, entrypoint.sh, systemd unit.
-# Image: retag localhost/koopa-freenotes:cachyos → localhost/koopa-silverbullet:cachyos (no rebuild).
-# Does NOT touch Caddy (sudo) — print next steps only.
-set -euo pipefail
-
-APPLY=0
-for a in "$@"; do
- case "$a" in
- --apply) APPLY=1 ;;
- -h|--help)
- sed -n '2,12p' "$0"
- exit 0
- ;;
- *)
- echo "ERROR: unknown arg: $a (use --apply)" >&2
- exit 2
- ;;
- esac
-done
-
-OLD_DIR="${HOME}/koopa-freenotes"
-NEW_DIR="${HOME}/koopa-silverbullet"
-OLD_CT=koopa-freenotes
-NEW_CT=koopa-silverbullet
-OLD_IMG=localhost/koopa-freenotes:cachyos
-NEW_IMG=localhost/koopa-silverbullet:cachyos
-PORT=9028
-
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CFG="${ROOT}/configs/notes"
-
-[[ -d "$CFG" ]] || { echo "ERROR: missing $CFG" >&2; exit 1; }
-[[ -f "$CFG/compose.yml" ]] || { echo "ERROR: missing $CFG/compose.yml" >&2; exit 1; }
-
-mode=dry-run
-[[ "$APPLY" -eq 1 ]] && mode=apply
-echo "STATUS=start MODE=$mode HOST=$(hostname -s) OLD_DIR=$OLD_DIR NEW_DIR=$NEW_DIR"
-
-have_old=0
-have_new=0
-[[ -d "$OLD_DIR" ]] && have_old=1
-[[ -d "$NEW_DIR" ]] && have_new=1
-echo "STATUS=dirs OLD=$have_old NEW=$have_new"
-
-ct_old=$(podman ps -a --format '{{.Names}}' 2>/dev/null | grep -Fx "$OLD_CT" || true)
-ct_new=$(podman ps -a --format '{{.Names}}' 2>/dev/null | grep -Fx "$NEW_CT" || true)
-echo "STATUS=containers OLD=${ct_old:-none} NEW=${ct_new:-none}"
-
-img_old=$(podman images -q "$OLD_IMG" 2>/dev/null || true)
-img_new=$(podman images -q "$NEW_IMG" 2>/dev/null || true)
-echo "STATUS=images OLD=${img_old:-none} NEW=${img_new:-none}"
-
-dir_err=0
-if [[ "$have_old" -eq 0 && "$have_new" -eq 1 ]]; then
- echo "STATUS=already_cutover DIR=$NEW_DIR"
-elif [[ "$have_old" -eq 0 && "$have_new" -eq 0 ]]; then
- echo "STATUS=warn neither $OLD_DIR nor $NEW_DIR (ok for laptop dry-run; apply needs koopa)"
- dir_err=1
-elif [[ "$have_old" -eq 1 && "$have_new" -eq 1 ]]; then
- echo "STATUS=warn both dirs exist — resolve manually before --apply"
- dir_err=1
-fi
-
-plan() { echo "PLAN: $*"; }
-
-plan "stop+rm container $OLD_CT (if present)"
-plan "mv $OLD_DIR → $NEW_DIR (if old present)"
-plan "copy compose/Containerfile/entrypoint/unit from $CFG (keep space .env)"
-plan "podman tag $OLD_IMG $NEW_IMG (if new image missing)"
-plan "podman-compose up -d in $NEW_DIR"
-plan "health curl :$PORT"
-plan "optional: systemctl --user enable container-koopa-silverbullet.service"
-
-if [[ "$APPLY" -ne 1 ]]; then
- echo "STATUS=dry-run NEXT=re-run on koopa with --apply (Freigabe)"
- echo "STATUS=caddy_next sudo bash $ROOT/scripts/caddy/merge-notes-site.sh"
- echo "STATUS=caddy_next sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt bash $ROOT/scripts/caddy/apply-notes-basicauth.sh"
- echo "STATUS=dns_need silverbullet.hacktivism.ch → koopa"
- exit 0
-fi
-
-if [[ "$dir_err" -eq 1 ]]; then
- echo "ERROR: refuse --apply with ambiguous/missing live dirs" >&2
- exit 1
-fi
-
-if [[ -n "$ct_old" ]]; then
- echo "STATUS=stop $OLD_CT"
- podman stop -t 20 "$OLD_CT" || true
- podman rm "$OLD_CT" || true
-fi
-
-if [[ "$have_old" -eq 1 ]]; then
- echo "STATUS=mv $OLD_DIR → $NEW_DIR"
- mv "$OLD_DIR" "$NEW_DIR"
-fi
-
-cd "$NEW_DIR"
-# Refresh stack files from admin-log; never overwrite space/ or .env
-cp -a "$CFG/compose.yml" "$NEW_DIR/compose.yml"
-cp -a "$CFG/Containerfile" "$NEW_DIR/Containerfile"
-cp -a "$CFG/entrypoint.sh" "$NEW_DIR/entrypoint.sh"
-chmod +x "$NEW_DIR/entrypoint.sh"
-cp -a "$CFG/container-koopa-silverbullet.service" "$NEW_DIR/container-koopa-silverbullet.service"
-cp -a "$CFG/.env.example" "$NEW_DIR/.env.example"
-# Drop obsolete unit name if present
-rm -f "$NEW_DIR/container-koopa-freenotes.service"
-echo "STATUS=refreshed_stack_files DIR=$NEW_DIR"
-
-if [[ -z "$img_new" ]]; then
- if [[ -n "$img_old" ]]; then
- echo "STATUS=tag $OLD_IMG → $NEW_IMG"
- podman tag "$OLD_IMG" "$NEW_IMG"
- else
- echo "STATUS=build missing image via build.sh"
- bash "$ROOT/scripts/notes/build.sh"
- fi
-fi
-
-if [[ -n "$ct_new" ]]; then
- echo "STATUS=rm stale $NEW_CT"
- podman stop -t 10 "$NEW_CT" || true
- podman rm "$NEW_CT" || true
-fi
-
-echo "STATUS=compose_up"
-podman-compose up -d
-
-ok=0
-for i in $(seq 1 40); do
- code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" || true)
- echo "STATUS=health try=$i code=$code"
- case "$code" in
- 2*|3*) ok=1; break ;;
- esac
- sleep 3
-done
-[[ "$ok" -eq 1 ]] || { echo "ERROR: no answer on :${PORT}" >&2; podman-compose ps; exit 1; }
-
-# User systemd unit (optional; ignore failures)
-UNIT_SRC="$NEW_DIR/container-koopa-silverbullet.service"
-UNIT_DST="${HOME}/.config/systemd/user/container-koopa-silverbullet.service"
-if [[ -f "$UNIT_SRC" ]] && command -v systemctl >/dev/null; then
- mkdir -p "${HOME}/.config/systemd/user"
- cp -a "$UNIT_SRC" "$UNIT_DST"
- systemctl --user daemon-reload || true
- systemctl --user disable --now container-koopa-freenotes.service 2>/dev/null || true
- systemctl --user enable --now container-koopa-silverbullet.service 2>/dev/null || true
- echo "STATUS=systemd_user unit=container-koopa-silverbullet.service"
-fi
-
-echo "STATUS=ok CONTAINER=$NEW_CT DIR=$NEW_DIR PORT=$PORT"
-echo "STATUS=caddy_next sudo bash $ROOT/scripts/caddy/merge-notes-site.sh"
-echo "STATUS=caddy_next sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt bash $ROOT/scripts/caddy/apply-notes-basicauth.sh"
-echo "STATUS=dns_need silverbullet.hacktivism.ch → koopa"
diff --git a/scripts/notes/prepare-basicauth-seed.sh b/scripts/notes/prepare-basicauth-seed.sh
deleted file mode 100755
index a9a6f1a..0000000
--- a/scripts/notes/prepare-basicauth-seed.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env bash
-# Copy silverbullet basicauth SoT → /tmp seed for apply-notes-basicauth.sh.
-# Run on magikoopa (or any host with unlocked koopa-admin-secrets-enc).
-# Does NOT touch /etc/caddy (no sudo). Does NOT print secret contents.
-#
-# bash scripts/notes/prepare-basicauth-seed.sh
-# # then on koopa (after scp seed or shared path):
-# sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt \
-# bash …/scripts/caddy/apply-notes-basicauth.sh
-set -euo pipefail
-
-OUT="${NOTES_AUTH_SEED_OUT:-/tmp/silverbullet-basicauth-seed.txt}"
-SOT_DEFAULT="${HOME}/git/koopa-admin-secrets-enc/koopa/home-hernani/caddy/silverbullet-basicauth.txt"
-SOT="${NOTES_AUTH_SOT:-$SOT_DEFAULT}"
-
-[[ -f "$SOT" ]] || {
- echo "ERROR: missing SoT $SOT (unlock git-crypt / set NOTES_AUTH_SOT=)" >&2
- exit 1
-}
-
-umask 077
-cp -a "$SOT" "$OUT"
-chmod 600 "$OUT"
-echo "STATUS=ok OUT=$OUT BYTES=$(wc -c <"$OUT" | tr -d ' ') NEXT=scp_or_use_NOTES_AUTH_SEED"
-echo "STATUS=hint scp $OUT hernani@koopa:/tmp/ && sudo NOTES_AUTH_SEED=$OUT bash …/apply-notes-basicauth.sh"
diff --git a/scripts/notes/status.sh b/scripts/notes/status.sh
deleted file mode 100755
index 6ae116e..0000000
--- a/scripts/notes/status.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env bash
-# Thin status for SilverBullet stack on koopa (hernani). Safe read-only.
-# bash scripts/notes/status.sh
-set -euo pipefail
-
-OLD_DIR="${HOME}/koopa-freenotes"
-NEW_DIR="${HOME}/koopa-silverbullet"
-OLD_CT=koopa-freenotes
-NEW_CT=koopa-silverbullet
-OLD_IMG=localhost/koopa-freenotes:cachyos
-NEW_IMG=localhost/koopa-silverbullet:cachyos
-PORT=9028
-
-echo "STATUS=host HOST=$(hostname -s)"
-echo "STATUS=dirs OLD=$([ -d "$OLD_DIR" ] && echo 1 || echo 0) NEW=$([ -d "$NEW_DIR" ] && echo 1 || echo 0)"
-
-if command -v podman >/dev/null 2>&1; then
- ct_old=$(podman ps -a --format '{{.Names}} {{.Status}}' 2>/dev/null | grep -E "^${OLD_CT} " || true)
- ct_new=$(podman ps -a --format '{{.Names}} {{.Status}}' 2>/dev/null | grep -E "^${NEW_CT} " || true)
- echo "STATUS=ct_old ${ct_old:-none}"
- echo "STATUS=ct_new ${ct_new:-none}"
- img_old=$(podman images -q "$OLD_IMG" 2>/dev/null || true)
- img_new=$(podman images -q "$NEW_IMG" 2>/dev/null || true)
- echo "STATUS=img_old ${img_old:-none}"
- echo "STATUS=img_new ${img_new:-none}"
-else
- echo "STATUS=podman missing"
-fi
-
-code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 "http://127.0.0.1:${PORT}/" 2>/dev/null || echo fail)
-echo "STATUS=local_http PORT=$PORT CODE=$code"
-
-if [[ -d "$NEW_DIR" ]]; then
- echo "STATUS=cutover done DIR=$NEW_DIR"
-elif [[ -d "$OLD_DIR" ]]; then
- echo "STATUS=cutover pending DIR=$OLD_DIR NEXT=cutover-to-silverbullet.sh"
-else
- echo "STATUS=cutover unknown (no live dir)"
-fi
diff --git a/scripts/notes/up.sh b/scripts/notes/up.sh
deleted file mode 100755
index 0d44305..0000000
--- a/scripts/notes/up.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env bash
-# Bring up koopa-silverbullet / SilverBullet.
-set -euo pipefail
-DIR="${HOME}/koopa-silverbullet"
-cd "${DIR}"
-set -a
-# shellcheck disable=SC1091
-[[ -f .env ]] && source .env
-set +a
-PORT="${HOST_PORT:-9028}"
-podman-compose up -d
-echo "== wait for :${PORT} =="
-ok=0
-for i in $(seq 1 40); do
- code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" || true)
- echo "try $i: $code"
- case "$code" in
- 2*|3*) ok=1; break ;;
- esac
- sleep 3
-done
-[[ "$ok" -eq 1 ]] || { echo "ERROR: no answer on ${PORT}"; podman-compose ps; exit 1; }
-echo "local ok. public (after Caddy+DNS): https://silverbullet.hacktivism.ch/"
diff --git a/scripts/pepsi/ensure-mon-users.sh b/scripts/pepsi/ensure-mon-users.sh
deleted file mode 100755
index 661edaa..0000000
--- a/scripts/pepsi/ensure-mon-users.sh
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/env bash
-# Idempotent: ensure mon-* OS users + Maildir inside koopa-pepsi.
-# Needed after every container recreate: /home is not in the image and is
-# not a volume by default → useradd state vanishes while DB login_assignment
-# survives.
-#
-# UIDs fixed 1000–1004 (match login_assignment / prior live).
-# STATUS= only; no secrets. Run on koopa as hernani (rootless podman exec).
-set -euo pipefail
-
-CTR="${PEPSI_CTR:-koopa-pepsi}"
-USERS=(
- "mon-smtp:1000"
- "mon-maildir:1001"
- "mon-dkim:1002"
- "mon-bounce:1003"
- "mon-local:1004"
-)
-
-echo "STATUS=start CTR=$CTR HOST=$(hostname -s)"
-
-if ! podman container exists "$CTR" 2>/dev/null; then
- echo "STATUS=missing_ctr CTR=$CTR"
- exit 2
-fi
-if [[ "$(podman inspect -f '{{.State.Running}}' "$CTR" 2>/dev/null || echo false)" != "true" ]]; then
- echo "STATUS=ctr_not_running CTR=$CTR"
- exit 3
-fi
-
-created=0
-present=0
-for entry in "${USERS[@]}"; do
- u="${entry%%:*}"
- uid="${entry##*:}"
- if podman exec "$CTR" getent passwd "$u" >/dev/null 2>&1; then
- echo "USER=$u STATE=present"
- present=$((present + 1))
- else
- podman exec -u root "$CTR" useradd -u "$uid" -U -m -d "/home/$u" -s /usr/sbin/nologin "$u"
- echo "USER=$u STATE=created UID=$uid"
- created=$((created + 1))
- fi
- # Maildir skeleton (cur/new/tmp) — safe if already present
- podman exec -u root "$CTR" bash -c "
- set -e
- home=/home/$u
- mkdir -p \"\$home/Maildir\"/{cur,new,tmp}
- chown -R $u:$u \"\$home\"
- chmod 700 \"\$home\"
- "
- echo "MAILDIR=$u OK=1"
-done
-
-echo "STATUS=done CREATED=$created PRESENT_BEFORE=$present EXPECT=${#USERS[@]}"
-podman exec "$CTR" getent passwd | grep '^mon-' || true
-exit 0
diff --git a/scripts/pepsi/firewalld-pepsi-highports.sh b/scripts/pepsi/firewalld-pepsi-highports.sh
deleted file mode 100755
index 1387fa1..0000000
--- a/scripts/pepsi/firewalld-pepsi-highports.sh
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env bash
-# Open rootless pepsi high-ports on koopa (firewalld).
-# Compose map (LAN bind 192.168.100.95):
-# 1025→25 1465→465 1587→587 1993→993 18443→443
-# Freigabe: human this session. Run on koopa as root / sudo.
-set -euo pipefail
-
-PORTS=(1025/tcp 1465/tcp 1587/tcp 1993/tcp 18443/tcp)
-ZONE="${FIREWALLD_ZONE:-}"
-
-if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
- echo "STATUS=need_root HINT='sudo $0'"
- exit 2
-fi
-
-if ! command -v firewall-cmd >/dev/null 2>&1; then
- echo "STATUS=no_firewalld"
- exit 3
-fi
-
-if ! firewall-cmd --state >/dev/null 2>&1; then
- echo "STATUS=firewalld_down"
- exit 4
-fi
-
-if [[ -z "$ZONE" ]]; then
- # Prefer the zone that owns the LAN NIC; else default.
- ZONE=$(firewall-cmd --get-zone-of-interface=eno1 2>/dev/null || true)
- if [[ -z "$ZONE" ]]; then
- ZONE=$(firewall-cmd --get-default-zone)
- fi
-fi
-
-echo "STATUS=start ZONE=$ZONE HOST=$(hostname -s)"
-
-added=0
-present=0
-for p in "${PORTS[@]}"; do
- if firewall-cmd --zone="$ZONE" --query-port="$p" >/dev/null 2>&1; then
- echo "PORT=$p STATE=present"
- present=$((present + 1))
- else
- firewall-cmd --zone="$ZONE" --add-port="$p" --permanent
- echo "PORT=$p STATE=added_permanent"
- added=$((added + 1))
- fi
-done
-
-firewall-cmd --reload
-echo "STATUS=reloaded"
-
-ok=0
-for p in "${PORTS[@]}"; do
- if firewall-cmd --zone="$ZONE" --query-port="$p" >/dev/null 2>&1; then
- echo "VERIFY=$p OK=1"
- ok=$((ok + 1))
- else
- echo "VERIFY=$p OK=0"
- fi
-done
-
-echo "STATUS=done ZONE=$ZONE ADDED=$added PRESENT_BEFORE=$present VERIFY_OK=$ok EXPECT=${#PORTS[@]}"
-if [[ "$ok" -eq "${#PORTS[@]}" ]]; then
- exit 0
-fi
-exit 1
diff --git a/scripts/pepsi/hostnet-pepsi-highports.sh b/scripts/pepsi/hostnet-pepsi-highports.sh
deleted file mode 100755
index aeacc55..0000000
--- a/scripts/pepsi/hostnet-pepsi-highports.sh
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/usr/bin/env bash
-# Switch koopa-pepsi to network_mode:host on high ports so SMTP client IP
-# is preserved (rootlessport SNAT otherwise forces 10.89.16.4 → SPF fail).
-#
-# Vecigate stays pure DNAT (25/465/587 → 1025/1465/1587). No mail on router.
-# DB stays bridged; publish 127.0.0.1:15432→5432 for host-net pepsi.
-#
-# Freigabe: human this session (Script hinlegen + apply+restart).
-# Run on koopa as hernani (rootless podman). Never echoes DB secrets.
-set -euo pipefail
-
-PEPSI_DIR="${PEPSI_DIR:-$HOME/koopa-pepsi}"
-TS="$(date +%Y%m%d-%H%M%S)"
-BAK="${PEPSI_DIR}/bak-hostnet-${TS}"
-COMPOSE_BIN=""
-
-echo "STATUS=start HOST=$(hostname -s) DIR=$PEPSI_DIR"
-
-if [[ ! -d "$PEPSI_DIR" ]]; then
- echo "STATUS=missing_dir PATH=$PEPSI_DIR"
- exit 2
-fi
-cd "$PEPSI_DIR"
-
-if command -v podman-compose >/dev/null 2>&1; then
- COMPOSE_BIN="podman-compose"
-elif podman compose version >/dev/null 2>&1; then
- COMPOSE_BIN="podman compose"
-else
- echo "STATUS=no_compose"
- exit 3
-fi
-
-if [[ ! -f compose.yml || ! -f conf/pepsi.conf ]]; then
- echo "STATUS=missing_files"
- exit 4
-fi
-
-already=0
-if grep -q 'network_mode:[[:space:]]*host' compose.yml \
- && grep -qE '^PORT = 1025$' conf/pepsi.conf \
- && grep -qE '@127\.0\.0\.1:15432/' conf/pepsi.conf; then
- already=1
- echo "STATUS=already_hostnet HINT=skip_patch"
-fi
-
-if [[ "$already" -eq 0 ]]; then
- mkdir -p "$BAK"
- cp -a compose.yml conf/pepsi.conf "$BAK/"
- echo "STATUS=bak PATH=$BAK"
-
- python3 - <<'PY'
-from pathlib import Path
-import re
-import sys
-
-root = Path(".")
-compose_path = root / "compose.yml"
-conf_path = root / "conf" / "pepsi.conf"
-compose = compose_path.read_text()
-conf = conf_path.read_text()
-
-# Split services crudely on "\n pepsi:" / "\n db:"
-def patch_compose(text: str) -> str:
- # Retarget env DB URLs first (password stays ${VAR})
- text = text.replace("@db:5432/", "@127.0.0.1:15432/")
-
- # Ensure db publishes 127.0.0.1:15432:5432
- if "15432:5432" not in text:
- text = re.sub(
- r"(container_name: koopa-pepsi-db\n)",
- r'\1 ports:\n - "127.0.0.1:15432:5432"\n',
- text,
- count=1,
- )
-
- # pepsi service: add network_mode host; remove its ports: block
- # Match from " pepsi:" until next top-level service or "volumes:"
- m = re.search(r"(?ms)^ pepsi:\n(.*?)(?=^volumes:|^ [a-z])", text)
- if not m:
- print("STATUS=pepsi_service_not_found")
- sys.exit(5)
- body = m.group(1)
- # drop ports block inside pepsi
- body2 = re.sub(r"(?m)^ ports:\n(?: [^\n]*\n)+", "", body)
- if "network_mode:" not in body2:
- body2 = re.sub(
- r"(container_name: koopa-pepsi\n)",
- r"\1 network_mode: host\n",
- body2,
- count=1,
- )
- text = text[: m.start(1)] + body2 + text[m.end(1) :]
- return text
-
-
-def set_port(text: str, section: str, port: int) -> str:
- pat = rf"(\[{re.escape(section)}\]\n(?:.*\n)*?PORT = )\d+"
- new, n = re.subn(pat, rf"\g<1>{port}", text, count=1)
- if n != 1:
- print(f"STATUS=port_patch_fail SECTION={section} N={n}")
- sys.exit(6)
- return new
-
-
-compose = patch_compose(compose)
-conf = conf.replace("@db:5432/", "@127.0.0.1:15432/")
-conf = set_port(conf, "pepsi-ingress-listener-mx", 1025)
-conf = set_port(conf, "pepsi-ingress-listener-submissions", 1465)
-conf = set_port(conf, "pepsi-ingress-listener-submission", 1587)
-conf = set_port(conf, "pepsi-httpd-listener-https", 18443)
-
-compose_path.write_text(compose)
-conf_path.write_text(conf)
-
-# sanity STATUS (no secrets)
-checks = [
- ("network_mode_host", bool(re.search(r"(?m)^ network_mode:\s*host\s*$", compose))),
- ("db_pub_15432", "15432:5432" in compose),
- ("no_db_hostname", "@db:5432/" not in compose and "@db:5432/" not in conf),
- ("port_1025", bool(re.search(r"(?m)^PORT = 1025\s*$", conf))),
- ("port_1465", bool(re.search(r"(?m)^PORT = 1465\s*$", conf))),
- ("port_1587", bool(re.search(r"(?m)^PORT = 1587\s*$", conf))),
- ("port_18443", bool(re.search(r"(?m)^PORT = 18443\s*$", conf))),
- ("pepsi_ports_gone", "192.168.100.95:1025" not in compose),
-]
-for k, v in checks:
- print(f"CHECK={k} OK={int(v)}")
-if not all(v for _, v in checks):
- print("STATUS=patch_sanity_fail")
- sys.exit(7)
-print("STATUS=patched")
-PY
-fi
-
-echo "STATUS=compose_up BIN=$COMPOSE_BIN"
-# shellcheck disable=SC2086
-$COMPOSE_BIN up -d --force-recreate
-sleep 4
-
-echo "STATUS=verify"
-ss_out="$(ss -lntp 2>/dev/null | grep -E ':1025|:1465|:1587|:18443|:15432' || true)"
-# redact pid/user clutter lightly
-echo "$ss_out" | sed -E 's/users:\(\("([^"]+)".*/users=(("\1"…)/' || true
-
-rootless_hit=0
-if echo "$ss_out" | grep -q rootlessport; then
- rootless_hit=1
-fi
-db_pub=0
-if echo "$ss_out" | grep -qE '127\.0\.0\.1:15432'; then
- db_pub=1
-fi
-mx_ok=0
-if echo "$ss_out" | grep -qE ':1025\b'; then
- mx_ok=1
-fi
-
-podman ps --format '{{.Names}} {{.Status}}' | grep pepsi || true
-
-if command -v pg_isready >/dev/null 2>&1; then
- if pg_isready -h 127.0.0.1 -p 15432 -d pepsi >/dev/null 2>&1; then
- echo "STATUS=db_ready HOST=127.0.0.1 PORT=15432"
- else
- echo "STATUS=db_not_ready HOST=127.0.0.1 PORT=15432"
- fi
-else
- if timeout 2 bash -c 'echo >/dev/tcp/127.0.0.1/15432' 2>/dev/null; then
- echo "STATUS=db_tcp_open HOST=127.0.0.1 PORT=15432"
- else
- echo "STATUS=db_tcp_closed HOST=127.0.0.1 PORT=15432"
- fi
-fi
-
-# recent log lines (strip any postgres URLs)
-podman logs --tail 40 koopa-pepsi 2>/dev/null \
- | grep -Ei 'listen|error|fatal|spf|client|1025|postgres|ready' \
- | sed -E 's#postgres://[^[:space:]]+#postgres://*#g' \
- | tail -12 || true
-
-echo "STATUS=done MX_1025=$mx_ok DB_PUB=$db_pub ROOTLESSPORT_ON_HIGH=$rootless_hit BAK=${BAK:-none}"
-echo "HINT=next ensure-mon-users.sh after recreate; then taler-mail send --expect-pong"
-echo "HINT=Vecigate DNAT unchanged (no mail on router); /home ephemeral unless volume+ensure"
-# Optional: run ensure if sibling script present (same dir or PEPSI_DIR/scripts)
-ENSURE=""
-here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-for cand in "$here/ensure-mon-users.sh" "$PEPSI_DIR/scripts/ensure-mon-users.sh" "$PEPSI_DIR/ensure-mon-users.sh"; do
- if [[ -x "$cand" ]]; then
- ENSURE="$cand"
- break
- fi
-done
-if [[ -n "$ENSURE" ]]; then
- echo "STATUS=ensure_mon_users SCRIPT=$ENSURE"
- PEPSI_CTR=koopa-pepsi "$ENSURE" || echo "STATUS=ensure_warn"
-fi
-if [[ "$mx_ok" -eq 1 && "$rootless_hit" -eq 0 && "$db_pub" -eq 1 ]]; then
- exit 0
-fi
-if [[ "$mx_ok" -eq 1 && "$db_pub" -eq 1 ]]; then
- echo "STATUS=warn_rootlessport_still_present"
- exit 0
-fi
-exit 1
diff --git a/scripts/podman-rss.sh b/scripts/podman-rss.sh
deleted file mode 100644
index 5e88d6e..0000000
--- a/scripts/podman-rss.sh
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/usr/bin/env bash
-# Human-readable container RSS when rootless podman stats shows 0B / host RAM.
-# Cause on openSUSE: user@.service Delegate= (20-defaults-SUSE.conf) clears
-# memory/cpu controllers → no memory.current. CPU% from podman stats still works.
-# Usage: podman-rss.sh [--json] [NAME_FILTER…]
-set -euo pipefail
-
-JSON=0
-FILTERS=()
-for a in "$@"; do
- case "$a" in
- --json|-j) JSON=1 ;;
- -h|--help)
- echo "usage: $0 [--json] [NAME_FILTER…]" >&2
- exit 0
- ;;
- *) FILTERS+=("$a") ;;
- esac
-done
-
-human() {
- # stdin: kibibytes integer
- awk '
- {
- k=$1+0
- if (k >= 1048576) printf "%.1f GiB", k/1048576
- else if (k >= 1024) printf "%.0f MiB", k/1024
- else printf "%d KiB", k
- }'
-}
-
-# CPU map: name -> cpu%
-declare -A CPU=()
-while IFS=$'\t' read -r name cpu; do
- [ -n "$name" ] || continue
- CPU["$name"]="$cpu"
-done < <(podman stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}' 2>/dev/null || true)
-
-rows=()
-total_kib=0
-while IFS= read -r cid; do
- [ -n "$cid" ] || continue
- name=$(podman inspect -f '{{.Name}}' "$cid" 2>/dev/null || true)
- [ -n "$name" ] || continue
- if [ ${#FILTERS[@]} -gt 0 ]; then
- ok=0
- for f in "${FILTERS[@]}"; do
- case "$name" in *"$f"*) ok=1; break ;; esac
- done
- [ "$ok" -eq 1 ] || continue
- fi
- # podman top rss is KiB per process; sum
- kib=$(podman top "$cid" -o rss 2>/dev/null | awk 'NR>1{s+=$1} END{print s+0}')
- cpu="${CPU[$name]:-?}"
- rows+=("${kib}|${name}|${cpu}")
- total_kib=$((total_kib + kib))
-done < <(podman ps -q)
-
-IFS=$'\n' sorted=($(printf '%s\n' "${rows[@]:-}" | sort -t'|' -k1,1nr))
-unset IFS
-
-if [ "$JSON" -eq 1 ]; then
- python3 - "$total_kib" "${sorted[@]:-}" <<'PY'
-import json, sys
-total = int(sys.argv[1]) if len(sys.argv) > 1 else 0
-items = []
-for row in sys.argv[2:]:
- if not row.strip():
- continue
- kib, name, cpu = row.split("|", 2)
- items.append({"name": name, "rss_kib": int(kib), "cpu": cpu})
-print(json.dumps({"ok": True, "total_rss_kib": total, "containers": items}, indent=2))
-PY
- exit 0
-fi
-
-printf '%-44s %10s %8s\n' "NAME" "RSS" "CPU%"
-printf '%-44s %10s %8s\n' "----" "---" "----"
-for row in "${sorted[@]:-}"; do
- [ -n "$row" ] || continue
- kib="${row%%|*}"
- rest="${row#*|}"
- name="${rest%%|*}"
- cpu="${rest##*|}"
- hum=$(printf '%s' "$kib" | human)
- printf '%-44s %10s %8s\n' "$name" "$hum" "$cpu"
-done
-printf '%-44s %10s\n' "TOTAL" "$(printf '%s' "$total_kib" | human)"
-echo "# note: RSS via podman top (no memory cgroup); CPU via podman stats" >&2
diff --git a/scripts/prime/apply-qbittorrent-bt-net.sh b/scripts/prime/apply-qbittorrent-bt-net.sh
deleted file mode 100755
index fab7fdb..0000000
--- a/scripts/prime/apply-qbittorrent-bt-net.sh
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/usr/bin/env bash
-# Apply qBittorrent BT net fixes on koopa (flame / firewalled inbound).
-#
-# Supersedes session one-shot /tmp/koopa-bt-fix.sh (2026-09-25).
-#
-# On koopa (hernani; firewalld needs sudo):
-# bash /path/to/koopa-admin-log/scripts/prime/apply-qbittorrent-bt-net.sh
-# # or after clone:
-# bash ~/koopa-admin-log/scripts/prime/apply-qbittorrent-bt-net.sh
-#
-# What it does:
-# 1) /downloads/incomplete/ as container user abc (host uid 100999)
-# 2) PortForwardingEnabled + Connection\UPnP = true (edit inside container)
-# 3) firewalld public: 6881/tcp + 6881/udp (sudo)
-# 4) podman restart koopa-prime-qbittorrent + STATUS= lines
-#
-# WAN inbound: VeciGate DNAT 6881 tcp+udp → koopa:6881 (+ hairpin LAN/LAN2)
-# applied 2026-09-25 (comment~qBittorrent). Re-check with script footer STATUS.
-#
-# Freigabe: podman + firewalld (session Freigabe required for live apply).
-set -euo pipefail
-
-CTR=koopa-prime-qbittorrent
-CONF_IN=/config/qBittorrent/qBittorrent.conf
-BT_PORT="${BT_PORT:-6881}"
-STAMP="$(date +%Y%m%d-%H%M%S)"
-
-die() { echo "ERROR: $*" >&2; exit 1; }
-
-command -v podman >/dev/null || die "podman missing"
-podman container exists "$CTR" || die "container $CTR missing"
-
-echo "===1_INCOMPLETE==="
-podman exec -u abc "$CTR" sh -c '
- set -e
- mkdir -p /downloads/incomplete
- ls -lad /downloads /downloads/incomplete
- touch /downloads/incomplete/.writeok && rm /downloads/incomplete/.writeok
- echo incomplete_write=OK
-'
-
-echo "===2_CONF_UPNP_PF==="
-# Host hernani cannot write config (owned by mapped abc/100999). Edit as abc.
-podman exec -u abc "$CTR" sh -c "
- set -e
- CONF='$CONF_IN'
- test -f \"\$CONF\" || { echo missing_conf; exit 1; }
- cp -a \"\$CONF\" \"\${CONF}.bak-btfix-$STAMP\"
- echo bak=\${CONF}.bak-btfix-$STAMP
- python3 - \"\$CONF\" <<'PY'
-import sys
-from pathlib import Path
-p = Path(sys.argv[1])
-text = p.read_text()
-orig = text
-repls = [
- ('PortForwardingEnabled=false', 'PortForwardingEnabled=true'),
- ('Connection\\\\UPnP=false', 'Connection\\\\UPnP=true'),
-]
-changed = False
-for a, b in repls:
- if a in text:
- text = text.replace(a, b)
- changed = True
-if not changed:
- # Already true, or unexpected keys — report and continue if already OK
- keys = [ln for ln in text.splitlines() if 'UPnP' in ln or 'PortForward' in ln]
- if any('=true' in ln for ln in keys) and not any('=false' in ln for ln in keys if 'UPnP' in ln or 'PortForward' in ln):
- print('already_ok')
- for ln in keys:
- print(ln)
- raise SystemExit(0)
- print('no_keys_replaced')
- for ln in keys:
- print(ln)
- raise SystemExit(2)
-p.write_text(text)
-print('replaced_ok')
-for ln in text.splitlines():
- if 'UPnP' in ln or 'PortForward' in ln:
- print(ln)
-PY
-"
-
-echo "===3_FIREWALLD==="
-fw_ok=0
-if command -v firewall-cmd >/dev/null 2>&1; then
- if sudo -n true 2>/dev/null; then
- SUDO=(sudo -n)
- elif [[ "$(id -u)" -eq 0 ]]; then
- SUDO=()
- else
- # Interactive sudo (Freigabe path)
- SUDO=(sudo)
- fi
- if "${SUDO[@]}" firewall-cmd --permanent --add-port="${BT_PORT}/tcp" \
- && "${SUDO[@]}" firewall-cmd --permanent --add-port="${BT_PORT}/udp" \
- && "${SUDO[@]}" firewall-cmd --reload; then
- echo "firewalld_ports=$("${SUDO[@]}" firewall-cmd --list-ports 2>/dev/null || true)"
- echo "firewalld=OK"
- fw_ok=1
- else
- echo "firewalld=FAIL"
- fi
-else
- echo "firewalld=MISSING_CMD"
-fi
-if [[ "$fw_ok" -ne 1 ]]; then
- echo "HINT: sudo firewall-cmd --permanent --add-port=${BT_PORT}/tcp"
- echo "HINT: sudo firewall-cmd --permanent --add-port=${BT_PORT}/udp"
- echo "HINT: sudo firewall-cmd --reload"
-fi
-
-echo "===4_RESTART==="
-podman restart "$CTR"
-sleep 4
-podman ps --filter "name=$CTR" --format '{{.Names}} {{.Status}} {{.Ports}}'
-
-echo "===5_POST==="
-podman exec -u abc "$CTR" sh -c "
- grep -E 'UPnP|PortForward' $CONF_IN || true
- ls -lad /downloads/incomplete
-"
-ss -lntup 2>/dev/null | grep -E "${BT_PORT}|9023" || true
-ss -lnup 2>/dev/null | grep "${BT_PORT}" || true
-
-echo "STATUS=apply_done ctr=$CTR bt_port=$BT_PORT firewalld_ok=$fw_ok stamp=$STAMP"
-# VeciGate DNAT live 2026-09-25: wan dstnat tcp+udp 6881 → 192.168.100.95:6881
-# + hairpin LAN 192.168.100.0/24 and LAN2 192.168.88.0/24 to 212.51.151.254:6881
-echo "STATUS=vecigate_dnat live wan+hairpin LAN/LAN2 → 192.168.100.95:$BT_PORT (comment~qBittorrent)"
-echo "STATUS=verify WAN: nc -vz 212.51.151.254 $BT_PORT ; LAN: nc -vz 192.168.100.95 $BT_PORT"
-echo "STATUS=verify WebUI connection icon (flame=firewalled) at bt.hacktivism.ch"
diff --git a/scripts/prime/koopa-bt-fix.sh b/scripts/prime/koopa-bt-fix.sh
deleted file mode 100755
index 18891b0..0000000
--- a/scripts/prime/koopa-bt-fix.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-# Compat wrapper: session name koopa-bt-fix.sh → durable apply.
-exec "$(cd "$(dirname "$0")" && pwd)/apply-qbittorrent-bt-net.sh" "$@"
diff --git a/scripts/taler-bank/README.md b/scripts/taler-bank/README.md
index d667a21..bf18099 100644
--- a/scripts/taler-bank/README.md
+++ b/scripts/taler-bank/README.md
@@ -1,6 +1,6 @@
# taler-bank scripts
-Container: **`goa-regio-ng`** (libeufin-bank, GOA, **no IBAN**; legacy name `taler-hacktivism-bank`).
+Container: **`taler-hacktivism-bank`** (libeufin-bank, GOA, **no IBAN**).
| File | Container path | User |
|------|----------------|------|
@@ -38,70 +38,42 @@ Details + JSON schema: `configs/bank-landing/README.md`.
## Demo withdraw + auto-account API
-**FP reference SoT:** `$HOME/git/admin-logs/taler/francpaysan-admin-log`
-(`files/testpaysan/demo-withdraw-api.py`, `files/caddy/Caddyfile` `@wdGet`).
-GOA ports that pattern: mint `amount`+`exchange_url`, and GET Integration
-injects `suggested_exchange` / `required_exchange` (Android/iOS).
-
-`demo-withdraw-api.py` listens on **127.0.0.1:19096** (in bank CTR; nginx on :9013):
+`demo-withdraw-api.py` listens on **127.0.0.1:19096** (proxied by nginx on the landing):
| Path | Behaviour |
|------|-----------|
| `GET /demo-withdraw.json` | Mint one-shot withdraw from shared **`explorer`** pool; write `withdraw.uri` + watch ids |
| `GET /auto-account.json` | Public `POST /accounts` with generated **`goa-account-`** user + password containing **pleasechangeme**; **balance GOA:0**; return credentials once |
-| `GET /taler-integration/withdrawal-operation/*` | Proxy libeufin + inject `suggested_exchange`/`required_exchange` |
-**Public path (Android):** Caddy `@wdGet` (GET only) → landing **:9013** → nginx → **:19096**.
-POST / other Integration stays Caddy → **:9012** (raw libeufin).
-FP stage proxies `@wdGet` straight to host `:19096`; GOA keeps 19096 CTR-only
-and routes via published 9013 instead.
-
-Install / restart (host, podman → `taler-hacktivism-bank`):
+Install / restart:
```bash
./install-demo-withdraw-api.sh
# Public checks:
curl -sS https://bank.hacktivism.ch/intro/demo-withdraw.json | head
curl -sS https://bank.hacktivism.ch/intro/auto-account.json | head # creates a real account
-# After Caddy @wdGet + nginx Integration are live:
-WID=… # from demo-withdraw.json
-curl -sS "https://bank.hacktivism.ch/taler-integration/withdrawal-operation/$WID" \
- | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("suggested_exchange"), d.get("required_exchange"))'
```
Requires **python3** in the bank container. Env: `BANK_URL`, `BANK_USER`/`BANK_PASS`
-(or `/root/bank-explorer-password.txt`), `AMOUNT` (default `GOA:10`),
-`EXCHANGE_URL` (default `https://exchange.hacktivism.ch/`).
-
-**Caddy (root on koopa — you apply):** insert `@wdGet` before the bank catch-all
-`reverse_proxy 127.0.0.1:9012` in `~/koopa-caddy/Caddyfile` (SoT:
-`configs/caddy/Caddyfile` / `host/caddy/Caddyfile`; paste-ready snippet:
-`scripts/caddy/wdGet-bank-snippet.caddy`, also copied live as
-`~/koopa-caddy/wdGet-bank-snippet.caddy`), then `sudo ~/bin/caddy-apply`.
-Caddy alone is not enough — demo-api + nginx Integration must be installed
-via `./install-demo-withdraw-api.sh` first.
+(or `/root/bank-explorer-password.txt`), `AMOUNT` (default `GOA:10` for shared withdraws).
### Auto-confirm (explorer only)
```bash
-# preferred: systemd inside CTR (Restart=always)
-systemctl status goa-auto-confirm-withdrawals.service
-# manual loop — refuses non-explorer unless ALLOW_NON_EXPLORER=1
+# loop inside container — refuses non-explorer unless ALLOW_NON_EXPLORER=1
auto-confirm-withdrawals.sh --loop 2
```
Only confirms withdrawals owned by **`explorer`** when status is `selected`
(community demo path). Does not confirm arbitrary customer withdraws.
-Skips amounts above explorer debt headroom (otherwise libeufin returns HTTP 409).
**Stable ops (via `koopa-external` if LAN `koopa` is down):**
-- Unit: `goa-auto-confirm-withdrawals.service` (install script enables it)
- One process only (`flock` on `/var/run/auto-confirm-withdrawals.lock`)
- `QUIET=1` + summary `tick checked=… selected=…` each loop
- Watch list capped (`WATCH_MAX=80`, prune of bloated `withdraw-watch.ids`)
- Status via `taler-integration/withdrawal-operation/{id}`
-- Reinstall: `BANK_CONTAINER=goa-regio-ng ./install-demo-withdraw-api.sh`
+- Reinstall: `./install-demo-withdraw-api.sh` on host with podman access to `taler-hacktivism-bank`
## Config
diff --git a/scripts/taler-bank/auto-confirm-withdrawals.sh b/scripts/taler-bank/auto-confirm-withdrawals.sh
index 1db8d32..a46c523 100755
--- a/scripts/taler-bank/auto-confirm-withdrawals.sh
+++ b/scripts/taler-bank/auto-confirm-withdrawals.sh
@@ -1,11 +1,6 @@
#!/bin/bash
# Auto-confirm bank withdrawals for the community demo pool ONLY (explorer).
#
-# Discovery: (1) landing watch files (withdraw.uri / withdraw-watch.ids),
-# (2) libeufin DB via local psql (selected && !confirmed && username=explorer).
-# Watch-file-only misses webui/wallet-selected ops not in the last WATCH_MAX
-# IDs — DB scan is required (same idea as testpaysan; here in-CTR, no podman).
-#
# Run once: auto-confirm-withdrawals.sh
# Loop: auto-confirm-withdrawals.sh --loop [SECS]
#
@@ -14,8 +9,6 @@
# BANK_USER (default explorer)
# BANK_PASS or /root/bank-explorer-password.txt
# LANDING_DIR (default /var/www/bank-landing)
-# DB_NAME (default libeufin)
-# DB_LIMIT max selected IDs from DB per tick (default 40)
# ALLOW_NON_EXPLORER=1
# WATCH_MAX max IDs kept in withdraw-watch.ids after prune (default 80)
# QUIET=1 less skip noise (default 1 in --loop)
@@ -29,8 +22,6 @@ LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
WATCH_FILE="${LANDING_DIR}/withdraw-watch.ids"
URI_FILE="${LANDING_DIR}/withdraw.uri"
WATCH_MAX="${WATCH_MAX:-80}"
-DB_NAME="${DB_NAME:-libeufin}"
-DB_LIMIT="${DB_LIMIT:-40}"
LOCK_FILE="${LOCK_FILE:-/var/run/auto-confirm-withdrawals.lock}"
LOOP=0
SLEEP=2
@@ -63,11 +54,6 @@ if [ "$LOOP" -eq 1 ]; then
if command -v flock >/dev/null 2>&1; then
if ! flock -n 9; then
echo "auto-confirm already running (lock $LOCK_FILE) — exit"
- # Under systemd Restart=always, exit 0 on lock contention flaps the unit
- # (activating/auto-restart) while the holder is outside the cgroup.
- if [ -n "${INVOCATION_ID:-}" ]; then
- exit 75
- fi
exit 0
fi
fi
@@ -110,76 +96,6 @@ known_ids() {
fi
}
-# True if WD amount fits explorer debt headroom (or psql unavailable → allow).
-id_affordable() {
- local wid="$1"
- if ! command -v psql >/dev/null 2>&1 || ! id -u postgres >/dev/null 2>&1; then
- return 0
- fi
- local ok
- ok=$(su -s /bin/bash postgres -c "psql -d ${DB_NAME} -At -c \"
-WITH explorer AS (
- SELECT a.bank_account_id, a.has_debt, a.balance, a.max_debt
- FROM libeufin_bank.bank_accounts a
- JOIN libeufin_bank.customers c ON c.customer_id = a.owning_customer_id
- WHERE c.username = '${USER}'
- LIMIT 1
-),
-headroom AS (
- SELECT CASE WHEN has_debt THEN (max_debt).val - (balance).val
- ELSE (max_debt).val + (balance).val END AS val
- FROM explorer
-)
-SELECT 1
-FROM libeufin_bank.taler_withdrawal_operations w
-JOIN explorer e ON e.bank_account_id = w.wallet_bank_account
-CROSS JOIN headroom h
-WHERE w.withdrawal_uuid = '${wid}'::uuid
- AND (w.amount).val <= h.val
-LIMIT 1;
-\"" 2>/dev/null | tr -d '[:space:]')
- [ "$ok" = "1" ]
-}
-
-# Selected, not yet confirmed, owner = explorer only (runs inside goa CTR).
-db_selected_explorer_ids() {
- if ! command -v psql >/dev/null 2>&1; then
- qlog "psql missing — skip DB discovery"
- return 0
- fi
- if ! id -u postgres >/dev/null 2>&1; then
- qlog "postgres user missing — skip DB discovery"
- return 0
- fi
- # Prefer affordable amounts (newest-first often hits oversized junk that
- # exceeds explorer debt headroom → HTTP 409 Insufficient funds forever).
- su -s /bin/bash postgres -c "psql -d ${DB_NAME} -At -c \"
-WITH explorer AS (
- SELECT a.bank_account_id, a.has_debt, a.balance, a.max_debt
- FROM libeufin_bank.bank_accounts a
- JOIN libeufin_bank.customers c ON c.customer_id = a.owning_customer_id
- WHERE c.username = '${USER}'
- LIMIT 1
-),
-headroom AS (
- SELECT CASE WHEN has_debt THEN (max_debt).val - (balance).val
- ELSE (max_debt).val + (balance).val END AS val
- FROM explorer
-)
-SELECT w.withdrawal_uuid::text
-FROM libeufin_bank.taler_withdrawal_operations w
-JOIN explorer e ON e.bank_account_id = w.wallet_bank_account
-CROSS JOIN headroom h
-WHERE w.selection_done = true
- AND w.confirmation_done = false
- AND w.aborted = false
- AND (w.amount).val <= h.val
-ORDER BY (w.amount).val ASC, (w.amount).frac ASC, w.creation_date DESC
-LIMIT ${DB_LIMIT};
-\"" 2>/dev/null \
- | grep -E '^[0-9a-fA-F-]{36}$' || true
-}
-
# Keep file small: drop confirmed/aborted; keep pending/selected + tail
prune_watch() {
[ -f "$WATCH_FILE" ] || return 0
@@ -220,11 +136,6 @@ confirm_one() {
case "$st" in
selected)
- # Watch-list IDs bypass DB headroom filter — skip oversized (HTTP 409 forever).
- if ! id_affordable "$wid"; then
- qlog "skip $wid selected but over explorer headroom"
- return 0
- fi
log "confirming $wid as $USER (status=selected) ..."
conf=$(curl -sS -m 15 -o /tmp/acw-conf.out -w '%{http_code}' \
-X POST \
@@ -261,17 +172,16 @@ once() {
return 1
fi
- # Watch files (landing) + DB selected(!confirmed) explorer ops
+ # Cap work per loop: last WATCH_MAX ids only (newest at end of file)
ids=$( {
[ -f "$URI_FILE" ] && basename "$(tr -d '\n' <"$URI_FILE")"
if [ -f "$WATCH_FILE" ]; then
grep -E '^[0-9a-fA-F-]{36}$' "$WATCH_FILE" | awk 'NF && !seen[$0]++' | tail -n "$WATCH_MAX"
fi
- db_selected_explorer_ids
} | awk 'NF && !seen[$0]++' )
if [ -z "$ids" ]; then
- qlog "no withdrawal ids (selected or watched)"
+ qlog "no withdrawal ids to watch"
return 0
fi
diff --git a/scripts/taler-bank/demo-withdraw-api.py b/scripts/taler-bank/demo-withdraw-api.py
index 292191f..f31243e 100755
--- a/scripts/taler-bank/demo-withdraw-api.py
+++ b/scripts/taler-bank/demo-withdraw-api.py
@@ -34,10 +34,6 @@ BANK = os.environ.get("BANK_URL", "http://127.0.0.1:9012").rstrip("/")
BANK_PUBLIC = os.environ.get("BANK_PUBLIC", "https://bank.hacktivism.ch").rstrip("/")
USER = os.environ.get("BANK_USER", "explorer")
AMOUNT = os.environ.get("AMOUNT", "GOA:10")
-EXCHANGE = (
- os.environ.get("EXCHANGE_URL", "https://exchange.hacktivism.ch/").rstrip("/")
- + "/"
-)
LANDING = Path(os.environ.get("LANDING_DIR", "/var/www/bank-landing"))
LISTEN = ("127.0.0.1", int(os.environ.get("DEMO_WITHDRAW_PORT", "19096")))
@@ -116,7 +112,7 @@ def http_json(method: str, url: str, body=None, headers=None, auth=None):
return e.code, {"raw": raw[:500]}
-def mint_withdraw(amount: str | None = None) -> dict:
+def mint_withdraw() -> dict:
pw = load_pass()
code, tok = http_json(
"POST",
@@ -127,14 +123,10 @@ def mint_withdraw(amount: str | None = None) -> dict:
if code != 200 or not tok.get("access_token"):
raise RuntimeError(f"token failed HTTP {code}: {tok}")
access = tok["access_token"]
- amt = amount or AMOUNT
- if ":" not in str(amt):
- amt = f"GOA:{amt}"
- # FP pattern: amount + exchange_url (not suggested_amount alone)
code, wd = http_json(
"POST",
f"{BANK}/accounts/{USER}/withdrawals",
- {"amount": amt, "exchange_url": EXCHANGE},
+ {"suggested_amount": AMOUNT},
headers={"Authorization": f"Bearer {access}"},
)
if code not in (200, 201):
@@ -150,7 +142,7 @@ def mint_withdraw(amount: str | None = None) -> dict:
uri = normalize_taler_withdraw_uri(str(uri).strip())
LANDING.mkdir(parents=True, exist_ok=True)
(LANDING / "withdraw.uri").write_text(uri + "\n")
- (LANDING / "withdraw.amount").write_text(amt + "\n")
+ (LANDING / "withdraw.amount").write_text(AMOUNT + "\n")
(LANDING / "withdraw.created").write_text(
time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()) + "\n"
)
@@ -159,8 +151,7 @@ def mint_withdraw(amount: str | None = None) -> dict:
"ok": True,
"taler_withdraw_uri": uri,
"withdrawal_id": wid,
- "amount": amt,
- "exchange_url": EXCHANGE,
+ "amount": AMOUNT,
"pool_account": USER,
"taler_integration_base": f"{BANK_PUBLIC}/taler-integration/",
"hint": "Open in GNU Taler Wallet (iOS/Android/desktop). No bank registration.",
@@ -310,54 +301,15 @@ class Handler(BaseHTTPRequestHandler):
self._cors()
self.end_headers()
- def _proxy_wd(self, parsed):
- """GET withdrawal-operation: inject suggested/required exchange (Android/iOS)."""
- q = ("?" + parsed.query) if parsed.query else ""
- url = f"{BANK}{parsed.path}{q}"
- ctx = ssl.create_default_context()
- req = urllib.request.Request(url, method="GET")
- try:
- with urllib.request.urlopen(req, context=ctx, timeout=70) as r:
- raw = r.read()
- status = r.status
- except urllib.error.HTTPError as e:
- raw = e.read()
- status = e.code
- except Exception as e:
- raw = json.dumps({"ok": False, "error": str(e)}).encode()
- status = 502
- try:
- body = json.loads(raw.decode() or "{}")
- if isinstance(body, dict):
- body.setdefault("suggested_exchange", EXCHANGE)
- body.setdefault("required_exchange", EXCHANGE)
- raw = json.dumps(body).encode()
- except Exception:
- pass
- self.send_response(status)
- self._cors()
- self.send_header("Content-Type", "application/json")
- self.send_header("Content-Length", str(len(raw)))
- self.end_headers()
- self.wfile.write(raw)
-
def do_GET(self):
- from urllib.parse import parse_qs, urlparse
-
- parsed = urlparse(self.path)
- path = parsed.path
- qs = parse_qs(parsed.query)
+ path = self.path.split("?", 1)[0]
try:
- if path.startswith("/taler-integration/withdrawal-operation/"):
- self._proxy_wd(parsed)
- return
if path in (
"/",
"/demo-withdraw.json",
"/intro/demo-withdraw.json",
):
- amt = (qs.get("amount") or qs.get("n") or [None])[0]
- body = mint_withdraw(amt)
+ body = mint_withdraw()
elif path in (
"/auto-account.json",
"/intro/auto-account.json",
diff --git a/scripts/taler-bank/install-demo-withdraw-api.sh b/scripts/taler-bank/install-demo-withdraw-api.sh
index b220030..c4cd2e8 100755
--- a/scripts/taler-bank/install-demo-withdraw-api.sh
+++ b/scripts/taler-bank/install-demo-withdraw-api.sh
@@ -4,101 +4,74 @@
# ./scripts/taler-bank/install-demo-withdraw-api.sh
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
-CTR="${BANK_CONTAINER:-goa-regio-ng}"
+CTR="${BANK_CONTAINER:-taler-hacktivism-bank}"
podman cp "$ROOT/demo-withdraw-api.py" "$CTR:/usr/local/bin/demo-withdraw-api.py"
podman cp "$ROOT/auto-confirm-withdrawals.sh" "$CTR:/usr/local/bin/auto-confirm-withdrawals.sh"
podman cp "$ROOT/refresh-demo-withdraw.sh" "$CTR:/usr/local/bin/refresh-demo-withdraw.sh"
-if [ -f "$ROOT/systemd/goa-auto-confirm-withdrawals.service" ]; then
- podman cp "$ROOT/systemd/goa-auto-confirm-withdrawals.service" \
- "$CTR:/etc/systemd/system/goa-auto-confirm-withdrawals.service"
-fi
podman exec -u root "$CTR" chmod 755 \
/usr/local/bin/demo-withdraw-api.py \
/usr/local/bin/auto-confirm-withdrawals.sh \
/usr/local/bin/refresh-demo-withdraw.sh
-# nginx: ensure demo-withdraw / auto-account / Integration → :19096
-# FP pattern: Caddy @wdGet GET → :9013 → this location → demo-api
-# (single in-CTR python; no nested podman)
+# nginx: proxy demo-withdraw.json
+NGX=/etc/nginx/sites-available/bank-landing
podman exec -u root "$CTR" bash -lc '
set -e
-python3 - </dev/null || true; done
-sleep 1
-ps -eo pid=,args= | awk "/demo-withdraw-api\\.py/ && !/awk/ {print \$1}" | while read p; do kill -9 \$p 2>/dev/null || true; done
-sleep 0.5
+sleep 0.3
nohup python3 /usr/local/bin/demo-withdraw-api.py \
>>/var/log/demo-withdraw-api.log 2>&1 /dev/null || true; done
sleep 0.5
-ps -eo pid=,args= | awk "/demo-withdraw-api\\.py/ && !/awk/" || {
- echo "ERROR: demo-withdraw-api failed to stay up (port busy?)" >&2
- tail -20 /var/log/demo-withdraw-api.log >&2 || true
- exit 1
-}
-# Prefer systemd unit (Restart=always). Fall back to nohup if systemctl missing.
if [ -f /var/log/auto-confirm-withdrawals.log ]; then
sz=$(wc -c > /var/log/auto-confirm-withdrawals.log
fi
fi
-# stop ad-hoc loops (by pid; avoid pkill -f self-match)
-ps -eo pid=,args= | awk "/auto-confirm-withdrawals\\.sh --loop/ && !/awk/ {print \$1}" | while read p; do kill \$p 2>/dev/null || true; done
-sleep 0.5
-if command -v systemctl >/dev/null 2>&1 && [ -f /etc/systemd/system/goa-auto-confirm-withdrawals.service ]; then
- systemctl daemon-reload
- systemctl enable --now goa-auto-confirm-withdrawals.service
- systemctl restart goa-auto-confirm-withdrawals.service
- systemctl --no-pager --full status goa-auto-confirm-withdrawals.service | head -20
-else
- nohup env QUIET=1 WATCH_MAX=80 \
- /usr/local/bin/auto-confirm-withdrawals.sh --loop 2 \
- >>/var/log/auto-confirm-withdrawals.log 2>&1 >/var/log/auto-confirm-withdrawals.log 2>&1 "$WORKDIR/usernames.txt" || true
- # grep -c exits 1 on zero matches; do not `|| echo 0` (would print 0\n0 and break JSON)
- ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || true)
- ACCOUNTS_N=${ACCOUNTS_N:-0}
- ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || true)
- ACCOUNTS_USERS=${ACCOUNTS_USERS:-0}
+ ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || echo 0)
+ ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || echo 0)
else
echo "$BANK_USER" >"$WORKDIR/usernames.txt"
ACCOUNTS_N=1
@@ -326,11 +323,9 @@ sort -t$'\t' -k2,2nr "$WORKDIR/all-in.tsv" -o "$WORKDIR/all-in-sorted.tsv" 2>/de
|| cp "$WORKDIR/all-in.tsv" "$WORKDIR/all-in-sorted.tsv"
# Unique reserves = individual wallet withdraws (each wallet reserve_pub)
-WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || true)
-WALLETS_N=${WALLETS_N:-0}
+WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
# Accounts that funded at least one withdraw
-ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || true)
-ACCOUNTS_WITH_WD=${ACCOUNTS_WITH_WD:-0}
+ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
# Aggregates
NOW=$(now_unix)
diff --git a/scripts/taler-bank/systemd/goa-auto-confirm-withdrawals.service b/scripts/taler-bank/systemd/goa-auto-confirm-withdrawals.service
deleted file mode 100644
index 96b5cc5..0000000
--- a/scripts/taler-bank/systemd/goa-auto-confirm-withdrawals.service
+++ /dev/null
@@ -1,19 +0,0 @@
-[Unit]
-Description=GOA bank auto-confirm explorer withdrawals
-After=network.target libeufin-bank.service goa-demo-withdraw-api.service
-Wants=libeufin-bank.service
-
-[Service]
-Type=simple
-Environment=QUIET=1
-Environment=WATCH_MAX=80
-Environment=BANK_URL=http://127.0.0.1:9012
-ExecStart=/usr/local/bin/auto-confirm-withdrawals.sh --loop 2
-Restart=always
-RestartSec=3
-# flock inside script; StandardOutput goes to journal + optional file via install
-StandardOutput=append:/var/log/auto-confirm-withdrawals.log
-StandardError=append:/var/log/auto-confirm-withdrawals.log
-
-[Install]
-WantedBy=multi-user.target
diff --git a/scripts/taler-exchange/landing-stats-exchange.sh b/scripts/taler-exchange/landing-stats-exchange.sh
index 6c77248..987e631 100644
--- a/scripts/taler-exchange/landing-stats-exchange.sh
+++ b/scripts/taler-exchange/landing-stats-exchange.sh
@@ -2,8 +2,7 @@
# Run INSIDE taler-hacktivism-exchange-ansible.
# Writes /var/www/exchange-landing/stats.json
#
-# Data lives in Postgres (DB name varies: vanilla regio-ng uses "exchange";
-# older hacktivism images use "taler-exchange"). Schema is exchange.*.
+# Data lives in Postgres DB taler-exchange, schema exchange.*
# (reserves, reserves_in, known_coins, withdraw, denominations, …)
#
# IMPORTANT: never use psql -F$'\t' -v ON_ERROR_STOP=1
@@ -17,38 +16,7 @@ LANDING_DIR="${LANDING_DIR:-/var/www/exchange-landing}"
OUT="$LANDING_DIR/stats.json"
RUN="$LANDING_DIR/stats-run.json"
TMP="${OUT}.tmp.$$"
-# Resolve DB: EXCHANGE_DB env → CONFIG=postgres:///NAME in secret conf → fallbacks
-_resolve_exchange_db() {
- if [ -n "${EXCHANGE_DB:-}" ]; then
- printf '%s' "$EXCHANGE_DB"
- return
- fi
- local conf c name
- for conf in \
- /etc/taler-exchange/secrets/exchange-db.secret.conf \
- /etc/taler/secrets/exchange-db.secret.conf; do
- if [ -r "$conf" ]; then
- c=$(grep -E '^[[:space:]]*CONFIG[[:space:]]*=' "$conf" | head -1 | sed 's/^[^=]*=[[:space:]]*//')
- # postgres:///dbname or postgres://user@/dbname
- name=$(printf '%s' "$c" | sed -n 's#.*postgres://[^/]*/\([^?]*\).*#\1#p')
- if [ -n "$name" ]; then
- printf '%s' "$name"
- return
- fi
- fi
- done
- for name in exchange taler-exchange; do
- if [ -d "/var/lib/postgresql" ] || command -v psql >/dev/null 2>&1; then
- if runuser -u postgres -- psql -d "$name" -At -c 'SELECT 1' >/dev/null 2>&1 \
- || su -s /bin/bash postgres -c "psql -d $name -At -c 'SELECT 1'" >/dev/null 2>&1; then
- printf '%s' "$name"
- return
- fi
- fi
- done
- printf '%s' "taler-exchange"
-}
-DB="$(_resolve_exchange_db)"
+DB="${EXCHANGE_DB:-taler-exchange}"
BASE_URL="${EXCHANGE_BASE_URL:-https://exchange.hacktivism.ch}"
ERRLOG="${LANDING_STATS_ERRLOG:-/var/log/landing-stats-exchange.err}"
mkdir -p "$LANDING_DIR"
@@ -285,7 +253,7 @@ cat >"$TMP" <>"$LOG_DIR/${label}.log" 2>&1
local ec=$?
set -e
diff --git a/scripts/taler-landing/collect_bank_stats.py b/scripts/taler-landing/collect_bank_stats.py
index 42f2c56..b8d4398 100755
--- a/scripts/taler-landing/collect_bank_stats.py
+++ b/scripts/taler-landing/collect_bank_stats.py
@@ -130,11 +130,6 @@ def get_token(bank: str, user: str, password: str) -> str:
def list_all_accounts(bank: str, token: str, delta: int) -> List[str]:
"""List accounts; page with start if needed."""
- # libeufin-bank: |delta| must be in 1..1024 (same floor as Param 'limit')
- if delta == 0:
- delta = -1024
- elif abs(delta) > 1024:
- delta = -1024 if delta < 0 else 1024
names: List[str] = []
seen: Set[str] = set()
start: Optional[int] = None
@@ -218,10 +213,9 @@ def iter_transactions(
pages = 0
safety = max_pages if max_pages > 0 else 10_000
- page = min(abs(page), 1024) or 1024
while pages < safety:
pages += 1
- q = f"delta=-{page}"
+ q = f"delta=-{abs(page)}"
if start is not None:
q += f"&start={start}"
code, data, raw = http_json(
@@ -370,7 +364,7 @@ def main() -> int:
ap.add_argument(
"--accounts-delta",
type=int,
- default=int(env("ACCOUNTS_DELTA", "-1024") or "-1024"),
+ default=int(env("ACCOUNTS_DELTA", "-10000") or "-10000"),
)
ap.add_argument("--recent", type=int, default=int(env("RECENT_WD_N", "10") or "10"))
ap.add_argument("--public-base", default=env("BANK_PUBLIC_URL", "https://bank.hacktivism.ch"))
diff --git a/scripts/taler-landing/deploy-landings.sh b/scripts/taler-landing/deploy-landings.sh
index af58e70..28aeb2b 100755
--- a/scripts/taler-landing/deploy-landings.sh
+++ b/scripts/taler-landing/deploy-landings.sh
@@ -1,26 +1,16 @@
-#!/usr/bin/env bash
-# Install bank + exchange + merchant landing pages inside containers and ensure
-# nginx listens on 9013 (bank) / 9014 (exchange) / 9015 (merchant).
+#!/bin/bash
+# Install exchange + merchant landing pages inside containers and ensure nginx
+# listens on 9014 (exchange) / 9015 (merchant).
#
# Port map (wire these in Caddy / podman -p):
-# 9013 bank landing
-# 9014 exchange landing
-# 9015 merchant landing
-# Unified vanilla CTR: set LANDING_CTR=goa-regio-ng (default) so all three
-# landings land in the same container publishing 9010-9015.
+# 9013 bank landing (already on taler-hacktivism-bank)
+# 9014 exchange landing (taler-hacktivism-exchange-ansible)
+# 9015 merchant landing (taler-hacktivism)
#
-# Usage:
-# # on koopa (podman local):
-# LANDING_CTR=goa-regio-ng ./scripts/taler-landing/deploy-landings.sh
-# # from laptop (rsync SoT → remote tree, then deploy there):
-# ./scripts/taler-landing/deploy-landings.sh --ssh hernani@192.168.100.95
-# ./scripts/taler-landing/deploy-landings.sh --ssh # uses LANDING_SSH
-#
-# After stamp-landing-stack / stamp-landing-version, prefer --deploy on those
-# scripts so publish cannot be skipped.
+# Run on koopa (host) with podman access.
set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+ROOT=$(cd "$(dirname "$0")/../.." && pwd)
# allow override when files already on host /tmp
SRC_EX="${SRC_EX:-$ROOT/configs/exchange-landing}"
SRC_MER="${SRC_MER:-$ROOT/configs/merchant-landing}"
@@ -28,37 +18,9 @@ SRC_BANK="${SRC_BANK:-$ROOT/configs/bank-landing}"
SRC_QR="${SRC_QR:-$ROOT/configs/bank-landing/qrcode.min.js}"
SRC_GOA_AMT="${SRC_GOA_AMT:-$ROOT/configs/shared/goa-amount.js}"
-LANDING_CTR="${LANDING_CTR:-goa-regio-ng}"
-C_EX="${C_EX:-$LANDING_CTR}"
-C_MER="${C_MER:-$LANDING_CTR}"
-C_BANK="${C_BANK:-$LANDING_CTR}"
-
-SSH_HOST="${LANDING_SSH:-hernani@192.168.100.95}"
-# Remote sync tree on koopa (not a git checkout — laptop SoT is Freigabe-pushed).
-REMOTE_ROOT="${LANDING_REMOTE_ROOT:-src/koopa/koopa-admin-log}"
-USE_SSH=0
-
-while [ $# -gt 0 ]; do
- case "$1" in
- --ctr) LANDING_CTR="${2:-}"; C_EX="$LANDING_CTR"; C_MER="$LANDING_CTR"; C_BANK="$LANDING_CTR"; shift 2 ;;
- --ssh)
- if [ $# -ge 2 ] && [[ "${2:-}" != -* ]]; then
- SSH_HOST="$2"
- shift 2
- else
- shift
- fi
- USE_SSH=1
- ;;
- --local) USE_SSH=0; shift ;;
- --remote-root) REMOTE_ROOT="${2:-}"; shift 2 ;;
- -h|--help)
- sed -n '2,22p' "$0"
- exit 0
- ;;
- *) echo "unknown arg: $1" >&2; exit 2 ;;
- esac
-done
+C_EX=taler-hacktivism-exchange-ansible
+C_MER=taler-hacktivism
+C_BANK="${C_BANK:-taler-hacktivism-bank}"
# Prefer per-landing copy, then shared (keeps bank/exchange/merchant in sync)
resolve_goa_amount() {
@@ -83,44 +45,6 @@ copy_goa_amount() {
fi
}
-deploy_via_ssh() {
- local host="$1"
- local remote_root="$2"
- echo "=== sync landings → ${host}:${remote_root} ==="
- ssh -o BatchMode=yes -o ConnectTimeout=12 "$host" "mkdir -p $(printf '%q' "$remote_root")/configs/shared $(printf '%q' "$remote_root")/scripts/taler-landing $(printf '%q' "$remote_root")/configs/bank-landing $(printf '%q' "$remote_root")/configs/exchange-landing $(printf '%q' "$remote_root")/configs/merchant-landing"
-
- # SoT HTML / nginx / shared JS + this script (so remote deploy matches laptop).
- rsync -az --relative \
- "$ROOT/./configs/bank-landing/index.html" \
- "$ROOT/./configs/exchange-landing/index.html" \
- "$ROOT/./configs/exchange-landing/nginx-landing.conf" \
- "$ROOT/./configs/merchant-landing/index.html" \
- "$ROOT/./configs/merchant-landing/nginx-landing.conf" \
- "$ROOT/./configs/shared/goa-amount.js" \
- "$ROOT/./scripts/taler-landing/deploy-landings.sh" \
- "${host}:${remote_root}/"
-
- if [ -f "$SRC_QR" ]; then
- rsync -az "$SRC_QR" "${host}:${remote_root}/configs/bank-landing/qrcode.min.js"
- fi
- for site in bank exchange merchant; do
- if [ -f "$ROOT/configs/${site}-landing/goa-amount.js" ]; then
- rsync -az "$ROOT/configs/${site}-landing/goa-amount.js" \
- "${host}:${remote_root}/configs/${site}-landing/goa-amount.js"
- fi
- done
-
- echo "=== remote deploy ctr=${LANDING_CTR} ==="
- # shellcheck disable=SC2029
- ssh -o BatchMode=yes -o ConnectTimeout=12 "$host" \
- "cd $(printf '%q' "$remote_root") && LANDING_CTR=$(printf '%q' "$LANDING_CTR") ./scripts/taler-landing/deploy-landings.sh --local"
-}
-
-if [ "$USE_SSH" = 1 ]; then
- deploy_via_ssh "$SSH_HOST" "$REMOTE_ROOT"
- exit 0
-fi
-
echo "=== bank landing → $C_BANK :9013 (html + goa-amount) ==="
if podman inspect -f '{{.State.Running}}' "$C_BANK" 2>/dev/null | grep -qx true; then
podman exec "$C_BANK" mkdir -p /var/www/bank-landing
@@ -180,9 +104,8 @@ podman exec "$C_MER" curl -sS -m 3 -o /dev/null -w "merchant_landing=%{http_code
echo
echo "PORTS TO WIRE (host → Caddy / firewall / podman -p):"
-echo " 9013 bank landing ($C_BANK)"
-echo " 9014 exchange landing ($C_EX)"
-echo " 9015 merchant landing ($C_MER)"
+echo " 9013 bank landing (taler-hacktivism-bank) already published"
+echo " 9014 exchange landing (taler-hacktivism-exchange-ansible) NEEDS -p 9014:9014"
+echo " 9015 merchant landing (taler-hacktivism) NEEDS -p 9015:9015"
echo
echo "If host curl to :9014/:9015 fails, republish pasta ports (commit+replace) — see README."
-echo "OK deploy-landings: ctr=${LANDING_CTR} mode=local"
diff --git a/scripts/taler-landing/profiles/goa.env b/scripts/taler-landing/profiles/goa.env
index 8ab7ffa..08c357a 100644
--- a/scripts/taler-landing/profiles/goa.env
+++ b/scripts/taler-landing/profiles/goa.env
@@ -1,8 +1,8 @@
-# Stack profile: GOA / goa-regio-ng vanilla (9010-9015)
+# Stack profile: GOA / hacktivism
# systemd EnvironmentFile: KEY=value only (no export)
-BANK_CTR=goa-regio-ng
-EX_CTR=goa-regio-ng
-MER_CTR=goa-regio-ng
+BANK_CTR=taler-hacktivism-bank
+EX_CTR=taler-hacktivism-exchange-ansible
+MER_CTR=taler-hacktivism
BANK_URL=http://127.0.0.1:9012
BANK_PUBLIC_URL=https://bank.hacktivism.ch
EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
@@ -15,4 +15,4 @@ COLLECT_EXCHANGE=1
COLLECT_MERCHANT=1
COLLECT_RESOURCES=1
PUBLISH_PODMAN=1
-STATS_SOURCE_LABEL="host collect_bank_stats.py goa regio-ng vanilla"
+STATS_SOURCE_LABEL="host collect_bank_stats.py goa hacktivism"
diff --git a/scripts/taler-landing/stamp-landing-stack.sh b/scripts/taler-landing/stamp-landing-stack.sh
deleted file mode 100644
index 484709e..0000000
--- a/scripts/taler-landing/stamp-landing-stack.sh
+++ /dev/null
@@ -1,196 +0,0 @@
-#!/usr/bin/env bash
-# Fill landing stack-bar package versions from live CTR dpkg (not hand-edited HTML).
-#
-# Reads: configs/shared/landing-stack.json (package lists + vanilla href)
-# Writes: configs/{bank,exchange,merchant}-landing/index.html ( block)
-# configs/shared/landing-stack-versions.json (last dpkg snapshot)
-#
-# Usage (from koopa-admin-log root, laptop or koopa):
-# ./scripts/taler-landing/stamp-landing-stack.sh
-# ./scripts/taler-landing/stamp-landing-stack.sh --ctr goa-regio-ng --ssh hernani@192.168.100.95
-# ./scripts/taler-landing/stamp-landing-stack.sh bank exchange
-# ./scripts/taler-landing/stamp-landing-stack.sh --deploy
-# stamp then publish via deploy-landings.sh --ssh (same host/ctr)
-#
-# After this, bump footer dates with:
-# ./scripts/taler-landing/stamp-landing-version.sh --bump --all
-# One-shot stamp+live (preferred — avoids stamped-but-not-deployed lag):
-# ./scripts/taler-landing/stamp-landing-stack.sh --deploy
-# ./scripts/taler-landing/stamp-landing-version.sh --bump --all --deploy
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-STACK_FILE="$ROOT/configs/shared/landing-stack.json"
-VERS_OUT="$ROOT/configs/shared/landing-stack-versions.json"
-
-CTR="${LANDING_CTR:-goa-regio-ng}"
-SSH_HOST="${LANDING_SSH:-hernani@192.168.100.95}"
-USE_SSH=1
-DO_DEPLOY=0
-sites=()
-
-while [ $# -gt 0 ]; do
- case "$1" in
- --ctr) CTR="${2:-}"; shift 2 ;;
- --ssh) SSH_HOST="${2:-}"; USE_SSH=1; shift 2 ;;
- --local) USE_SSH=0; shift ;;
- --deploy) DO_DEPLOY=1; shift ;;
- bank|exchange|merchant) sites+=("$1"); shift ;;
- -h|--help)
- sed -n '2,24p' "$0"
- exit 0
- ;;
- *) echo "unknown arg: $1" >&2; exit 2 ;;
- esac
-done
-
-if [ "${#sites[@]}" -eq 0 ]; then
- sites=(bank exchange merchant)
-fi
-
-[ -f "$STACK_FILE" ] || { echo "missing $STACK_FILE" >&2; exit 1; }
-
-# Collect unique package names from JSON for the selected sites
-mapfile -t PKGS < <(python3 - "$STACK_FILE" "${sites[@]}" <<'PY'
-import json, sys
-data = json.load(open(sys.argv[1], encoding="utf-8"))
-sites = sys.argv[2:]
-seen = []
-for s in sites:
- for p in data["sites"][s]["packages"]:
- if p not in seen:
- seen.append(p)
-print("\n".join(seen))
-PY
-)
-
-if [ "${#PKGS[@]}" -eq 0 ]; then
- echo "no packages configured" >&2
- exit 1
-fi
-
-pkg_args=$(printf '%q ' "${PKGS[@]}")
-query_cmd="podman exec $(printf '%q' "$CTR") dpkg-query -W -f='\${Package}=\${Version}\n' ${pkg_args}"
-
-if [ "$USE_SSH" = 1 ]; then
- echo "dpkg ← ${SSH_HOST} ctr=${CTR}"
- # shellcheck disable=SC2086
- raw=$(ssh -o BatchMode=yes -o ConnectTimeout=12 "$SSH_HOST" "podman exec $(printf '%q' "$CTR") dpkg-query -W -f='\${Package}=\${Version}\n' ${pkg_args}")
-else
- echo "dpkg ← local ctr=${CTR}"
- # shellcheck disable=SC2086
- raw=$(podman exec "$CTR" dpkg-query -W -f='${Package}=${Version}\n' "${PKGS[@]}")
-fi
-
-python3 - "$STACK_FILE" "$VERS_OUT" "$ROOT" "$CTR" "$SSH_HOST" "$raw" "${sites[@]}" <<'PY'
-import json, re, sys
-from pathlib import Path
-from datetime import datetime
-from zoneinfo import ZoneInfo
-
-stack_path, vers_out, root, ctr, ssh_host, raw = sys.argv[1:7]
-sites = sys.argv[7:]
-stack = json.load(open(stack_path, encoding="utf-8"))
-tz = ZoneInfo("Europe/Zurich")
-now = datetime.now(tz)
-iso = now.isoformat(timespec="seconds")
-human = now.strftime("%Y-%m-%d %H:%M %Z")
-
-versions = {}
-for line in raw.splitlines():
- line = line.strip()
- if not line or "=" not in line:
- continue
- pkg, ver = line.split("=", 1)
- # display upstream-ish: drop Debian revision (1.6.10-0+trixie → 1.6.10)
- short = re.sub(r"-[0-9].*$", "", ver)
- versions[pkg] = {"dpkg": ver, "display": short}
-
-missing = []
-for site in sites:
- for pkg in stack["sites"][site]["packages"]:
- if pkg not in versions:
- missing.append(pkg)
-if missing:
- raise SystemExit(f"dpkg missing packages: {', '.join(sorted(set(missing)))}")
-
-vanilla_href = stack["vanilla_href"]
-vanilla_title = stack.get("vanilla_title", "vanilla")
-sep = '· '
-
-def render_block(site: str) -> str:
- meta = stack["sites"][site]
- parts = [
- f' ',
- f' ',
- f' stack {sep}',
- f' vanilla ',
- ]
- for pkg in meta["packages"]:
- disp = versions[pkg]["display"]
- parts.append(f" {sep}")
- parts.append(f" {pkg} {disp}")
- parts.append("
")
- parts.append(" ")
- return "\n".join(parts) + "\n"
-
-site_html = {
- "bank": Path(root) / "configs/bank-landing/index.html",
- "exchange": Path(root) / "configs/exchange-landing/index.html",
- "merchant": Path(root) / "configs/merchant-landing/index.html",
-}
-
-marker_re = re.compile(r"[ \t]*.*?\n?", re.S)
-bare_re = re.compile(
- r"[ \t]*]*>.*?
\n?",
- re.S,
-)
-
-for site in sites:
- path = site_html[site]
- text = path.read_text(encoding="utf-8")
- block = render_block(site)
- if marker_re.search(text):
- text = marker_re.sub(block, text, count=1)
- elif bare_re.search(text):
- text = bare_re.sub(block, text, count=1)
- else:
- raise SystemExit(f"no stack-bar in {path}")
- path.write_text(text, encoding="utf-8")
- print(f"stack → {path.relative_to(root)} ({site})")
-
-out = {
- "updated_iso": iso,
- "updated_human": human,
- "ctr": ctr,
- "ssh": ssh_host if ssh_host else None,
- "packages": versions,
- "sites": {
- site: {
- "packages": {
- pkg: versions[pkg]["display"]
- for pkg in stack["sites"][site]["packages"]
- }
- }
- for site in ("bank", "exchange", "merchant")
- if site in stack["sites"]
- },
-}
-Path(vers_out).write_text(json.dumps(out, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
-print(f"versions → {Path(vers_out).relative_to(root)}")
-print(f"OK stack stamped: {' '.join(sites)} @ {human}")
-PY
-
-echo "OK stamp-landing-stack: ${sites[*]} ← ${CTR}"
-
-if [ "$DO_DEPLOY" = 1 ]; then
- deploy="$ROOT/scripts/taler-landing/deploy-landings.sh"
- [ -x "$deploy" ] || { echo "missing $deploy" >&2; exit 1; }
- if [ "$USE_SSH" = 1 ]; then
- echo "=== --deploy → deploy-landings.sh --ssh ${SSH_HOST} --ctr ${CTR} ==="
- LANDING_CTR="$CTR" "$deploy" --ssh "$SSH_HOST" --ctr "$CTR"
- else
- echo "=== --deploy → deploy-landings.sh --local --ctr ${CTR} ==="
- LANDING_CTR="$CTR" "$deploy" --local --ctr "$CTR"
- fi
-fi
diff --git a/scripts/taler-landing/stamp-landing-version.sh b/scripts/taler-landing/stamp-landing-version.sh
index 2070112..9616dec 100755
--- a/scripts/taler-landing/stamp-landing-version.sh
+++ b/scripts/taler-landing/stamp-landing-version.sh
@@ -21,17 +21,11 @@
# 3) git add configs/ && git commit -m "landing(bank): …"
# 4) ./scripts/taler-landing/stamp-landing-version.sh bank
# git add configs/ && git commit --amend --no-edit # optional pin
-# 5) ./scripts/taler-landing/deploy-landings.sh --ssh
-# or combine: stamp-landing-version.sh --bump bank --deploy
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
VER_FILE="$ROOT/configs/shared/landing-version.json"
REPO_WEB="${LANDING_REPO_WEB:-https://git.hacktivism.ch/hernani/koopa-admin-log}"
-SSH_HOST="${LANDING_SSH:-hernani@192.168.100.95}"
-CTR="${LANDING_CTR:-goa-regio-ng}"
-DO_DEPLOY=0
-USE_SSH=1
declare -A SITE_HTML=(
[bank]="$ROOT/configs/bank-landing/index.html"
@@ -61,13 +55,9 @@ while [ $# -gt 0 ]; do
--bump) bump=1; shift ;;
--set) set_ver="${2:-}"; shift 2 ;;
--all) do_all=1; shift ;;
- --deploy) DO_DEPLOY=1; shift ;;
- --ssh) SSH_HOST="${2:-}"; USE_SSH=1; shift 2 ;;
- --local) USE_SSH=0; shift ;;
- --ctr) CTR="${2:-}"; shift 2 ;;
bank|exchange|merchant) sites+=("$1"); shift ;;
-h|--help)
- sed -n '2,32p' "$0"
+ sed -n '2,28p' "$0"
exit 0
;;
*) echo "unknown arg: $1 (sites: bank exchange merchant)" >&2; exit 2 ;;
@@ -201,15 +191,3 @@ PY
done
echo "OK stamped: ${sites[*]}"
-
-if [ "$DO_DEPLOY" = 1 ]; then
- deploy="$ROOT/scripts/taler-landing/deploy-landings.sh"
- [ -x "$deploy" ] || { echo "missing $deploy" >&2; exit 1; }
- if [ "$USE_SSH" = 1 ]; then
- echo "=== --deploy → deploy-landings.sh --ssh ${SSH_HOST} --ctr ${CTR} ==="
- LANDING_CTR="$CTR" "$deploy" --ssh "$SSH_HOST" --ctr "$CTR"
- else
- echo "=== --deploy → deploy-landings.sh --local --ctr ${CTR} ==="
- LANDING_CTR="$CTR" "$deploy" --local --ctr "$CTR"
- fi
-fi
diff --git a/scripts/taler-monitoring/README.md b/scripts/taler-monitoring/README.md
index 107180f..b29815c 100644
--- a/scripts/taler-monitoring/README.md
+++ b/scripts/taler-monitoring/README.md
@@ -1,23 +1,18 @@
-# Monitoring suite → **elelmon**
+# taler-monitoring has moved
-The monitoring **suite** lives in a standalone repo. Product name **LLM** =
-Lightweight Monitoring; git tree **elelmon**. CLI wrapper stays
-`taler-monitoring.sh` (compat). Archived twin `hernani/taler-monitoring` is
-**not** SoT — do not clone or push there.
+The monitoring **suite** is a standalone repository (not maintained under this tree).
| | |
|--|--|
-| **Repository** | https://git.hacktivism.ch/hernani/elelmon |
-| **Clone** | `git clone https://git.hacktivism.ch/hernani/elelmon.git ~/src/elelmon` |
-| **Install path** | `~/src/elelmon` |
-| **Version** | `./taler-monitoring.sh --ver` |
-| **Config (product name)** | `~/.config/taler-monitoring/env` |
-| **State dir name** | `~/.local/state/taler-hacktivism-monitoring/` (legacy string OK) |
+| **Repository** | https://git.hacktivism.ch/hernani/taler-monitoring |
+| **Clone** | `git clone https://git.hacktivism.ch/hernani/taler-monitoring.git ~/src/taler-monitoring` |
+| **Install path** | `~/src/taler-monitoring` |
+| **Version** | `./taler-monitoring.sh --ver` (tags `vMAJOR.FEATURE.FIX`, e.g. v1.7.1) |
## Host-agent (user systemd on koopa)
```bash
-~/src/elelmon/host-agent/install-host-agent.sh
+~/src/taler-monitoring/host-agent/install-host-agent.sh
```
## Public HTML (infra on this host)
@@ -30,4 +25,3 @@ Generated under `~/monitoring-sites-staging/`.
- Apply: `sudo ~/koopa-caddy/apply-monitoring-live.sh`
Do not re-introduce the full suite under `scripts/taler-monitoring/`.
-URL paths `/taler-monitoring-*` are **product paths**, not the archived repo.
diff --git a/scripts/taler-shared/ensure-taler-apps.sh b/scripts/taler-shared/ensure-taler-apps.sh
index 5fa1014..58dca1a 100644
--- a/scripts/taler-shared/ensure-taler-apps.sh
+++ b/scripts/taler-shared/ensure-taler-apps.sh
@@ -2,8 +2,7 @@
# Host (hernani@koopa): start Taler *apps inside* merchant/bank containers.
#
# Containers use CMD sleep infinity — podman start alone is not enough.
-# Called by user systemd units after goa-regio-ng.service (vanilla AIO).
-# Override MER_CTR/BANK_CTR via env or ~/.config/taler-landing/stack.env.
+# Called by user systemd units after container-taler-*.service.
#
# Usage:
# ensure-taler-apps.sh # merchant + bank + helpers
@@ -12,8 +11,8 @@
# ensure-taler-apps.sh status
set -euo pipefail
-MER_CTR="${MER_CTR:-goa-regio-ng}"
-BANK_CTR="${BANK_CTR:-goa-regio-ng}"
+MER_CTR="${MER_CTR:-taler-hacktivism}"
+BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}"
AUTO_LOOP_SECS="${AUTO_CONFIRM_LOOP_SECS:-2}"
WAIT_SECS="${WAIT_SECS:-90}"
@@ -141,9 +140,8 @@ ensure_bank_helpers() {
}
status() {
- echo "=== containers (MER_CTR=$MER_CTR BANK_CTR=$BANK_CTR) ==="
- podman ps -a --format '{{.Names}} {{.Status}}' \
- | awk '/goa-regio-ng|taler-hacktivism/ {print}'
+ echo "=== containers ==="
+ podman ps -a --filter name='taler-hacktivism' --format '{{.Names}} {{.Status}}'
echo "=== merchant procs (sample) ==="
podman exec "$MER_CTR" bash -lc "ps -eo args= | grep -E 'taler-merchant-httpd|postgres -D|nginx: master' | grep -v grep || true" 2>/dev/null || echo "(container down)"
echo "=== bank procs (sample) ==="
diff --git a/scripts/taler-shared/goa-live-backup-upgrade.sh b/scripts/taler-shared/goa-live-backup-upgrade.sh
deleted file mode 100755
index 6bce47f..0000000
--- a/scripts/taler-shared/goa-live-backup-upgrade.sh
+++ /dev/null
@@ -1,332 +0,0 @@
-#!/usr/bin/env bash
-# Live GOA: dry-run → backup → apt upgrade (bank/exchange/merchant) → podman commit.
-# Host: koopa (hernani, podman). No Freigabe-bypass — caller must have Freigabe.
-#
-# ./goa-live-backup-upgrade.sh dry-run
-# ./goa-live-backup-upgrade.sh backup # stamp only (no apt / no commit)
-# ./goa-live-backup-upgrade.sh apply # backup + apt + commit + smoke
-# ./goa-live-backup-upgrade.sh status
-#
-# State/log: ~/backups/goa/upgrade-runs//
-# Mirror: koopa-admin-log/scripts/taler-shared/
-set -euo pipefail
-
-BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}"
-EXCHANGE_CTR="${EXCHANGE_CTR:-taler-hacktivism-exchange-ansible}"
-MERCHANT_CTR="${MERCHANT_CTR:-taler-hacktivism}"
-
-BANK_IMG_LIVE="${BANK_IMG_LIVE:-localhost/taler-hacktivism-banking:live}"
-EXCHANGE_IMG_LIVE="${EXCHANGE_IMG_LIVE:-localhost/taler-hacktivism-exchange-ansible:landing}"
-MERCHANT_IMG_LIVE="${MERCHANT_IMG_LIVE:-localhost/taler-hacktivism-live:landing}"
-
-STAMP="${STAMP:-$(date -u +%Y%m%dT%H%MZ)}"
-ROOT="${ROOT:-$HOME/backups/goa}"
-RUN_DIR="${RUN_DIR:-$ROOT/upgrade-runs/$STAMP}"
-MODE="${1:-dry-run}"
-
-mkdir -p "$RUN_DIR"
-LOG="$RUN_DIR/run.log"
-STATUS="$RUN_DIR/STATUS"
-exec > >(tee -a "$LOG") 2>&1
-
-log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; }
-set_status() {
- printf 'STATUS=%s\nSTAMP=%s\nWHEN=%s\nMODE=%s\n' "$1" "$STAMP" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$MODE" >"$STATUS"
- shift || true
- for kv in "$@"; do printf '%s\n' "$kv" >>"$STATUS"; done
- ln -sfn "$STAMP" "$ROOT/upgrade-runs/current" 2>/dev/null || true
- ln -sfn "upgrade-runs/$STAMP" "$ROOT/upgrade-current-run" 2>/dev/null || true
-}
-
-pod_root() {
- local ctr="$1"; shift
- podman exec -u root "$ctr" bash -lc "$*"
-}
-
-dry_one() {
- local name="$1" ctr="$2" pkgs="$3"
- log "=== DRY $name ($ctr) ==="
- pod_root "$ctr" "
- set -e
- export DEBIAN_FRONTEND=noninteractive
- apt-get update -qq
- echo '--- installed ---'
- dpkg -l $pkgs 2>/dev/null | awk '/^ii/{print \$2,\$3}' || true
- echo '--- candidates (policy sample) ---'
- for p in $pkgs; do
- apt-cache policy \"\$p\" 2>/dev/null | awk -v p=\"\$p\" 'NR<=6{print p\": \"\$0}'
- done
- echo '--- simulate install ---'
- apt-get -s install -y $pkgs 2>&1 | egrep -i '^(Inst |Remv |Conf |The following packages)' || true
- "
-}
-
-cmd_dry_run() {
- set_status DRY_RUN_RUNNING
- log "RUN_DIR=$RUN_DIR"
- df -h "$HOME" | tee "$RUN_DIR/df.txt"
- podman ps --format '{{.Names}} {{.Status}} {{.Image}}' | grep -E 'taler-hacktivism|goa-regio' | tee "$RUN_DIR/ctrs.txt" || true
-
- dry_one bank "$BANK_CTR" "libeufin-bank libeufin-common"
- dry_one exchange "$EXCHANGE_CTR" "taler-exchange taler-exchange-database taler-exchange-offline libtalerexchange taler-terms-generator"
- dry_one merchant "$MERCHANT_CTR" "taler-merchant taler-merchant-webui libtalermerchant taler-terms-generator"
-
- cat >"$RUN_DIR/PLAN.txt" </dev/null || true
- cp -a /var/lib/libeufin-bank/. /tmp/goa-bak/var-lib-libeufin-bank/ 2>/dev/null || true
- runuser -u postgres -- psql -lAt > /tmp/goa-bak/pg-databases.txt || true
- runuser -u postgres -- pg_dump -Fc libeufinbank > /tmp/goa-bak/libeufinbank.dump || \
- runuser -u postgres -- pg_dump -Fc libeufinbank > /tmp/goa-bak/libeufinbank.dump
- runuser -u postgres -- pg_dump -Fc libeufin > /tmp/goa-bak/libeufin.dump 2>/dev/null || true
- du -sh /tmp/goa-bak/* > /tmp/goa-bak/du.txt || true
- tar -C /tmp/goa-bak -czf /tmp/goa-bank-bak.tgz .
- '
- podman cp "$BANK_CTR:/tmp/goa-bank-bak.tgz" "$dest/goa-bank-bak.tgz"
- pod_root "$BANK_CTR" 'rm -rf /tmp/goa-bak /tmp/goa-bank-bak.tgz'
-}
-
-backup_ctr_exchange() {
- local dest="$1/exchange"
- mkdir -p "$dest"
- pod_root "$EXCHANGE_CTR" '
- set -e
- mkdir -p /tmp/goa-bak
- rm -rf /tmp/goa-bak/*
- mkdir -p /tmp/goa-bak/etc-taler-exchange /tmp/goa-bak/offline
- cp -a /etc/taler-exchange/. /tmp/goa-bak/etc-taler-exchange/ 2>/dev/null || true
- cp -a /var/lib/taler-exchange/offline/. /tmp/goa-bak/offline/ 2>/dev/null || true
- runuser -u postgres -- psql -lAt > /tmp/goa-bak/pg-databases.txt || true
- DB=$(runuser -u postgres -- psql -lAt | awk -F"|" "/taler/ {print \$1}" | head -1 | tr -d " ")
- [ -n "$DB" ] || DB=taler-exchange
- runuser -u postgres -- pg_dump -Fc "$DB" > /tmp/goa-bak/taler-exchange.dump
- tar -C /tmp/goa-bak -czf /tmp/goa-exchange-bak.tgz .
- '
- podman cp "$EXCHANGE_CTR:/tmp/goa-exchange-bak.tgz" "$dest/goa-exchange-bak.tgz"
- pod_root "$EXCHANGE_CTR" 'rm -rf /tmp/goa-bak /tmp/goa-exchange-bak.tgz'
-}
-
-backup_ctr_merchant() {
- local dest="$1/merchant"
- mkdir -p "$dest"
- pod_root "$MERCHANT_CTR" '
- set -e
- mkdir -p /tmp/goa-bak
- rm -rf /tmp/goa-bak/*
- mkdir -p /tmp/goa-bak/etc-taler /tmp/goa-bak/etc-taler-merchant
- cp -a /etc/taler/. /tmp/goa-bak/etc-taler/ 2>/dev/null || true
- cp -a /etc/taler-merchant/. /tmp/goa-bak/etc-taler-merchant/ 2>/dev/null || true
- runuser -u postgres -- psql -lAt > /tmp/goa-bak/pg-databases.txt || true
- DB=$(runuser -u postgres -- psql -lAt | awk -F"|" "/merchant|taler/ {print \$1}" | head -1 | tr -d " ")
- [ -n "$DB" ] || DB=taler-merchant
- runuser -u postgres -- pg_dump -Fc "$DB" > /tmp/goa-bak/taler-merchant.dump
- tar -C /tmp/goa-bak -czf /tmp/goa-merchant-bak.tgz .
- '
- podman cp "$MERCHANT_CTR:/tmp/goa-merchant-bak.tgz" "$dest/goa-merchant-bak.tgz"
- pod_root "$MERCHANT_CTR" 'rm -rf /tmp/goa-bak /tmp/goa-merchant-bak.tgz'
-}
-
-cmd_backup() {
- local bak="$ROOT/$STAMP"
- mkdir -p "$bak/meta"
- log "BACKUP → $bak"
- {
- date -R
- hostname
- podman ps -a --format '{{.Names}} | {{.Image}} | {{.Status}}'
- } >"$bak/meta/inventory.txt"
- backup_ctr_bank "$bak"
- backup_ctr_exchange "$bak"
- backup_ctr_merchant "$bak"
- du -sh "$bak"/* >"$bak/meta/du.txt" || true
- find "$bak" -type f -printf '%s %p\n' >"$bak/meta/manifest-sizes.txt" || true
- ln -sfn "$STAMP" "$ROOT/current"
- log "BACKUP_OK current→$STAMP"
- echo "$bak"
-}
-
-upgrade_bank() {
- log "======== apt bank ========"
- pod_root "$BANK_CTR" '
- set -e
- export DEBIAN_FRONTEND=noninteractive
- apt-get update -qq
- apt-get install -y libeufin-bank libeufin-common
- dpkg -l libeufin-bank libeufin-common | awk "/^ii/{print \$2,\$3}"
- '
- pod_root "$BANK_CTR" '
- set -e
- if ! runuser -u postgres -- psql -c "SELECT 1" >/dev/null 2>&1; then
- pg_ctlcluster 17 main start || true
- sleep 2
- fi
- mkdir -p /var/log/libeufin-bank
- chown libeufin-bank:libeufin-bank /var/log/libeufin-bank 2>/dev/null || true
- runuser -u libeufin-bank -- /usr/local/bin/start_bank.sh --restart
- '
-}
-
-upgrade_exchange() {
- log "======== apt exchange ========"
- pod_root "$EXCHANGE_CTR" '
- set -e
- export DEBIAN_FRONTEND=noninteractive
- apt-get update -qq
- apt-get install -y \
- taler-exchange taler-exchange-database taler-exchange-offline \
- taler-exchange-typst libtalerexchange taler-terms-generator \
- taler-exchange-aml-webui taler-exchange-kyc-webui || true
- dpkg -l "taler-exchange*" "libtalerexchange" "taler-terms-generator" 2>/dev/null \
- | awk "/^ii/{print \$2,\$3}"
- '
- pod_root "$EXCHANGE_CTR" '
- set -e
- if id taler-exchange-httpd >/dev/null 2>&1; then
- runuser -u taler-exchange-httpd -- taler-exchange-dbinit 2>&1 | tail -20 || true
- fi
- if [ -x /root/start_base_services_for_taler_exchange.sh ]; then
- /root/start_base_services_for_taler_exchange.sh --no-shell 2>/dev/null || true
- fi
- if [ -x /usr/local/bin/start_exchange.sh ]; then
- runuser -u taler-exchange-httpd -- /usr/local/bin/start_exchange.sh --restart 2>&1 | tail -30 || true
- fi
- pgrep -a taler-exchange-httpd | head -3 || true
- '
-}
-
-upgrade_merchant() {
- log "======== apt merchant ========"
- pod_root "$MERCHANT_CTR" '
- set -e
- export DEBIAN_FRONTEND=noninteractive
- apt-get update -qq
- apt-get install -y \
- taler-merchant taler-merchant-webui taler-merchant-typst \
- libtalermerchant taler-terms-generator
- dpkg -l "taler-merchant*" "libtalermerchant" 2>/dev/null | awk "/^ii/{print \$2,\$3}"
- '
- pod_root "$MERCHANT_CTR" '
- set -e
- if id taler-merchant-httpd >/dev/null 2>&1; then
- runuser -u taler-merchant-httpd -- taler-merchant-dbinit 2>&1 | tail -20 || true
- fi
- runuser -u taler-merchant-httpd -- /usr/local/bin/start_merchant.sh --restart
- '
-}
-
-commit_images() {
- log "======== podman commit ========"
- local tag="upgraded-$STAMP"
- podman commit "$BANK_CTR" "localhost/taler-hacktivism-banking:$tag"
- podman tag "localhost/taler-hacktivism-banking:$tag" "$BANK_IMG_LIVE"
- podman commit "$EXCHANGE_CTR" "localhost/taler-hacktivism-exchange-ansible:$tag"
- podman tag "localhost/taler-hacktivism-exchange-ansible:$tag" "$EXCHANGE_IMG_LIVE"
- podman commit "$MERCHANT_CTR" "localhost/taler-hacktivism-live:$tag"
- podman tag "localhost/taler-hacktivism-live:$tag" "$MERCHANT_IMG_LIVE"
- podman images --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.Size}}' \
- | grep -E "upgraded-$STAMP|:live|:landing" | tee "$RUN_DIR/images-after.txt" || true
-}
-
-smoke() {
- # Return 0=ok / 1=fail. Do NOT echo ok into stdout for capture:
- # with exec>tee, sm=$(smoke) would swallow log lines and never equal "1".
- log "======== smoke ========"
- local ok=1
- for pair in "bank:9012:http" "exchange:9011:http" "merchant:9010:https"; do
- IFS=: read -r name port sch <<<"$pair"
- code=$(curl -skS -m 10 -o /dev/null -w '%{http_code}' "${sch}://127.0.0.1:${port}/config" || echo 000)
- log "loopback $name :$port → $code"
- [ "$code" = "200" ] || ok=0
- done
- for url in \
- https://bank.hacktivism.ch/config \
- https://exchange.hacktivism.ch/config \
- https://taler.hacktivism.ch/config; do
- code=$(curl -skS -m 15 -o /dev/null -w '%{http_code}' "$url" || echo 000)
- log "public $url → $code"
- [ "$code" = "200" ] || ok=0
- done
- [ "$ok" = "1" ]
-}
-
-versions_snap() {
- log "======== versions after ========"
- pod_root "$BANK_CTR" 'dpkg -l libeufin-bank libeufin-common | awk "/^ii/{print \$2,\$3}"' | tee "$RUN_DIR/ver-bank.txt"
- pod_root "$EXCHANGE_CTR" 'dpkg -l "taler-exchange" "libtalerexchange" 2>/dev/null | awk "/^ii/{print \$2,\$3}"' | tee "$RUN_DIR/ver-exchange.txt"
- pod_root "$MERCHANT_CTR" 'dpkg -l "taler-merchant" "taler-merchant-webui" "libtalermerchant" 2>/dev/null | awk "/^ii/{print \$2,\$3}"' | tee "$RUN_DIR/ver-merchant.txt"
-}
-
-cmd_apply() {
- set_status APPLY_RUNNING
- cmd_backup
- set_status APPLY_BACKUP_OK "BAK=$ROOT/$STAMP"
- upgrade_bank
- upgrade_exchange
- upgrade_merchant
- set_status APPLY_APT_OK
- commit_images
- set_status APPLY_COMMIT_OK
- versions_snap
- if smoke; then
- set_status APPLY_OK "NEXT=elelmon-moderate" "BAK=$ROOT/$STAMP" "LOG=$LOG"
- log "APPLY_OK — next: elelmon moderate from laptop"
- else
- set_status APPLY_SMOKE_FAIL "NEXT=triage-smoke" "BAK=$ROOT/$STAMP" "LOG=$LOG"
- log "APPLY_SMOKE_FAIL"
- exit 1
- fi
-}
-
-cmd_status() {
- local cur="$ROOT/upgrade-runs/current"
- if [ -L "$cur" ] || [ -d "$cur" ]; then
- echo "CURRENT_RUN=$(readlink -f "$ROOT/upgrade-runs/current" 2>/dev/null || echo "$cur")"
- cat "$ROOT/upgrade-runs/current/STATUS" 2>/dev/null || cat "$cur/STATUS" 2>/dev/null || echo 'no STATUS'
- else
- echo 'NO_RUN'
- fi
- ls -la "$ROOT/upgrade-runs/" 2>/dev/null | tail -10 || true
-}
-
-case "$MODE" in
- dry-run|dry) cmd_dry_run ;;
- backup)
- set_status BACKUP_RUNNING
- cmd_backup
- set_status BACKUP_OK "STAMP=$STAMP" "CURRENT=$ROOT/current"
- log "BACKUP_OK"
- ;;
- apply) cmd_apply ;;
- status) cmd_status ;;
- -h|--help|help)
- sed -n '2,16p' "$0"
- exit 0
- ;;
- *)
- echo "usage: $0 dry-run|backup|apply|status" >&2
- exit 2
- ;;
-esac
diff --git a/scripts/taler-shared/goa-regio-ng/README.md b/scripts/taler-shared/goa-regio-ng/README.md
deleted file mode 100644
index 49c224a..0000000
--- a/scripts/taler-shared/goa-regio-ng/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# GOA regio-ng lab (koopa) — empty → restore
-
-Operator path for live GOA on `goa-regio-ng` (ports 9010–9015 via pasta).
-
-1. Empty image SoT: `localhost/goa-regio-ng:empty-current` (do **not** commit a restored CTR as empty).
-2. Bak: `~/backups/goa/current` on koopa.
-3. Copy helpers to `/tmp/` on koopa (or use `~/goa-regio-ng-scripts/`), then run `./goa-all-in-one.sh` (recreate from empty → restore → probes).
-4. Helpers: `goa-vanilla-restore-do.sh`, `goa-pg-acl-fixup.sh`, `goa-wire-post-restore.sh` (hosts `*.goa.local`, nginx bank proxy → `:9012`, `taler-exchange-dbconfig -p -s`, wirewatch/transfer `-a exchange-account-default` drop-ins). Restore calls the wire helper after core units are up and requires wirewatch active.
-5. Ansible SoT is laptop `taler-deployment` branch `dev/hernani/goa-regio-ng-vanilla` (lab inventory: `configs/lab-local/goa-regio-ng-koopa.yml`). koopa has no git.taler.net deploy key — rsync/copy tip when needed.
-6. Host landings 9013–9015 + Caddy stay outside the CTR; deploy from `~/goa-landing-deploy`.
-
-Greenfield Ansible `site.yml` on a fresh host is separate; this tree covers the verified restore bring-up.
diff --git a/scripts/taler-shared/goa-regio-ng/goa-901x-recreate.sh b/scripts/taler-shared/goa-regio-ng/goa-901x-recreate.sh
deleted file mode 100755
index 5fa3ca1..0000000
--- a/scripts/taler-shared/goa-regio-ng/goa-901x-recreate.sh
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/bin/bash
-# Recreate goa-regio-ng publishing 0.0.0.0:9010-9015 (host Caddy untouched).
-# Freigabe: live cutover restore / 901x replace. Rollback hacktivism = recreate from existing images.
-set -euo pipefail
-
-TS="$(date -u +%Y%m%dT%H%MZ)"
-IMG="localhost/goa-regio-ng:901x-wired-${TS}"
-NAME="goa-regio-ng"
-LOG="/tmp/goa-901x-recreate-${TS}.log"
-exec > >(tee -a "$LOG") 2>&1
-
-echo "TS=$TS IMG=$IMG LOG=$LOG"
-
-echo "=== pre: CTR/ports ==="
-podman ps -a --format '{{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'goa-regio|hacktivism' || true
-ss -lntp 2>/dev/null | grep -E ':901[0-5]\b' || echo '(host 901x none)'
-
-echo "=== save hacktivism CreateCommand (rollback recipe) ==="
-ROLLBACK="/tmp/hacktivism-rollback-create-${TS}.txt"
-: >"$ROLLBACK"
-for c in taler-hacktivism-bank taler-hacktivism taler-hacktivism-exchange-ansible; do
- if podman container exists "$c" 2>/dev/null; then
- echo "--- $c ---" >>"$ROLLBACK"
- podman inspect "$c" --format '{{json .Config.CreateCommand}}' >>"$ROLLBACK"
- echo >>"$ROLLBACK"
- podman inspect "$c" --format 'ImageName={{.ImageName}} ImageID={{.Image}}' >>"$ROLLBACK"
- echo >>"$ROLLBACK"
- fi
-done
-echo "ROLLBACK_FILE=$ROLLBACK"
-
-echo "=== commit $NAME -> $IMG ==="
-podman commit --pause=false "$NAME" "$IMG"
-podman images "$IMG" --format '{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}'
-
-echo "=== free 901x: rm exited hacktivism CTRs (images kept) ==="
-for c in taler-hacktivism-bank taler-hacktivism taler-hacktivism-exchange-ansible; do
- if podman container exists "$c" 2>/dev/null; then
- st="$(podman inspect "$c" --format '{{.State.Status}}')"
- echo "rm $c (was $st)"
- podman rm "$c"
- else
- echo "skip missing $c"
- fi
-done
-
-echo "=== stop+rm old $NAME ==="
-podman stop -t 30 "$NAME" || true
-podman rm "$NAME"
-
-echo "=== run $NAME from $IMG publishing 9010-9015 + lab 9180/9322 ==="
-podman run -d \
- --name "$NAME" \
- --privileged \
- --systemd=always \
- --network pasta \
- -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
- --tmpfs /run \
- --tmpfs /run/lock \
- -p 127.0.0.1:9322:22 \
- -p 127.0.0.1:9180:80 \
- -p 0.0.0.0:9010-9015:9010-9015 \
- --hostname "$NAME" \
- "$IMG" \
- /sbin/init
-
-echo "=== wait systemd + services ==="
-sleep 8
-for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
- if podman exec -u root "$NAME" systemctl is-system-running --wait 2>/dev/null | grep -qE 'running|degraded'; then
- break
- fi
- sleep 2
-done
-
-echo "=== host ports ==="
-podman ps --filter "name=^${NAME}$" --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'
-ss -lntp 2>/dev/null | grep -E ':901[0-5]\b' || echo '(WARN host 901x none)'
-
-echo "=== in-CTR listeners + units ==="
-podman exec -u root "$NAME" bash -c '
-ss -lntp 2>/dev/null | grep -E ":901[0-5]\b|:19096\b" || true
-for u in nginx taler-exchange-httpd libeufin-bank goa-demo-withdraw-api postgresql; do
- printf "%s=%s\n" "$u" "$(systemctl is-active "$u" 2>/dev/null || echo missing)"
-done
-'
-
-echo "RECREATE_OK IMG=$IMG LOG=$LOG ROLLBACK=$ROLLBACK"
diff --git a/scripts/taler-shared/goa-regio-ng/goa-all-in-one.sh b/scripts/taler-shared/goa-regio-ng/goa-all-in-one.sh
deleted file mode 100755
index 52bed90..0000000
--- a/scripts/taler-shared/goa-regio-ng/goa-all-in-one.sh
+++ /dev/null
@@ -1,269 +0,0 @@
-#!/usr/bin/env bash
-# All-in-one: restore empty → verify → recreate from EMPTY_IMG → restore →
-# hacktivism boot off → stack.env → probes. Landings stamp/deploy from laptop after.
-set -uo pipefail
-# intentionally NOT set -e: settle/grep and optional probes must not abort
-
-CTR=goa-regio-ng
-EMPTY_IMG=${EMPTY_IMG:-localhost/goa-regio-ng:empty-current}
-EXPECT_MASTER=6a1cececf164063ecd48172c21d56061ce7881fbd7a88d4ffd25504552508706
-TS=$(date -u +%Y%m%dT%H%MZ)
-LOG=/tmp/goa-all-in-one-${TS}.log
-FAIL=0
-exec > >(tee -a "$LOG") 2>&1
-
-echo "==== ALL-IN-ONE START $(date -u +%Y-%m-%dT%H:%M:%SZ) LOG=$LOG ===="
-echo "EMPTY_IMG=$EMPTY_IMG CTR=$CTR"
-
-die() { echo "FAIL: $*"; FAIL=1; }
-ok() { echo "OK: $*"; }
-
-settle_ctr() {
- local i st
- for i in $(seq 1 90); do
- st=$(podman exec -u root "$CTR" systemctl is-system-running 2>/dev/null || true)
- case "$st" in
- running|degraded) echo "SYSTEMD_SETTLE=$st after ${i}s"; return 0 ;;
- esac
- sleep 2
- done
- echo "SYSTEMD_SETTLE=timeout last=${st:-none}"
- return 1
-}
-
-wait_pg_host() {
- local max="${1:-600}" i=0 st
- echo "==== WAIT PG (host-side, max ${max}s) ===="
- podman exec -u root "$CTR" bash -c 'systemctl start postgresql 2>/dev/null; systemctl start postgresql@17-main 2>/dev/null; true' || true
- while [ "$i" -lt "$max" ]; do
- st=$(podman exec -u root "$CTR" bash -c '
-set +e
-systemctl is-active postgresql@17-main 2>/dev/null
-runuser -u postgres -- psql -d postgres -v ON_ERROR_STOP=1 -tAc "SELECT 1" 2>/dev/null
-' 2>/dev/null | tr '\n' ' ')
- if echo "$st" | grep -q 'active' && echo "$st" | grep -qw '1'; then
- echo "PG_ACCEPT after ${i}s"
- return 0
- fi
- [ $((i % 30)) -eq 0 ] && echo "PG_WAIT ${i}s [$st]"
- sleep 2
- i=$((i + 2))
- done
- echo "PG_TIMEOUT after ${max}s"
- return 1
-}
-
-# Scheme-aware probes; hard-fail on bad codes.
-# 9010 merchant HTTPS; 9011 exchange HTTP; 9012 bank HTTP; 9013-15 landings 302 OK.
-probe_ports() {
- local tag=$1 hard=${2:-0} p code okc failc=0
- echo "=== probes $tag (hard=$hard) ==="
-
- code=$(curl -skS -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 10 "https://127.0.0.1:9010/config" 2>/dev/null || echo ERR)
- echo "MERCHANT_HTTPS_9010_CONFIG=${code}"
- case "$code" in 200|204) ;; *) failc=$((failc+1)) ;; esac
-
- code=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 10 "http://127.0.0.1:9011/config" 2>/dev/null || echo ERR)
- echo "EXCHANGE_HTTP_9011_CONFIG=${code}"
- case "$code" in 200|204) ;; *)
- code=$(curl -skS -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 10 "https://127.0.0.1:9011/config" 2>/dev/null || echo ERR)
- echo "EXCHANGE_HTTPS_9011_CONFIG=${code}"
- case "$code" in 200|204) ;; *) failc=$((failc+1)) ;; esac
- ;;
- esac
-
- code=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 10 "http://127.0.0.1:9012/config" 2>/dev/null || echo ERR)
- echo "BANK_HTTP_9012_CONFIG=${code}"
- case "$code" in 200|204) ;; *) failc=$((failc+1)) ;; esac
-
- for p in 9013 9014 9015; do
- code=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time 8 "http://127.0.0.1:${p}/" 2>/dev/null || echo ERR)
- echo "LANDING_${p}=${code}"
- case "$code" in 200|301|302|303|307|308) ;; *) failc=$((failc+1)) ;; esac
- done
-
- echo "PROBE_FAILS_${tag}=${failc}"
- if [ "$hard" = "1" ] && [ "$failc" -gt 0 ]; then
- die "probes $tag had $failc failures"
- return 1
- fi
- return 0
-}
-
-smoke_inside() {
- local tag=$1 ww
- echo "=== inside smoke $tag ==="
- podman exec -u root "$CTR" bash -lc '
-set +e
-echo MASTER_SHA=$(sha256sum /var/lib/taler-exchange/offline/master.priv 2>/dev/null | awk "{print \$1}")
-echo EXPECT='"$EXPECT_MASTER"'
-for u in postgresql postgresql@17-main nginx taler-exchange-httpd taler-merchant-httpd libeufin-bank goa-demo-withdraw-api taler-exchange-wirewatch taler-exchange-transfer; do
- printf "%s=%s\n" "$u" "$(systemctl is-active "$u" 2>/dev/null || echo missing)"
-done
-sudo -u postgres psql -Atc "SELECT datname||\"=\"||pg_size_pretty(pg_database_size(datname)) FROM pg_database WHERE datname ~ '"'"'exchange|libeufin|merchant'"'"' ORDER BY 1;"
-'
- ww=$(podman exec -u root "$CTR" systemctl is-active taler-exchange-wirewatch.service 2>/dev/null || echo missing)
- if [ "$ww" = "active" ]; then
- ok "wirewatch active ($tag)"
- else
- die "wirewatch not active ($tag)=$ww"
- fi
-}
-
-run_restore() {
- local tag=$1
- echo "==== RESTORE $tag ===="
- if ! bash /tmp/goa-vanilla-restore-do.sh; then
- die "restore $tag failed"
- return 1
- fi
- ok "restore $tag"
- return 0
-}
-
-recreate_from_empty() {
- echo "==== RECREATE from EMPTY_IMG (no commit of restored) ===="
- podman image exists "$EMPTY_IMG" || { die "missing EMPTY_IMG $EMPTY_IMG"; return 1; }
- podman stop -t 30 "$CTR" || true
- podman rm "$CTR" || true
- podman run -d \
- --name "$CTR" \
- --privileged \
- --systemd=always \
- --network pasta \
- -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
- --tmpfs /run \
- --tmpfs /run/lock \
- -p 127.0.0.1:9322:22 \
- -p 127.0.0.1:9180:80 \
- -p 0.0.0.0:9010-9015:9010-9015 \
- --hostname "$CTR" \
- "$EMPTY_IMG" \
- /sbin/init
- sleep 8
- settle_ctr || { die "settle after recreate"; return 1; }
- wait_pg_host 600 || { die "PG not ready after recreate"; return 1; }
- podman ps --filter "name=^${CTR}$" --format '{{.Names}} {{.Status}} {{.Image}} {{.Ports}}'
- echo "=== pre-restore DBs (expect empty/minimal) ==="
- podman exec -u root "$CTR" bash -lc 'sudo -u postgres psql -Atc "SELECT datname FROM pg_database WHERE datname ~ '"'"'exchange|libeufin|merchant'"'"' ORDER BY 1;"' || true
- ok "recreate from empty"
-}
-
-disable_hacktivism_boot() {
- echo "==== DISABLE hacktivism boot units ===="
- local u
- for u in \
- container-taler-hacktivism-bank.service \
- container-taler-hacktivism-exchange-ansible.service \
- container-taler-hacktivism.service \
- taler-monitoring-hacktivism.timer \
- taler-monitoring-hacktivism.path
- do
- if systemctl --user list-unit-files "$u" >/dev/null 2>&1; then
- systemctl --user disable --now "$u" 2>/dev/null || systemctl --user disable "$u" 2>/dev/null || true
- echo "disabled $u"
- else
- echo "skip missing $u"
- fi
- done
- systemctl --user daemon-reload || true
- echo "=== enabled leftover hacktivism? ==="
- systemctl --user list-unit-files 2>/dev/null | grep -i hacktivism || echo '(none listed)'
- ok "hacktivism boot disable attempted"
-}
-
-install_stack_env() {
- echo "==== INSTALL stack.env → goa-regio-ng ===="
- mkdir -p "$HOME/.config/taler-landing"
- cat > "$HOME/.config/taler-landing/stack.env" <<'ENV'
-# Stack profile: GOA / goa-regio-ng vanilla (9010-9015)
-# systemd EnvironmentFile: KEY=value only (no export)
-BANK_CTR=goa-regio-ng
-EX_CTR=goa-regio-ng
-MER_CTR=goa-regio-ng
-BANK_URL=http://127.0.0.1:9012
-BANK_PUBLIC_URL=https://bank.hacktivism.ch
-EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
-BANK_CURRENCY=GOA
-BANK_LANDING_IN=/var/www/bank-landing
-EX_LANDING_IN=/var/www/exchange-landing
-MER_LANDING_IN=/var/www/merchant-landing
-COLLECT_BANK=1
-COLLECT_EXCHANGE=1
-COLLECT_MERCHANT=1
-COLLECT_RESOURCES=1
-PUBLISH_PODMAN=1
-STATS_SOURCE_LABEL="host collect_bank_stats.py goa regio-ng vanilla"
-ENV
- echo "--- stack.env ---"
- cat "$HOME/.config/taler-landing/stack.env"
- local unit="$HOME/.config/systemd/user/taler-landing-stats.service"
- if [ -f "$unit" ]; then
- if grep -q hacktivism "$unit"; then
- cp -a "$unit" "${unit}.bak-${TS}"
- sed -i \
- -e 's/container-taler-hacktivism[^ ]*\.service/goa-regio-ng.service/g' \
- -e 's/After=.*/After=goa-regio-ng.service/' \
- "$unit" || true
- if ! systemctl --user cat goa-regio-ng.service >/dev/null 2>&1; then
- sed -i 's/^After=.*/# After= (no user unit for goa-regio-ng; started via podman)/' "$unit" || true
- fi
- systemctl --user daemon-reload || true
- echo "patched $unit"
- grep -E 'After=|Requires=|EnvironmentFile' "$unit" || true
- else
- echo "stats unit already non-hacktivism"
- fi
- else
- echo "no taler-landing-stats.service yet"
- fi
- ok "stack.env installed"
-}
-
-check_master() {
- local sha
- sha=$(podman exec -u root "$CTR" sha256sum /var/lib/taler-exchange/offline/master.priv 2>/dev/null | awk '{print $1}')
- echo "MASTER_LIVE=$sha"
- if [ "$sha" = "$EXPECT_MASTER" ]; then
- ok "master.priv matches bak"
- else
- die "master.priv mismatch got=$sha expect=$EXPECT_MASTER"
- fi
-}
-
-# -------- Phase 1: restore current empty CTR --------
-echo "==== PHASE1: restore current CTR (expect empty image) ===="
-podman ps --filter "name=^${CTR}$" --format '{{.Names}} {{.Status}} {{.Image}}'
-settle_ctr || die "settle before phase1 restore"
-wait_pg_host 600 || die "PG not ready before phase1"
-run_restore phase1 || true
-if [ "$FAIL" -eq 0 ]; then
- smoke_inside phase1
- check_master
- probe_ports phase1 1 || true
-fi
-
-# -------- Phase 2: recreate from EMPTY + restore again --------
-echo "==== PHASE2: recreate-from-EMPTY + restore again ===="
-if [ "$FAIL" -eq 0 ]; then
- recreate_from_empty || true
-fi
-if [ "$FAIL" -eq 0 ]; then
- run_restore phase2 || true
- smoke_inside phase2
- check_master
- probe_ports phase2 1 || true
-fi
-
-# -------- Phase 3: hacktivism off + stack.env --------
-disable_hacktivism_boot
-install_stack_env
-
-probe_ports final 1 || true
-echo "==== ALL-IN-ONE END $(date -u +%Y-%m-%dT%H:%M:%SZ) FAIL=$FAIL LOG=$LOG ===="
-if [ "$FAIL" -ne 0 ]; then
- echo "ALL_IN_ONE_FAILED"
- exit 1
-fi
-echo "ALL_IN_ONE_OK"
-exit 0
diff --git a/scripts/taler-shared/goa-regio-ng/goa-e2e-recreate-restore.sh b/scripts/taler-shared/goa-regio-ng/goa-e2e-recreate-restore.sh
deleted file mode 100755
index 23fa267..0000000
--- a/scripts/taler-shared/goa-regio-ng/goa-e2e-recreate-restore.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-set -euo pipefail
-CTR=goa-regio-ng
-LOG=/tmp/goa-e2e-recreate-restore.log
-exec > >(tee -a "$LOG") 2>&1
-echo "=== START $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
-
-echo "=== 1) RECREATE (empty CTR) ==="
-bash /tmp/goa-901x-recreate.sh
-echo "RECREATE_RC=$?"
-
-echo "=== wait ports / systemd settle ==="
-for i in $(seq 1 60); do
- if podman exec "$CTR" systemctl is-system-running >/dev/null 2>&1 \
- || podman exec "$CTR" systemctl is-system-running 2>&1 | grep -qE 'running|degraded'; then
- echo "SYSTEMD_OK after ${i}s"
- break
- fi
- sleep 5
-done
-podman ps --filter name="$CTR" --format '{{.Names}} {{.Status}}'
-ss -lntp 2>/dev/null | grep -E '901[0-5]|9180|9322' || true
-
-echo "=== pre-restore: expect empty/minimal DBs ==="
-podman exec "$CTR" bash -lc 'sudo -u postgres psql -Atc "SELECT datname FROM pg_database WHERE datname ~ '"'"'exchange|libeufin|merchant'"'"' ORDER BY 1;"' || true
-
-echo "=== 2) RESTORE from bak via script ==="
-bash /tmp/goa-vanilla-restore-do.sh
-echo "RESTORE_RC=$?"
-
-echo "=== 3) post-restore smoke inside CTR ==="
-podman exec "$CTR" bash -lc '
-set +e
-echo MASTER_SHA=$(sha256sum /var/lib/taler-exchange/offline/master.priv 2>/dev/null | awk "{print \$1}")
-echo EXPECT=6a1cececf164063ecd48172c21d56061ce7881fbd7a88d4ffd25504552508706
-for u in taler-exchange-httpd taler-merchant-httpd libeufin-bank libeufin-nexus; do
- echo -n "$u: "; systemctl is-active $u 2>/dev/null || systemctl is-active ${u}.service 2>/dev/null
-done
-sudo -u postgres psql -Atc "SELECT datname FROM pg_database WHERE datname ~ '"'"'exchange|libeufin|merchant'"'"' ORDER BY 1;"
-'
-
-echo "=== 4) host probes 9010-9015 ==="
-for p in 9010 9011 9012 9013 9014 9015; do
- code=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 3 "http://127.0.0.1:${p}/" || echo ERR)
- echo "PORT_${p}=${code}"
-done
-
-echo "=== 5) leftover hacktivism / failed units ==="
-systemctl list-units --all --no-pager 'container-taler-hacktivism*' 2>/dev/null || true
-systemctl list-units --failed --no-pager 2>/dev/null | head -40 || true
-podman ps -a --format '{{.Names}} {{.Status}}' | grep -iE 'hacktivism|goa|taler' || true
-
-echo "=== END $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
diff --git a/scripts/taler-shared/goa-regio-ng/goa-pg-acl-fixup.sh b/scripts/taler-shared/goa-regio-ng/goa-pg-acl-fixup.sh
deleted file mode 100755
index d4d6f88..0000000
--- a/scripts/taler-shared/goa-regio-ng/goa-pg-acl-fixup.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-# After pg_restore --no-owner --no-acl: give app role full rights on non-system schemas.
-# Do NOT ALTER SEQUENCE OWNER for serials linked to tables (Postgres rejects that);
-# ALTER TABLE OWNER covers owned serials.
-# Usage inside CTR: goa-pg-acl-fixup.sh
-set -euo pipefail
-db="${1:?db}"
-owner="${2:?owner}"
-# owner is a fixed role name from the restore caller (no user input).
-runuser -u postgres -- psql -d "$db" -v ON_ERROR_STOP=1 < >(tee -a "$LOG") 2>&1
-
-echo "==== VANILLA RESTORE START $(date -u +%Y-%m-%dT%H:%M:%SZ) ===="
-echo "CTR=$CTR DUMP_ROOT=$DUMP_ROOT SECMOD_SRC=$SECMOD_SRC LOG=$LOG"
-
-need() { [ -f "$1" ] || { echo "FAIL missing $1"; exit 1; }; }
-need "$DUMP_ROOT/bank/libeufin.dump"
-need "$DUMP_ROOT/exchange/taler-exchange.dump"
-need "$DUMP_ROOT/merchant/taler-merchant.dump"
-need "$MASTER_BAK"
-need "$SECMOD_SRC/master.priv"
-[ -d "$SECMOD_SRC/secmod-rsa" ] || { echo "FAIL missing secmod-rsa"; exit 1; }
-
-BAK_SHA="$(sha256sum "$MASTER_BAK" | awk '{print $1}')"
-STG_SHA="$(sha256sum "$SECMOD_SRC/master.priv" | awk '{print $1}')"
-echo "MASTER_BAK_SHA=$BAK_SHA"
-echo "MASTER_STG_SHA=$STG_SHA"
-[ "$BAK_SHA" = "$STG_SHA" ] || { echo "FAIL master.priv bak≠staging"; exit 1; }
-
-podman container exists "$CTR" || { echo "FAIL CTR missing $CTR"; exit 1; }
-
-wait_pg() {
- # EMPTY_IMG may need long crash-recovery after unclean shutdown.
- local max="${1:-600}" i=0 st
- echo "==== WAIT PG accept (max ${max}s) ===="
- podman exec -u root "$CTR" bash -c '
-set +e
-systemctl start postgresql 2>/dev/null || true
-systemctl start postgresql@17-main 2>/dev/null || true
-'
- while [ "$i" -lt "$max" ]; do
- st=$(podman exec -u root "$CTR" bash -c '
-set +e
-systemctl is-active postgresql@17-main 2>/dev/null
-runuser -u postgres -- psql -d postgres -v ON_ERROR_STOP=1 -tAc "SELECT 1" 2>/dev/null
-' 2>/dev/null | tr '\n' ' ')
- if echo "$st" | grep -q 'active' && echo "$st" | grep -qw '1'; then
- echo "PG_ACCEPT after ${i}s ($st)"
- return 0
- fi
- if [ $((i % 30)) -eq 0 ]; then
- echo "PG_WAIT ${i}s state=[$st]"
- podman exec -u root "$CTR" bash -c 'tail -n 3 /var/log/postgresql/postgresql-17-main.log 2>/dev/null || journalctl -u postgresql@17-main -n 3 --no-pager 2>/dev/null | tail -n 3' || true
- fi
- sleep 2
- i=$((i + 2))
- done
- echo "FAIL PG not accepting after ${max}s"
- podman exec -u root "$CTR" bash -c 'systemctl status postgresql@17-main --no-pager -l 2>&1 | head -40; journalctl -u postgresql@17-main -n 40 --no-pager 2>&1 | tail -40' || true
- return 1
-}
-
-wait_unit_active() {
- local unit="$1" max="${2:-120}" i=0 st
- while [ "$i" -lt "$max" ]; do
- st=$(podman exec -u root "$CTR" systemctl is-active "$unit" 2>/dev/null || echo missing)
- if [ "$st" = "active" ]; then
- echo "UNIT_ACTIVE $unit after ${i}s"
- return 0
- fi
- if [ "$st" = "failed" ]; then
- echo "UNIT_FAILED $unit after ${i}s"
- podman exec -u root "$CTR" bash -c "systemctl status $unit --no-pager -l 2>&1 | head -50; journalctl -u $unit -n 80 --no-pager 2>&1 | tail -80" || true
- return 1
- fi
- sleep 2
- i=$((i + 2))
- done
- echo "UNIT_TIMEOUT $unit last=$st after ${max}s"
- podman exec -u root "$CTR" bash -c "systemctl status $unit --no-pager -l 2>&1 | head -50; journalctl -u $unit -n 80 --no-pager 2>&1 | tail -80" || true
- return 1
-}
-
-echo "==== STOP app units (keep postgresql) ===="
-podman exec -u root "$CTR" bash -c '
-set -e
-for u in \
- goa-demo-withdraw-api \
- taler-merchant-httpd \
- taler-exchange-httpd \
- taler-exchange-wirewatch \
- taler-exchange-aggregator \
- taler-exchange-closer \
- taler-exchange-expire \
- taler-exchange-transfer \
- taler-exchange-secmod-rsa \
- taler-exchange-secmod-eddsa \
- taler-exchange-secmod-cs \
- libeufin-bank \
- libeufin-bank-nginx \
- nginx
-do
- systemctl stop "$u" 2>/dev/null || true
-done
-systemctl reset-failed 2>/dev/null || true
-'
-
-wait_pg 600 || exit 1
-
-echo "==== COPY dumps into CTR ===="
-podman cp "$DUMP_ROOT/bank/libeufin.dump" "${CTR}:/tmp/libeufin.dump"
-podman cp "$DUMP_ROOT/exchange/taler-exchange.dump" "${CTR}:/tmp/taler-exchange.dump"
-podman cp "$DUMP_ROOT/merchant/taler-merchant.dump" "${CTR}:/tmp/taler-merchant.dump"
-# ACL helper must be in CTR before restore_one (post pg_restore --no-acl)
-if [ ! -f /tmp/goa-pg-acl-fixup.sh ]; then
- echo "FAIL missing /tmp/goa-pg-acl-fixup.sh on host"
- exit 2
-fi
-podman cp /tmp/goa-pg-acl-fixup.sh "${CTR}:/tmp/goa-pg-acl-fixup.sh"
-
-echo "==== DROP+CREATE+RESTORE DBs ===="
-# Live vanilla CONFIG=postgres:///exchange (bak dump dbname was taler-exchange).
-podman exec -u root "$CTR" bash -c '
-set -euo pipefail
-restore_one() {
- local db="$1" dump="$2" owner="$3"
- echo "--- restore $db from $dump owner=$owner ---"
- runuser -u postgres -- psql -d postgres -c \
- "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '"'"'$db'"'"' AND pid <> pg_backend_pid();" \
- || true
- runuser -u postgres -- dropdb --if-exists --force "$db" || true
- runuser -u postgres -- createdb -O "$owner" "$db"
- set +e
- runuser -u postgres -- pg_restore --no-owner --no-acl --dbname="$db" "$dump"
- RC=$?
- set -e
- # pg_restore exits 1 on warnings; 0 ok; >=2 hard fail
- echo "PG_RESTORE_$db RC=$RC"
- if [ "$RC" -ge 2 ]; then
- echo "FAIL pg_restore $db"
- exit 2
- fi
- rm -f "$dump"
- # --no-owner --no-acl leaves schemas owned by postgres with no grants →
- # app roles hit "permission denied for schema _v". Fix ownership + GRANTs.
- # Skip ALTER SEQUENCE OWNER for linked serials; ALTER TABLE OWNER covers them.
- bash /tmp/goa-pg-acl-fixup.sh "$db" "$owner"
- runuser -u postgres -- psql -d "$db" -v ON_ERROR_STOP=1 -c "SELECT current_database() AS db;"
- runuser -u postgres -- psql -d "$db" -tAc "SELECT pg_size_pretty(pg_database_size(current_database()));"
- runuser -u postgres -- psql -d "$db" -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema='"'"'public'"'"';"
- # also count exchange schema (bak dumps use schema exchange, not public)
- runuser -u postgres -- psql -d "$db" -tAc "SELECT '"'"'schema_exchange_tables='"'"'||count(*)::text FROM information_schema.tables WHERE table_schema='"'"'exchange'"'"';" || true
-}
-
-restore_one libeufin /tmp/libeufin.dump libeufin-bank
-restore_one exchange /tmp/taler-exchange.dump taler-exchange-httpd
-restore_one taler-merchant /tmp/taler-merchant.dump taler-merchant-httpd
-
-# drop leftover bak-named DB if any
-runuser -u postgres -- dropdb --if-exists --force taler-exchange || true
-runuser -u postgres -- psql -d postgres -c "\l"
-'
-
-echo "==== INSTALL master.priv + secmod from staging ===="
-HOST_STG="/tmp/goa-restore-host-secmod-$$"
-rm -rf "$HOST_STG"
-mkdir -p "$HOST_STG"
-cp -a "$SECMOD_SRC/." "$HOST_STG/"
-cp -a "$MASTER_BAK" "$HOST_STG/master.priv"
-# also secm_tofus.pub from bak offline if present
-if [ -f "$DUMP_ROOT/exchange/offline/secm_tofus.pub" ]; then
- # staging copy is often mode 0400; plain cp -a cannot overwrite it
- rm -f "$HOST_STG/secm_tofus.pub"
- cp -a "$DUMP_ROOT/exchange/offline/secm_tofus.pub" "$HOST_STG/secm_tofus.pub"
-fi
-
-podman exec -u root "$CTR" bash -c '
-set -euo pipefail
-rm -rf /tmp/secmod-in
-mkdir -p /tmp/secmod-in
-'
-podman cp "$HOST_STG/." "${CTR}:/tmp/secmod-in/"
-
-podman exec -u root "$CTR" bash -c '
-set -euo pipefail
-OFF=/var/lib/taler-exchange/offline
-mkdir -p "$OFF"
-install -o taler-exchange-offline -g taler-exchange-offline -m 400 /tmp/secmod-in/master.priv "$OFF/master.priv"
-if [ -f /tmp/secmod-in/secm_tofus.pub ]; then
- install -o taler-exchange-offline -g taler-exchange-offline -m 400 /tmp/secmod-in/secm_tofus.pub "$OFF/secm_tofus.pub"
-fi
-# offline-keys home path (also used live)
-HOME_OFF=/home/taler-exchange-offline/.local/share/taler/exchange/offline-keys
-mkdir -p "$HOME_OFF"
-install -o taler-exchange-offline -g taler-exchange-offline -m 400 /tmp/secmod-in/master.priv "$HOME_OFF/master.priv"
-
-# secmod trees
-for kind in rsa eddsa cs; do
- src="/tmp/secmod-in/secmod-$kind"
- dst="/var/lib/taler-exchange/secmod-$kind"
- [ -d "$src" ] || { echo "FAIL missing $src"; exit 1; }
- rm -rf "$dst"
- mkdir -p "$dst"
- cp -a "$src/." "$dst/"
- # ownership: taler-exchange-secmod- : taler-exchange-secmod
- u="taler-exchange-secmod-$kind"
- chown -R "$u:taler-exchange-secmod" "$dst"
- chmod -R u=rwX,g=rX,o= "$dst"
- find "$dst" -type f -name "*private*" -exec chmod 400 {} \; || true
-done
-sha256sum "$OFF/master.priv"
-rm -rf /tmp/secmod-in
-echo SECMOD_INSTALL_OK
-'
-rm -rf "$HOST_STG"
-
-echo "==== START units ===="
-podman exec -u root "$CTR" bash -c '
-set -e
-systemctl start postgresql
-systemctl start postgresql@17-main || true
-systemctl start taler-exchange-secmod-rsa taler-exchange-secmod-eddsa taler-exchange-secmod-cs
-sleep 2
-systemctl start taler-exchange-httpd
-systemctl start libeufin-bank
-systemctl start taler-merchant-httpd
-systemctl start nginx || true
-systemctl start goa-demo-withdraw-api || true
-# helpers
-for u in taler-exchange-wirewatch taler-exchange-aggregator taler-exchange-closer taler-exchange-expire taler-exchange-transfer; do
- systemctl start "$u" 2>/dev/null || true
-done
-'
-
-UNIT_FAIL=0
-for u in taler-exchange-secmod-rsa taler-exchange-secmod-eddsa taler-exchange-secmod-cs \
- taler-exchange-httpd libeufin-bank taler-merchant-httpd nginx; do
- if ! wait_unit_active "$u" 180; then
- UNIT_FAIL=1
- fi
-done
-# demo-withdraw optional
-wait_unit_active goa-demo-withdraw-api 60 || echo "WARN goa-demo-withdraw-api not active (optional)"
-
-echo "==== UNIT STATUS SNAPSHOT ===="
-podman exec -u root "$CTR" bash -c '
-for u in postgresql postgresql@17-main taler-exchange-httpd libeufin-bank taler-merchant-httpd \
- taler-exchange-secmod-rsa taler-exchange-secmod-eddsa taler-exchange-secmod-cs \
- nginx goa-demo-withdraw-api; do
- printf "%s=%s\n" "$u" "$(systemctl is-active "$u" 2>/dev/null || echo missing)"
-done
-'
-
-if [ "$UNIT_FAIL" -ne 0 ]; then
- echo "FAIL required units not active"
- exit 1
-fi
-
-echo "==== WIRE POST-RESTORE (hosts + nginx 9012 + dbconfig + -a drop-ins) ===="
-if [ ! -f /tmp/goa-wire-post-restore.sh ]; then
- echo "FAIL missing /tmp/goa-wire-post-restore.sh on host"
- exit 2
-fi
-podman cp /tmp/goa-wire-post-restore.sh "${CTR}:/tmp/goa-wire-post-restore.sh"
-podman exec -u root "$CTR" bash /tmp/goa-wire-post-restore.sh
-
-echo "==== LOCAL SMOKE (scheme-aware) ===="
-SMOKE_FAIL=0
-code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 10 http://127.0.0.1:9012/config 2>/dev/null || echo ERR)
-echo "bank_http_9012=$code"
-case "$code" in 200|204) ;; *) SMOKE_FAIL=1 ;; esac
-
-code=$(curl -skS -o /dev/null -w "%{http_code}" --max-time 10 https://127.0.0.1:9010/config 2>/dev/null || echo ERR)
-echo "merchant_https_9010=$code"
-case "$code" in 200|204) ;; *) SMOKE_FAIL=1 ;; esac
-
-code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 10 http://127.0.0.1:9011/config 2>/dev/null || echo ERR)
-echo "exchange_http_9011=$code"
-case "$code" in 200|204) ;; *)
- # fallback https in case nginx wraps it
- code2=$(curl -skS -o /dev/null -w "%{http_code}" --max-time 10 https://127.0.0.1:9011/config 2>/dev/null || echo ERR)
- echo "exchange_https_9011=$code2"
- case "$code2" in 200|204) ;; *) SMOKE_FAIL=1 ;; esac
- ;;
-esac
-
-ww=$(podman exec -u root "$CTR" systemctl is-active taler-exchange-wirewatch.service 2>/dev/null || echo missing)
-echo "wirewatch=$ww"
-[ "$ww" = "active" ] || SMOKE_FAIL=1
-
-if [ "$SMOKE_FAIL" -ne 0 ]; then
- echo "FAIL local smoke probes"
- podman exec -u root "$CTR" bash -c '
-ss -lntp 2>/dev/null | grep -E ":901[0-5]|:80 " || netstat -lntp 2>/dev/null | grep -E ":901[0-5]|:80 " || true
-for u in taler-exchange-httpd libeufin-bank taler-merchant-httpd nginx taler-exchange-wirewatch; do
- echo "--- journal $u ---"
- journalctl -u "$u" -n 40 --no-pager 2>&1 | tail -40
-done
-' || true
- exit 1
-fi
-
-echo "==== PUBLIC SMOKE ===="
-for u in \
- https://exchange.hacktivism.ch/config \
- https://bank.hacktivism.ch/config \
- https://backend.hacktivism.ch/config
-do
- code=$(curl -skS -o /dev/null -w "%{http_code}" --max-time 15 "$u" || echo ERR)
- echo "PUB $code $u"
-done
-
-echo "==== POST DB SIZES ===="
-podman exec -u root "$CTR" runuser -u postgres -- psql -d postgres -tAc \
- "SELECT datname||'='||pg_size_pretty(pg_database_size(datname)) FROM pg_database WHERE datname IN ('exchange','libeufin','taler-merchant') ORDER BY 1;"
-podman exec -u root "$CTR" runuser -u postgres -- psql -d exchange -tAc \
- "SELECT 'exchange_schema_tables='||count(*)::text FROM information_schema.tables WHERE table_schema='exchange';"
-
-echo "RESTORE_SCRIPT_OK $(date -u +%Y-%m-%dT%H:%M:%SZ) LOG=$LOG"
diff --git a/scripts/taler-shared/goa-regio-ng/goa-wire-post-restore.sh b/scripts/taler-shared/goa-regio-ng/goa-wire-post-restore.sh
deleted file mode 100755
index 4eda445..0000000
--- a/scripts/taler-shared/goa-regio-ng/goa-wire-post-restore.sh
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env bash
-# Inside CTR (as root): wire path after empty→restore.
-# - /etc/hosts *.goa.local
-# - nginx bank proxy → libeufin :9012 (empty image still has :8080)
-# - taler-exchange-dbconfig -p -s (schema + grants for wire role)
-# - systemd -a drop-ins (dual exchange-account-1 + exchange-account-default)
-# - start wirewatch + transfer; require wirewatch active
-set -euo pipefail
-
-echo "==== WIRE POST-RESTORE ===="
-
-if ! grep -q 'bank.goa.local' /etc/hosts 2>/dev/null; then
- echo '127.0.0.1 bank.goa.local exchange.goa.local backend.goa.local' >> /etc/hosts
-fi
-grep -E 'goa\.local' /etc/hosts || true
-
-SNIP=/etc/nginx/snippets/regional-currency-bank-proxy.conf
-if [ -f "$SNIP" ] && grep -q '127.0.0.1:8080' "$SNIP"; then
- sed -i 's|proxy_pass http://127.0.0.1:8080;|proxy_pass http://127.0.0.1:9012;|' "$SNIP"
- echo "NGINX_PROXY_9012=patched"
-fi
-if command -v nginx >/dev/null 2>&1; then
- nginx -t
- systemctl reload nginx 2>/dev/null || systemctl restart nginx || true
-fi
-
-taler-exchange-dbconfig -p -s
-echo "DBCONFIG_OK"
-
-ACCT=exchange-account-default
-for unit in taler-exchange-wirewatch taler-exchange-transfer; do
- bin="/usr/bin/${unit}"
- d="/etc/systemd/system/${unit}.service.d"
- mkdir -p "$d"
- cat > "${d}/account.conf" </dev/null || true
-systemctl restart taler-exchange-wirewatch.service
-systemctl start taler-exchange-transfer.service 2>/dev/null || true
-
-# wait wirewatch
-ok=0
-for i in $(seq 1 30); do
- st=$(systemctl is-active taler-exchange-wirewatch.service 2>/dev/null || echo missing)
- if [ "$st" = "active" ]; then
- ok=1
- break
- fi
- sleep 1
-done
-echo "WIREWATCH=$(systemctl is-active taler-exchange-wirewatch.service 2>/dev/null || echo missing)"
-echo "TRANSFER=$(systemctl is-active taler-exchange-transfer.service 2>/dev/null || echo missing)"
-if [ "$ok" -ne 1 ]; then
- journalctl -u taler-exchange-wirewatch.service -n 40 --no-pager || true
- echo "FAIL wirewatch not active"
- exit 1
-fi
-
-TOKEN=$(awk -F= '/^TOKEN=/{print $2; exit}' \
- /etc/taler-exchange/secrets/exchange-accountcredentials-default.secret.conf 2>/dev/null || true)
-if [ -n "${TOKEN:-}" ]; then
- code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
- -H "Authorization: Bearer ${TOKEN}" \
- http://bank.goa.local/accounts/exchange/taler-wire-gateway/config 2>/dev/null || echo ERR)
- echo "WIRE_GATEWAY_VIA_NGINX=${code}"
- case "$code" in 200|204) ;; *)
- echo "FAIL wire gateway via bank.goa.local"
- exit 1
- ;;
- esac
-fi
-
-echo "WIRE_POST_RESTORE_OK"
diff --git a/scripts/taler-shared/install-ensure-taler-apps.sh b/scripts/taler-shared/install-ensure-taler-apps.sh
index 83d589d..60f2367 100644
--- a/scripts/taler-shared/install-ensure-taler-apps.sh
+++ b/scripts/taler-shared/install-ensure-taler-apps.sh
@@ -1,8 +1,6 @@
#!/usr/bin/env bash
# Install ensure-taler-apps + user systemd units on this host (hernani@koopa).
# Run from admin-log checkout or with ADMIN_LOG set.
-# Defaults target goa-regio-ng (vanilla AIO). Legacy hacktivism drop-ins stay
-# installable but are not required after cutover.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
@@ -16,20 +14,14 @@ mkdir -p "$BIN_DST" "$UNIT_DST"
install -m 0755 "$ADMIN_LOG/scripts/taler-shared/ensure-taler-apps.sh" \
"$BIN_DST/ensure-taler-apps.sh"
-install -m 0644 "$UNIT_SRC/goa-regio-ng.service" "$UNIT_DST/"
install -m 0644 "$UNIT_SRC/taler-merchant-apps.service" "$UNIT_DST/"
install -m 0644 "$UNIT_SRC/taler-bank-apps.service" "$UNIT_DST/"
-install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/"
mkdir -p \
- "$UNIT_DST/goa-regio-ng.service.d" \
"$UNIT_DST/container-taler-hacktivism.service.d" \
"$UNIT_DST/container-taler-hacktivism-bank.service.d" \
"$UNIT_DST/container-koopa-paivana.service.d"
-install -m 0644 "$UNIT_SRC/goa-regio-ng.service.d/apps.conf" \
- "$UNIT_DST/goa-regio-ng.service.d/apps.conf"
-# Legacy drop-ins (harmless if old container units stay disabled)
install -m 0644 "$UNIT_SRC/container-taler-hacktivism.service.d/apps.conf" \
"$UNIT_DST/container-taler-hacktivism.service.d/apps.conf"
install -m 0644 "$UNIT_SRC/container-taler-hacktivism-bank.service.d/apps.conf" \
@@ -38,18 +30,14 @@ install -m 0644 "$UNIT_SRC/container-koopa-paivana.service.d/order.conf" \
"$UNIT_DST/container-koopa-paivana.service.d/order.conf"
systemctl --user daemon-reload
-systemctl --user enable goa-regio-ng.service \
- taler-merchant-apps.service taler-bank-apps.service
+systemctl --user enable taler-merchant-apps.service taler-bank-apps.service
echo "Installed:"
echo " $BIN_DST/ensure-taler-apps.sh"
-echo " $UNIT_DST/goa-regio-ng.service (enabled)"
echo " $UNIT_DST/taler-{merchant,bank}-apps.service (enabled)"
-echo " $UNIT_DST/taler-landing-stats.service (After=goa-regio-ng)"
-echo " drop-in: goa-regio-ng.service.d/apps.conf"
-echo " drop-in: container-koopa-paivana.service.d/order.conf"
+echo " drop-ins: container-taler-hacktivism{,-bank}.service.d/apps.conf"
+echo " drop-in: container-koopa-paivana.service.d/order.conf"
echo
-echo "Start now (if container already up):"
-echo " systemctl --user start goa-regio-ng.service"
+echo "Start now (if containers already up):"
echo " systemctl --user start taler-merchant-apps.service taler-bank-apps.service"
echo " $BIN_DST/ensure-taler-apps.sh status"
diff --git a/scripts/upgrade-containers.sh b/scripts/upgrade-containers.sh
deleted file mode 100755
index 6a54d6e..0000000
--- a/scripts/upgrade-containers.sh
+++ /dev/null
@@ -1,654 +0,0 @@
-#!/usr/bin/env bash
-# Upgrade hernani podman containers on koopa: per-target dry-run (prod-safe),
-# then full backup (compose + mounts/volumes + DB dumps), then apply.
-#
-# Run on koopa as hernani:
-# ~/upgrade-containers.sh # dry-run only (default)
-# ~/upgrade-containers.sh --apply # dry-run → backup → upgrade per target
-# ~/upgrade-containers.sh --apply --images-only
-# ~/upgrade-containers.sh --apply --os-only
-# ~/upgrade-containers.sh --apply --recreate-only
-# ~/upgrade-containers.sh --apply --only koopa-forgejo,koopa-notes
-# ~/upgrade-containers.sh --apply --skip 'taler-hacktivism*,goa-regio-ng'
-# ~/upgrade-containers.sh --apply --taler-debs
-# ~/upgrade-containers.sh --apply --rebuild-local
-# ~/upgrade-containers.sh --apply --include-stopped
-# ~/upgrade-containers.sh --apply --backup-max-gb 80
-# ~/upgrade-containers.sh --apply --no-backup # emergency only
-#
-# Per-target --apply flow:
-# 1) dry-run checks that do not recreate/restart or mutate packages
-# 2) if dry-run fails → skip that target (others continue)
-# 3) full backup under ~/backups/container-upgrades///
-# 4) if backup fails → skip apply for that target
-# 5) pull / recreate / OS upgrade as selected
-set -euo pipefail
-
-APPLY=0
-DO_IMAGES=1
-DO_RECREATE=1
-DO_OS=1
-DO_TALER_DEBS=0
-REBUILD_LOCAL=0
-INCLUDE_STOPPED=0
-DO_BACKUP=1
-BACKUP_MAX_GB=50
-ONLY_RAW=""
-SKIP_RAW=""
-HOME_DIR="${HOME:-/home/hernani}"
-ADMIN_LOG="${ADMIN_LOG:-$HOME_DIR/koopa-admin-log}"
-BACKUP_ROOT="${BACKUP_ROOT:-$HOME_DIR/backups/container-upgrades}"
-RUN_TS="$(date '+%Y%m%d-%H%M%S')"
-BACKUP_RUN_DIR="$BACKUP_ROOT/$RUN_TS"
-
-LOG_TS() { date '+%Y-%m-%dT%H:%M:%S%z'; }
-log() { printf '[%s] %s\n' "$(LOG_TS)" "$*"; }
-run() {
- if [[ "$APPLY" -eq 1 ]]; then
- log "RUN: $*"
- "$@"
- else
- log "DRY: $*"
- fi
-}
-
-usage() {
- sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'
- exit "${1:-0}"
-}
-
-match_glob_list() {
- local name="$1" raw="$2" g
- [[ -z "$raw" ]] && return 1
- raw="${raw//,/ }"
- for g in $raw; do
- # shellcheck disable=SC2254
- case "$name" in
- $g) return 0 ;;
- esac
- done
- return 1
-}
-
-while [[ $# -gt 0 ]]; do
- case "$1" in
- -h|--help) usage 0 ;;
- --apply) APPLY=1; shift ;;
- --dry-run) APPLY=0; shift ;;
- --images-only) DO_IMAGES=1; DO_RECREATE=0; DO_OS=0; shift ;;
- --os-only) DO_IMAGES=0; DO_RECREATE=0; DO_OS=1; shift ;;
- --recreate-only) DO_IMAGES=0; DO_RECREATE=1; DO_OS=0; shift ;;
- --taler-debs) DO_TALER_DEBS=1; shift ;;
- --rebuild-local) REBUILD_LOCAL=1; shift ;;
- --include-stopped) INCLUDE_STOPPED=1; shift ;;
- --no-backup) DO_BACKUP=0; shift ;;
- --backup-max-gb) BACKUP_MAX_GB="${2:-50}"; shift 2 ;;
- --backup-max-gb=*) BACKUP_MAX_GB="${1#*=}"; shift ;;
- --only) ONLY_RAW="${2:-}"; shift 2 ;;
- --skip) SKIP_RAW="${2:-}"; shift 2 ;;
- --only=*) ONLY_RAW="${1#*=}"; shift ;;
- --skip=*) SKIP_RAW="${1#*=}"; shift ;;
- *) log "ERROR: unknown arg: $1"; usage 2 ;;
- esac
-done
-
-if [[ "$(id -un)" != "hernani" ]] && [[ "${FORCE_USER:-}" != "1" ]]; then
- log "WARN: expected user hernani (got $(id -un)); set FORCE_USER=1 to override"
-fi
-
-declare -a NAMES=()
-declare -A IMG=()
-declare -A CDIR=()
-declare -A STATUS=() # ok|dry_fail|backup_fail|upgraded|skipped
-declare -A NOTE=()
-
-list_cmd=(podman ps --format '{{.Names}}|{{.Image}}')
-if [[ "$INCLUDE_STOPPED" -eq 1 ]]; then
- list_cmd=(podman ps -a --format '{{.Names}}|{{.Image}}')
-fi
-
-while IFS='|' read -r name image; do
- [[ -z "$name" ]] && continue
- if [[ ! "$name" =~ ^(koopa-|taler-|goa-|mytops-) ]]; then
- continue
- fi
- if [[ -n "$ONLY_RAW" ]] && ! match_glob_list "$name" "$ONLY_RAW"; then
- continue
- fi
- if match_glob_list "$name" "$SKIP_RAW"; then
- log "SKIP name=$name (matched --skip)"
- continue
- fi
- NAMES+=("$name")
- IMG["$name"]="$image"
- dir=$(podman inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$name" 2>/dev/null || true)
- CDIR["$name"]="$dir"
- STATUS["$name"]="pending"
-done < <("${list_cmd[@]}")
-
-if [[ ${#NAMES[@]} -eq 0 ]]; then
- log "No matching containers."
- exit 0
-fi
-
-log "MODE=$([ "$APPLY" -eq 1 ] && echo apply || echo dry-run) images=$DO_IMAGES recreate=$DO_RECREATE os=$DO_OS backup=$DO_BACKUP backup_max_gb=$BACKUP_MAX_GB"
-log "Targets (${#NAMES[@]}): ${NAMES[*]}"
-if [[ "$APPLY" -eq 1 && "$DO_BACKUP" -eq 1 ]]; then
- mkdir -p "$BACKUP_RUN_DIR"
- log "BACKUP_DIR=$BACKUP_RUN_DIR"
-fi
-
-is_pinned_tag() {
- local ref="$1" tag="${1##*:}"
- [[ "$ref" != *:* ]] && return 1
- case "$tag" in
- latest|stable|stable-*|main|master|landing|live|setup|rootless|*-rootless|alpine|*-alpine|[0-9]|[0-9][0-9]|[0-9].*-alpine|[0-9][0-9]-alpine) return 1 ;;
- esac
- if [[ "$tag" =~ ^[0-9]+\.[0-9]+ ]]; then
- return 0
- fi
- return 1
-}
-
-# --- prod-safe dry-run for one container (no recreate, no package mutate) ---
-dry_run_one() {
- local name="$1" ref="${IMG[$name]}" dir="${CDIR[$name]}"
- local rc=0
- local tmp
- tmp=$(mktemp)
-
- # running?
- if ! podman inspect "$name" >/dev/null 2>&1; then
- log "DRYFAIL $name: inspect failed"
- NOTE["$name"]="inspect failed"
- rm -f "$tmp"
- return 1
- fi
-
- # image phase checks
- if [[ "$DO_IMAGES" -eq 1 ]]; then
- if [[ "$ref" == localhost/* ]]; then
- if ! podman image exists "$ref" 2>/dev/null; then
- log "DRYFAIL $name: local image missing $ref"
- NOTE["$name"]="local image missing"
- rm -f "$tmp"
- return 1
- fi
- log "DRYOK $name: local image present $ref"
- else
- # pull --dry-run is not universal; probe registry without replacing running CT
- if podman pull --help 2>&1 | grep -q -- '--quiet'; then
- :
- fi
- if ! podman image exists "$ref" 2>/dev/null; then
- log "DRYWARN $name: image not local yet ($ref) — apply will pull"
- else
- log "DRYOK $name: image present locally $ref"
- fi
- # non-mutating registry probe when possible
- if command -v curl >/dev/null 2>&1; then
- :
- fi
- # Try a no-restart pull into local store is mutating cache but not prod CT.
- # For dry-run we only validate the ref string / inspect local digest.
- if is_pinned_tag "$ref"; then
- log "DRYOK $name: pinned tag $ref (compose bump needed to move)"
- fi
- fi
- fi
-
- # compose config validate (no up)
- if [[ "$DO_RECREATE" -eq 1 ]]; then
- if [[ -n "$dir" ]]; then
- if [[ ! -f "$dir/compose.yml" && ! -f "$dir/compose.yaml" && ! -f "$dir/docker-compose.yml" ]]; then
- log "DRYFAIL $name: compose dir without compose file: $dir"
- NOTE["$name"]="no compose file"
- rm -f "$tmp"
- return 1
- fi
- if ! (cd "$dir" && podman-compose config >/dev/null 2>"$tmp"); then
- log "DRYFAIL $name: podman-compose config failed"
- sed -n '1,20p' "$tmp" | while IFS= read -r line; do log " $line"; done
- NOTE["$name"]="compose config failed"
- rm -f "$tmp"
- return 1
- fi
- log "DRYOK $name: compose config ok ($dir)"
- else
- log "DRYOK $name: no compose label — recreate will be skipped"
- fi
- fi
-
- # OS simulate (no real upgrade)
- if [[ "$DO_OS" -eq 1 ]]; then
- if podman ps --format '{{.Names}}' | grep -qx "$name"; then
- local os_script
- os_script=$(cat <<'INNER'
-set -e
-export DEBIAN_FRONTEND=noninteractive
-if command -v apt-get >/dev/null 2>&1; then
- apt-get update -qq
- apt-get -s -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade >/tmp/os-dry.out
- echo OS_DRY=apt ok
-elif command -v apk >/dev/null 2>&1; then
- apk update >/dev/null
- apk upgrade --simulate >/tmp/os-dry.out 2>&1 || apk upgrade -s >/tmp/os-dry.out 2>&1 || true
- echo OS_DRY=apk ok
-elif command -v zypper >/dev/null 2>&1; then
- zypper -n refresh >/dev/null
- zypper -n update -D >/tmp/os-dry.out 2>&1 || true
- echo OS_DRY=zypper ok
-elif command -v microdnf >/dev/null 2>&1; then
- echo OS_DRY=microdnf none-sim
-elif command -v dnf >/dev/null 2>&1; then
- dnf -y check-update >/tmp/os-dry.out 2>&1 || true
- echo OS_DRY=dnf ok
-else
- echo OS_DRY=none
-fi
-INNER
-)
- if podman exec -u 0 "$name" bash -lc "$os_script" >"$tmp" 2>&1 \
- || podman exec -u 0 "$name" sh -lc "$os_script" >"$tmp" 2>&1; then
- log "DRYOK $name: OS simulate ($(grep -E '^OS_DRY=' "$tmp" | tail -1 || echo ok))"
- else
- # many distroless/app images have no pkg manager — treat as ok/skip
- if grep -q 'OS_DRY=none' "$tmp" 2>/dev/null; then
- log "DRYOK $name: no package manager (OS skip)"
- else
- log "DRYWARN $name: OS simulate soft-fail (will try apply carefully)"
- sed -n '1,8p' "$tmp" | while IFS= read -r line; do log " $line"; done
- fi
- fi
- else
- log "DRYOK $name: not running — OS phase will skip"
- fi
- fi
-
- rm -f "$tmp"
- return "$rc"
-}
-
-# --- full backup with data ---
-backup_one() {
- local name="$1"
- local dest="$BACKUP_RUN_DIR/$name"
- local meta="$dest/META.txt"
- local max_bytes=$((BACKUP_MAX_GB * 1024 * 1024 * 1024))
- mkdir -p "$dest/binds" "$dest/volumes" "$dest/db" "$dest/compose"
-
- {
- echo "name=$name"
- echo "image=${IMG[$name]}"
- echo "compose_dir=${CDIR[$name]:-}"
- echo "ts=$RUN_TS"
- } >"$meta"
-
- podman inspect "$name" >"$dest/inspect.json" 2>/dev/null || true
-
- # compose project files
- local dir="${CDIR[$name]}"
- if [[ -n "$dir" && -d "$dir" ]]; then
- for f in compose.yml compose.yaml docker-compose.yml docker-compose.yaml .env; do
- if [[ -f "$dir/$f" ]]; then
- cp -a "$dir/$f" "$dest/compose/$f"
- fi
- done
- # shallow copy of known config sidecars (not whole tree)
- for f in Caddyfile Containerfile Dockerfile; do
- [[ -f "$dir/$f" ]] && cp -a "$dir/$f" "$dest/compose/$f"
- done
- fi
-
- # bind mounts + named volumes from inspect
- local mounts
- mounts=$(podman inspect -f '{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}|{{.RW}}{{println}}{{end}}' "$name" 2>/dev/null || true)
- while IFS='|' read -r mtype mname src dst rw; do
- [[ -z "$mtype" ]] && continue
- # skip ephemeral / host tz
- case "$dst" in
- /etc/localtime|/etc/timezone|/run|/run/*|/tmp|/tmp/*|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*) continue ;;
- esac
- case "$src" in
- /usr/share/zoneinfo/*|/etc/localtime) continue ;;
- esac
-
- if [[ "$mtype" == "bind" ]]; then
- if [[ ! -e "$src" ]]; then
- log "BACKUPWARN $name: bind missing $src"
- continue
- fi
- local sz
- sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
- if [[ -n "$sz" && "$sz" -gt "$max_bytes" ]]; then
- log "BACKUPWARN $name: skip huge bind $src (${sz}B > ${BACKUP_MAX_GB}G); raise --backup-max-gb"
- echo "SKIP_HUGE_BIND $src size=$sz" >>"$meta"
- continue
- fi
- local base
- base=$(echo "$dst" | sed 's#^/##; s#/#_#g')
- [[ -z "$base" ]] && base="root"
- log "BACKUP $name: bind $src → binds/${base}.tar.gz"
- if ! tar -C "$(dirname "$src")" -czf "$dest/binds/${base}.tar.gz" "$(basename "$src")" 2>>"$dest/backup.err"; then
- log "BACKUPFAIL $name: tar bind $src"
- NOTE["$name"]="backup bind failed: $src"
- return 1
- fi
- echo "BIND $src -> binds/${base}.tar.gz" >>"$meta"
- elif [[ "$mtype" == "volume" ]]; then
- local vname="${mname:-}"
- if [[ -z "$vname" ]]; then
- # fallback: export via source path
- if [[ -d "$src" ]]; then
- local vbase
- vbase=$(basename "$(dirname "$src")")
- log "BACKUP $name: volume-path $src → volumes/${vbase}.tar.gz"
- if ! tar -C "$src" -czf "$dest/volumes/${vbase}.tar.gz" . 2>>"$dest/backup.err"; then
- log "BACKUPFAIL $name: tar volume path $src"
- NOTE["$name"]="backup volume failed"
- return 1
- fi
- echo "VOLPATH $src -> volumes/${vbase}.tar.gz" >>"$meta"
- fi
- continue
- fi
- local sz
- sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
- if [[ -n "$sz" && "$sz" -gt "$max_bytes" ]]; then
- log "BACKUPWARN $name: skip huge volume $vname (${sz}B > ${BACKUP_MAX_GB}G)"
- echo "SKIP_HUGE_VOL $vname size=$sz" >>"$meta"
- continue
- fi
- log "BACKUP $name: volume $vname → volumes/${vname}.tar"
- if podman volume export "$vname" -o "$dest/volumes/${vname}.tar" 2>>"$dest/backup.err"; then
- gzip -f "$dest/volumes/${vname}.tar" 2>/dev/null || true
- echo "VOLUME $vname -> volumes/${vname}.tar.gz" >>"$meta"
- else
- # fallback tar of mount source
- if [[ -d "$src" ]] && tar -C "$src" -czf "$dest/volumes/${vname}.tar.gz" . 2>>"$dest/backup.err"; then
- echo "VOLPATH $src -> volumes/${vname}.tar.gz" >>"$meta"
- else
- log "BACKUPFAIL $name: volume $vname"
- NOTE["$name"]="backup volume failed: $vname"
- return 1
- fi
- fi
- fi
- done <<<"$mounts"
-
- # DB dumps when obvious
- if podman ps --format '{{.Names}}' | grep -qx "$name"; then
- local img_l
- img_l=$(echo "${IMG[$name]}" | tr '[:upper:]' '[:lower:]')
- case "$img_l" in
- *postgres*|*postgis*)
- local dbuser
- dbuser=$(podman exec "$name" printenv POSTGRES_USER 2>/dev/null || echo postgres)
- local dbname
- dbname=$(podman exec "$name" printenv POSTGRES_DB 2>/dev/null || echo postgres)
- log "BACKUP $name: pg_dumpall"
- if podman exec -u 0 "$name" bash -lc "pg_dumpall -U '$dbuser'" >"$dest/db/pg_dumpall.sql" 2>>"$dest/backup.err"; then
- gzip -f "$dest/db/pg_dumpall.sql"
- echo "PG_DUMPALL ok db=$dbname user=$dbuser" >>"$meta"
- else
- # single-db fallback
- if podman exec -u 0 "$name" bash -lc "pg_dump -U '$dbuser' '$dbname'" >"$dest/db/pg_dump.sql" 2>>"$dest/backup.err"; then
- gzip -f "$dest/db/pg_dump.sql"
- echo "PG_DUMP ok db=$dbname" >>"$meta"
- else
- log "BACKUPWARN $name: postgres dump failed (mounts still saved)"
- fi
- fi
- ;;
- *mariadb*|*mysql*)
- local muser mpass mdb
- muser=$(podman exec "$name" printenv MYSQL_USER 2>/dev/null || podman exec "$name" printenv MARIADB_USER 2>/dev/null || echo root)
- mpass=$(podman exec "$name" printenv MYSQL_ROOT_PASSWORD 2>/dev/null || podman exec "$name" printenv MARIADB_ROOT_PASSWORD 2>/dev/null || podman exec "$name" printenv MYSQL_PASSWORD 2>/dev/null || true)
- mdb=$(podman exec "$name" printenv MYSQL_DATABASE 2>/dev/null || podman exec "$name" printenv MARIADB_DATABASE 2>/dev/null || echo "")
- log "BACKUP $name: mariadb/mysql dump"
- if [[ -n "$mpass" ]]; then
- if podman exec -u 0 "$name" bash -lc "MYSQL_PWD='$mpass' mysqldump -u root --all-databases" >"$dest/db/mysql_all.sql" 2>>"$dest/backup.err" \
- || podman exec -u 0 "$name" bash -lc "MYSQL_PWD='$mpass' mariadb-dump -u root --all-databases" >"$dest/db/mysql_all.sql" 2>>"$dest/backup.err"; then
- gzip -f "$dest/db/mysql_all.sql"
- echo "MYSQL_DUMPALL ok" >>"$meta"
- else
- log "BACKUPWARN $name: mysql dump failed (mounts still saved)"
- fi
- else
- log "BACKUPWARN $name: no mysql password in env — skip dump"
- fi
- ;;
- esac
- fi
-
- echo "BACKUP_OK" >>"$meta"
- log "BACKUPOK $name → $dest"
- return 0
-}
-
-# --- apply phases for one container ---
-apply_images_one() {
- local name="$1" ref="${IMG[$name]}"
- [[ "$DO_IMAGES" -eq 1 ]] || return 0
- if [[ "$ref" == localhost/* ]]; then
- log "LOCAL image=$ref (container=$name) — pull skipped"
- return 0
- fi
- if is_pinned_tag "$ref"; then
- log "PINNED image=$ref (container=$name) — pull same tag only"
- fi
- run podman pull "$ref"
-}
-
-rebuild_one_local() {
- local name="$1" ref="${IMG[$name]}" dir="${CDIR[$name]}"
- [[ "$ref" == localhost/* ]] || return 0
- local cf=""
- if [[ -n "$dir" && -f "$dir/Containerfile" ]]; then
- cf="$dir/Containerfile"
- elif [[ -n "$dir" && -f "$dir/Dockerfile" ]]; then
- cf="$dir/Dockerfile"
- fi
- case "$name" in
- koopa-nym)
- [[ -x "$ADMIN_LOG/scripts/nym/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/nym/build.sh"; return; }
- ;;
- koopa-tor-relay)
- if [[ -f "$HOME_DIR/koopa-tor-relay/build/Containerfile" ]]; then
- run podman build -t localhost/koopa-tor-relay:latest -f "$HOME_DIR/koopa-tor-relay/build/Containerfile" "$HOME_DIR/koopa-tor-relay/build"
- return
- fi
- ;;
- koopa-paivana)
- if [[ -f "$dir/Containerfile" ]]; then
- run podman build -t localhost/koopa-paivana:latest -f "$dir/Containerfile" "$dir"
- return
- fi
- ;;
- koopa-bbb-front)
- [[ -x "$ADMIN_LOG/scripts/bbb/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/bbb/build.sh"; return; }
- ;;
- koopa-meet-web)
- [[ -x "$ADMIN_LOG/scripts/meet/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/meet/build.sh"; return; }
- ;;
- koopa-galene|koopa-paivana-galene)
- [[ -x "$ADMIN_LOG/scripts/galene/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/galene/build.sh"; return; }
- ;;
- koopa-cryptpad|koopa-cryptpad-edge|koopa-paivana-cryptpad)
- [[ -x "$ADMIN_LOG/scripts/cryptpad/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/cryptpad/build.sh"; return; }
- ;;
- koopa-notes|koopa-silverbullet)
- [[ -x "$ADMIN_LOG/scripts/notes/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/notes/build.sh"; return; }
- ;;
- esac
- if [[ -n "$cf" ]]; then
- run podman build -t "$ref" -f "$cf" "$(dirname "$cf")"
- else
- log "NO-REBUILD path for $name ($ref)"
- fi
-}
-
-apply_recreate_one() {
- local name="$1" dir="${CDIR[$name]}"
- [[ "$DO_RECREATE" -eq 1 ]] || return 0
- if [[ -z "$dir" ]]; then
- log "NO-COMPOSE container=$name — recreate skipped"
- return 0
- fi
- if [[ ! -f "$dir/compose.yml" && ! -f "$dir/compose.yaml" && ! -f "$dir/docker-compose.yml" ]]; then
- log "WARN: compose dir without compose file: $dir"
- return 0
- fi
- if [[ "$APPLY" -eq 1 ]]; then
- log "RUN: (cd $dir && podman-compose pull && podman-compose up -d)"
- (cd "$dir" && podman-compose pull && podman-compose up -d)
- else
- log "DRY: (cd $dir && podman-compose pull && podman-compose up -d)"
- fi
-}
-
-os_upgrade_one() {
- local name="$1"
- [[ "$DO_OS" -eq 1 ]] || return 0
- if ! podman ps --format '{{.Names}}' | grep -qx "$name"; then
- log "SKIP os name=$name (not running)"
- return 0
- fi
- local script
- script=$(cat <<'INNER'
-set -e
-export DEBIAN_FRONTEND=noninteractive
-if command -v apt-get >/dev/null 2>&1; then
- apt-get update -qq
- apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade
- echo OS=apt ok
-elif command -v apk >/dev/null 2>&1; then
- apk update
- apk upgrade
- echo OS=apk ok
-elif command -v zypper >/dev/null 2>&1; then
- zypper -n refresh
- zypper -n update
- echo OS=zypper ok
-elif command -v microdnf >/dev/null 2>&1; then
- microdnf -y upgrade
- echo OS=microdnf ok
-elif command -v dnf >/dev/null 2>&1; then
- dnf -y upgrade
- echo OS=dnf ok
-else
- echo OS=none
- exit 0
-fi
-INNER
-)
- if [[ "$APPLY" -eq 1 ]]; then
- log "RUN: podman exec -u 0 $name OS-upgrade"
- if ! podman exec -u 0 "$name" bash -lc "$script"; then
- podman exec -u 0 "$name" sh -lc "$script" || log "WARN: OS upgrade failed for $name"
- fi
- else
- log "DRY: podman exec -u 0 $name OS-upgrade"
- fi
-}
-
-# Deduplicate compose recreate by dir
-declare -A COMPOSE_DONE=()
-declare -A IMAGE_DONE=()
-declare -A BACKUP_DONE=()
-
-process_one() {
- local name="$1"
- log "==== target: $name (${IMG[$name]}) ===="
-
- # 1) always dry-run first (prod-safe)
- if ! dry_run_one "$name"; then
- STATUS["$name"]="dry_fail"
- log "SKIP apply $name (dry-run failed)"
- return 0
- fi
- STATUS["$name"]="dry_ok"
-
- # global dry-run mode: stop after checks
- if [[ "$APPLY" -eq 0 ]]; then
- log "DRY-RUN only for $name (no backup/apply)"
- return 0
- fi
-
- # 2) full backup with data BEFORE any mutate (even after dry-run ok)
- # every container — do not skip siblings in a compose stack (DB/data)
- if [[ "$DO_BACKUP" -eq 1 ]]; then
- if [[ -z "${BACKUP_DONE[$name]:-}" ]]; then
- if ! backup_one "$name"; then
- STATUS["$name"]="backup_fail"
- log "SKIP apply $name (backup failed)"
- return 0
- fi
- BACKUP_DONE["$name"]=1
- else
- log "BACKUP skip $name (already backed this container)"
- fi
- else
- log "WARN: --no-backup set; proceeding without backup for $name"
- fi
-
- # 3) apply
- if [[ -z "${IMAGE_DONE[${IMG[$name]}]:-}" ]]; then
- apply_images_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="image pull failed"; return 0; }
- IMAGE_DONE["${IMG[$name]}"]=1
- fi
- if [[ "$REBUILD_LOCAL" -eq 1 ]]; then
- rebuild_one_local "$name" || true
- fi
- local dir="${CDIR[$name]}"
- if [[ -n "$dir" ]]; then
- if [[ -z "${COMPOSE_DONE[$dir]:-}" ]]; then
- apply_recreate_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="recreate failed"; return 0; }
- COMPOSE_DONE["$dir"]=1
- else
- log "recreate skip $name (compose dir already done)"
- fi
- else
- apply_recreate_one "$name" || true
- fi
- os_upgrade_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="os upgrade failed"; return 0; }
- STATUS["$name"]="upgraded"
- log "DONE $name"
-}
-
-# --- main loop: per container ---
-for name in "${NAMES[@]}"; do
- process_one "$name"
-done
-
-# optional Taler GOA debs (only on --apply, after per-CT work)
-if [[ "$DO_TALER_DEBS" -eq 1 ]]; then
- log "==== phase: taler-debs ===="
- tscript="$ADMIN_LOG/scripts/taler-shared/upgrade-goa-debs.sh"
- if [[ "$APPLY" -eq 0 ]]; then
- log "DRY: bash $tscript"
- elif [[ -x "$tscript" ]]; then
- run bash "$tscript"
- else
- log "ERROR: missing $tscript"
- exit 1
- fi
-fi
-
-log "==== summary ===="
-fail=0
-for name in "${NAMES[@]}"; do
- st="${STATUS[$name]:-?}"
- printf ' %-42s %-12s %s %s\n' "$name" "$st" "${IMG[$name]}" "${NOTE[$name]:-}"
- case "$st" in
- dry_fail|backup_fail|apply_fail) fail=1 ;;
- esac
-done
-if [[ "$APPLY" -eq 0 ]]; then
- log "Dry-run only (default). Re-run with --apply for backup+upgrade per target that passed dry-run."
-fi
-if [[ "$fail" -eq 1 ]]; then
- log "STATUS=partial_fail"
- exit 1
-fi
-log "STATUS=ok"
diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css
deleted file mode 100644
index feefe10..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.css
+++ /dev/null
@@ -1,141 +0,0 @@
-html {
- padding-top: var(--cascade-pad, 0px);
-}
-.cascade-bar {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 1000;
- display: flex;
- justify-content: center;
- box-sizing: border-box;
- width: 100%;
- max-width: none;
- margin: 0;
- padding: 0.2rem 0 0.25rem;
- background: #10141a;
- border-bottom: 1px solid #243040;
- color: #c8d0d8;
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
- font-size: 11px;
- line-height: 1.25;
-}
-.cascade-tree {
- /* 45rem = site #navigation/#content 45em at root 16px (bar font is 11px) */
- width: 100%;
- max-width: 45rem;
- margin: 0 auto;
- box-sizing: border-box;
-}
-.cascade-tree-lines {
- display: flex;
- flex-direction: column;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
-}
-a.cascade-tree-row {
- display: grid;
- grid-template-columns: 1.05rem minmax(6rem, 1fr) 7.2rem 10.5rem 4.4rem 3.6rem;
- column-gap: 0.55rem;
- align-items: baseline;
- color: #7ec8ff;
- text-decoration: none;
- padding: 0 0.15rem;
- border-radius: 2px;
-}
-a.cascade-tree-row:hover {
- background: #1a2430;
- text-decoration: none;
-}
-a.cascade-tree-row.is-active {
- color: #e8eef4;
- background: #1e2a38;
- box-shadow: inset 3px 0 0 #e8eef4;
-}
-a.cascade-tree-row.is-active:hover {
- text-decoration: none;
-}
-.cascade-bar code {
- color: inherit;
- font-size: inherit;
-}
-.cascade-name {
- white-space: pre;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- color: inherit;
-}
-.cascade-when,
-.cascade-built,
-.cascade-plus,
-.cascade-minus {
- text-align: right;
- font-variant-numeric: tabular-nums;
- white-space: nowrap;
- color: #8a96a3;
-}
-.cascade-timing {
- display: none;
-}
-
-@media (max-width: 45rem) {
- .cascade-bar {
- padding: 0.15rem 0.4rem 0.2rem;
- font-size: 10px;
- }
- .cascade-tree {
- max-width: 100%;
- }
- a.cascade-tree-row {
- grid-template-columns: 0.9rem minmax(5rem, 1fr) 5.6rem 8.4rem 3.6rem 3rem;
- column-gap: 0.35rem;
- }
-}
-
-.cascade-mark {
- text-align: center;
- font-weight: 700;
- line-height: 1;
-}
-.cascade-bar .cascade-ok {
- color: #3fb950;
-}
-.cascade-bar .cascade-fail {
- color: #f85149;
-}
-.cascade-bar .diff-plus {
- color: #3fb950;
- font-weight: 600;
-}
-.cascade-bar .diff-minus {
- color: #f85149;
- font-weight: 600;
-}
-.cascade-bar .diff-loc,
-.cascade-bar .diff-loc-unit {
- 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;
-}
diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html
deleted file mode 100644
index d81564d..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- Loading builds…
-
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js
deleted file mode 100644
index 8db6e4a..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/_cascade/bar.js
+++ /dev/null
@@ -1,466 +0,0 @@
-(function () {
- var POLL_MS = 30000;
- var MONTHS = [
- "jan",
- "feb",
- "mar",
- "apr",
- "may",
- "jun",
- "jul",
- "aug",
- "sep",
- "oct",
- "nov",
- "dec",
- ];
-
- function cookieEdition() {
- var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
- if (!m) return "flask";
- try {
- return decodeURIComponent(m[1]) || "flask";
- } catch (e) {
- return "flask";
- }
- }
-
- 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, "&")
- .replace(//g, ">")
- .replace(/"/g, """);
- }
-
- /** ISO → "sep30 16:12" (3-letter English month + DD + HH:MM). */
- function shortTime(iso) {
- if (!iso) return "";
- var m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/);
- if (m) {
- var mon = MONTHS[Number(m[2]) - 1] || m[2];
- return mon + m[3] + " " + m[4];
- }
- return String(iso).slice(0, 16).replace("T", " ");
- }
-
- function fmtBuildSec(sec) {
- if (sec == null || sec === "") return "";
- var n = Number(sec);
- if (!isFinite(n)) return "";
- if (n < 10) return n.toFixed(1) + "s";
- return Math.round(n) + "s";
- }
-
- function builtText(row) {
- var sec = fmtBuildSec(row.build_seconds);
- var at = shortTime(row.built_at || "");
- if (sec && at) return sec + " " + at;
- return sec || at;
- }
-
- function freezeOk(row) {
- return row && row.freeze_ok === true;
- }
-
- function freezeLabel(row) {
- if (freezeOk(row)) return "freeze ok";
- var err = row && row.freeze_error ? String(row.freeze_error) : "";
- return err ? "freeze failed: " + err : "freeze failed";
- }
-
- function freezeMarkHtml(row) {
- var ok = freezeOk(row);
- return (
- '' +
- (ok ? "✓" : "✗") +
- " "
- );
- }
-
- function plusHtml(row) {
- if (row.vs_parent_plus != null) {
- var p = Number(row.vs_parent_plus);
- if (!isFinite(p)) return "";
- return '+' + esc(String(p)) + " ";
- }
- if (row.loc != null) {
- var n = Number(row.loc);
- if (!isFinite(n)) return "";
- return '' + esc(String(n)) + " ";
- }
- return "";
- }
-
- function minusHtml(row) {
- if (row.vs_parent_minus != null) {
- var m = Number(row.vs_parent_minus);
- if (!isFinite(m)) return "";
- return '−' + esc(String(m)) + " ";
- }
- if (row.loc != null) {
- return 'LOC ';
- }
- return "";
- }
-
- function setText(el, text) {
- if (!el) return;
- var t = text || "";
- if (el.textContent !== t) el.textContent = t;
- }
-
- function rowTitle(row) {
- var tip = row.tip || "";
- var tipTime = row.tip_time || "";
- var vs = "";
- if (row.vs_parent_plus != null || row.vs_parent_minus != null) {
- vs =
- " vs parent +" +
- String(row.vs_parent_plus == null ? 0 : row.vs_parent_plus) +
- "/−" +
- String(row.vs_parent_minus == null ? 0 : row.vs_parent_minus);
- } else if (row.loc != null) {
- vs = " loc=" + String(row.loc);
- }
- var built = builtText(row);
- return (
- "Switch build to " +
- (row.branch || "") +
- (tip ? " @ " + tip : "") +
- (tipTime ? " tip=" + shortTime(tipTime) : "") +
- (built ? " build=" + built : "") +
- " " +
- freezeLabel(row) +
- vs
- );
- }
-
- function structureKey(list) {
- var parts = [];
- for (var i = 0; i < list.length; i++) {
- var row = list[i];
- parts.push(
- (row.edition || "") +
- "\t" +
- (row.branch || "") +
- "\t" +
- (row.prefix || "") +
- "\t" +
- (row.parent || "")
- );
- }
- return parts.join("\n");
- }
-
- function pinBar(timingMsg) {
- var bar = document.getElementById("cascade-bar");
- if (!bar) return;
- if (timingMsg) {
- bar.setAttribute("data-timing", timingMsg);
- bar.setAttribute("title", timingMsg);
- }
- var h = bar.offsetHeight || 0;
- document.documentElement.style.setProperty("--cascade-pad", h + "px");
- }
-
- function markActive(root, edition) {
- var links = root.querySelectorAll("a[data-edition]");
- for (var i = 0; i < links.length; i++) {
- var a = links[i];
- var ed = a.getAttribute("data-edition");
- var on =
- ed === edition ||
- (edition === "flask" && a.getAttribute("data-default") === "1");
- if (on) {
- a.classList.add("is-active");
- a.setAttribute("aria-current", "page");
- } else {
- a.classList.remove("is-active");
- a.removeAttribute("aria-current");
- }
- }
- }
-
- function timingText(timing, listLen) {
- if (!timing || timing.wall_seconds == null) return "";
- return (
- "last full pass ~" +
- String(timing.wall_seconds) +
- "s · built " +
- String(timing.built || 0) +
- " / skipped " +
- String(timing.skipped || 0) +
- " / " +
- String(timing.branches || listLen)
- );
- }
-
- function updateValues(mount, data) {
- var list = (data && data.branches) || [];
- var defaultBranch = (data && data.default_branch) || "feat/website-views";
- var timing = (data && data.rebuild_timing) || null;
- var rows = mount.querySelectorAll("a.cascade-tree-row[data-edition]");
- if (rows.length !== list.length) return false;
- for (var i = 0; i < list.length; i++) {
- var row = list[i];
- var a = rows[i];
- if (a.getAttribute("data-edition") !== (row.edition || "")) return false;
- if (a.getAttribute("data-branch") !== (row.branch || "")) return false;
- var isDefault = (row.branch || "") === defaultBranch;
- if (isDefault) a.setAttribute("data-default", "1");
- else a.removeAttribute("data-default");
- setText(a.querySelector(".cascade-when"), shortTime(row.tip_time || ""));
- setText(a.querySelector(".cascade-built"), builtText(row));
- var markEl = a.querySelector(".cascade-mark");
- if (markEl) {
- var markHtml = freezeMarkHtml(row);
- if (markEl.outerHTML !== markHtml) markEl.outerHTML = markHtml;
- }
- var plusEl = a.querySelector(".cascade-plus");
- if (plusEl) {
- var ph = plusHtml(row);
- if (plusEl.innerHTML !== ph) plusEl.innerHTML = ph;
- }
- var minusEl = a.querySelector(".cascade-minus");
- if (minusEl) {
- var mh = minusHtml(row);
- if (minusEl.innerHTML !== mh) minusEl.innerHTML = mh;
- }
- a.setAttribute("title", rowTitle(row));
- }
- var timingEl = mount.querySelector(".cascade-timing");
- var t = timingText(timing, list.length);
- if (t) {
- if (!timingEl) {
- timingEl = document.createElement("div");
- timingEl.className = "cascade-timing";
- mount.appendChild(timingEl);
- }
- if (timingEl.textContent !== t) timingEl.textContent = t;
- } else if (timingEl) {
- timingEl.parentNode.removeChild(timingEl);
- }
- markActive(mount, cookieEdition());
- pinBar(t);
- return true;
- }
-
- function renderTree(data, soft) {
- var mount = document.getElementById("cascade-tree");
- if (!mount) return;
- var list = (data && data.branches) || [];
- if (!list.length) {
- mount.textContent = "No branches in branch-status.json";
- pinBar();
- return;
- }
- if (soft && mount.getAttribute("data-structure") === structureKey(list)) {
- if (updateValues(mount, data)) return;
- }
- var defaultBranch = (data && data.default_branch) || "feat/website-views";
- var timing = (data && data.rebuild_timing) || null;
- var html = [];
- html.push('");
- var t = timingText(timing, list.length);
- if (t) {
- html.push('' + esc(t) + "
");
- }
- mount.innerHTML = html.join("");
- mount.setAttribute("data-structure", structureKey(list));
- markActive(mount, cookieEdition());
- pinBar(t);
- }
-
- function loadStatus(soft) {
- var mount = document.getElementById("cascade-tree");
- return fetch("/branch-status.json", { cache: "no-store" })
- .then(function (res) {
- if (!res.ok) throw new Error("status");
- return res.json();
- })
- .then(function (data) {
- renderTree(data, !!soft);
- })
- .catch(function () {
- if (!soft && mount && !mount.querySelector("a[data-edition]")) {
- mount.innerHTML =
- 'flask ' +
- ' (branch-status unavailable) ';
- markActive(mount, cookieEdition());
- pinBar();
- }
- });
- }
-
- 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);
- }, POLL_MS);
- window.addEventListener("resize", function () {
- pinBar();
- });
-})();
diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/index.html b/sites/dossiers.ngi.hacktivism.ch/_cascade/index.html
deleted file mode 100644
index db0bc96..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/_cascade/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-cascade editions
-
-
-
-
- Loading builds…
-
-
-Stuck on an edition without a bar? Pick a build above, or reset to flask .
-
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/_cascade/missing.html b/sites/dossiers.ngi.hacktivism.ch/_cascade/missing.html
deleted file mode 100644
index 449d470..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/_cascade/missing.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-path not in this branch
-
-
-
-
- Loading builds…
-
-
-This address is not in the current branch. Choose another branch above.
-
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/_edition-placeholder/index.html b/sites/dossiers.ngi.hacktivism.ch/_edition-placeholder/index.html
deleted file mode 100644
index 369b971..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/_edition-placeholder/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-edition pending
-
-
-
-{% include "_cascade/bar.html" . %}
-edition pending
-Full freeze for this tip is not staged under editions/ yet. Sticky bar still switches other builds.
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/branch-status.json b/sites/dossiers.ngi.hacktivism.ch/branch-status.json
deleted file mode 100644
index 8870c12..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/branch-status.json
+++ /dev/null
@@ -1,124 +0,0 @@
-{
- "generated": "2026-09-30T15:34:26+02:00",
- "host": "koopa",
- "default_branch": "feat/website-views",
- "default_edition": "flask",
- "branches": [
- {
- "branch": "feature/nl-net-injestion-injestor-update",
- "edition": "feature-nl-net-injestion-injestor-update",
- "tip": "ba68fe9",
- "tip_time": "2026-08-27T10:42:02+02:00",
- "parent": "",
- "prefix": "",
- "url": "/_cascade/use/feature-nl-net-injestion-injestor-update",
- "built_at": "2026-09-30T15:23:10+02:00",
- "build_seconds": 3.841
- },
- {
- "branch": "main",
- "edition": "main",
- "tip": "d2fbd79",
- "tip_time": "2026-09-01T16:52:35+02:00",
- "parent": "",
- "prefix": "",
- "url": "/_cascade/use/main",
- "built_at": "2026-09-30T15:23:10+02:00",
- "build_seconds": 8.447
- },
- {
- "branch": "feat/sm-content",
- "edition": "feat-sm-content",
- "tip": "089ef1f",
- "tip_time": "2026-09-28T15:52:12+02:00",
- "parent": "main",
- "prefix": "\u251c\u2500\u2500 ",
- "url": "/_cascade/use/feat-sm-content",
- "built_at": "2026-09-30T15:23:10+02:00",
- "build_seconds": 12.845
- },
- {
- "branch": "feat/website-features",
- "edition": "feat-website-features",
- "tip": "53a3176",
- "tip_time": "2026-09-28T19:38:22+02:00",
- "parent": "feat/sm-content",
- "prefix": "\u2502 \u2514\u2500\u2500 ",
- "url": "/_cascade/use/feat-website-features",
- "built_at": "2026-09-30T15:23:10+02:00",
- "build_seconds": 8.34
- },
- {
- "branch": "feat/website-design",
- "edition": "feat-website-design",
- "tip": "2e26c80",
- "tip_time": "2026-09-28T22:42:08+02:00",
- "parent": "feat/website-features",
- "prefix": "\u2502 \u2514\u2500\u2500 ",
- "url": "/_cascade/use/feat-website-design",
- "built_at": "2026-09-30T15:23:10+02:00",
- "build_seconds": 10.358
- },
- {
- "branch": "feat/website-views",
- "edition": "feat-website-views",
- "tip": "bbafc38",
- "tip_time": "2026-09-28T23:17:42+02:00",
- "parent": "feat/website-design",
- "prefix": "\u2502 \u2514\u2500\u2500 ",
- "url": "/_cascade/use/feat-website-views",
- "built_at": "2026-09-30T15:23:10+02:00",
- "build_seconds": 5.576
- },
- {
- "branch": "project_template",
- "edition": "project_template",
- "tip": "a97c263",
- "tip_time": "2026-09-28T15:03:35+02:00",
- "parent": "main",
- "prefix": "\u2514\u2500\u2500 ",
- "url": "/_cascade/use/project_template",
- "built_at": "2026-09-30T15:23:10+02:00",
- "build_seconds": 2.245
- }
- ],
- "rebuild": {
- "feat/sm-content": {
- "status": "skipped",
- "seconds": 0.077
- },
- "feat/website-design": {
- "status": "skipped",
- "seconds": 0.083
- },
- "feat/website-features": {
- "status": "skipped",
- "seconds": 0.091
- },
- "feat/website-views": {
- "status": "skipped",
- "seconds": 0.09
- },
- "feature/nl-net-injestion-injestor-update": {
- "status": "skipped",
- "seconds": 0.098
- },
- "main": {
- "status": "skipped",
- "seconds": 0.102
- },
- "project_template": {
- "status": "skipped",
- "seconds": 0.083
- }
- },
- "rebuild_timing": {
- "wall_seconds": 0.627,
- "branches": 7,
- "built": 0,
- "skipped": 7,
- "errors": 0,
- "built_seconds_sum": 0.0,
- "built_seconds_max": 0.0
- }
-}
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css
deleted file mode 100644
index feefe10..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.css
+++ /dev/null
@@ -1,141 +0,0 @@
-html {
- padding-top: var(--cascade-pad, 0px);
-}
-.cascade-bar {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 1000;
- display: flex;
- justify-content: center;
- box-sizing: border-box;
- width: 100%;
- max-width: none;
- margin: 0;
- padding: 0.2rem 0 0.25rem;
- background: #10141a;
- border-bottom: 1px solid #243040;
- color: #c8d0d8;
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
- font-size: 11px;
- line-height: 1.25;
-}
-.cascade-tree {
- /* 45rem = site #navigation/#content 45em at root 16px (bar font is 11px) */
- width: 100%;
- max-width: 45rem;
- margin: 0 auto;
- box-sizing: border-box;
-}
-.cascade-tree-lines {
- display: flex;
- flex-direction: column;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
-}
-a.cascade-tree-row {
- display: grid;
- grid-template-columns: 1.05rem minmax(6rem, 1fr) 7.2rem 10.5rem 4.4rem 3.6rem;
- column-gap: 0.55rem;
- align-items: baseline;
- color: #7ec8ff;
- text-decoration: none;
- padding: 0 0.15rem;
- border-radius: 2px;
-}
-a.cascade-tree-row:hover {
- background: #1a2430;
- text-decoration: none;
-}
-a.cascade-tree-row.is-active {
- color: #e8eef4;
- background: #1e2a38;
- box-shadow: inset 3px 0 0 #e8eef4;
-}
-a.cascade-tree-row.is-active:hover {
- text-decoration: none;
-}
-.cascade-bar code {
- color: inherit;
- font-size: inherit;
-}
-.cascade-name {
- white-space: pre;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- color: inherit;
-}
-.cascade-when,
-.cascade-built,
-.cascade-plus,
-.cascade-minus {
- text-align: right;
- font-variant-numeric: tabular-nums;
- white-space: nowrap;
- color: #8a96a3;
-}
-.cascade-timing {
- display: none;
-}
-
-@media (max-width: 45rem) {
- .cascade-bar {
- padding: 0.15rem 0.4rem 0.2rem;
- font-size: 10px;
- }
- .cascade-tree {
- max-width: 100%;
- }
- a.cascade-tree-row {
- grid-template-columns: 0.9rem minmax(5rem, 1fr) 5.6rem 8.4rem 3.6rem 3rem;
- column-gap: 0.35rem;
- }
-}
-
-.cascade-mark {
- text-align: center;
- font-weight: 700;
- line-height: 1;
-}
-.cascade-bar .cascade-ok {
- color: #3fb950;
-}
-.cascade-bar .cascade-fail {
- color: #f85149;
-}
-.cascade-bar .diff-plus {
- color: #3fb950;
- font-weight: 600;
-}
-.cascade-bar .diff-minus {
- color: #f85149;
- font-weight: 600;
-}
-.cascade-bar .diff-loc,
-.cascade-bar .diff-loc-unit {
- 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;
-}
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.html b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.html
deleted file mode 100644
index d81564d..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- Loading builds…
-
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js
deleted file mode 100644
index 8db6e4a..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/bar.js
+++ /dev/null
@@ -1,466 +0,0 @@
-(function () {
- var POLL_MS = 30000;
- var MONTHS = [
- "jan",
- "feb",
- "mar",
- "apr",
- "may",
- "jun",
- "jul",
- "aug",
- "sep",
- "oct",
- "nov",
- "dec",
- ];
-
- function cookieEdition() {
- var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
- if (!m) return "flask";
- try {
- return decodeURIComponent(m[1]) || "flask";
- } catch (e) {
- return "flask";
- }
- }
-
- 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, "&")
- .replace(//g, ">")
- .replace(/"/g, """);
- }
-
- /** ISO → "sep30 16:12" (3-letter English month + DD + HH:MM). */
- function shortTime(iso) {
- if (!iso) return "";
- var m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/);
- if (m) {
- var mon = MONTHS[Number(m[2]) - 1] || m[2];
- return mon + m[3] + " " + m[4];
- }
- return String(iso).slice(0, 16).replace("T", " ");
- }
-
- function fmtBuildSec(sec) {
- if (sec == null || sec === "") return "";
- var n = Number(sec);
- if (!isFinite(n)) return "";
- if (n < 10) return n.toFixed(1) + "s";
- return Math.round(n) + "s";
- }
-
- function builtText(row) {
- var sec = fmtBuildSec(row.build_seconds);
- var at = shortTime(row.built_at || "");
- if (sec && at) return sec + " " + at;
- return sec || at;
- }
-
- function freezeOk(row) {
- return row && row.freeze_ok === true;
- }
-
- function freezeLabel(row) {
- if (freezeOk(row)) return "freeze ok";
- var err = row && row.freeze_error ? String(row.freeze_error) : "";
- return err ? "freeze failed: " + err : "freeze failed";
- }
-
- function freezeMarkHtml(row) {
- var ok = freezeOk(row);
- return (
- '' +
- (ok ? "✓" : "✗") +
- " "
- );
- }
-
- function plusHtml(row) {
- if (row.vs_parent_plus != null) {
- var p = Number(row.vs_parent_plus);
- if (!isFinite(p)) return "";
- return '+' + esc(String(p)) + " ";
- }
- if (row.loc != null) {
- var n = Number(row.loc);
- if (!isFinite(n)) return "";
- return '' + esc(String(n)) + " ";
- }
- return "";
- }
-
- function minusHtml(row) {
- if (row.vs_parent_minus != null) {
- var m = Number(row.vs_parent_minus);
- if (!isFinite(m)) return "";
- return '−' + esc(String(m)) + " ";
- }
- if (row.loc != null) {
- return 'LOC ';
- }
- return "";
- }
-
- function setText(el, text) {
- if (!el) return;
- var t = text || "";
- if (el.textContent !== t) el.textContent = t;
- }
-
- function rowTitle(row) {
- var tip = row.tip || "";
- var tipTime = row.tip_time || "";
- var vs = "";
- if (row.vs_parent_plus != null || row.vs_parent_minus != null) {
- vs =
- " vs parent +" +
- String(row.vs_parent_plus == null ? 0 : row.vs_parent_plus) +
- "/−" +
- String(row.vs_parent_minus == null ? 0 : row.vs_parent_minus);
- } else if (row.loc != null) {
- vs = " loc=" + String(row.loc);
- }
- var built = builtText(row);
- return (
- "Switch build to " +
- (row.branch || "") +
- (tip ? " @ " + tip : "") +
- (tipTime ? " tip=" + shortTime(tipTime) : "") +
- (built ? " build=" + built : "") +
- " " +
- freezeLabel(row) +
- vs
- );
- }
-
- function structureKey(list) {
- var parts = [];
- for (var i = 0; i < list.length; i++) {
- var row = list[i];
- parts.push(
- (row.edition || "") +
- "\t" +
- (row.branch || "") +
- "\t" +
- (row.prefix || "") +
- "\t" +
- (row.parent || "")
- );
- }
- return parts.join("\n");
- }
-
- function pinBar(timingMsg) {
- var bar = document.getElementById("cascade-bar");
- if (!bar) return;
- if (timingMsg) {
- bar.setAttribute("data-timing", timingMsg);
- bar.setAttribute("title", timingMsg);
- }
- var h = bar.offsetHeight || 0;
- document.documentElement.style.setProperty("--cascade-pad", h + "px");
- }
-
- function markActive(root, edition) {
- var links = root.querySelectorAll("a[data-edition]");
- for (var i = 0; i < links.length; i++) {
- var a = links[i];
- var ed = a.getAttribute("data-edition");
- var on =
- ed === edition ||
- (edition === "flask" && a.getAttribute("data-default") === "1");
- if (on) {
- a.classList.add("is-active");
- a.setAttribute("aria-current", "page");
- } else {
- a.classList.remove("is-active");
- a.removeAttribute("aria-current");
- }
- }
- }
-
- function timingText(timing, listLen) {
- if (!timing || timing.wall_seconds == null) return "";
- return (
- "last full pass ~" +
- String(timing.wall_seconds) +
- "s · built " +
- String(timing.built || 0) +
- " / skipped " +
- String(timing.skipped || 0) +
- " / " +
- String(timing.branches || listLen)
- );
- }
-
- function updateValues(mount, data) {
- var list = (data && data.branches) || [];
- var defaultBranch = (data && data.default_branch) || "feat/website-views";
- var timing = (data && data.rebuild_timing) || null;
- var rows = mount.querySelectorAll("a.cascade-tree-row[data-edition]");
- if (rows.length !== list.length) return false;
- for (var i = 0; i < list.length; i++) {
- var row = list[i];
- var a = rows[i];
- if (a.getAttribute("data-edition") !== (row.edition || "")) return false;
- if (a.getAttribute("data-branch") !== (row.branch || "")) return false;
- var isDefault = (row.branch || "") === defaultBranch;
- if (isDefault) a.setAttribute("data-default", "1");
- else a.removeAttribute("data-default");
- setText(a.querySelector(".cascade-when"), shortTime(row.tip_time || ""));
- setText(a.querySelector(".cascade-built"), builtText(row));
- var markEl = a.querySelector(".cascade-mark");
- if (markEl) {
- var markHtml = freezeMarkHtml(row);
- if (markEl.outerHTML !== markHtml) markEl.outerHTML = markHtml;
- }
- var plusEl = a.querySelector(".cascade-plus");
- if (plusEl) {
- var ph = plusHtml(row);
- if (plusEl.innerHTML !== ph) plusEl.innerHTML = ph;
- }
- var minusEl = a.querySelector(".cascade-minus");
- if (minusEl) {
- var mh = minusHtml(row);
- if (minusEl.innerHTML !== mh) minusEl.innerHTML = mh;
- }
- a.setAttribute("title", rowTitle(row));
- }
- var timingEl = mount.querySelector(".cascade-timing");
- var t = timingText(timing, list.length);
- if (t) {
- if (!timingEl) {
- timingEl = document.createElement("div");
- timingEl.className = "cascade-timing";
- mount.appendChild(timingEl);
- }
- if (timingEl.textContent !== t) timingEl.textContent = t;
- } else if (timingEl) {
- timingEl.parentNode.removeChild(timingEl);
- }
- markActive(mount, cookieEdition());
- pinBar(t);
- return true;
- }
-
- function renderTree(data, soft) {
- var mount = document.getElementById("cascade-tree");
- if (!mount) return;
- var list = (data && data.branches) || [];
- if (!list.length) {
- mount.textContent = "No branches in branch-status.json";
- pinBar();
- return;
- }
- if (soft && mount.getAttribute("data-structure") === structureKey(list)) {
- if (updateValues(mount, data)) return;
- }
- var defaultBranch = (data && data.default_branch) || "feat/website-views";
- var timing = (data && data.rebuild_timing) || null;
- var html = [];
- html.push('");
- var t = timingText(timing, list.length);
- if (t) {
- html.push('' + esc(t) + "
");
- }
- mount.innerHTML = html.join("");
- mount.setAttribute("data-structure", structureKey(list));
- markActive(mount, cookieEdition());
- pinBar(t);
- }
-
- function loadStatus(soft) {
- var mount = document.getElementById("cascade-tree");
- return fetch("/branch-status.json", { cache: "no-store" })
- .then(function (res) {
- if (!res.ok) throw new Error("status");
- return res.json();
- })
- .then(function (data) {
- renderTree(data, !!soft);
- })
- .catch(function () {
- if (!soft && mount && !mount.querySelector("a[data-edition]")) {
- mount.innerHTML =
- 'flask ' +
- ' (branch-status unavailable) ';
- markActive(mount, cookieEdition());
- pinBar();
- }
- });
- }
-
- 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);
- }, POLL_MS);
- window.addEventListener("resize", function () {
- pinBar();
- });
-})();
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/index.html
deleted file mode 100644
index db0bc96..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/_cascade/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-cascade editions
-
-
-
-
- Loading builds…
-
-
-Stuck on an edition without a bar? Pick a build above, or reset to flask .
-
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-sm-content/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/feat-sm-content/index.html
deleted file mode 100644
index 13f4285..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-sm-content/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-feat/sm-content
-
-
-
-{% include "_cascade/bar.html" . %}
-/feat-sm-content/
-last-built: 2026-09-30T02:30:50+02:00 · feat/sm-content @ 089ef1f
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-design/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-design/index.html
deleted file mode 100644
index 142b80e..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-design/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-feat/website-design
-
-
-
-{% include "_cascade/bar.html" . %}
-/feat-website-design/
-last-built: 2026-09-30T02:30:50+02:00 · feat/website-design @ 2e26c80
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-features/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-features/index.html
deleted file mode 100644
index 3271f8f..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-features/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-feat/website-features
-
-
-
-{% include "_cascade/bar.html" . %}
-/feat-website-features/
-last-built: 2026-09-30T02:30:50+02:00 · feat/website-features @ 53a3176
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-views/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-views/index.html
deleted file mode 100644
index 5896890..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/feat-website-views/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-feat/website-views
-
-
-
-{% include "_cascade/bar.html" . %}
-/feat-website-views/
-last-built: 2026-09-30T02:30:50+02:00 · feat/website-views @ bbafc38
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/index.html
deleted file mode 100644
index 71a5d33..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/index.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-cascade landing
-
-
-
-{% include "_cascade/bar.html" . %}
-cascade
-Flask freeze lives at / (views). This ladder is tip stubs only.
-last-built: 2026-09-30T02:30:50+02:00
-
-
-
diff --git a/sites/dossiers.ngi.hacktivism.ch/cascade/main/index.html b/sites/dossiers.ngi.hacktivism.ch/cascade/main/index.html
deleted file mode 100644
index 941036c..0000000
--- a/sites/dossiers.ngi.hacktivism.ch/cascade/main/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-main
-
-
-
-{% include "_cascade/bar.html" . %}
-/main/
-last-built: 2026-09-30T02:30:50+02:00 · main @ d2fbd79
-
-