docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable

This commit is contained in:
Hernâni Marques 2026-07-13 10:12:00 +02:00
commit 96961f23f5
268 changed files with 24161 additions and 0 deletions

19
configs/README.md Normal file
View file

@ -0,0 +1,19 @@
# Config mirrors (from host koopa)
Directories are named to match **live podman container names** where possible.
| Directory | Live container | Image (typical) |
|-----------|----------------|-----------------|
| `taler-hacktivism/` | `taler-hacktivism` | `taler-hacktivism-live:landing` |
| `taler-hacktivism-bank/` | `taler-hacktivism-bank` | **`taler-hacktivism-banking:live`** |
| `taler-exchange/` | conf inside exchange container | (see exchange-ansible) |
| `taler-exchange-ansible/` | **`taler-hacktivism-exchange-ansible`** | `taler-hacktivism-exchange-ansible:landing` |
| `bank-landing/` `exchange-landing/` `merchant-landing/` | nginx landing snippets | ports 90139015 |
| `koopa-*` apps | `koopa-castopod`, `koopa-bonfire`, … | compose mirrors |
| `tops/` | `koopa-tops-ng1``ng3` | `nginx:1.27-alpine` |
| `caddy/` `firewalld/` `systemd/` | host services | |
| `tor/` | **`koopa-tor-relay`** (podman host net) | `localhost/koopa-tor-relay:latest` |
**Authoritative running inventory:** `host/overview/LIVE.md`.
Secrets never live here — `SECRETS.md` / `koopa-admin-secrets`.

View file

@ -0,0 +1,250 @@
# Bank landing (`bank.hacktivism.ch`)
English intro for **GOA exploration** (badge: **Intergalactic · GOA · no IBAN**):
put coins in a GNU Taler wallet, optionally open your own bank account, and
point merchants at `taler.hacktivism.ch`.
| Public URL | What |
|------------|------|
| `https://bank.hacktivism.ch/` · `/intro/` | Landing (nginx in bank container → host **9013**, Caddy) |
| `https://bank.hacktivism.ch/intro/stats.json` | **Live stats** (written *inside* bank container) |
| `https://bank.hacktivism.ch/intro/demo-withdraw.json` | Mint **shared-pool** (`explorer`) withdraw URI |
| `https://bank.hacktivism.ch/intro/auto-account.json` | Auto-create **personal** bank user (credentials once; balance **GOA:0**) |
| `https://bank.hacktivism.ch/webui/` | libeufin-bank SPA (register · login · withdraw) when libeufin is up on **9012** |
| `https://exchange.hacktivism.ch/` | Exchange (ToS `/terms`, keys `/keys`) |
| `https://taler.hacktivism.ch/` | Merchant backend |
## Page layout (top → bottom)
1. Hero (badge, short shared-account line)
2. **Get the wallet** — Play / F-Droid / iOS / web hub + **Debian/Ubuntu · Command line** (apt) + **Unix / POSIX · from source** (build monorepo or run `.mjs`)
3. **Withdraw GOA** — compact QR_Taler, open CTA, live pool balance note, generate-own-account link
4. **GOA shop · samples** — always visible (2 products + merchant link)
5. **How the shared account works** — collapsed fold (click to expand)
6. **Optional · own bank account** — collapsed fold: auto-create account + credentials + QR_Taler for bank UI
7. **GOA flow** stats
8. **Performance**
9. Footer
## User flow (documented on the page)
### Primary path (works without your own bank account)
1. **Install wallet** — Android (Play / F-Droid), iOS, Ubuntu Touch (via Waydroid + Android wallet), web (Chrome/Firefox/Opera on taler.net).
2. **One QR / link** — live `taler://withdraw/…` from the **community-shared** bank pool (`explorer`).
Minted via `GET /intro/demo-withdraw.json` (or fallback `withdraw.uri`).
Auto-confirm finishes the bank side so coins reach the wallet.
3. **Surprising balance note** — shows live `balance_explorer` from `stats.json`
(“pool holds GOA:… (shared, but also yours)”).
QR encoding matches merchant shop popup (**QR_Taler**): animated `#0042B3` conic
ring, off-DOM PNG, center `qr-logo.svg` (`shop-pay.css` classes). Label text:
**Scan in wallet**.
### Optional: personal bank account (auto-created)
In the optional fold:
1. User clicks **Create my bank account**.
2. Browser calls `GET /intro/auto-account.json` (no form fields).
3. Server registers a new libeufin user via public `POST /accounts`.
4. UI shows **username / password** once (“created for you”;
not stored for recovery).
5. Username shape: **`goa-account-<random>`** (shown to the user).
Password embeds **`pleasechangeme`** with random chars around it.
6. QR_Taler + link open `/webui/` so the user can log in with those credentials.
Balance **starts at zero** — not the shared `explorer` pool.
#### Deploy status — own bank account (**missing** until verified)
The **own bank account** path is **not complete** unless all of the following
are live on `taler-hacktivism-bank`. Treat this as **missing** until checked:
| Check | Expected |
|-------|----------|
| Landing HTML | Optional fold has **Create my bank account** |
| API process | `python3 /usr/local/bin/demo-withdraw-api.py` on **127.0.0.1:19096** |
| Nginx | `location = /intro/auto-account.json``:19096` |
| Public | `curl -sS https://bank.hacktivism.ch/intro/auto-account.json` returns JSON with `username` / `password` / `balance: "GOA:0"` |
| WebUI login | Credentials work at `https://bank.hacktivism.ch/webui/` |
Install helper: `scripts/taler-bank/install-demo-withdraw-api.sh`
(or manual copy of `demo-withdraw-api.py` + `nginx-landing.conf` + restart API).
### How the shared pool works
1. Shared account `explorer` is pre-funded with GOA.
2. Each demo withdraw is a **one-shot** op from that pool (not a personal login).
3. Wallet accepts the URI; **auto-confirm only for `explorer`** (see
`auto-confirm-withdrawals.sh`).
4. Pool balance is communal; amounts you withdraw are yours in the wallet.
### Helpers
| Script | Role |
|--------|------|
| `scripts/taler-bank/demo-withdraw-api.py` | Loopback HTTP **:19096** — `/demo-withdraw.json` + `/auto-account.json` |
| `scripts/taler-bank/install-demo-withdraw-api.sh` | Copy into bank container, nginx locations, start API + auto-confirm loop |
| `scripts/taler-bank/auto-confirm-withdrawals.sh` | Confirm **explorer-only** withdrawals when status=`selected` |
| `scripts/taler-bank/refresh-demo-withdraw.sh` | Refresh static `withdraw.uri` (no python) |
| `configs/bank-landing/nginx-landing.conf` | `/intro/demo-withdraw.json`, `/intro/auto-account.json` proxies |
Needs **python3** in the bank container for `demo-withdraw-api.py`.
### CLI sketch (fresh wallet DB)
```bash
# equivalent one-step in CLI terms:
taler-wallet-cli exchanges add https://exchange.hacktivism.ch/
taler-wallet-cli exchanges accept-tos https://exchange.hacktivism.ch/
# then withdraw from the exchange (amount chosen in wallet UI)
```
---
## Live stats (in bank container)
Stats are **not** computed on the laptop or host browser. A small script runs
**inside the bank container**, queries libeufin for the demo funding account
(`explorer`), and writes a public JSON file next to the landing assets.
| Piece | Path / role |
|-------|-------------|
| Generator (in-container) | `/usr/local/bin/landing-stats.sh` |
| Source in admin-log | `scripts/taler-bank/landing-stats.sh` |
| Host installer | `scripts/taler-bank/landing-stats-install.sh` |
| Output | `/var/www/bank-landing/stats.json``https://bank.hacktivism.ch/intro/stats.json` |
| Landing UI | `index.html` section **GOA flow · live** |
### What the page shows
- **Bank accounts** — registered accounts (libeufin `GET /accounts`, admin)
- **Wallets involved** — unique reserve pubs from Taler withdrawal debits
- **Withdraws / bank credits** — flow amounts (exchange mirror skipped where noted)
- **Last withdraws** — recent list with amount, time (**CEST** / `Europe/Zurich`)
- **Last 24h / 7d** — amount
- **`balance_explorer`** — live shared-pool balance (shown on withdraw step)
- Performance probes + container RSS (bottom of page)
### Scan depth (must stay deep enough)
| Env | Default | Meaning |
|-----|---------|---------|
| `TX_DELTA` | `-50000` | per-account ledger window (`GET …/transactions?delta=`) |
| `ACCOUNTS_DELTA` | `-500` | account-list window (`GET /accounts?delta=`) |
| `MAX_SCAN_ACCOUNTS` | `500` | max usernames to scan |
Older defaults (`TX_DELTA=-100`, `MAX_SCAN_ACCOUNTS=80`) **undercounted** credits/withdraws
and account totals on this stack. Empty accounts often return **HTTP 204** (no body) —
that is normal (e.g. fresh auto-accounts), not a stats failure.
### `stats.json` shape (abridged)
```json
{
"ok": true,
"currency": "GOA",
"generated_at": "2026-07-10T20:35+02:00",
"balance_explorer": "GOA:960",
"bank_accounts": { "total": 56, "users": 55 },
"wallets": { "unique_reserves": 51 },
"withdraws": {
"count": 51,
"last_24h": { "amount": "GOA:…" },
"last_7d": { "amount": "GOA:…" }
},
"flow": {
"incoming": { "amount": "GOA:…", "count": 68 },
"withdraw": { "amount": "GOA:…", "count": 51 }
},
"source": "in-container landing-stats.sh"
}
```
### Install + refresh (on koopa host)
```bash
# from laptop: copy scripts, then on koopa:
cd /path/to/koopa-admin-log/scripts/taler-bank
# copy into container + one-shot run
sudo ./landing-stats-install.sh
# every minute inside the container (* * * * *)
sudo ./landing-stats-install.sh --cron
# optional: LANDING_STATS_CRON='*/5 * * * *' ./landing-stats-install.sh --cron
# later: only re-run
sudo ./landing-stats-install.sh --run-only
```
Requirements **inside** the container:
- `curl`, `awk` (mawk OK), `sed`, `date` — **no python**
- libeufin-bank listening on loopback (script auto-detects `PORT` / 9012 / 8080)
- `/root/bank-explorer-password.txt` (or `BANK_PASS=…` on the `podman exec`)
- writable `/var/www/bank-landing/` (same tree nginx uses for `/intro/`)
`make-demo-withdraw-qr.sh` also calls `landing-stats.sh` after refreshing the demo URI,
when the binary is already installed at `/usr/local/bin/landing-stats.sh`.
### Manual one-liner (debug)
```bash
podman exec -e LANDING_DIR=/var/www/bank-landing taler-hacktivism-bank \
/usr/local/bin/landing-stats.sh
curl -sS https://bank.hacktivism.ch/intro/stats.json | python3 -m json.tool
```
---
## Host / container files
Landing root (nginx in bank container), typically under something like
`/var/www/bank-landing/` or the container path mapped for **9013**:
| File | Role |
|------|------|
| `index.html` | landing (wallet, shared withdraw, folds, stats) + Open Graph meta |
| `og-goa-shop.png` | **link preview** image (1200×630) for chats / social (OG + Twitter) |
| `shop-pay.css` / `shop-pay.js` | GOA shop popup + **QR_Taler** frame styles |
| `qr-logo.svg` | center logo on QR_Taler frames |
| `qrcode.min.js` | QR encode helper |
| `stats.json` | **live stats** (from in-container script) |
| `nginx-landing.conf` | bank container nginx (**9013**) |
| `withdraw.uri` | current demo `taler://withdraw/…` (optional) |
| `withdraw.amount` | e.g. `GOA:10` (optional) |
| `withdraw-watch.ids` | ids for auto-confirm helper (optional) |
## Deploy landing HTML + helper API
```bash
# landing assets
podman cp configs/bank-landing/index.html \
taler-hacktivism-bank:/var/www/bank-landing/index.html
podman cp configs/bank-landing/og-goa-shop.png \
taler-hacktivism-bank:/var/www/bank-landing/og-goa-shop.png
podman cp configs/bank-landing/shop-pay.js \
taler-hacktivism-bank:/var/www/bank-landing/shop-pay.js
# … shop-pay.css, qr-logo.svg, qrcode.min.js as needed
# demo-withdraw + auto-account API (python3 in container)
./scripts/taler-bank/install-demo-withdraw-api.sh
# or manually:
podman cp scripts/taler-bank/demo-withdraw-api.py \
taler-hacktivism-bank:/usr/local/bin/demo-withdraw-api.py
podman cp configs/bank-landing/nginx-landing.conf \
taler-hacktivism-bank:/etc/nginx/sites-available/bank-landing
# restart: python3 /usr/local/bin/demo-withdraw-api.py on 127.0.0.1:19096
# nginx -s reload; auto-confirm-withdrawals.sh --loop
```
### Demo QR refresh (in container, no python)
```bash
podman exec taler-hacktivism-bank /usr/local/bin/refresh-demo-withdraw.sh
# source: scripts/taler-bank/refresh-demo-withdraw.sh
```
Older host script (needs python3): `scripts/taler-bank/make-demo-withdraw-qr.sh`

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
# Snippet: CORS so exchange/merchant landings can fetch stats.json
# Include inside bank landing server { } or merge into bank-landing site.
# location for stats only:
# include /etc/nginx/snippets/goa-stats-cors.conf; (if placed as snippet)
# Applied as extra location on :9013 bank-landing:

View file

@ -0,0 +1,71 @@
# Bank landing — :9013 (behind Caddy :443 — never put 9013 in redirects)
server {
listen 9013;
listen [::]:9013;
server_name bank.hacktivism.ch _;
root /var/www/bank-landing;
index index.html;
# Critical behind reverse_proxy: no :9013 in Location headers
absolute_redirect off;
port_in_redirect off;
location = / {
return 302 /intro/;
}
location = /intro {
return 302 /intro/;
}
# Terms (same dark palette as merchant/exchange terms pages)
location = /terms {
alias /var/www/bank-landing/terms.html;
default_type text/html;
add_header Cache-Control "no-store" always;
}
location = /terms/ {
return 302 /terms;
}
location = /privacy {
alias /var/www/bank-landing/privacy.html;
default_type text/html;
add_header Cache-Control "no-store" always;
}
location = /privacy/ {
return 302 /privacy;
}
# Fresh community-pool withdraw for one-click wallet funding
location = /intro/demo-withdraw.json {
proxy_pass http://127.0.0.1:19096/demo-withdraw.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
add_header Cache-Control "no-store" always;
add_header Access-Control-Allow-Origin * always;
}
# Auto-create personal bank account (credentials returned once; balance GOA:0)
location = /intro/auto-account.json {
proxy_pass http://127.0.0.1:19096/auto-account.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
add_header Cache-Control "no-store" always;
add_header Access-Control-Allow-Origin * always;
}
location /intro/ {
alias /var/www/bank-landing/;
}
location = /intro/stats.json {
alias /var/www/bank-landing/stats.json;
default_type application/json;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Cache-Control "no-store" always;
}
location = /stats.json {
alias /var/www/bank-landing/stats.json;
default_type application/json;
add_header Access-Control-Allow-Origin * always;
add_header Cache-Control "no-store" always;
}
location / {
try_files $uri $uri/ =404;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

View file

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Privacy · GOA Bank · Swiss FADP</title>
<style>
:root { color-scheme: dark light; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 42rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
line-height: 1.5; color: #e8e6e3; background: #1a1520;
}
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; color: #e8c878; }
p, li, td, th { font-size: 0.95rem; }
ul { padding-left: 1.2rem; }
code, a { color: #5eead4; }
a { text-decoration: none; }
a:hover { text-decoration: underline; }
.badge {
display: inline-block; font-size: 0.72rem; font-weight: 700;
letter-spacing: 0.06em; text-transform: uppercase;
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
}
.note {
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
font-size: 0.9rem; color: #c8c4bf;
}
table { width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem; font-size: 0.88rem; }
th, td { border: 1px solid rgba(255,255,255,0.12); padding: 0.45rem 0.55rem; text-align: left; vertical-align: top; }
th { background: rgba(0,0,0,0.35); color: #e8c878; font-weight: 700; }
.muted { color: #a39e98; font-size: 0.88rem; }
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
</style>
</head>
<body>
<div class="badge">bank.hacktivism.ch · privacy · CH</div>
<h1>Privacy notice · GOA Exploration Bank</h1>
<p class="note">
This notice describes personal data processing for the self-hosted bank at
<code>bank.hacktivism.ch</code> under the Swiss Federal Act on Data Protection
(<strong>FADP / revDSG</strong>, in force since 1&nbsp;Sep&nbsp;2023). It is an
explorational service, not a licensed Swiss bank.
</p>
<h2>1. Controller</h2>
<p>Operators of the hacktivism.ch GNU Taler stack (GOA exploration deployment).
Contact via the operational channels published for this host. No separate DPO
is appointed for this experimental service.</p>
<h2>2. Categories of data retained</h2>
<table>
<thead><tr><th>Data</th><th>Examples</th><th>Typical retention</th></tr></thead>
<tbody>
<tr>
<td>Account identifiers</td>
<td>Username, account serial, registration timestamp</td>
<td>For account lifetime + up to 12 months after deletion or archive wipe</td>
</tr>
<tr>
<td>Authentication secrets</td>
<td>Password hashes / tokens (not plaintext passwords)</td>
<td>While account exists; tokens until expiry or revoke</td>
</tr>
<tr>
<td>Ledger / transactions</td>
<td>Credits, debits, amounts (GOA), subjects, counterparty account names, timestamps</td>
<td>Operational retention for the service lifetime; may be wiped on stack reset</td>
</tr>
<tr>
<td>Balances &amp; limits</td>
<td>Account balance, debt limit, conversion flags</td>
<td>While account exists</td>
</tr>
<tr>
<td>Withdrawal operations</td>
<td>Withdrawal IDs, amounts, status (pending/selected/confirmed), reserve pub when known</td>
<td>Until completed/aborted + short operational logs (daysweeks)</td>
</tr>
<tr>
<td>Technical logs</td>
<td>HTTP access logs (IP, User-Agent, path, status), application logs</td>
<td>Typically days to a few weeks (rotation); not used for marketing</td>
</tr>
<tr>
<td>Landing / demo artefacts</td>
<td>Public <code>stats.json</code> aggregates, demo withdraw URI state</td>
<td>Stats overwritten continuously; demo URI until used or rotated</td>
</tr>
</tbody>
</table>
<p><strong>Not retained as payment content:</strong> wallet coin private keys (never sent to the bank).
Full browser history or device contacts are not collected by this bank service.</p>
<h2>3. Purposes</h2>
<ul>
<li>Operating bank accounts and GOA transfers for exploration of GNU Taler</li>
<li>Authenticating users and preventing abuse</li>
<li>Enabling withdrawals to the GOA exchange</li>
<li>Security, debugging, and capacity monitoring</li>
</ul>
<h2>4. Legal basis (Swiss FADP)</h2>
<p>Processing is necessary to provide the service requested by the user (account / withdraw)
and for overriding private interests of secure operation of an experimental public stack
(Art.&nbsp;6 and 31 FADP principles: lawfulness, proportionality, purpose limitation).</p>
<h2>5. Recipients &amp; transfers</h2>
<ul>
<li><strong>GOA exchange</strong> (<code>exchange.hacktivism.ch</code>): reserve/wire-related data required by the protocol</li>
<li><strong>Infrastructure operators</strong> of this host (hosting, backups) under operational control</li>
<li>No sale of personal data. No intentional transfer outside Switzerland/EEA for this service;
infrastructure may use standard CDN/DNS resolvers</li>
</ul>
<h2>6. Your rights (FADP)</h2>
<p>Subject to legal limits: right to information/access, rectification, deletion, and to object
to processing. Contact the operators. You may lodge a complaint with the Swiss Federal Data
Protection and Information Commissioner (<strong>FDPIC / EDÖB</strong>).</p>
<h2>7. Security</h2>
<p>TLS in transit; access control on bank API; experimental service — no certified ISMS.
Do not store sensitive personal data in transaction subjects.</p>
<h2>Related</h2>
<ul>
<li><a href="https://bank.hacktivism.ch/terms">Bank terms</a></li>
<li><a href="https://exchange.hacktivism.ch/privacy">Exchange privacy</a></li>
<li><a href="https://taler.hacktivism.ch/privacy">Merchant privacy</a></li>
<li><a href="https://bank.hacktivism.ch/intro/">Bank intro</a></li>
</ul>
<footer class="muted">bank-pp-swiss-v0 · Swiss FADP (revDSG)</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

1
configs/bank-landing/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,348 @@
/* GOA shop pay popup — QR_Taler style from taler-merchant-webui / @gnu-taler/web-util */
/* Animated blue ring (same as webui QR.tsx) */
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes goa-pay-qr-rotate {
to {
--angle: 360deg;
}
}
.shop-item[data-product] {
cursor: pointer;
transition: border-color 0.15s, transform 0.12s, background 0.15s;
}
.shop-item[data-product]:hover,
.shop-item[data-product]:focus-visible {
border-color: rgba(196, 181, 253, 0.75);
background: rgba(0, 0, 0, 0.4);
transform: translateY(-1px);
outline: none;
}
button.shop-item {
font: inherit;
color: inherit;
width: 100%;
appearance: none;
-webkit-appearance: none;
}
.goa-pay-modal {
position: fixed;
inset: 0;
z-index: 80;
display: none;
align-items: center;
justify-content: center;
padding: 1rem;
background: rgba(8, 6, 14, 0.75);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.goa-pay-modal.open {
display: flex;
}
.goa-pay-card {
position: relative;
background: rgba(36, 28, 48, 0.98);
border: 1px solid rgba(167, 139, 250, 0.45);
border-radius: 16px;
padding: 1.25rem 1.2rem 1.2rem;
max-width: 24rem;
width: 100%;
text-align: center;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
max-height: 92vh;
overflow-y: auto;
}
.goa-pay-x {
position: absolute;
top: 0.45rem;
right: 0.55rem;
border: 0;
background: transparent;
color: #c4b5fd;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
padding: 0.2rem 0.45rem;
}
.goa-pay-card h3 {
margin: 0 0 0.2rem;
font-size: 1.08rem;
color: #faf5ff;
padding-right: 1.5rem;
}
.goa-pay-amount {
margin: 0 0 0.85rem;
font-weight: 750;
color: #c4b5fd;
font-variant-numeric: tabular-nums;
}
/* === QR_Taler frame (webui QR.tsx) === */
.goa-pay-taler-qr {
width: 100%;
max-width: 280px;
margin: 0 auto 0.75rem;
padding: 10px;
border-radius: 20px;
box-sizing: border-box;
position: relative;
background: conic-gradient(
from var(--angle),
#0042b3 0deg,
#f1f1f4 20deg,
#f1f1f4 150deg,
#f1f1f4 160deg,
#0042b3 180deg,
#f1f1f4 200deg,
#f1f1f4 330deg,
#f1f1f4 340deg,
#0042b3
);
animation: goa-pay-qr-rotate 10s linear infinite;
}
.goa-pay-taler-qr__inner {
padding: 10px;
border-radius: 20px;
background: #fff;
line-height: 0;
min-height: 180px;
box-sizing: border-box;
}
/* Always an <img data-url> with explicit pixel size from JS */
.goa-pay-taler-qr__inner img {
display: block !important;
margin: 0 auto !important;
border: 0 !important;
background: #fff;
image-rendering: pixelated;
}
/* Center logo plate — official qr-logo.png (webui ~100×50; PNG for Android) */
.goa-pay-taler-qr__logo {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 28%;
height: auto;
max-width: 100px;
aspect-ratio: 200 / 95;
object-fit: contain;
pointer-events: none;
background: #fff;
border-radius: 4px;
}
/* Full encoded payload under QR — clickable (wallet / webextension) */
.goa-pay-payload {
display: block;
margin: 0.45rem 0 0.55rem;
padding: 0 0.25rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.68rem;
line-height: 1.35;
color: #7eb6ff;
word-break: break-all;
text-align: center;
text-decoration: none;
border-bottom: 1px solid rgba(126, 182, 255, 0.4);
cursor: pointer;
}
.goa-pay-payload:hover {
color: #b8d6ff;
border-bottom-color: #7eb6ff;
}
/* Settlement: collapsed by default, expand on click */
.goa-pay-settle {
margin-top: 0.85rem;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 12px;
overflow: hidden;
background: rgba(0, 0, 0, 0.22);
}
.goa-pay-settle-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
margin: 0;
padding: 0.65rem 0.75rem;
border: 0;
background: transparent;
color: #cbd5e1;
font: inherit;
font-size: 0.78rem;
font-weight: 650;
text-align: left;
cursor: pointer;
line-height: 1.3;
}
.goa-pay-settle-toggle:hover {
background: rgba(148, 163, 184, 0.1);
color: #e2e8f0;
}
.goa-pay-settle-toggle-text {
flex: 1;
}
.goa-pay-settle-chevron {
flex-shrink: 0;
transition: transform 0.15s ease;
color: #94a3b8;
font-size: 0.85rem;
}
.goa-pay-modal.settle-open .goa-pay-settle-chevron {
transform: rotate(90deg);
}
.goa-pay-settle-body {
padding: 0 0.75rem 0.75rem;
border-top: 1px solid rgba(148, 163, 184, 0.25);
}
.goa-pay-settle-note {
margin-top: 0.55rem;
}
/* Settlement payto QR — plain box (not wallet pay / no logo / no blue ring) */
.goa-pay-payto-qr {
width: fit-content;
max-width: 160px;
margin: 0.35rem auto 0.55rem;
padding: 0.45rem 0.5rem 0.5rem;
border-radius: 10px;
background: #fff;
border: 1px dashed rgba(100, 116, 139, 0.75);
box-sizing: border-box;
}
.goa-pay-payto-qr__caption {
font-size: 0.62rem;
font-weight: 750;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #64748b;
text-align: center;
margin: 0 0 0.35rem;
line-height: 1.2;
}
.goa-pay-payto-qr__inner {
line-height: 0;
min-height: 0;
}
.goa-pay-payto-qr__inner img {
display: block !important;
margin: 0 auto !important;
background: #fff;
image-rendering: pixelated;
}
.goa-pay-sub {
margin: 0 0 0.55rem;
font-size: 0.75rem;
line-height: 1.35;
color: #b8a8c9;
font-weight: 500;
text-align: center;
}
.goa-pay-hint {
margin: 0 0 0.75rem;
font-size: 0.82rem;
color: #b8a8c9;
}
.goa-pay-hint.err {
color: #fca5a5;
}
.goa-pay-cta {
display: block;
text-decoration: none;
font-weight: 750;
padding: 0.65rem 1rem;
border-radius: 11px;
background: linear-gradient(135deg, #a78bfa, #6366f1);
color: #0f0a1a;
margin-bottom: 0.75rem;
}
.goa-pay-cta:hover {
filter: brightness(1.06);
}
.goa-pay-cta.disabled {
opacity: 0.45;
pointer-events: none;
}
.goa-pay-label {
margin: 0.55rem 0 0.2rem;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #b8a8c9;
}
.goa-pay-line {
margin: 0 0 0.35rem;
text-align: center;
font-size: 0.88rem;
font-weight: 650;
}
.goa-pay-link {
color: #93c5fd;
text-decoration: none;
white-space: nowrap;
}
.goa-pay-link:hover {
text-decoration: underline;
}
.goa-pay-link.disabled {
opacity: 0.45;
pointer-events: none;
}
.goa-pay-sep {
margin: 0 0.4rem;
color: #b8a8c9;
font-weight: 500;
}
.goa-pay-close {
display: block;
width: 100%;
margin-top: 0.85rem;
border: 1px solid rgba(167, 139, 250, 0.45);
background: transparent;
color: #c4b5fd;
font-weight: 650;
padding: 0.5rem;
border-radius: 10px;
cursor: pointer;
}
/* Bank (gold/teal) overrides */
.goa-pay-modal.bank-theme .goa-pay-card {
background: rgba(42, 32, 24, 0.98);
border-color: rgba(232, 168, 56, 0.4);
}
.goa-pay-modal.bank-theme .goa-pay-amount {
color: #5eead4;
}
.goa-pay-modal.bank-theme .goa-pay-cta {
background: linear-gradient(135deg, #5eead4, #0d9488);
color: #042f2e;
}
.goa-pay-modal.bank-theme .goa-pay-link {
color: #5eead4;
}
.goa-pay-modal.bank-theme .goa-pay-x,
.goa-pay-modal.bank-theme .goa-pay-close {
color: #e8c878;
border-color: rgba(232, 168, 56, 0.35);
}
.goa-pay-modal.bank-theme .goa-pay-settle {
border-color: rgba(232, 168, 56, 0.3);
background: rgba(0, 0, 0, 0.28);
}
.goa-pay-modal.bank-theme .goa-pay-settle-toggle {
color: #e8c878;
}
.goa-pay-modal.bank-theme .goa-pay-settle-body {
border-top-color: rgba(232, 168, 56, 0.25);
}

View file

@ -0,0 +1,410 @@
/**
* GOA shop pay popup public only (no merchant secrets).
* Flow: POST templates/{id} taler://pay/… + payto links.
*
* QR display matches taler-merchant-webui QR_Taler
* (@gnu-taler/web-util QR.tsx): animated #0042B3 conic ring + qr-logo.png.
* Uses qrcode-generator (same lib as webui) via global QRCode if present,
* else falls back to canvas from qrcode.min.js (davidshimjs).
*/
(function () {
var MERCHANT_HOST = "taler.hacktivism.ch";
/* goa-shop: dedicated instance with fixed-order product templates */
var INSTANCE = "goa-shop";
var SHOP_PAYTO =
"payto://x-taler-bank/bank.hacktivism.ch/goa-shop?receiver-name=GOA%20Shop";
var SHOP_PAYTO_HTTPS = "https://bank.hacktivism.ch/webui/";
function introBase() {
var b = document.querySelector("base");
return b && b.href ? b.href : "/intro/";
}
function logoSrc() {
/* PNG: Android often drops SVG wordmark fill inheritance (missing "taler") */
return introBase() + "qr-logo.png";
}
function templateHttps(productId) {
return (
"https://" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/templates/" +
encodeURIComponent(productId)
);
}
function payTemplateUri(productId) {
return (
"taler://pay-template/" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/" +
encodeURIComponent(productId)
);
}
function ensureModal() {
var m = document.getElementById("goa-pay-modal");
if (m) return m;
m = document.createElement("div");
m.id = "goa-pay-modal";
m.className = "goa-pay-modal";
m.setAttribute("role", "dialog");
m.setAttribute("aria-modal", "true");
m.setAttribute("aria-labelledby", "goa-pay-title");
m.hidden = true;
m.innerHTML =
'<div class="goa-pay-card">' +
' <button type="button" class="goa-pay-x" id="goa-pay-x" aria-label="Close">×</button>' +
' <h3 id="goa-pay-title">Pay with Taler</h3>' +
' <p class="goa-pay-amount" id="goa-pay-amount"></p>' +
' <p class="goa-pay-hint" id="goa-pay-status">Preparing payment…</p>' +
' <p class="goa-pay-label">Wallet payment</p>' +
' <p class="goa-pay-sub">Scan with the GNU Taler Wallet to pay this product</p>' +
' <div class="goa-pay-taler-qr" id="goa-pay-qr-pay-wrap">' +
' <div class="goa-pay-taler-qr__inner" id="goa-pay-qr-pay"></div>' +
' <img class="goa-pay-taler-qr__logo" id="goa-pay-qr-pay-logo" alt="" />' +
" </div>" +
' <a class="goa-pay-payload" id="goa-pay-qr-payload" href="#">…</a>' +
' <a class="goa-pay-cta" id="goa-pay-open" href="#">Open payment link →</a>' +
' <p class="goa-pay-line">' +
' <a class="goa-pay-link" id="goa-pay-uri" href="#">taler:// URI</a>' +
' <span class="goa-pay-sep">·</span>' +
' <a class="goa-pay-link" id="goa-pay-uri-https" href="#" target="_blank" rel="noopener">HTTPS</a>' +
" </p>" +
' <div class="goa-pay-settle" id="goa-pay-settle">' +
' <button type="button" class="goa-pay-settle-toggle" id="goa-pay-settle-toggle" aria-expanded="false" aria-controls="goa-pay-settle-body">' +
' <span class="goa-pay-settle-toggle-text">Settlement account (payto) · not a wallet pay QR</span>' +
' <span class="goa-pay-settle-chevron" aria-hidden="true">▸</span>' +
" </button>" +
' <div class="goa-pay-settle-body" id="goa-pay-settle-body" hidden>' +
' <p class="goa-pay-sub goa-pay-settle-note">' +
" This is the merchants bank account address (payto://). " +
" The exchange wires funds here after a wallet payment. " +
" Do not scan this to buy the product — use the wallet QR above." +
" </p>" +
' <div class="goa-pay-payto-qr" id="goa-pay-qr-payto-wrap">' +
' <div class="goa-pay-payto-qr__caption">Settlement only</div>' +
' <div class="goa-pay-payto-qr__inner" id="goa-pay-qr-payto"></div>' +
" </div>" +
' <a class="goa-pay-payload" id="goa-pay-payto-payload" href="#">…</a>' +
' <p class="goa-pay-line">' +
' <a class="goa-pay-link" id="goa-pay-payto" href="#">payto:// URI</a>' +
' <span class="goa-pay-sep">·</span>' +
' <a class="goa-pay-link" id="goa-pay-payto-https" href="#" target="_blank" rel="noopener">HTTPS</a>' +
" </p>" +
" </div>" +
" </div>" +
' <button type="button" class="goa-pay-close" id="goa-pay-close">Close</button>' +
"</div>";
document.body.appendChild(m);
function close() {
m.classList.remove("open");
m.hidden = true;
// collapse settlement on close
var body = document.getElementById("goa-pay-settle-body");
var btn = document.getElementById("goa-pay-settle-toggle");
if (body) body.hidden = true;
if (btn) {
btn.setAttribute("aria-expanded", "false");
m.classList.remove("settle-open");
}
}
m.addEventListener("click", function (e) {
if (e.target === m) close();
});
document.getElementById("goa-pay-x").onclick = close;
document.getElementById("goa-pay-close").onclick = close;
document.getElementById("goa-pay-settle-toggle").onclick = function () {
var body = document.getElementById("goa-pay-settle-body");
var btn = document.getElementById("goa-pay-settle-toggle");
var open = body.hidden;
body.hidden = !open;
btn.setAttribute("aria-expanded", open ? "true" : "false");
m.classList.toggle("settle-open", open);
if (open) {
// paint payto QR when first expanded
renderQr(document.getElementById("goa-pay-qr-payto"), SHOP_PAYTO, 140);
var pp = document.getElementById("goa-pay-payto-payload");
if (pp) {
pp.href = SHOP_PAYTO;
pp.textContent = SHOP_PAYTO;
}
}
};
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && m.classList.contains("open")) close();
});
return m;
}
/**
* Render URI as a real <img> PNG (not a live canvas).
* davidshimjs paints canvas then often hides it for an img; if the modal is
* still display:none, layout collapses and you only see the blue ring.
* We always encode off-DOM and inject a fixed-size image.
*/
function renderQr(hostEl, text, size) {
if (!hostEl) return;
hostEl.innerHTML = "";
size = size || 220;
if (!text) {
hostEl.innerHTML = '<p class="goa-pay-hint">…</p>';
return;
}
if (typeof QRCode === "undefined") {
hostEl.innerHTML =
'<p class="goa-pay-hint err">QR library missing (qrcode.min.js)</p>';
return;
}
var level =
QRCode.CorrectLevel && QRCode.CorrectLevel.M != null
? QRCode.CorrectLevel.M
: QRCode.CorrectLevel && QRCode.CorrectLevel.L != null
? QRCode.CorrectLevel.L
: 1;
var scratch = document.createElement("div");
scratch.setAttribute("aria-hidden", "true");
scratch.style.cssText =
"position:fixed;left:-9999px;top:0;width:" +
size +
"px;height:" +
size +
"px;overflow:hidden;opacity:0;pointer-events:none";
document.body.appendChild(scratch);
var dataUrl = "";
try {
new QRCode(scratch, {
text: String(text),
width: size,
height: size,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: level,
});
var canvas = scratch.querySelector("canvas");
var libImg = scratch.querySelector("img");
if (canvas && canvas.width > 0) {
try {
dataUrl = canvas.toDataURL("image/png");
} catch (e1) {}
}
if (!dataUrl && libImg && libImg.src && libImg.src.indexOf("data:") === 0) {
dataUrl = libImg.src;
}
} catch (err) {
dataUrl = "";
}
if (scratch.parentNode) scratch.parentNode.removeChild(scratch);
if (!dataUrl) {
hostEl.innerHTML =
'<p class="goa-pay-hint err">QR encode failed</p>';
return;
}
var img = document.createElement("img");
img.alt = "QR code";
img.width = size;
img.height = size;
img.src = dataUrl;
img.style.display = "block";
img.style.width = size + "px";
img.style.height = size + "px";
img.style.maxWidth = "100%";
img.style.margin = "0 auto";
img.style.background = "#fff";
hostEl.appendChild(img);
}
function setLogo(imgEl) {
if (!imgEl) return;
imgEl.alt = "Taler";
imgEl.width = 100;
imgEl.height = 50;
imgEl.decoding = "async";
imgEl.src = logoSrc();
imgEl.onerror = function () {
imgEl.src = introBase() + "qr-logo.svg";
};
imgEl.style.display = "";
}
function normalizePayUri(uri) {
if (!uri) return "";
return String(uri)
.replace(/taler\.hacktivism\.ch:443/g, "taler.hacktivism.ch")
.replace(/:443\//g, "/")
.replace(/:443\?/g, "?");
}
/** Public: template → order → taler_pay_uri (no secrets). */
function createPayUri(productId) {
var url = templateHttps(productId);
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
cache: "no-store",
})
.then(function (r) {
if (!r.ok) throw new Error("template POST HTTP " + r.status);
return r.json();
})
.then(function (created) {
var oid = created.order_id;
var tok = created.token;
if (!oid || !tok) throw new Error("no order_id/token");
var statusUrl =
"https://" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/orders/" +
encodeURIComponent(oid) +
"?token=" +
encodeURIComponent(tok);
return fetch(statusUrl, { cache: "no-store" }).then(function (r) {
return r.text().then(function (t) {
var pay = "";
try {
var d = JSON.parse(t);
pay = d.taler_pay_uri || "";
} catch (e) {}
if (!pay) {
pay =
"taler://pay/" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/" +
oid +
"/?c=" +
tok;
}
return {
taler_pay_uri: normalizePayUri(pay),
order_id: oid,
token: tok,
status_https: statusUrl.replace(
"taler.hacktivism.ch:443",
"taler.hacktivism.ch"
),
template_https: url,
};
});
});
});
}
function showPay(productId, name, amount) {
var m = ensureModal();
if (document.body && document.body.getAttribute("data-shop-theme") === "bank") {
m.classList.add("bank-theme");
} else {
m.classList.remove("bank-theme");
}
document.getElementById("goa-pay-title").textContent = name || productId;
document.getElementById("goa-pay-amount").textContent = amount || "";
var status = document.getElementById("goa-pay-status");
status.textContent = "Preparing payment…";
status.className = "goa-pay-hint";
var open = document.getElementById("goa-pay-open");
open.removeAttribute("href");
open.classList.add("disabled");
open.onclick = null;
var payLink = document.getElementById("goa-pay-uri");
payLink.removeAttribute("href");
payLink.classList.add("disabled");
payLink.textContent = "taler:// URI";
document.getElementById("goa-pay-uri-https").href = templateHttps(productId);
document.getElementById("goa-pay-uri-https").textContent = "HTTPS";
var paytoLink = document.getElementById("goa-pay-payto");
paytoLink.href = SHOP_PAYTO;
paytoLink.textContent = "payto:// URI";
document.getElementById("goa-pay-payto-https").href = SHOP_PAYTO_HTTPS;
document.getElementById("goa-pay-payto-https").textContent = "HTTPS";
setLogo(document.getElementById("goa-pay-qr-pay-logo"));
// Reset settlement panel (collapsed until user expands)
var settleBody = document.getElementById("goa-pay-settle-body");
var settleBtn = document.getElementById("goa-pay-settle-toggle");
if (settleBody) settleBody.hidden = true;
if (settleBtn) settleBtn.setAttribute("aria-expanded", "false");
m.classList.remove("settle-open");
var paytoHost = document.getElementById("goa-pay-qr-payto");
if (paytoHost) paytoHost.innerHTML = "";
// Open first so layout exists, then paint wallet QR only
m.hidden = false;
m.classList.add("open");
var tpl = payTemplateUri(productId);
renderQr(
document.getElementById("goa-pay-qr-pay"),
tpl,
220
);
var payload = document.getElementById("goa-pay-qr-payload");
if (payload) {
payload.href = tpl;
payload.textContent = tpl;
}
createPayUri(productId)
.then(function (info) {
var pay = info.taler_pay_uri;
open.href = pay;
open.classList.remove("disabled");
open.textContent = "Open payment link →";
payLink.href = pay;
payLink.classList.remove("disabled");
payLink.textContent = "taler:// URI";
var uh = document.getElementById("goa-pay-uri-https");
uh.href = info.status_https || info.template_https;
uh.textContent = "HTTPS";
// Live unpaid taler://pay (fresh order each open)
renderQr(document.getElementById("goa-pay-qr-pay"), pay, 220);
if (payload) {
payload.href = pay;
payload.textContent = pay;
}
status.textContent = "Ready — scan in wallet";
})
.catch(function (err) {
status.textContent =
"Payment setup failed: " + (err && err.message ? err.message : err);
status.className = "goa-pay-hint err";
open.textContent = "Retry";
open.onclick = function (e) {
e.preventDefault();
showPay(productId, name, amount);
};
});
}
function bind() {
document.querySelectorAll("[data-product]").forEach(function (el) {
el.addEventListener("click", function (e) {
e.preventDefault();
var id = el.getAttribute("data-product");
var nameEl = el.querySelector(".name");
var priceEl = el.querySelector(".price");
showPay(
id,
nameEl ? nameEl.textContent.trim() : id,
priceEl ? priceEl.textContent.trim() : ""
);
});
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bind);
} else {
bind();
}
})();

View file

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>No Formal Terms · GOA Bank</title>
<style>
:root { color-scheme: dark light; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 40rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
line-height: 1.5; color: #e8e6e3; background: #1a1520;
}
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
h2 { font-size: 1.05rem; margin: 1.4rem 0 0.5rem; color: #e8c878; }
p, li { font-size: 0.98rem; }
ul { padding-left: 1.2rem; }
code, a { color: #5eead4; }
a { text-decoration: none; }
a:hover { text-decoration: underline; }
.badge {
display: inline-block; font-size: 0.72rem; font-weight: 700;
letter-spacing: 0.06em; text-transform: uppercase;
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
}
.cur {
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
}
.cur strong { display: block; font-size: 1.05rem; margin-bottom: 0.25rem; color: #5eead4; }
.muted { color: #a39e98; font-size: 0.88rem; }
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
</style>
</head>
<body>
<div class="badge">bank.hacktivism.ch · GOA</div>
<h1>No Formal Terms · GOA Exploration Bank</h1>
<p>This is a <strong>self-hosted GNU Taler regional bank</strong> at
<code>bank.hacktivism.ch</code> (hacktivism.ch) for the explorational currency
<strong>GOA</strong>.</p>
<p><strong>No formal terms of service</strong> apply beyond this short notice.</p>
<div class="cur">
<strong>GOA · explorational</strong>
Not legal tender. No guaranteed real-world value, redemption, or convertibility.
Accounts and demo withdraws are for exploration of the GNU Taler stack only.
</div>
<h2>By using this bank you acknowledge</h2>
<ul>
<li>GOA is for exploration and testing only.</li>
<li>There is no guaranteed availability, support, or uptime.</li>
<li>Operators may reset accounts, balances, or configuration without notice.</li>
<li>Registration data and wire-style transfers may identify account holders to operators.</li>
<li>Software is provided as-is, without warranty.</li>
</ul>
<p>If you do not agree, do not use this bank.</p>
<h2>Related</h2>
<ul>
<li><a href="https://bank.hacktivism.ch/intro/">Bank intro</a></li>
<li><a href="https://exchange.hacktivism.ch/terms">Exchange terms (GOA)</a></li>
<li><a href="https://taler.hacktivism.ch/terms">Merchant terms</a></li>
<li><a href="https://bank.hacktivism.ch/privacy">Bank privacy</a></li>
<li><a href="https://bank.hacktivism.ch/webui/">Bank UI</a></li>
</ul>
<h2>Privacy</h2>
<p class="muted">Processing under Swiss FADP (revDSG). What data is retained
(accounts, ledger, logs, …) is listed on
<a href="https://bank.hacktivism.ch/privacy">/privacy</a>.</p>
<footer class="muted">bank.hacktivism.ch · landing terms</footer>
</body>
</html>

28
configs/bonfire/README.md Normal file
View file

@ -0,0 +1,28 @@
# Bonfire — bonfire.hacktivism.ch
| Item | Value |
|------|--------|
| Live | `/home/hernani/koopa-bonfire/` |
| Image | `bonfirenetworks/bonfire:1.0.5-social-amd64` |
| Port | **9021** → Caddy |
| Ground zero | `2026/2026-07-09--bonfire-ground-zero.md` |
Secrets: `koopa-admin-secrets/…/koopa-bonfire/{.env,users.env}`.
**Upstream:** [Running your own](https://docs.bonfirenetworks.org/running-your-own.html), [Hosting guide](https://docs.bonfirenetworks.org/deploy.html) (we use podman-compose, not Co-op Cloud).
## Branding
Hacktivism theme + hacktivism magician logo. `scripts/bonfire/apply-branding.sh`.
Logo bind-mount: `2026/2026-07-11--bonfire-logo-not-served.md`.
## Boot
```bash
scripts/bonfire/install-systemd.sh
```
Units: `container-koopa-bonfire-db`, `container-koopa-bonfire`, `gitbot-mirror`.
Needs `loginctl enable-linger hernani`. Run `podman-compose up -d` once before first boot.
Public feeds: `public-feeds.md`.

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Wizard flying on magic wand, blue robe">
<!-- Original Kamek-inspired mark; not Nintendo art. No filters/transforms for clean raster. -->
<!-- motion streaks -->
<line x1="4" y1="64" x2="22" y2="64" stroke="#5eead4" stroke-width="2.5" stroke-linecap="round" opacity="0.55"/>
<line x1="6" y1="74" x2="20" y2="74" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<line x1="8" y1="54" x2="20" y2="54" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<!-- ground shadow -->
<ellipse cx="64" cy="118" rx="30" ry="5" fill="#000" opacity="0.2"/>
<!-- ===== MAGIC WAND (mount) — diagonal, thick gold shaft ===== -->
<!-- sits under the rider from lower-left to upper-right -->
<line x1="6" y1="100" x2="108" y2="72" stroke="#92400e" stroke-width="11" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#e8a838" stroke-width="7" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<!-- wand jewel (front tip) -->
<circle cx="112" cy="70" r="13" fill="#f59e0b" stroke="#fff7ed" stroke-width="3"/>
<circle cx="108" cy="66" r="4.5" fill="#fef9c3"/>
<!-- sparkles at tip -->
<polygon points="120,54 121.6,58.5 126,60 121.6,61.5 120,66 118.4,61.5 114,60 118.4,58.5" fill="#fef08a"/>
<polygon points="102,56 103,59 106,60 103,61 102,64 101,61 98,60 101,59" fill="#5eead4"/>
<circle cx="118" cy="78" r="2" fill="#bae6fd"/>
<!-- ===== RIDER on top of wand ===== -->
<!-- blue cape fluttering left -->
<path d="M38 78 C18 82, 10 98, 8 110 C22 98, 34 96, 40 92 Z" fill="#1e3a8a"/>
<path d="M40 80 C22 88, 14 104, 12 112 C28 100, 36 98, 42 94 Z" fill="#1d4ed8"/>
<!-- BLUE ROBE (body sitting ON shaft) -->
<ellipse cx="54" cy="86" rx="28" ry="22" fill="#1d4ed8"/>
<ellipse cx="54" cy="84" rx="24" ry="18" fill="#2563eb"/>
<!-- white trim -->
<path d="M32 74 Q54 94 76 74" fill="none" stroke="#f8fafc" stroke-width="4" stroke-linecap="round"/>
<path d="M36 82 Q54 98 72 82" fill="none" stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<!-- green shell -->
<ellipse cx="66" cy="92" rx="13" ry="11" fill="#16a34a"/>
<ellipse cx="66" cy="92" rx="6" ry="5" fill="#86efac" opacity="0.75"/>
<!-- legs over the wand -->
<ellipse cx="42" cy="100" rx="10" ry="7" fill="#1e40af"/>
<ellipse cx="60" cy="102" rx="10" ry="7" fill="#1e40af"/>
<!-- yellow feet hanging past shaft -->
<ellipse cx="38" cy="106" rx="7" ry="4.5" fill="#fbbf24"/>
<ellipse cx="64" cy="108" rx="7" ry="4.5" fill="#fbbf24"/>
<!-- left arm forward -->
<ellipse cx="30" cy="82" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="24" cy="78" r="5.5" fill="#fbbf24"/>
<!-- right arm up -->
<ellipse cx="74" cy="74" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="80" cy="68" r="5.5" fill="#fbbf24"/>
<circle cx="82" cy="60" r="5" fill="#38bdf8"/>
<circle cx="81" cy="58" r="1.8" fill="#e0f2fe"/>
<!-- ===== HEAD ===== -->
<ellipse cx="54" cy="50" rx="26" ry="24" fill="#fbbf24"/>
<ellipse cx="54" cy="53" rx="21" ry="17" fill="#fcd34d"/>
<ellipse cx="34" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<ellipse cx="74" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<!-- glasses -->
<circle cx="42" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<circle cx="66" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<line x1="52" y1="50" x2="56" y2="50" stroke="#0f172a" stroke-width="3"/>
<line x1="32" y1="47" x2="24" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<line x1="76" y1="47" x2="84" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="44" cy="50" r="3" fill="#0f172a"/>
<circle cx="68" cy="50" r="3" fill="#0f172a"/>
<circle cx="45.2" cy="48.8" r="1.1" fill="#fff"/>
<circle cx="69.2" cy="48.8" r="1.1" fill="#fff"/>
<!-- snout + mustache -->
<ellipse cx="54" cy="62" rx="8" ry="5" fill="#f59e0b"/>
<path d="M36 64 Q46 74 54 70 Q62 74 72 64 Q64 72 54 74 Q44 72 36 64 Z" fill="#f8fafc"/>
<path d="M44 66 Q54 74 64 66" fill="none" stroke="#92400e" stroke-width="2.2" stroke-linecap="round"/>
<!-- BLUE pointed hat -->
<path d="M28 44 L54 2 L80 44 Z" fill="#1d4ed8"/>
<path d="M36 44 L54 10 L72 44 Z" fill="#3b82f6" opacity="0.5"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="#1e40af"/>
<ellipse cx="54" cy="42.5" rx="26" ry="5" fill="#2563eb"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="none" stroke="#f8fafc" stroke-width="2.2"/>
<polygon points="54,12 55.6,16.8 60.5,18.4 55.6,20 54,24.8 52.4,20 47.5,18.4 52.4,16.8" fill="#f8fafc"/>
<polygon points="42,22 43.1,25.2 46.2,26.4 43.1,27.6 42,30.8 40.9,27.6 37.8,26.4 40.9,25.2" fill="#fef08a"/>
<circle cx="54" cy="4" r="6" fill="#f8fafc"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Wizard flying on magic wand, blue robe">
<!-- Original Kamek-inspired mark; not Nintendo art. No filters/transforms for clean raster. -->
<!-- motion streaks -->
<line x1="4" y1="64" x2="22" y2="64" stroke="#5eead4" stroke-width="2.5" stroke-linecap="round" opacity="0.55"/>
<line x1="6" y1="74" x2="20" y2="74" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<line x1="8" y1="54" x2="20" y2="54" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<!-- ground shadow -->
<ellipse cx="64" cy="118" rx="30" ry="5" fill="#000" opacity="0.2"/>
<!-- ===== MAGIC WAND (mount) — diagonal, thick gold shaft ===== -->
<!-- sits under the rider from lower-left to upper-right -->
<line x1="6" y1="100" x2="108" y2="72" stroke="#92400e" stroke-width="11" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#e8a838" stroke-width="7" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<!-- wand jewel (front tip) -->
<circle cx="112" cy="70" r="13" fill="#f59e0b" stroke="#fff7ed" stroke-width="3"/>
<circle cx="108" cy="66" r="4.5" fill="#fef9c3"/>
<!-- sparkles at tip -->
<polygon points="120,54 121.6,58.5 126,60 121.6,61.5 120,66 118.4,61.5 114,60 118.4,58.5" fill="#fef08a"/>
<polygon points="102,56 103,59 106,60 103,61 102,64 101,61 98,60 101,59" fill="#5eead4"/>
<circle cx="118" cy="78" r="2" fill="#bae6fd"/>
<!-- ===== RIDER on top of wand ===== -->
<!-- blue cape fluttering left -->
<path d="M38 78 C18 82, 10 98, 8 110 C22 98, 34 96, 40 92 Z" fill="#1e3a8a"/>
<path d="M40 80 C22 88, 14 104, 12 112 C28 100, 36 98, 42 94 Z" fill="#1d4ed8"/>
<!-- BLUE ROBE (body sitting ON shaft) -->
<ellipse cx="54" cy="86" rx="28" ry="22" fill="#1d4ed8"/>
<ellipse cx="54" cy="84" rx="24" ry="18" fill="#2563eb"/>
<!-- white trim -->
<path d="M32 74 Q54 94 76 74" fill="none" stroke="#f8fafc" stroke-width="4" stroke-linecap="round"/>
<path d="M36 82 Q54 98 72 82" fill="none" stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<!-- green shell -->
<ellipse cx="66" cy="92" rx="13" ry="11" fill="#16a34a"/>
<ellipse cx="66" cy="92" rx="6" ry="5" fill="#86efac" opacity="0.75"/>
<!-- legs over the wand -->
<ellipse cx="42" cy="100" rx="10" ry="7" fill="#1e40af"/>
<ellipse cx="60" cy="102" rx="10" ry="7" fill="#1e40af"/>
<!-- yellow feet hanging past shaft -->
<ellipse cx="38" cy="106" rx="7" ry="4.5" fill="#fbbf24"/>
<ellipse cx="64" cy="108" rx="7" ry="4.5" fill="#fbbf24"/>
<!-- left arm forward -->
<ellipse cx="30" cy="82" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="24" cy="78" r="5.5" fill="#fbbf24"/>
<!-- right arm up -->
<ellipse cx="74" cy="74" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="80" cy="68" r="5.5" fill="#fbbf24"/>
<circle cx="82" cy="60" r="5" fill="#38bdf8"/>
<circle cx="81" cy="58" r="1.8" fill="#e0f2fe"/>
<!-- ===== HEAD ===== -->
<ellipse cx="54" cy="50" rx="26" ry="24" fill="#fbbf24"/>
<ellipse cx="54" cy="53" rx="21" ry="17" fill="#fcd34d"/>
<ellipse cx="34" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<ellipse cx="74" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<!-- glasses -->
<circle cx="42" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<circle cx="66" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<line x1="52" y1="50" x2="56" y2="50" stroke="#0f172a" stroke-width="3"/>
<line x1="32" y1="47" x2="24" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<line x1="76" y1="47" x2="84" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="44" cy="50" r="3" fill="#0f172a"/>
<circle cx="68" cy="50" r="3" fill="#0f172a"/>
<circle cx="45.2" cy="48.8" r="1.1" fill="#fff"/>
<circle cx="69.2" cy="48.8" r="1.1" fill="#fff"/>
<!-- snout + mustache -->
<ellipse cx="54" cy="62" rx="8" ry="5" fill="#f59e0b"/>
<path d="M36 64 Q46 74 54 70 Q62 74 72 64 Q64 72 54 74 Q44 72 36 64 Z" fill="#f8fafc"/>
<path d="M44 66 Q54 74 64 66" fill="none" stroke="#92400e" stroke-width="2.2" stroke-linecap="round"/>
<!-- BLUE pointed hat -->
<path d="M28 44 L54 2 L80 44 Z" fill="#1d4ed8"/>
<path d="M36 44 L54 10 L72 44 Z" fill="#3b82f6" opacity="0.5"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="#1e40af"/>
<ellipse cx="54" cy="42.5" rx="26" ry="5" fill="#2563eb"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="none" stroke="#f8fafc" stroke-width="2.2"/>
<polygon points="54,12 55.6,16.8 60.5,18.4 55.6,20 54,24.8 52.4,20 47.5,18.4 52.4,16.8" fill="#f8fafc"/>
<polygon points="42,22 43.1,25.2 46.2,26.4 43.1,27.6 42,30.8 40.9,27.6 37.8,26.4 40.9,25.2" fill="#fef08a"/>
<circle cx="54" cy="4" r="6" fill="#f8fafc"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,55 @@
# koopa-bonfire — ground zero (host Caddy → :9021)
services:
web:
image: docker.io/bonfirenetworks/bonfire:1.0.5-social-amd64
container_name: koopa-bonfire
restart: unless-stopped
env_file: [.env]
environment:
POSTGRES_HOST: db
HOSTNAME: bonfire.hacktivism.ch
PUBLIC_PORT: "443"
SERVER_PORT: "4000"
DB_MIGRATE_INDEXES_CONCURRENTLY: "false"
MIX_ENV: prod
ports:
- "9021:4000"
volumes:
- ./data/uploads:/opt/app/data/uploads
# Bind-mount into priv/static/images (Bonfire serves /images/* from there).
- ./data/branding/logo.svg:/opt/app/lib/bonfire-1.0.5-social/priv/static/images/hacktivism-logo.svg:ro
- ./data/branding/favicon.svg:/opt/app/lib/bonfire-1.0.5-social/priv/static/images/hacktivism-favicon.svg:ro
depends_on:
db:
condition: service_healthy
labels:
org.hacktivism.service: bonfire
org.hacktivism.host_port: "9021"
org.hacktivism.site: bonfire.hacktivism.ch
org.hacktivism.managed_by: koopa-admin
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:4000/ || exit 1"]
interval: 30s
timeout: 10s
retries: 15
start_period: 180s
db:
image: docker.io/library/postgres:15-alpine
container_name: koopa-bonfire-db
restart: unless-stopped
env_file: [.env]
environment:
POSTGRES_USER: postgres
POSTGRES_DB: bonfire_db
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d bonfire_db"]
interval: 5s
timeout: 5s
retries: 20
start_period: 10s
volumes:
db-data:

View file

@ -0,0 +1,21 @@
# user systemd - Postgres for Bonfire
[Unit]
Description=Bonfire Postgres (koopa-bonfire-db)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
RestartSec=15
TimeoutStartSec=300
TimeoutStopSec=70
WorkingDirectory=/home/hernani/koopa-bonfire
ExecStart=/usr/bin/podman-compose up -d db
ExecStop=/usr/bin/podman stop -t 30 koopa-bonfire-db
Type=oneshot
RemainAfterExit=yes
[Install]
WantedBy=default.target

View file

@ -0,0 +1,21 @@
# user systemd - Bonfire web
[Unit]
Description=Bonfire web (koopa-bonfire)
Wants=network-online.target container-koopa-bonfire-db.service
After=network-online.target container-koopa-bonfire-db.service
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
RestartSec=20
TimeoutStartSec=300
TimeoutStopSec=120
WorkingDirectory=/home/hernani/koopa-bonfire
ExecStart=/usr/bin/podman-compose up -d web
ExecStop=/usr/bin/podman stop -t 30 koopa-bonfire
Type=oneshot
RemainAfterExit=yes
[Install]
WantedBy=default.target

View file

@ -0,0 +1,18 @@
# user systemd - git.ngi-0.eu -> Bonfire gitbot
[Unit]
Description=Bonfire gitbot (git.ngi-0.eu poll)
Wants=network-online.target container-koopa-bonfire.service
After=network-online.target container-koopa-bonfire.service
[Service]
Type=simple
Restart=always
RestartSec=30
WorkingDirectory=/home/hernani/koopa-bonfire
ExecStart=/usr/bin/python3 /home/hernani/koopa-bonfire/bin/gitbot-mirror.py --loop
StandardOutput=append:/home/hernani/koopa-bonfire/gitbot.log
StandardError=append:/home/hernani/koopa-bonfire/gitbot.log
[Install]
WantedBy=default.target

View file

@ -0,0 +1,11 @@
# Bonfire public feeds
Posts land in the author outbox first. Copy to local/internet feeds or guests see stale timelines.
**Official:** [Feed structure](https://docs.bonfirenetworks.org/feed_structure.html) (`FeedPublish`).
No official republish recipe — SQL + gitbot below are ops workarounds.
- One-off SQL: `scripts/bonfire/publish-outbox-to-public.sql`
- Gitbot: republish every cycle (`gitbot-mirror.py`)
Check: https://bonfire.hacktivism.ch/feed/local (not only `/`).

View file

@ -0,0 +1,29 @@
{
"name": "hacktivism",
"palette": "exchange-dark (git.hacktivism.ch / forgejo theme-hacktivism)",
"colors": {
"color-base-100": "#1a1410",
"color-base-200": "#221c16",
"color-base-300": "#2a2018",
"color-base-content": "#fff6e8",
"color-primary": "#e8a838",
"color-primary-content": "#1a1410",
"color-secondary": "#3d3128",
"color-secondary-content": "#fff6e8",
"color-accent": "#3ecfbf",
"color-accent-content": "#0e1c1e",
"color-neutral": "#14110e",
"color-neutral-content": "#ebe0d0",
"color-info": "#2563eb",
"color-info-content": "#ffffff",
"color-success": "#16a34a",
"color-success-content": "#ffffff",
"color-warning": "#f0d090",
"color-warning-content": "#1a1410",
"color-error": "#b91c1c",
"color-error-content": "#ffffff",
"radius-box": "0.875rem",
"radius-field": "0.5rem",
"radius-selector": "0.75rem"
}
}

185
configs/caddy/Caddyfile Normal file
View file

@ -0,0 +1,185 @@
# 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 | | 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
}
}
# 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
}
}
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
}
}
# 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}
}
}
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://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
}
handle {
redir https://{host}{uri} permanent
}
}

View file

@ -0,0 +1,24 @@
# LEGACY — historical Taler-only Caddy sketch (merchant backend :8081).
# Do NOT use on koopa. Current multi-vhost config:
# configs/caddy/Caddyfile (canonical)
# host/caddy/Caddyfile (same content)
#
# Kept only as archaeology of the early taler.hacktivism.ch TLS layout.
# Multi-domain reverse proxy (TLS terminate on host)
# Backend at the time: container nginx TLS on 8081
{
# no global auto-HTTPS email yet; per-site manual certs
auto_https disable_redirects
}
taler.hacktivism.ch {
tls /etc/caddy/certs/taler.hacktivism.ch/fullchain.pem /etc/caddy/certs/taler.hacktivism.ch/privkey.pem
reverse_proxy https://127.0.0.1:8081 {
transport http {
tls_insecure_skip_verify
}
}
}

9
configs/caddy/README.md Normal file
View file

@ -0,0 +1,9 @@
# Caddy config mirrors
| File | Role |
|------|------|
| **`Caddyfile`** | Multi-vhost config mirror (`/etc/caddy/Caddyfile` on host) |
| `git.hacktivism.ch.caddy` | Site-block snippet for Forgejo |
| `Caddyfile.taler-host` | **Legacy** Taler-only sketch — do not deploy |
Also: `host/caddy/Caddyfile` (same as `Caddyfile` here).

View file

@ -0,0 +1,15 @@
# 9024 forgejo HTTP (SSH :9200 host-direct, not via Caddy)
# Live: merged into /etc/caddy/Caddyfile (see also full Caddyfile mirror)
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
}
}
}

View file

@ -0,0 +1,21 @@
# Castopod — castopod.hacktivism.ch
| Item | Value |
|------|--------|
| Live | `/home/hernani/koopa-castopod/` |
| Compose | `compose.yml` (mirror, no secrets) |
| Port | **9020** → Caddy |
| Site | https://castopod.hacktivism.ch/ |
Secrets: `koopa-admin-secrets/…/koopa-castopod/` and host `~/koopa-castopod/{.env,users.env}`.
```bash
cd ~/koopa-castopod && set -a && source .env && set +a && podman-compose up -d
scripts/castopod/install-systemd.sh
scripts/castopod/apply-branding.sh
```
Daylogs: `2026/2026-07-09--castopod-content.md` (stack/accounts),
`2026/2026-07-13--castopod-boot-branding.md` (boot/branding).
Podcast content is **not** versioned in admin-log.

View file

@ -0,0 +1,64 @@
/*
* Optional warm-dark overlay for castopod.hacktivism.ch
* NOT an official Castopod theme Castopod only documents 6 accent colors
* (pine, crimson, lake, amber, jacaranda, onyx). See:
* https://docs.castopod.org/main/en/user-guide/instance/settings/
*
* Palette matches git.hacktivism.ch / bonfire (exchange-dark):
* body #1a1410, cream text, gold accent #e8a838, teal #3ecfbf.
* Prefer official theme "amber" first; this CSS deepens the page chrome.
*/
:root,
.theme-amber,
.theme-pine,
.theme-onyx {
--color-accent-base: 38 78% 56% !important; /* ~#e8a838 */
--color-accent-hover: 38 72% 48% !important;
--color-accent-muted: 38 60% 32% !important;
--color-accent-contrast: 28 22% 8% !important;
--color-heading-foreground: 40 80% 75% !important;
--color-heading-background: 28 22% 14% !important;
--color-background-elevated: 28 18% 14% !important;
--color-background-base: 28 22% 8% !important;
--color-background-navigation: 28 20% 10% !important;
--color-background-header: 28 22% 9% !important;
--color-background-highlight: 28 18% 16% !important;
--color-background-backdrop: 0 0% 0% !important;
--color-border-subtle: 32 18% 28% !important;
--color-border-contrast: 40 50% 70% !important;
--color-border-navigation: 32 18% 28% !important;
--color-text-base: 36 80% 95% !important; /* ~#fff6e8 */
--color-text-muted: 32 20% 72% !important;
}
body {
background-color: #1a1410 !important;
background-image:
radial-gradient(ellipse 90% 55% at 50% 108%, rgba(26, 107, 110, 0.35) 0%, transparent 55%),
radial-gradient(circle 420px at 12% 18%, rgba(232, 168, 56, 0.22) 0%, transparent 62%),
linear-gradient(165deg, #2c1e14 0%, #1a1410 38%, #12181a 72%, #0e1c1e 100%) !important;
background-attachment: fixed !important;
color: #fff6e8 !important;
}
a {
color: #f0c86a;
}
a:hover {
color: #5eead4;
}
/* Primary-ish buttons / chips that use solid white on accent */
.bg-white {
background-color: rgba(42, 32, 24, 0.92) !important;
color: #fff6e8 !important;
border: 1px solid rgba(232, 168, 56, 0.28);
}
/* Footer */
footer,
.page-footer {
border-top: 1px solid rgba(232, 168, 56, 0.22);
color: #c9b8a0 !important;
}

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Wizard flying on magic wand, blue robe">
<!-- Original Kamek-inspired mark; not Nintendo art. No filters/transforms for clean raster. -->
<!-- motion streaks -->
<line x1="4" y1="64" x2="22" y2="64" stroke="#5eead4" stroke-width="2.5" stroke-linecap="round" opacity="0.55"/>
<line x1="6" y1="74" x2="20" y2="74" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<line x1="8" y1="54" x2="20" y2="54" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<!-- ground shadow -->
<ellipse cx="64" cy="118" rx="30" ry="5" fill="#000" opacity="0.2"/>
<!-- ===== MAGIC WAND (mount) — diagonal, thick gold shaft ===== -->
<!-- sits under the rider from lower-left to upper-right -->
<line x1="6" y1="100" x2="108" y2="72" stroke="#92400e" stroke-width="11" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#e8a838" stroke-width="7" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<!-- wand jewel (front tip) -->
<circle cx="112" cy="70" r="13" fill="#f59e0b" stroke="#fff7ed" stroke-width="3"/>
<circle cx="108" cy="66" r="4.5" fill="#fef9c3"/>
<!-- sparkles at tip -->
<polygon points="120,54 121.6,58.5 126,60 121.6,61.5 120,66 118.4,61.5 114,60 118.4,58.5" fill="#fef08a"/>
<polygon points="102,56 103,59 106,60 103,61 102,64 101,61 98,60 101,59" fill="#5eead4"/>
<circle cx="118" cy="78" r="2" fill="#bae6fd"/>
<!-- ===== RIDER on top of wand ===== -->
<!-- blue cape fluttering left -->
<path d="M38 78 C18 82, 10 98, 8 110 C22 98, 34 96, 40 92 Z" fill="#1e3a8a"/>
<path d="M40 80 C22 88, 14 104, 12 112 C28 100, 36 98, 42 94 Z" fill="#1d4ed8"/>
<!-- BLUE ROBE (body sitting ON shaft) -->
<ellipse cx="54" cy="86" rx="28" ry="22" fill="#1d4ed8"/>
<ellipse cx="54" cy="84" rx="24" ry="18" fill="#2563eb"/>
<!-- white trim -->
<path d="M32 74 Q54 94 76 74" fill="none" stroke="#f8fafc" stroke-width="4" stroke-linecap="round"/>
<path d="M36 82 Q54 98 72 82" fill="none" stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<!-- green shell -->
<ellipse cx="66" cy="92" rx="13" ry="11" fill="#16a34a"/>
<ellipse cx="66" cy="92" rx="6" ry="5" fill="#86efac" opacity="0.75"/>
<!-- legs over the wand -->
<ellipse cx="42" cy="100" rx="10" ry="7" fill="#1e40af"/>
<ellipse cx="60" cy="102" rx="10" ry="7" fill="#1e40af"/>
<!-- yellow feet hanging past shaft -->
<ellipse cx="38" cy="106" rx="7" ry="4.5" fill="#fbbf24"/>
<ellipse cx="64" cy="108" rx="7" ry="4.5" fill="#fbbf24"/>
<!-- left arm forward -->
<ellipse cx="30" cy="82" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="24" cy="78" r="5.5" fill="#fbbf24"/>
<!-- right arm up -->
<ellipse cx="74" cy="74" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="80" cy="68" r="5.5" fill="#fbbf24"/>
<circle cx="82" cy="60" r="5" fill="#38bdf8"/>
<circle cx="81" cy="58" r="1.8" fill="#e0f2fe"/>
<!-- ===== HEAD ===== -->
<ellipse cx="54" cy="50" rx="26" ry="24" fill="#fbbf24"/>
<ellipse cx="54" cy="53" rx="21" ry="17" fill="#fcd34d"/>
<ellipse cx="34" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<ellipse cx="74" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<!-- glasses -->
<circle cx="42" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<circle cx="66" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<line x1="52" y1="50" x2="56" y2="50" stroke="#0f172a" stroke-width="3"/>
<line x1="32" y1="47" x2="24" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<line x1="76" y1="47" x2="84" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="44" cy="50" r="3" fill="#0f172a"/>
<circle cx="68" cy="50" r="3" fill="#0f172a"/>
<circle cx="45.2" cy="48.8" r="1.1" fill="#fff"/>
<circle cx="69.2" cy="48.8" r="1.1" fill="#fff"/>
<!-- snout + mustache -->
<ellipse cx="54" cy="62" rx="8" ry="5" fill="#f59e0b"/>
<path d="M36 64 Q46 74 54 70 Q62 74 72 64 Q64 72 54 74 Q44 72 36 64 Z" fill="#f8fafc"/>
<path d="M44 66 Q54 74 64 66" fill="none" stroke="#92400e" stroke-width="2.2" stroke-linecap="round"/>
<!-- BLUE pointed hat -->
<path d="M28 44 L54 2 L80 44 Z" fill="#1d4ed8"/>
<path d="M36 44 L54 10 L72 44 Z" fill="#3b82f6" opacity="0.5"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="#1e40af"/>
<ellipse cx="54" cy="42.5" rx="26" ry="5" fill="#2563eb"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="none" stroke="#f8fafc" stroke-width="2.2"/>
<polygon points="54,12 55.6,16.8 60.5,18.4 55.6,20 54,24.8 52.4,20 47.5,18.4 52.4,16.8" fill="#f8fafc"/>
<polygon points="42,22 43.1,25.2 46.2,26.4 43.1,27.6 42,30.8 40.9,27.6 37.8,26.4 40.9,25.2" fill="#fef08a"/>
<circle cx="54" cy="4" r="6" fill="#f8fafc"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Wizard flying on magic wand, blue robe">
<!-- Original Kamek-inspired mark; not Nintendo art. No filters/transforms for clean raster. -->
<!-- motion streaks -->
<line x1="4" y1="64" x2="22" y2="64" stroke="#5eead4" stroke-width="2.5" stroke-linecap="round" opacity="0.55"/>
<line x1="6" y1="74" x2="20" y2="74" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<line x1="8" y1="54" x2="20" y2="54" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<!-- ground shadow -->
<ellipse cx="64" cy="118" rx="30" ry="5" fill="#000" opacity="0.2"/>
<!-- ===== MAGIC WAND (mount) — diagonal, thick gold shaft ===== -->
<!-- sits under the rider from lower-left to upper-right -->
<line x1="6" y1="100" x2="108" y2="72" stroke="#92400e" stroke-width="11" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#e8a838" stroke-width="7" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<!-- wand jewel (front tip) -->
<circle cx="112" cy="70" r="13" fill="#f59e0b" stroke="#fff7ed" stroke-width="3"/>
<circle cx="108" cy="66" r="4.5" fill="#fef9c3"/>
<!-- sparkles at tip -->
<polygon points="120,54 121.6,58.5 126,60 121.6,61.5 120,66 118.4,61.5 114,60 118.4,58.5" fill="#fef08a"/>
<polygon points="102,56 103,59 106,60 103,61 102,64 101,61 98,60 101,59" fill="#5eead4"/>
<circle cx="118" cy="78" r="2" fill="#bae6fd"/>
<!-- ===== RIDER on top of wand ===== -->
<!-- blue cape fluttering left -->
<path d="M38 78 C18 82, 10 98, 8 110 C22 98, 34 96, 40 92 Z" fill="#1e3a8a"/>
<path d="M40 80 C22 88, 14 104, 12 112 C28 100, 36 98, 42 94 Z" fill="#1d4ed8"/>
<!-- BLUE ROBE (body sitting ON shaft) -->
<ellipse cx="54" cy="86" rx="28" ry="22" fill="#1d4ed8"/>
<ellipse cx="54" cy="84" rx="24" ry="18" fill="#2563eb"/>
<!-- white trim -->
<path d="M32 74 Q54 94 76 74" fill="none" stroke="#f8fafc" stroke-width="4" stroke-linecap="round"/>
<path d="M36 82 Q54 98 72 82" fill="none" stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<!-- green shell -->
<ellipse cx="66" cy="92" rx="13" ry="11" fill="#16a34a"/>
<ellipse cx="66" cy="92" rx="6" ry="5" fill="#86efac" opacity="0.75"/>
<!-- legs over the wand -->
<ellipse cx="42" cy="100" rx="10" ry="7" fill="#1e40af"/>
<ellipse cx="60" cy="102" rx="10" ry="7" fill="#1e40af"/>
<!-- yellow feet hanging past shaft -->
<ellipse cx="38" cy="106" rx="7" ry="4.5" fill="#fbbf24"/>
<ellipse cx="64" cy="108" rx="7" ry="4.5" fill="#fbbf24"/>
<!-- left arm forward -->
<ellipse cx="30" cy="82" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="24" cy="78" r="5.5" fill="#fbbf24"/>
<!-- right arm up -->
<ellipse cx="74" cy="74" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="80" cy="68" r="5.5" fill="#fbbf24"/>
<circle cx="82" cy="60" r="5" fill="#38bdf8"/>
<circle cx="81" cy="58" r="1.8" fill="#e0f2fe"/>
<!-- ===== HEAD ===== -->
<ellipse cx="54" cy="50" rx="26" ry="24" fill="#fbbf24"/>
<ellipse cx="54" cy="53" rx="21" ry="17" fill="#fcd34d"/>
<ellipse cx="34" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<ellipse cx="74" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<!-- glasses -->
<circle cx="42" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<circle cx="66" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<line x1="52" y1="50" x2="56" y2="50" stroke="#0f172a" stroke-width="3"/>
<line x1="32" y1="47" x2="24" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<line x1="76" y1="47" x2="84" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="44" cy="50" r="3" fill="#0f172a"/>
<circle cx="68" cy="50" r="3" fill="#0f172a"/>
<circle cx="45.2" cy="48.8" r="1.1" fill="#fff"/>
<circle cx="69.2" cy="48.8" r="1.1" fill="#fff"/>
<!-- snout + mustache -->
<ellipse cx="54" cy="62" rx="8" ry="5" fill="#f59e0b"/>
<path d="M36 64 Q46 74 54 70 Q62 74 72 64 Q64 72 54 74 Q44 72 36 64 Z" fill="#f8fafc"/>
<path d="M44 66 Q54 74 64 66" fill="none" stroke="#92400e" stroke-width="2.2" stroke-linecap="round"/>
<!-- BLUE pointed hat -->
<path d="M28 44 L54 2 L80 44 Z" fill="#1d4ed8"/>
<path d="M36 44 L54 10 L72 44 Z" fill="#3b82f6" opacity="0.5"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="#1e40af"/>
<ellipse cx="54" cy="42.5" rx="26" ry="5" fill="#2563eb"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="none" stroke="#f8fafc" stroke-width="2.2"/>
<polygon points="54,12 55.6,16.8 60.5,18.4 55.6,20 54,24.8 52.4,20 47.5,18.4 52.4,16.8" fill="#f8fafc"/>
<polygon points="42,22 43.1,25.2 46.2,26.4 43.1,27.6 42,30.8 40.9,27.6 37.8,26.4 40.9,25.2" fill="#fef08a"/>
<circle cx="54" cy="4" r="6" fill="#f8fafc"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,81 @@
# koopa-castopod — Castopod behind host Caddy on :9020
# Docs: https://docs.castopod.org/main/en/getting-started/docker/
services:
castopod:
image: docker.io/castopod/castopod:1
container_name: koopa-castopod
volumes:
- castopod-media:/app/public/media
# optional branding host dir (logo + CSS overlay); safe if empty
- ./branding:/branding:ro
environment:
MYSQL_DATABASE: castopod
MYSQL_USER: castopod
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
CP_BASEURL: ${CP_BASEURL}
CP_ANALYTICS_SALT: ${CP_ANALYTICS_SALT}
CP_CACHE_HANDLER: redis
CP_REDIS_HOST: redis
CP_REDIS_PASSWORD: ${MYSQL_PASSWORD}
networks:
- castopod-app
- castopod-db
ports:
- "9020:8080"
labels:
org.hacktivism.service: castopod
org.hacktivism.host_port: "9020"
org.hacktivism.site: castopod.hacktivism.ch
org.hacktivism.managed_by: koopa-admin
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
depends_on:
mariadb:
condition: service_healthy
redis:
condition: service_started
mariadb:
image: docker.io/library/mariadb:11
container_name: koopa-castopod-mariadb
networks:
- castopod-db
volumes:
- castopod-db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: castopod
MYSQL_USER: castopod
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
restart: unless-stopped
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 20s
interval: 10s
timeout: 5s
retries: 5
redis:
image: docker.io/library/redis:7-alpine
container_name: koopa-castopod-redis
command: --requirepass ${MYSQL_PASSWORD}
volumes:
- castopod-cache:/data
networks:
- castopod-app
restart: unless-stopped
volumes:
castopod-media:
castopod-db:
castopod-cache:
networks:
castopod-app:
castopod-db:
internal: true

View file

@ -0,0 +1,21 @@
# user systemd — MariaDB for Castopod
[Unit]
Description=Castopod MariaDB (koopa-castopod-mariadb)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
RestartSec=15
TimeoutStartSec=300
TimeoutStopSec=70
WorkingDirectory=/home/hernani/koopa-castopod
ExecStart=/usr/bin/podman-compose up -d mariadb
ExecStop=/usr/bin/podman stop -t 30 koopa-castopod-mariadb
Type=oneshot
RemainAfterExit=yes
[Install]
WantedBy=default.target

View file

@ -0,0 +1,21 @@
# user systemd — Redis for Castopod
[Unit]
Description=Castopod Redis (koopa-castopod-redis)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
RestartSec=15
TimeoutStartSec=180
TimeoutStopSec=40
WorkingDirectory=/home/hernani/koopa-castopod
ExecStart=/usr/bin/podman-compose up -d redis
ExecStop=/usr/bin/podman stop -t 15 koopa-castopod-redis
Type=oneshot
RemainAfterExit=yes
[Install]
WantedBy=default.target

View file

@ -0,0 +1,21 @@
# user systemd — Castopod web (depends on MariaDB + Redis)
[Unit]
Description=Castopod web (koopa-castopod)
Wants=network-online.target container-koopa-castopod-mariadb.service container-koopa-castopod-redis.service
After=network-online.target container-koopa-castopod-mariadb.service container-koopa-castopod-redis.service
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
RestartSec=20
TimeoutStartSec=300
TimeoutStopSec=120
WorkingDirectory=/home/hernani/koopa-castopod
ExecStart=/usr/bin/podman-compose up -d castopod
ExecStop=/usr/bin/podman stop -t 30 koopa-castopod
Type=oneshot
RemainAfterExit=yes
[Install]
WantedBy=default.target

View file

@ -0,0 +1,23 @@
# Exchange landing (`exchange.hacktivism.ch` intro)
Static intro (stats + links) for the **GOA exchange**. Withdraw QR removed (was wrong).
| Item | Value |
|------|--------|
| Container | `taler-hacktivism-exchange-ansible` |
| In-container path | `/var/www/exchange-landing/` |
| nginx | listen **9014** (`nginx-landing.conf`) |
| **Host port to publish / wire** | **`9014`** → `127.0.0.1:9014` |
| Public path (once Caddy wired) | e.g. `https://exchange.hacktivism.ch/intro/` *or* dedicated intro host |
| Link previews | Open Graph / Twitter meta → `og-goa-shop.png` (1200×630 GOA shop grid) |
## Deploy
```bash
# from laptop → koopa
scp -r configs/exchange-landing/* koopa:/tmp/exchange-landing/
# on koopa: install nginx if missing, copy files, enable site, publish port
# ensure og-goa-shop.png is under /var/www/exchange-landing/ (served as /intro/og-goa-shop.png)
```
See `scripts/taler-landing/deploy-landings.sh`.

View file

@ -0,0 +1,603 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GOA Exchange</title>
<meta name="description" content="GOA GNU Taler exchange — withdraw coins, then try GOA shop products on the merchant." />
<link rel="canonical" href="https://exchange.hacktivism.ch/intro/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="hacktivism.ch · GOA" />
<meta property="og:locale" content="en_US" />
<meta property="og:title" content="GOA Exchange" />
<meta property="og:description" content="GOA exchange for GNU Taler — withdraw coins and explore GOA shop products." />
<meta property="og:url" content="https://exchange.hacktivism.ch/intro/" />
<meta property="og:image" content="https://exchange.hacktivism.ch/intro/og-goa-shop.png?v=android1" />
<meta property="og:image:secure_url" content="https://exchange.hacktivism.ch/intro/og-goa-shop.png?v=android1" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="GOA Shop — Intergalactic · GOA · no IBAN — pay with GNU Taler" />
<link rel="image_src" href="https://exchange.hacktivism.ch/intro/og-goa-shop.png?v=android1" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="GOA Exchange" />
<meta name="twitter:description" content="GOA exchange for GNU Taler — withdraw coins and explore GOA shop products (Intergalactic · GOA · no IBAN)." />
<meta name="twitter:image" content="https://exchange.hacktivism.ch/intro/og-goa-shop.png?v=android1" />
<meta name="twitter:image:alt" content="GOA Shop — Intergalactic · GOA · no IBAN — pay with GNU Taler" />
<base href="/intro/" />
<style>
:root {
--bg: #1a1410; --card: rgba(42, 32, 24, 0.82); --text: #fff6e8; --muted: #c9b8a0;
--accent2: #3ecfbf; --border: rgba(232, 168, 56, 0.28);
--glow-gold: rgba(232, 168, 56, 0.35); --glow-teal: rgba(62, 207, 191, 0.22);
--val: #f0d090;
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--text); line-height: 1.55; background-color: var(--bg);
background-image:
radial-gradient(ellipse 90% 55% at 50% 108%, rgba(26, 107, 110, 0.55) 0%, transparent 55%),
radial-gradient(circle 420px at 12% 18%, var(--glow-gold) 0%, transparent 62%),
linear-gradient(165deg, #2c1e14 0%, #1a1410 38%, #12181a 72%, #0e1c1e 100%);
background-attachment: fixed;
}
main { max-width: 42rem; margin: 0 auto; padding: 2.25rem 1.25rem 3.5rem; }
.stack-bar {
margin: 0; padding: 0.35rem 0.75rem; background: #0042b3; color: #d6e4ff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.62rem; font-weight: 500; letter-spacing: 0.02em; line-height: 1.35;
text-align: center; border-bottom: 1px solid rgba(164, 201, 255, 0.35);
}
.stack-bar strong { color: #fff; font-weight: 650; }
.stack-bar .sep { color: rgba(164, 201, 255, 0.55); margin: 0 0.2rem; }
.stack-bar a {
color: #a4c9ff; text-decoration: none;
border-bottom: 1px solid rgba(164, 201, 255, 0.4);
}
.stack-bar a:hover { color: #fff; border-bottom-color: #fff; }
.hero { text-align: center; margin: 0 0 1.25rem; }
h1 {
font-size: 1.85rem; font-weight: 750; margin: 0 0 0.45rem;
background: linear-gradient(120deg, #fff6e8 0%, #f0c86a 45%, #3ecfbf 120%);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.sub { color: var(--muted); max-width: 32rem; margin: 0 auto; }
.badge {
display: inline-block; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.04em;
padding: 0.2rem 0.7rem; border-radius: 999px; margin-bottom: 0.75rem;
border: 1px solid rgba(232, 168, 56, 0.4); color: #f0d090;
background: linear-gradient(135deg, rgba(232, 168, 56, 0.2), rgba(62, 207, 191, 0.15));
}
.stats {
background: rgba(18, 28, 28, 0.55); border: 1px solid rgba(62, 207, 191, 0.28);
border-radius: 16px; padding: 1.1rem 1.15rem; margin: 0 0 1.1rem;
}
.stats h2 {
margin: 0 0 0.35rem; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--muted); text-align: center;
}
.note { text-align: center; font-size: 0.82rem; color: var(--muted); margin: 0 0 0.9rem; }
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.65rem; }
@media (min-width: 420px) { .stats-grid { grid-template-columns: 1fr 1fr 1fr; } }
.stat { background: rgba(0,0,0,0.22); border-radius: 10px; padding: 0.55rem 0.65rem; }
.stat .k { display: block; font-size: 0.68rem; color: var(--muted); text-transform: uppercase; text-align: center; margin-bottom: 0.15rem; letter-spacing: 0.04em; }
.stat .v { display: block; font-size: 1.05rem; font-weight: 750; color: var(--val); text-align: center; font-variant-numeric: tabular-nums; word-break: break-word; }
.stat .v.teal { color: #5eead4; }
.stat .v.sm { font-size: 0.92rem; }
.stats-foot { margin: 0.75rem 0 0; font-size: 0.72rem; color: var(--muted); text-align: center; }
.stats-foot.err { color: #f0a090; }
.stats-run-note {
margin: 0.35rem 0 0; font-size: 0.7rem; color: #f0a090; text-align: center;
}
.sub-block {
margin-top: 0.85rem; padding: 0.75rem 0.85rem;
border-radius: 12px; border: 1px solid rgba(232, 168, 56, 0.22);
background: rgba(0,0,0,0.18);
}
.sub-block h3 {
margin: 0 0 0.5rem; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--muted); text-align: center;
}
.ladder {
display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center;
}
.pill {
font-size: 0.72rem; font-weight: 700; padding: 0.18rem 0.45rem; border-radius: 999px;
border: 1px solid rgba(62, 207, 191, 0.35); color: #99f6e4;
background: rgba(62, 207, 191, 0.1); font-variant-numeric: tabular-nums;
}
.by-denom { font-size: 0.82rem; color: var(--muted); text-align: center; }
.by-denom table { width: 100%; border-collapse: collapse; margin-top: 0.35rem; }
.by-denom th, .by-denom td {
padding: 0.28rem 0.35rem; border-bottom: 1px solid rgba(255,255,255,0.06);
text-align: center; font-variant-numeric: tabular-nums;
}
.by-denom th { color: var(--muted); font-size: 0.68rem; text-transform: uppercase; font-weight: 700; }
.by-denom td { color: var(--val); font-weight: 650; }
.step-card {
background: var(--card); border: 1px solid var(--border); border-radius: 18px;
padding: 1.3rem 1.25rem; margin-bottom: 1.2rem;
}
.step-card h2 { margin: 0 0 0.4rem; text-align: center; font-size: 1.2rem; }
.step-tagline { text-align: center; color: var(--muted); margin: 0 0 1rem; }
.step-body { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.cta {
display: inline-block; text-decoration: none; font-weight: 750; padding: 0.7rem 1.15rem;
border-radius: 12px; background: linear-gradient(135deg, #f0c86a, #c47a18); color: #1a1208;
}
.cta.teal { background: linear-gradient(135deg, #5eead4, #0d9488); color: #042f2e; }
a.cta.wallet-open,
a.cta.teal.wallet-open {
display: inline-flex !important; align-items: center; justify-content: center;
width: auto !important; min-width: 0 !important; max-width: none !important;
margin: 0.15rem auto 0 !important; padding: 0.28rem 0.7rem !important;
font-size: 0.78rem !important; font-weight: 650; line-height: 1.2;
border-radius: 999px !important;
box-shadow: 0 1px 4px rgba(20, 184, 166, 0.22) !important;
white-space: nowrap; flex: 0 0 auto; align-self: center !important;
text-align: center;
}
.links { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; }
.cta-sec {
display: block; text-align: center; text-decoration: none; color: var(--accent2);
border: 1px solid rgba(62, 207, 191, 0.4); padding: 0.55rem; border-radius: 10px; font-weight: 600;
}
.meta { font-size: 0.78rem; color: var(--muted); word-break: break-all; text-align: center; }
.app-links {
display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; width: 100%;
}
.app-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem;
min-width: 9rem; text-decoration: none; font-weight: 700; font-size: 0.88rem;
padding: 0.55rem 0.85rem; border-radius: 10px;
border: 1px solid rgba(232, 168, 56, 0.4); background: rgba(0,0,0,0.28); color: #f0d090;
}
.app-btn.ios { border-color: rgba(232, 168, 56, 0.4); color: #f0d090; }
.app-btn.hub {
flex: 1 1 100%; border-color: rgba(201, 184, 160, 0.35); color: var(--muted);
font-weight: 600; font-size: 0.82rem; gap: 0.5rem;
}
.wallet-fold {
margin: 0 0 1.15rem;
border: 1px solid rgba(232, 168, 56, 0.35);
border-radius: 14px;
overflow: hidden;
background: rgba(18, 28, 28, 0.55);
}
.wallet-fold-toggle {
display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
width: 100%; margin: 0; padding: 0.75rem 0.9rem; border: 0;
background: transparent; color: #f0d090; font: inherit; font-size: 0.95rem;
font-weight: 750; text-align: left; cursor: pointer;
}
.wallet-fold-toggle:hover { background: rgba(232, 168, 56, 0.1); }
.wallet-fold-toggle-text { flex: 1; }
.wallet-fold-chevron {
flex-shrink: 0; color: #3ecfbf; font-size: 1.1rem; font-weight: 700;
transition: transform 0.15s ease;
}
.wallet-fold.open .wallet-fold-chevron { transform: rotate(90deg); }
.wallet-fold-body {
display: none; padding: 0.15rem 0.9rem 1rem;
border-top: 1px solid rgba(232, 168, 56, 0.28);
}
.wallet-fold.open .wallet-fold-body { display: block; }
.wallet-fold-body .app-links { margin-top: 0.55rem; }
button.app-btn.cli-toggle {
flex: 1 1 100%; width: 100%; justify-content: flex-start; gap: 0.5rem;
font: inherit; font-weight: 700; font-size: 0.88rem; cursor: pointer;
color: #f0d090; border-color: rgba(232, 168, 56, 0.4); background: rgba(0,0,0,0.28);
}
button.app-btn.cli-toggle .cli-chevron {
margin-left: auto; flex-shrink: 0; color: #3ecfbf;
font-size: 1.15rem; font-weight: 700; line-height: 1;
transition: transform 0.15s ease;
}
button.app-btn.cli-toggle[aria-expanded="true"] .cli-chevron { transform: rotate(90deg); }
button.app-btn.cli-toggle[aria-expanded="true"] {
border-color: rgba(62, 207, 191, 0.5); background: rgba(62, 207, 191, 0.08);
}
.cli-panel {
display: none; width: 100%; margin: 0.35rem 0 0.55rem; text-align: left;
border: 1px solid rgba(232, 168, 56, 0.32); border-radius: 12px;
background: rgba(0, 0, 0, 0.28); padding: 0.75rem 0.85rem 0.9rem; box-sizing: border-box;
}
.cli-panel.open { display: block; }
.cli-panel .cli-lead {
margin: 0 0 0.55rem; color: var(--muted); font-size: 0.84rem; line-height: 1.4;
}
.cli-panel .cli-lead a { color: #e8c878; }
.cli-block {
margin: 0; padding: 0.55rem 0.65rem; border-radius: 10px;
background: rgba(0, 0, 0, 0.4); border: 1px solid rgba(240, 200, 106, 0.2);
color: #e8e0d0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.7rem; line-height: 1.45; overflow-x: auto; white-space: pre; user-select: all;
}
.cli-block .cmt { color: #8a9a90; }
.sym-gold path, .sym-gold circle { fill: #f0c86a; stroke: #f0c86a; }
.sym-gold path[data-ring], .sym-gold circle[data-ring] { fill: none; stroke: #f0c86a; }
.sym-blue path, .sym-blue circle { fill: #3ecfbf; stroke: #3ecfbf; }
.sym-blue path[data-ring], .sym-blue circle[data-ring] { fill: none; stroke: #3ecfbf; }
.app-ico { width: 1.15rem; height: 1.15rem; flex-shrink: 0; display: block; }
.app-ico:not(.browser-ico) path,
.app-ico:not(.browser-ico) circle,
.app-ico:not(.browser-ico) rect { fill: currentColor; }
/* hub browser marks use explicit gold/blue fills in SVG */
.app-ico.stroke-ico path, .app-ico.stroke-ico circle, .app-ico.stroke-ico rect {
fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.app-ico.branch-ico circle { fill: currentColor; stroke: none; }
.app-ico.branch-ico path {
fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}
.app-btn:not(.hub) .app-ico { color: #f0c86a; }
.app-btn.ios .app-ico { color: #f0c86a; }
.hub-icons { display: inline-flex; align-items: center; gap: 0.4rem; }
.hub-icons .app-ico { width: 1.25rem; height: 1.25rem; }
.hub-icons .browser-ico { border-radius: 3px; flex-shrink: 0; }
.step-tagline .app-ico {
width: 1rem; height: 1rem; display: inline-block; vertical-align: -0.15em; margin: 0 0.12em;
}
.step-tagline .app-ico.android { color: #f0c86a; }
.step-tagline .app-ico.ios { color: #f0c86a; }
.step-body ol { margin: 0; padding-left: 1.2rem; width: 100%; align-self: stretch; color: var(--muted); }
.step-body li { margin: 0.35rem 0; }
footer { margin-top: 1.75rem; text-align: center; color: var(--muted); font-size: 0.85rem; }
footer a { color: #e8c878; }
code { font-family: ui-monospace, Menlo, monospace; font-size: 0.86em; background: rgba(0,0,0,0.35); padding: 0.1em 0.35em; border-radius: 4px; color: #f0d090; }
</style>
</head>
<body>
<p class="stack-bar" role="note" title="Taler software on this exchange service (links → git.taler.net tags)">
<strong>stack</strong><span class="sep">·</span>
<a href="https://git.taler.net/taler/exchange/commit/6dbfa41e3fc6657eff28d11166d24301cdfa93ff.html" target="_blank" rel="noopener noreferrer" title="exchange tag v1.6.6">taler-exchange&nbsp;1.6.6</a>
<span class="sep">·</span>
<a href="https://git.taler.net/taler/exchange/commit/6dbfa41e3fc6657eff28d11166d24301cdfa93ff.html" target="_blank" rel="noopener noreferrer" title="exchange tag v1.6.6">libtalerexchange&nbsp;1.6.6</a>
<span class="sep">·</span>
<a href="https://git.taler.net/taler/exchange/commit/6dbfa41e3fc6657eff28d11166d24301cdfa93ff.html" target="_blank" rel="noopener noreferrer" title="exchange tag v1.6.6">taler-exchange-database&nbsp;1.6.6</a>
<span class="sep">·</span>
<a href="https://git.taler.net/taler/exchange/commit/6dbfa41e3fc6657eff28d11166d24301cdfa93ff.html" target="_blank" rel="noopener noreferrer" title="exchange tag v1.6.6">taler-exchange-offline&nbsp;1.6.6</a>
<span class="sep">·</span>
<a href="https://git.taler.net/taler/exchange/commit/6dbfa41e3fc6657eff28d11166d24301cdfa93ff.html" target="_blank" rel="noopener noreferrer" title="exchange tag v1.6.6">taler-terms-generator&nbsp;1.6.6</a>
</p>
<main>
<header class="hero">
<span class="badge">GOA · Exchange</span>
<h1>GOA Exchange</h1>
<p class="sub">Explorational exchange — coins, denoms, one-step withdraw.</p>
</header>
<section class="stats" id="stats-coins" aria-labelledby="coins-title">
<h2 id="coins-title">Coins</h2>
<p class="note">From exchange DB · coin lifecycle</p>
<div class="stats-grid">
<div class="stat"><span class="k">Known coins</span><span class="v teal" id="st-coins"></span></div>
<div class="stat"><span class="k">Live (remaining)</span><span class="v" id="st-coins-live"></span></div>
<div class="stat"><span class="k">Spent / zero</span><span class="v" id="st-coins-spent"></span></div>
<div class="stat"><span class="k">Remaining amt</span><span class="v teal sm" id="st-remaining"></span></div>
<div class="stat"><span class="k">Withdraw ops</span><span class="v" id="st-withdraw"></span></div>
<div class="stat"><span class="k">Withdraw amt</span><span class="v teal sm" id="st-withdraw-amt"></span></div>
<div class="stat"><span class="k">Refresh</span><span class="v" id="st-refresh"></span></div>
<div class="stat"><span class="k">Coin deposits</span><span class="v" id="st-coin-dep"></span></div>
<div class="stat"><span class="k">Batch deposits</span><span class="v" id="st-batch-dep"></span></div>
<div class="stat"><span class="k">Recoup</span><span class="v" id="st-recoup"></span></div>
<div class="stat"><span class="k">Refunds</span><span class="v" id="st-refunds"></span></div>
<div class="stat"><span class="k">History events</span><span class="v" id="st-hist"></span></div>
</div>
<div class="sub-block" id="by-denom-wrap" hidden>
<h3>Known coins by value</h3>
<div class="by-denom" id="st-by-denom"></div>
</div>
</section>
<section class="stats" id="stats-denoms" aria-labelledby="denoms-title">
<h2 id="denoms-title">Denominations</h2>
<p class="note">Signed keys in DB · public ladder on <code>/keys</code></p>
<div class="stats-grid">
<div class="stat"><span class="k">Denom keys</span><span class="v teal" id="st-denoms"></span></div>
<div class="stat"><span class="k">Unique values</span><span class="v" id="st-denom-vals"></span></div>
<div class="stat"><span class="k">Withdrawable now</span><span class="v teal" id="st-denoms-wd"></span></div>
</div>
<div class="sub-block">
<h3>Value ladder</h3>
<div class="ladder" id="st-ladder"></div>
</div>
</section>
<section class="stats" id="stats-wire" aria-labelledby="wire-title">
<h2 id="wire-title">Reserves · wire</h2>
<p class="note">Backing money movement</p>
<div class="stats-grid">
<div class="stat"><span class="k">Reserves</span><span class="v teal" id="st-reserves"></span></div>
<div class="stat"><span class="k">Wire in</span><span class="v" id="st-wire-in-n"></span></div>
<div class="stat"><span class="k">Wire in amt</span><span class="v teal sm" id="st-wire-in-amt"></span></div>
<div class="stat"><span class="k">Wire out</span><span class="v" id="st-wire-out"></span></div>
</div>
<p class="stats-foot" id="st-foot">Loading…</p>
<p class="stats-run-note" id="st-run-note" hidden></p>
</section>
<section class="stats" id="stats-perf" aria-labelledby="perf-title">
<h2 id="perf-title">Performance</h2>
<p class="note">Live HTTP probes + container memory (RSS)</p>
<div class="stats-grid">
<div class="stat"><span class="k">/keys</span><span class="v teal" id="st-keys-ms"></span></div>
<div class="stat"><span class="k">/config</span><span class="v" id="st-config-ms"></span></div>
<div class="stat"><span class="k">Loadavg</span><span class="v sm" id="st-load"></span></div>
<div class="stat"><span class="k">Container</span><span class="v teal" id="st-mem-ctr"></span></div>
<div class="stat"><span class="k">PostgreSQL</span><span class="v" id="st-mem-pg"></span></div>
<div class="stat"><span class="k">Taler procs</span><span class="v" id="st-mem-taler"></span></div>
<div class="stat"><span class="k">Nginx</span><span class="v sm" id="st-mem-nginx"></span></div>
</div>
<div class="sub-block" style="margin-top:0.85rem">
<h3>Top processes (RSS)</h3>
<div id="st-mem-top" class="by-denom"></div>
</div>
<p class="stats-foot" id="st-perf-foot">Memory from /proc + cgroup inside podman container.</p>
</section>
<div class="wallet-fold" id="wallet-fold">
<button type="button" class="wallet-fold-toggle" id="wallet-fold-toggle"
aria-expanded="false" aria-controls="wallet-fold-body">
<span class="wallet-fold-toggle-text">Get the wallet · Android / iOS / web / packages</span>
<span class="wallet-fold-chevron" aria-hidden="true"></span>
</button>
<div class="wallet-fold-body" id="wallet-fold-body" hidden>
<div class="app-links">
<a class="app-btn" href="https://play.google.com/store/apps/details?id=net.taler.wallet" target="_blank" rel="noopener noreferrer" aria-label="Android Play Store">
<svg class="app-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M8.2 2.4l-.9-1.6c-.1-.2 0-.4.2-.5.2-.1.4 0 .5.2l.9 1.6c.8-.3 1.7-.5 2.6-.5s1.8.2 2.6.5l.9-1.6c.1-.2.3-.3.5-.2.2.1.3.3.2.5l-.9 1.6c1.8.8 3.1 2.5 3.3 4.6H4.9c.2-2.1 1.5-3.8 3.3-4.6zM7 8.5c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zm10 0c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zM5 10.5h14c.6 0 1 .4 1 1v6.2c0 1.3-1 2.3-2.3 2.3h-.5v2.2c0 .5-.4.9-.9.9s-.9-.4-.9-.9v-2.2h-4.8v2.2c0 .5-.4.9-.9.9s-.9-.4-.9-.9v-2.2h-.5C5.9 20 5 19 5 17.7v-6.2c0-.6.4-1 1-1z"/></svg>
Play Store
</a>
<a class="app-btn" href="https://f-droid.org/en/packages/net.taler.wallet.fdroid/" target="_blank" rel="noopener noreferrer" aria-label="Android F-Droid">
<svg class="app-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M8.2 2.4l-.9-1.6c-.1-.2 0-.4.2-.5.2-.1.4 0 .5.2l.9 1.6c.8-.3 1.7-.5 2.6-.5s1.8.2 2.6.5l.9-1.6c.1-.2.3-.3.5-.2.2.1.3.3.2.5l-.9 1.6c1.8.8 3.1 2.5 3.3 4.6H4.9c.2-2.1 1.5-3.8 3.3-4.6zM7 8.5c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zm10 0c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zM5 10.5h14c.6 0 1 .4 1 1v6.2c0 1.3-1 2.3-2.3 2.3h-.5v2.2c0 .5-.4.9-.9.9s-.9-.4-.9-.9v-2.2h-4.8v2.2c0 .5-.4.9-.9.9s-.9-.4-.9-.9v-2.2h-.5C5.9 20 5 19 5 17.7v-6.2c0-.6.4-1 1-1z"/></svg>
F-Droid
</a>
<a class="app-btn ios" href="https://apps.apple.com/app/taler-wallet/id6463440117" target="_blank" rel="noopener noreferrer" aria-label="iOS App Store">
<svg class="app-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M16.7 12.6c0-2.1 1.7-3.1 1.8-3.2-1-1.4-2.5-1.6-3-1.7-1.3-.1-2.5.8-3.1.8s-1.6-.7-2.7-.7c-1.4 0-2.7.8-3.4 2.1-1.5 2.5-.4 6.3 1 8.3.7 1 1.5 2.1 2.6 2 1-.1 1.5-.7 2.8-.7s1.7.7 2.8.7 1.9-1 2.6-2c.8-1.1 1.1-2.2 1.1-2.3-.1 0-2.2-.8-2.2-3.3zm-2-6.2c.6-.7 1-1.7.9-2.7-1 .1-2.1.6-2.8 1.4-.6.7-1.1 1.7-.9 2.6 1 .1 2-.5 2.8-1.3z"/></svg>
App Store
</a>
<a class="app-btn hub" href="https://wallet.taler.net/" target="_blank" rel="noopener noreferrer" aria-label="Web browsers (experimental): Chrome, Firefox, Opera, Ubuntu Touch">
<span class="hub-icons" aria-hidden="true" title="Chrome · Firefox · Opera · Ubuntu Touch">
<svg class="app-ico browser-ico" viewBox="0 0 24 24" role="img" aria-label="Chrome">
<circle cx="12" cy="12" r="10" fill="#3ecfbf" opacity="0.22"/>
<path fill="#3ecfbf" d="M12 2a10 10 0 0 1 8.66 5H12a4 4 0 0 0-3.46 2L5.1 4.1A10 10 0 0 1 12 2z"/>
<path fill="#3ecfbf" opacity="0.8" d="M20.66 7A10 10 0 0 1 12 22l3.46-6A4 4 0 0 0 12 8h8.66z"/>
<path fill="#3ecfbf" opacity="0.55" d="M5.1 4.1 8.54 9A4 4 0 0 0 12 16l-3.46 6A10 10 0 0 1 5.1 4.1z"/>
<circle cx="12" cy="12" r="3.2" fill="#3ecfbf"/>
</svg>
<svg class="app-ico browser-ico" viewBox="0 0 24 24" role="img" aria-label="Firefox">
<circle cx="12" cy="12.2" r="8.2" fill="#3ecfbf" opacity="0.28"/>
<path fill="#3ecfbf" d="M20.2 9.2c-.2-1.4-1.1-2.6-2.2-3.4.5 1.1.6 2.3.3 3.4-.9-1.6-2.5-2.6-4.4-2.6-1.3 0-2.5.4-3.4 1.1C9.3 5.4 10.9 3.8 13 3.2c-2.5.1-4.7 1.4-6.1 3.4C5.3 8.2 4.6 10.2 4.8 12.3c.3 3.9 3.5 7 7.4 7.1 3.6.1 6.7-2.2 7.7-5.5.4-1.4.4-2.9.3-4.7z"/>
<circle cx="12.2" cy="13.2" r="2.4" fill="#3ecfbf"/>
</svg>
<svg class="app-ico browser-ico" viewBox="0 0 24 24" role="img" aria-label="Opera">
<circle cx="12" cy="12" r="9.2" fill="none" stroke="#3ecfbf" stroke-width="2"/>
<ellipse cx="12" cy="12" rx="4.15" ry="7.1" fill="none" stroke="#3ecfbf" stroke-width="1.85"/>
</svg>
<svg class="app-ico browser-ico" viewBox="0 0 24 24" role="img" aria-label="Ubuntu Touch">
<circle cx="12" cy="12" r="2.25" fill="#3ecfbf"/>
<circle cx="12" cy="4.85" r="1.7" fill="#3ecfbf"/>
<circle cx="18.15" cy="15.55" r="1.7" fill="#3ecfbf"/>
<circle cx="5.85" cy="15.55" r="1.7" fill="#3ecfbf"/>
</svg>
</span>
Web browsers (experimental)
</a>
<button type="button" class="app-btn hub cli-toggle" id="pkg-fold-toggle"
aria-expanded="false" aria-controls="pkg-fold-body"
aria-label="Linux packages: Debian and Ubuntu (taler-wallet-cli)">
<svg class="app-ico browser-ico penguin-ico" viewBox="0 0 24 24" role="img" aria-label="Linux">
<ellipse cx="12" cy="14.2" rx="6.4" ry="7.2" fill="#f0c86a"/>
<ellipse cx="12" cy="15.2" rx="3.6" ry="4.8" fill="#1a1410" opacity="0.35"/>
<circle cx="12" cy="7" r="4.1" fill="#f0c86a"/>
<circle cx="10.35" cy="6.6" r="0.85" fill="#1a1410"/>
<circle cx="13.65" cy="6.6" r="0.85" fill="#1a1410"/>
<path fill="#e8a838" d="M11.15 8.1 12 9.55 12.85 8.1z"/>
<ellipse cx="9.4" cy="21" rx="2.1" ry="1.05" fill="#e8a838"/>
<ellipse cx="14.6" cy="21" rx="2.1" ry="1.05" fill="#e8a838"/>
<ellipse cx="6.6" cy="13.5" rx="1.4" ry="2.6" fill="#f0c86a" transform="rotate(-18 6.6 13.5)"/>
<ellipse cx="17.4" cy="13.5" rx="1.4" ry="2.6" fill="#f0c86a" transform="rotate(18 17.4 13.5)"/>
</svg>
Debian · Ubuntu packages
<span class="cli-chevron" aria-hidden="true"></span>
</button>
<div class="cli-panel" id="pkg-fold-body" hidden>
<p class="cli-lead">
Official <strong>taler-wallet-cli</strong> from
<a href="https://deb.taler.net/" target="_blank" rel="noopener noreferrer">deb.taler.net</a>.
More on the <a href="https://bank.hacktivism.ch/intro/">bank landing</a>.
</p>
<pre class="cli-block"><span class="cmt"># Debian example</span>
sudo apt-get update
sudo apt-get install -y taler-wallet-cli</pre>
</div>
</div>
</div>
</div>
<section class="step-card">
<h2>Withdraw GOA</h2>
<p class="step-tagline">Use the bank landing or wallet to add the exchange and withdraw.</p>
<div class="step-body">
<a class="cta teal" href="https://bank.hacktivism.ch/intro/">Bank landing →</a>
<div class="links">
<a class="cta-sec" href="https://exchange.hacktivism.ch/keys">/keys</a>
<a class="cta-sec" href="https://exchange.hacktivism.ch/terms">/terms</a>
<a class="cta-sec" href="https://exchange.hacktivism.ch/privacy">/privacy</a>
<a class="cta-sec" href="https://exchange.hacktivism.ch/config">/config</a>
</div>
</div>
</section>
<footer>
<a href="https://bank.hacktivism.ch/intro/">Bank</a> ·
<a href="https://exchange.hacktivism.ch/intro/">Exchange</a> ·
<a href="https://taler.hacktivism.ch/intro/">Merchant</a> ·
<a href="https://exchange.hacktivism.ch/terms">Terms</a> ·
<a href="https://exchange.hacktivism.ch/privacy">Privacy</a> ·
<a href="https://git.hacktivism.ch/hernani/koopa-admin-log/src/branch/main/configs/exchange-landing">Source</a>
<p class="privacy-note" style="margin:0.65rem 0 0;font-size:0.78rem;color:var(--muted);line-height:1.4">
Privacy: processing under Swiss FADP (revDSG). What is retained is listed on
<a href="https://exchange.hacktivism.ch/privacy" style="color:#5eead4">/privacy</a>.
</p>
</footer>
</main>
<script>
(function () {
function set(id, v) {
var el = document.getElementById(id);
if (el) el.textContent = v == null || v === "" ? "—" : String(v);
}
function msLabel(ms, http) {
if (ms == null || ms === "") return "—";
var s = ms + " ms";
if (http && http !== "200") s += " (" + http + ")";
return s;
}
fetch("/intro/stats.json", { cache: "no-store" })
.then(function (r) { if (!r.ok) throw new Error("x"); return r.json(); })
.then(function (d) {
if (!d || !d.ok) throw new Error("bad");
set("st-coins", d.known_coins);
set("st-coins-live", d.coins_live);
set("st-coins-spent", d.coins_spent);
set("st-remaining", d.coins_remaining_amount);
set("st-withdraw", d.withdraw_ops);
set("st-withdraw-amt", d.withdraw_amount);
set("st-refresh", d.refresh_ops);
set("st-coin-dep", d.coin_deposits);
set("st-batch-dep", d.batch_deposits);
set("st-recoup", d.recoup);
set("st-refunds", d.refunds);
set("st-hist", d.coin_history_events);
set("st-denoms", d.denominations);
set("st-denom-vals", d.denom_values);
set("st-denoms-wd", d.denoms_withdrawable);
set("st-reserves", d.reserves);
set("st-wire-in-n", d.wire_in_count);
set("st-wire-in-amt", d.wire_in_amount);
set("st-wire-out", d.wire_out);
var ladder = document.getElementById("st-ladder");
if (ladder) {
ladder.innerHTML = "";
(d.denom_ladder || []).forEach(function (x) {
var s = document.createElement("span");
s.className = "pill";
s.textContent = x.value + (x.keys > 1 ? " ×" + x.keys : "");
ladder.appendChild(s);
});
if (!(d.denom_ladder || []).length) ladder.textContent = "—";
}
var by = d.by_denom || [];
var wrap = document.getElementById("by-denom-wrap");
var box = document.getElementById("st-by-denom");
if (wrap && box && by.length) {
wrap.hidden = false;
var html = "<table><thead><tr><th>Value</th><th>Coins</th><th>Live</th></tr></thead><tbody>";
by.forEach(function (r) {
html += "<tr><td>" + r.value + "</td><td>" + r.coins + "</td><td>" + r.live + "</td></tr>";
});
html += "</tbody></table>";
box.innerHTML = html;
}
var foot = document.getElementById("st-foot");
if (foot) {
foot.textContent = (d.wirewatch_running ? "wirewatch on · " : "wirewatch off · ") +
(d.generated_at_human || d.generated_at || "");
}
var p = d.performance || {};
set("st-keys-ms", msLabel(p.keys_ms, p.keys_http));
set("st-config-ms", msLabel(p.config_ms, p.config_http));
set("st-load", p.loadavg || "—");
var mem = p.memory || {};
set("st-mem-ctr", mem.container_rss_human || "—");
set("st-mem-pg", mem.postgres_rss_human
? mem.postgres_rss_human + (mem.postgres_n ? " · " + mem.postgres_n + "p" : "")
: "—");
set("st-mem-taler", mem.taler_rss_human
? mem.taler_rss_human + (mem.taler_n ? " · " + mem.taler_n + "p" : "")
: "—");
set("st-mem-nginx", mem.nginx_rss_human || "—");
var topBox = document.getElementById("st-mem-top");
if (topBox) {
var tops = mem.top || [];
if (!tops.length) {
topBox.textContent = "—";
} else {
var th = "<table><thead><tr><th>RSS</th><th>Comm</th><th>Command</th></tr></thead><tbody>";
tops.forEach(function (t) {
th += "<tr><td>" + (t.rss_human || "?") + "</td><td>" +
(t.comm || "") + "</td><td style=\"font-size:0.72rem;word-break:break-all\">" +
(t.cmd || "") + "</td></tr>";
});
th += "</tbody></table>";
topBox.innerHTML = th;
}
}
var pf = document.getElementById("st-perf-foot");
if (pf) {
pf.textContent = "Container RSS + process groups · " +
(d.generated_at_human || d.generated_at || "");
}
return fetch("/intro/stats-run.json", { cache: "no-store" })
.then(function (r) { return r.ok ? r.json() : null; })
.catch(function () { return null; });
})
.then(function (run) {
var note = document.getElementById("st-run-note");
if (!note) return;
if (run && run.ok === false) {
note.hidden = false;
note.textContent = "Last refresh failed" +
(run.at_human ? " · " + run.at_human : "") +
" · showing last good data";
} else {
note.hidden = true;
note.textContent = "";
}
})
.catch(function () {
var foot = document.getElementById("st-foot");
if (foot) foot.innerHTML = 'Stats offline · <a href="https://bank.hacktivism.ch/intro/" style="color:#e8c878">bank.hacktivism.ch</a>';
});
})();
</script>
<script>
(function () {
var fold = document.getElementById("wallet-fold");
var btn = document.getElementById("wallet-fold-toggle");
var body = document.getElementById("wallet-fold-body");
if (fold && btn && body) {
btn.addEventListener("click", function () {
var open = !fold.classList.contains("open");
fold.classList.toggle("open", open);
btn.setAttribute("aria-expanded", open ? "true" : "false");
if (open) body.removeAttribute("hidden");
else body.setAttribute("hidden", "");
});
}
function wireCli(btnId, bodyId) {
var b = document.getElementById(btnId);
var p = document.getElementById(bodyId);
if (!b || !p) return;
b.addEventListener("click", function () {
var open = !p.classList.contains("open");
p.classList.toggle("open", open);
b.setAttribute("aria-expanded", open ? "true" : "false");
if (open) p.removeAttribute("hidden");
else p.setAttribute("hidden", "");
});
}
wireCli("pkg-fold-toggle", "pkg-fold-body");
})();
</script>
</body>
</html>

View file

@ -0,0 +1,24 @@
# Exchange landing — :9014 (behind Caddy — no port in Location)
server {
listen 9014;
listen [::]:9014;
server_name exchange.hacktivism.ch _;
root /var/www/exchange-landing;
index index.html;
absolute_redirect off;
port_in_redirect off;
location = / {
return 302 /intro/;
}
location = /intro {
return 302 /intro/;
}
location /intro/ {
alias /var/www/exchange-landing/;
}
location / {
try_files $uri $uri/ =404;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

View file

@ -0,0 +1,61 @@
# firewalld zone public (eno1)
## Current (2026-07-10, verified)
Expected open ports after Forgejo git-SSH:
```
sudo firewall-cmd --list-ports
# … 80/tcp 443/tcp 8080/tcp 9000/tcp 9001/tcp 23235/tcp 9200/tcp
```
| Port | Purpose | LAN probe (2026-07-10) |
|------|---------|------------------------|
| 80/tcp | `http-proxy` → Caddy 9000 (IPv6 HTTP/ACME) | via Caddy path |
| **443/tcp** | `https-proxy` → Caddy 9001 (IPv6 HTTPS) | via Caddy path |
| 9000/tcp | Caddy HTTP (WAN:80 → 9000) | **OK** |
| 9001/tcp | Caddy HTTPS (WAN:443 → 9001) | **OK** |
| 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 |
### Not opened on purpose
| Port | Note |
|------|------|
| 90209024 | rootless apps; public HTTP only via Caddy → `127.0.0.1:90xx` |
| 9024 | Forgejo HTTP internal to Caddy only |
LAN `nc` to 9021/9024 → **connection refused** is expected without firewalld open (and not required).
### Commands
```bash
sudo firewall-cmd --permanent --add-port=9200/tcp
sudo firewall-cmd --reload # or systemctl restart firewalld
sudo firewall-cmd --list-ports
```
### Verify (from another host on LAN / WAN)
```bash
nc -vz 192.168.100.95 9200
nc -vz 212.51.151.254 9200 # hairpin / public A
ssh -p 9200 -T git@git.hacktivism.ch
# expect: Hi there, … authenticated … Forgejo does not provide shell access.
```
### Why 443
IPv4 HTTPS is DNATd to **9001**. IPv6 has **no DNAT** — clients hit host **:443** directly.
### Why 9200
Forgejo advertises `SSH_PORT=9200`. Traffic hits **host:9200** (not Caddy). Without firewalld **9200/tcp**, clients get **connection refused** even while `ss` shows LISTEN and localhost works.
## History
| When | Change |
|------|--------|
| 2026-07-09 | 443/tcp re-added for IPv6 |
| 2026-07-10 | **9200/tcp** for Forgejo git-SSH (verified LAN + hairpin + SSH auth) |

60
configs/forgejo/README.md Normal file
View file

@ -0,0 +1,60 @@
# Forgejo (git.hacktivism.ch)
Rootless stack on koopa: `~/koopa-forgejo/` (podman user hernani).
Site: https://git.hacktivism.ch — HTTP `:9024`, git-SSH `:9200`.
## Branding theme: `hacktivism` (exchange-dark)
Global default and intended look match **exchange.hacktivism.ch**:
- Warm dark body (`#1a1410`), cream text (`#fff6e8`)
- Gold primary (`#e8a838` / `#f0c86a`), teal accents (`#3ecfbf`)
- Readable contrast on body, menus, code, forms, labels, messages
### Files (this repo)
| Path | Role |
|------|------|
| `assets/css/theme-hacktivism.css` | Full dark theme (based on forgejo-dark vars + exchange palette) |
| `assets/img/logo.svg` / `logo.png` / `favicon.svg` | Kamek-inspired logo (blue robe, flying on wand); not Nintendo art |
| `compose.yml` | Rootless compose + `FORGEJO__ui__DEFAULT_THEME=hacktivism` |
| `app.ini.example` | Config sketch |
### Live paths on koopa
```
~/koopa-forgejo/data/custom/public/assets/css/theme-hacktivism.css
~/koopa-forgejo/data/custom/public/assets/img/{logo.svg,logo.png,favicon.svg}
~/koopa-forgejo/data/custom/conf/app.ini # [ui] DEFAULT_THEME = hacktivism
```
Compose also sets `FORGEJO__ui__DEFAULT_THEME=hacktivism`.
### Deploy assets (do not wipe git data)
```bash
install -m 644 configs/forgejo/assets/css/theme-hacktivism.css \
~/koopa-forgejo/data/custom/public/assets/css/
install -m 644 configs/forgejo/assets/img/* \
~/koopa-forgejo/data/custom/public/assets/img/
# optional mirror if present:
# cp same under data/gitea/custom/public/assets/...
```
**Never** remove `~/koopa-forgejo/data/git/` — repositories live there (`hernani/koopa-admin-log`, …).
### Logged-in users
`DEFAULT_THEME` only applies to new sessions / users without a preference.
Existing accounts store `user.theme` in Postgres. Force all users:
```bash
podman exec koopa-forgejo-db psql -U forgejo -d forgejo \
-c "UPDATE \"user\" SET theme = 'hacktivism';"
```
Or: Profile → Appearance → theme **hacktivism**, then hard-reload.
### Registration
Public signup is disabled (`DISABLE_REGISTRATION`, no registration button).

View file

@ -0,0 +1,79 @@
APP_NAME = hacktivism git
RUN_USER = git
RUN_MODE = prod
WORK_PATH = /var/lib/gitea
[repository]
ROOT = /var/lib/gitea/git/repositories
ENABLE_PUSH_CREATE_USER = true
ENABLE_PUSH_CREATE_ORG = true
DEFAULT_PRIVATE = public
[repository.local]
LOCAL_COPY_PATH = /tmp/gitea/local-repo
[repository.upload]
TEMP_PATH = /tmp/gitea/uploads
[server]
APP_DATA_PATH = /var/lib/gitea
SSH_DOMAIN = git.hacktivism.ch
HTTP_PORT = 3000
ROOT_URL = https://git.hacktivism.ch/
DISABLE_SSH = false
; In rootless gitea container only internal ssh server is supported
START_SSH_SERVER = true
SSH_PORT = 9200
SSH_LISTEN_PORT = 2222
BUILTIN_SSH_SERVER_USER = git
LFS_START_SERVER =
DOMAIN = git.hacktivism.ch
LANDING_PAGE = explore
[database]
PATH = /var/lib/gitea/data/gitea.db
DB_TYPE = postgres
HOST = db:5432
NAME = forgejo
USER = forgejo
PASSWD = @inline-secret@
[session]
PROVIDER_CONFIG = /var/lib/gitea/data/sessions
PROVIDER = memory
[picture]
AVATAR_UPLOAD_PATH = /var/lib/gitea/data/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = /var/lib/gitea/data/repo-avatars
[attachment]
PATH = /var/lib/gitea/data/attachments
[log]
ROOT_PATH = /var/lib/gitea/data/log
LEVEL = Info
[security]
INSTALL_LOCK = true
SECRET_KEY = @inline-secret@
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = *
INTERNAL_TOKEN = @inline-secret@
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
SHOW_REGISTRATION_BUTTON = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
[lfs]
PATH = /var/lib/gitea/git/lfs
[openid]
ENABLE_OPENID_SIGNUP = false
ENABLE_OPENID_SIGNIN = false
[oauth2]
JWT_SECRET = @inline-secret@

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Wizard flying on magic wand, blue robe">
<!-- Original Kamek-inspired mark; not Nintendo art. No filters/transforms for clean raster. -->
<!-- motion streaks -->
<line x1="4" y1="64" x2="22" y2="64" stroke="#5eead4" stroke-width="2.5" stroke-linecap="round" opacity="0.55"/>
<line x1="6" y1="74" x2="20" y2="74" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<line x1="8" y1="54" x2="20" y2="54" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<!-- ground shadow -->
<ellipse cx="64" cy="118" rx="30" ry="5" fill="#000" opacity="0.2"/>
<!-- ===== MAGIC WAND (mount) — diagonal, thick gold shaft ===== -->
<!-- sits under the rider from lower-left to upper-right -->
<line x1="6" y1="100" x2="108" y2="72" stroke="#92400e" stroke-width="11" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#e8a838" stroke-width="7" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<!-- wand jewel (front tip) -->
<circle cx="112" cy="70" r="13" fill="#f59e0b" stroke="#fff7ed" stroke-width="3"/>
<circle cx="108" cy="66" r="4.5" fill="#fef9c3"/>
<!-- sparkles at tip -->
<polygon points="120,54 121.6,58.5 126,60 121.6,61.5 120,66 118.4,61.5 114,60 118.4,58.5" fill="#fef08a"/>
<polygon points="102,56 103,59 106,60 103,61 102,64 101,61 98,60 101,59" fill="#5eead4"/>
<circle cx="118" cy="78" r="2" fill="#bae6fd"/>
<!-- ===== RIDER on top of wand ===== -->
<!-- blue cape fluttering left -->
<path d="M38 78 C18 82, 10 98, 8 110 C22 98, 34 96, 40 92 Z" fill="#1e3a8a"/>
<path d="M40 80 C22 88, 14 104, 12 112 C28 100, 36 98, 42 94 Z" fill="#1d4ed8"/>
<!-- BLUE ROBE (body sitting ON shaft) -->
<ellipse cx="54" cy="86" rx="28" ry="22" fill="#1d4ed8"/>
<ellipse cx="54" cy="84" rx="24" ry="18" fill="#2563eb"/>
<!-- white trim -->
<path d="M32 74 Q54 94 76 74" fill="none" stroke="#f8fafc" stroke-width="4" stroke-linecap="round"/>
<path d="M36 82 Q54 98 72 82" fill="none" stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<!-- green shell -->
<ellipse cx="66" cy="92" rx="13" ry="11" fill="#16a34a"/>
<ellipse cx="66" cy="92" rx="6" ry="5" fill="#86efac" opacity="0.75"/>
<!-- legs over the wand -->
<ellipse cx="42" cy="100" rx="10" ry="7" fill="#1e40af"/>
<ellipse cx="60" cy="102" rx="10" ry="7" fill="#1e40af"/>
<!-- yellow feet hanging past shaft -->
<ellipse cx="38" cy="106" rx="7" ry="4.5" fill="#fbbf24"/>
<ellipse cx="64" cy="108" rx="7" ry="4.5" fill="#fbbf24"/>
<!-- left arm forward -->
<ellipse cx="30" cy="82" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="24" cy="78" r="5.5" fill="#fbbf24"/>
<!-- right arm up -->
<ellipse cx="74" cy="74" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="80" cy="68" r="5.5" fill="#fbbf24"/>
<circle cx="82" cy="60" r="5" fill="#38bdf8"/>
<circle cx="81" cy="58" r="1.8" fill="#e0f2fe"/>
<!-- ===== HEAD ===== -->
<ellipse cx="54" cy="50" rx="26" ry="24" fill="#fbbf24"/>
<ellipse cx="54" cy="53" rx="21" ry="17" fill="#fcd34d"/>
<ellipse cx="34" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<ellipse cx="74" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<!-- glasses -->
<circle cx="42" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<circle cx="66" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<line x1="52" y1="50" x2="56" y2="50" stroke="#0f172a" stroke-width="3"/>
<line x1="32" y1="47" x2="24" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<line x1="76" y1="47" x2="84" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="44" cy="50" r="3" fill="#0f172a"/>
<circle cx="68" cy="50" r="3" fill="#0f172a"/>
<circle cx="45.2" cy="48.8" r="1.1" fill="#fff"/>
<circle cx="69.2" cy="48.8" r="1.1" fill="#fff"/>
<!-- snout + mustache -->
<ellipse cx="54" cy="62" rx="8" ry="5" fill="#f59e0b"/>
<path d="M36 64 Q46 74 54 70 Q62 74 72 64 Q64 72 54 74 Q44 72 36 64 Z" fill="#f8fafc"/>
<path d="M44 66 Q54 74 64 66" fill="none" stroke="#92400e" stroke-width="2.2" stroke-linecap="round"/>
<!-- BLUE pointed hat -->
<path d="M28 44 L54 2 L80 44 Z" fill="#1d4ed8"/>
<path d="M36 44 L54 10 L72 44 Z" fill="#3b82f6" opacity="0.5"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="#1e40af"/>
<ellipse cx="54" cy="42.5" rx="26" ry="5" fill="#2563eb"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="none" stroke="#f8fafc" stroke-width="2.2"/>
<polygon points="54,12 55.6,16.8 60.5,18.4 55.6,20 54,24.8 52.4,20 47.5,18.4 52.4,16.8" fill="#f8fafc"/>
<polygon points="42,22 43.1,25.2 46.2,26.4 43.1,27.6 42,30.8 40.9,27.6 37.8,26.4 40.9,25.2" fill="#fef08a"/>
<circle cx="54" cy="4" r="6" fill="#f8fafc"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Wizard flying on magic wand, blue robe">
<!-- Original Kamek-inspired mark; not Nintendo art. No filters/transforms for clean raster. -->
<!-- motion streaks -->
<line x1="4" y1="64" x2="22" y2="64" stroke="#5eead4" stroke-width="2.5" stroke-linecap="round" opacity="0.55"/>
<line x1="6" y1="74" x2="20" y2="74" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<line x1="8" y1="54" x2="20" y2="54" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<!-- ground shadow -->
<ellipse cx="64" cy="118" rx="30" ry="5" fill="#000" opacity="0.2"/>
<!-- ===== MAGIC WAND (mount) — diagonal, thick gold shaft ===== -->
<!-- sits under the rider from lower-left to upper-right -->
<line x1="6" y1="100" x2="108" y2="72" stroke="#92400e" stroke-width="11" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#e8a838" stroke-width="7" stroke-linecap="round"/>
<line x1="6" y1="100" x2="108" y2="72" stroke="#fde68a" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<!-- wand jewel (front tip) -->
<circle cx="112" cy="70" r="13" fill="#f59e0b" stroke="#fff7ed" stroke-width="3"/>
<circle cx="108" cy="66" r="4.5" fill="#fef9c3"/>
<!-- sparkles at tip -->
<polygon points="120,54 121.6,58.5 126,60 121.6,61.5 120,66 118.4,61.5 114,60 118.4,58.5" fill="#fef08a"/>
<polygon points="102,56 103,59 106,60 103,61 102,64 101,61 98,60 101,59" fill="#5eead4"/>
<circle cx="118" cy="78" r="2" fill="#bae6fd"/>
<!-- ===== RIDER on top of wand ===== -->
<!-- blue cape fluttering left -->
<path d="M38 78 C18 82, 10 98, 8 110 C22 98, 34 96, 40 92 Z" fill="#1e3a8a"/>
<path d="M40 80 C22 88, 14 104, 12 112 C28 100, 36 98, 42 94 Z" fill="#1d4ed8"/>
<!-- BLUE ROBE (body sitting ON shaft) -->
<ellipse cx="54" cy="86" rx="28" ry="22" fill="#1d4ed8"/>
<ellipse cx="54" cy="84" rx="24" ry="18" fill="#2563eb"/>
<!-- white trim -->
<path d="M32 74 Q54 94 76 74" fill="none" stroke="#f8fafc" stroke-width="4" stroke-linecap="round"/>
<path d="M36 82 Q54 98 72 82" fill="none" stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<!-- green shell -->
<ellipse cx="66" cy="92" rx="13" ry="11" fill="#16a34a"/>
<ellipse cx="66" cy="92" rx="6" ry="5" fill="#86efac" opacity="0.75"/>
<!-- legs over the wand -->
<ellipse cx="42" cy="100" rx="10" ry="7" fill="#1e40af"/>
<ellipse cx="60" cy="102" rx="10" ry="7" fill="#1e40af"/>
<!-- yellow feet hanging past shaft -->
<ellipse cx="38" cy="106" rx="7" ry="4.5" fill="#fbbf24"/>
<ellipse cx="64" cy="108" rx="7" ry="4.5" fill="#fbbf24"/>
<!-- left arm forward -->
<ellipse cx="30" cy="82" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="24" cy="78" r="5.5" fill="#fbbf24"/>
<!-- right arm up -->
<ellipse cx="74" cy="74" rx="9" ry="7" fill="#1d4ed8"/>
<circle cx="80" cy="68" r="5.5" fill="#fbbf24"/>
<circle cx="82" cy="60" r="5" fill="#38bdf8"/>
<circle cx="81" cy="58" r="1.8" fill="#e0f2fe"/>
<!-- ===== HEAD ===== -->
<ellipse cx="54" cy="50" rx="26" ry="24" fill="#fbbf24"/>
<ellipse cx="54" cy="53" rx="21" ry="17" fill="#fcd34d"/>
<ellipse cx="34" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<ellipse cx="74" cy="54" rx="6" ry="4" fill="#fb923c" opacity="0.4"/>
<!-- glasses -->
<circle cx="42" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<circle cx="66" cy="50" r="10" fill="#bae6fd" fill-opacity="0.4" stroke="#0f172a" stroke-width="3"/>
<line x1="52" y1="50" x2="56" y2="50" stroke="#0f172a" stroke-width="3"/>
<line x1="32" y1="47" x2="24" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<line x1="76" y1="47" x2="84" y2="42" stroke="#0f172a" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="44" cy="50" r="3" fill="#0f172a"/>
<circle cx="68" cy="50" r="3" fill="#0f172a"/>
<circle cx="45.2" cy="48.8" r="1.1" fill="#fff"/>
<circle cx="69.2" cy="48.8" r="1.1" fill="#fff"/>
<!-- snout + mustache -->
<ellipse cx="54" cy="62" rx="8" ry="5" fill="#f59e0b"/>
<path d="M36 64 Q46 74 54 70 Q62 74 72 64 Q64 72 54 74 Q44 72 36 64 Z" fill="#f8fafc"/>
<path d="M44 66 Q54 74 64 66" fill="none" stroke="#92400e" stroke-width="2.2" stroke-linecap="round"/>
<!-- BLUE pointed hat -->
<path d="M28 44 L54 2 L80 44 Z" fill="#1d4ed8"/>
<path d="M36 44 L54 10 L72 44 Z" fill="#3b82f6" opacity="0.5"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="#1e40af"/>
<ellipse cx="54" cy="42.5" rx="26" ry="5" fill="#2563eb"/>
<ellipse cx="54" cy="44" rx="30" ry="8" fill="none" stroke="#f8fafc" stroke-width="2.2"/>
<polygon points="54,12 55.6,16.8 60.5,18.4 55.6,20 54,24.8 52.4,20 47.5,18.4 52.4,16.8" fill="#f8fafc"/>
<polygon points="42,22 43.1,25.2 46.2,26.4 43.1,27.6 42,30.8 40.9,27.6 37.8,26.4 40.9,25.2" fill="#fef08a"/>
<circle cx="54" cy="4" r="6" fill="#f8fafc"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,86 @@
# koopa-forgejo — ROOTLESS Forgejo image + rootless podman (user hernani)
# Site: https://git.hacktivism.ch | HTTP :9024 | SSH :9200
services:
forgejo:
image: codeberg.org/forgejo/forgejo:11-rootless
container_name: koopa-forgejo
restart: unless-stopped
user: "1000:1000"
userns_mode: keep-id
env_file: [.env]
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
FORGEJO__server__DOMAIN: git.hacktivism.ch
FORGEJO__server__SSH_DOMAIN: git.hacktivism.ch
FORGEJO__server__ROOT_URL: https://git.hacktivism.ch/
FORGEJO__server__HTTP_PORT: "3000"
FORGEJO__server__SSH_PORT: "9200"
FORGEJO__server__SSH_LISTEN_PORT: "2222"
FORGEJO__server__START_SSH_SERVER: "true"
FORGEJO__server__DISABLE_SSH: "false"
FORGEJO__server__LANDING_PAGE: explore
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "false"
FORGEJO__service__SHOW_REGISTRATION_BUTTON: "false"
FORGEJO__service__REQUIRE_SIGNIN_VIEW: "false"
FORGEJO__service__DEFAULT_ALLOW_CREATE_ORGANIZATION: "true"
FORGEJO__openid__ENABLE_OPENID_SIGNIN: "false"
FORGEJO__openid__ENABLE_OPENID_SIGNUP: "false"
FORGEJO__repository__DEFAULT_PRIVATE: public
FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true"
FORGEJO__repository__ENABLE_PUSH_CREATE_ORG: "true"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__session__PROVIDER: memory
FORGEJO__log__LEVEL: Info
FORGEJO____APP_NAME: "hacktivism git"
FORGEJO__ui__DEFAULT_THEME: hacktivism
FORGEJO__ui__THEMES: hacktivism,forgejo-auto,forgejo-light,forgejo-dark,gitea-auto,gitea-light,gitea-dark
ports:
- "9024:3000"
- "9200:2222"
volumes:
- ./data:/var/lib/gitea
- ./config:/etc/gitea
- /etc/localtime:/etc/localtime:ro
depends_on:
db:
condition: service_healthy
labels:
org.hacktivism.service: forgejo
org.hacktivism.variant: rootless
org.hacktivism.host_port: "9024"
org.hacktivism.ssh_port: "9200"
org.hacktivism.site: git.hacktivism.ch
org.hacktivism.managed_by: koopa-admin
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/healthz"]
interval: 30s
timeout: 10s
retries: 10
start_period: 90s
db:
image: docker.io/library/postgres:16-alpine
container_name: koopa-forgejo-db
restart: unless-stopped
environment:
POSTGRES_USER: forgejo
POSTGRES_PASSWORD: ${FORGEJO_DB_PASSWORD}
POSTGRES_DB: forgejo
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forgejo -d forgejo"]
interval: 5s
timeout: 5s
retries: 20
start_period: 10s
volumes:
db-data:

View file

@ -0,0 +1,42 @@
# Merchant landing (`taler.hacktivism.ch` intro)
Static intro for the **GOA merchant** backend (dual currency GOA + CHF).
| Item | Value |
|------|--------|
| Container | `taler-hacktivism` |
| In-container path | `/var/www/merchant-landing/` |
| nginx | listen **9015** (`nginx-landing.conf`) |
| **Host port to publish / wire** | **`9015`** → `127.0.0.1:9015` |
| Public path | `https://taler.hacktivism.ch/intro/` |
## Features (page)
| Piece | Notes |
|-------|--------|
| Wallet fold | Collapsed “Get the wallet · Android / iOS / web browsers” |
| Web icons | Chrome · Firefox · Opera · Ubuntu Touch (gold+blue; [taler.net wallet](https://www.taler.net/de/wallet.html)) |
| GOA shop samples | `data-product` buttons → public template pay (`goa-shop` instance) |
| Link previews | Open Graph / Twitter meta → `og-goa-shop.png` (1200×630) for chats & social |
| Shop pay popup | `shop-pay.js` / `shop-pay.css`**QR_Taler** (blue ring + `qr-logo.svg`), settlement payto expandable |
| Get GOA first | Hint + link to bank (plain “pay here”) |
| Stats | Dual-currency aggregates; **recent activity** split GOA/CHF (≤5 each); refund badge violet |
| Performance | Live HTTP probes + container RSS |
Shop product ids/amounts must stay in sync with
`scripts/taler-monitoring/check_e2e.sh``E2E_SHOP_PRODUCTS`
(e2e random-picks **2** products per run by default).
## Deploy
```bash
podman cp configs/merchant-landing/index.html \
taler-hacktivism:/var/www/merchant-landing/index.html
podman cp configs/merchant-landing/og-goa-shop.png \
taler-hacktivism:/var/www/merchant-landing/og-goa-shop.png
podman cp configs/merchant-landing/shop-pay.js \
taler-hacktivism:/var/www/merchant-landing/shop-pay.js
# shop-pay.css, qr-logo.svg, qrcode.min.js as needed
```
Also see `scripts/taler-landing/deploy-landings.sh` if present.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
# Merchant landing — :9015 (behind Caddy — no port in Location)
server {
listen 9015;
listen [::]:9015;
server_name taler.hacktivism.ch _;
root /var/www/merchant-landing;
index index.html;
absolute_redirect off;
port_in_redirect off;
location = / {
return 302 /intro/;
}
location = /intro {
return 302 /intro/;
}
location /intro/ {
alias /var/www/merchant-landing/;
}
location / {
try_files $uri $uri/ =404;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,348 @@
/* GOA shop pay popup — QR_Taler style from taler-merchant-webui / @gnu-taler/web-util */
/* Animated blue ring (same as webui QR.tsx) */
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes goa-pay-qr-rotate {
to {
--angle: 360deg;
}
}
.shop-item[data-product] {
cursor: pointer;
transition: border-color 0.15s, transform 0.12s, background 0.15s;
}
.shop-item[data-product]:hover,
.shop-item[data-product]:focus-visible {
border-color: rgba(196, 181, 253, 0.75);
background: rgba(0, 0, 0, 0.4);
transform: translateY(-1px);
outline: none;
}
button.shop-item {
font: inherit;
color: inherit;
width: 100%;
appearance: none;
-webkit-appearance: none;
}
.goa-pay-modal {
position: fixed;
inset: 0;
z-index: 80;
display: none;
align-items: center;
justify-content: center;
padding: 1rem;
background: rgba(8, 6, 14, 0.75);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.goa-pay-modal.open {
display: flex;
}
.goa-pay-card {
position: relative;
background: rgba(36, 28, 48, 0.98);
border: 1px solid rgba(167, 139, 250, 0.45);
border-radius: 16px;
padding: 1.25rem 1.2rem 1.2rem;
max-width: 24rem;
width: 100%;
text-align: center;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
max-height: 92vh;
overflow-y: auto;
}
.goa-pay-x {
position: absolute;
top: 0.45rem;
right: 0.55rem;
border: 0;
background: transparent;
color: #c4b5fd;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
padding: 0.2rem 0.45rem;
}
.goa-pay-card h3 {
margin: 0 0 0.2rem;
font-size: 1.08rem;
color: #faf5ff;
padding-right: 1.5rem;
}
.goa-pay-amount {
margin: 0 0 0.85rem;
font-weight: 750;
color: #c4b5fd;
font-variant-numeric: tabular-nums;
}
/* === QR_Taler frame (webui QR.tsx) === */
.goa-pay-taler-qr {
width: 100%;
max-width: 280px;
margin: 0 auto 0.75rem;
padding: 10px;
border-radius: 20px;
box-sizing: border-box;
position: relative;
background: conic-gradient(
from var(--angle),
#0042b3 0deg,
#f1f1f4 20deg,
#f1f1f4 150deg,
#f1f1f4 160deg,
#0042b3 180deg,
#f1f1f4 200deg,
#f1f1f4 330deg,
#f1f1f4 340deg,
#0042b3
);
animation: goa-pay-qr-rotate 10s linear infinite;
}
.goa-pay-taler-qr__inner {
padding: 10px;
border-radius: 20px;
background: #fff;
line-height: 0;
min-height: 180px;
box-sizing: border-box;
}
/* Always an <img data-url> with explicit pixel size from JS */
.goa-pay-taler-qr__inner img {
display: block !important;
margin: 0 auto !important;
border: 0 !important;
background: #fff;
image-rendering: pixelated;
}
/* Center logo plate — official qr-logo.png (webui ~100×50; PNG for Android) */
.goa-pay-taler-qr__logo {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 28%;
height: auto;
max-width: 100px;
aspect-ratio: 200 / 95;
object-fit: contain;
pointer-events: none;
background: #fff;
border-radius: 4px;
}
/* Full encoded payload under QR — clickable (wallet / webextension) */
.goa-pay-payload {
display: block;
margin: 0.45rem 0 0.55rem;
padding: 0 0.25rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.68rem;
line-height: 1.35;
color: #7eb6ff;
word-break: break-all;
text-align: center;
text-decoration: none;
border-bottom: 1px solid rgba(126, 182, 255, 0.4);
cursor: pointer;
}
.goa-pay-payload:hover {
color: #b8d6ff;
border-bottom-color: #7eb6ff;
}
/* Settlement: collapsed by default, expand on click */
.goa-pay-settle {
margin-top: 0.85rem;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 12px;
overflow: hidden;
background: rgba(0, 0, 0, 0.22);
}
.goa-pay-settle-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
margin: 0;
padding: 0.65rem 0.75rem;
border: 0;
background: transparent;
color: #cbd5e1;
font: inherit;
font-size: 0.78rem;
font-weight: 650;
text-align: left;
cursor: pointer;
line-height: 1.3;
}
.goa-pay-settle-toggle:hover {
background: rgba(148, 163, 184, 0.1);
color: #e2e8f0;
}
.goa-pay-settle-toggle-text {
flex: 1;
}
.goa-pay-settle-chevron {
flex-shrink: 0;
transition: transform 0.15s ease;
color: #94a3b8;
font-size: 0.85rem;
}
.goa-pay-modal.settle-open .goa-pay-settle-chevron {
transform: rotate(90deg);
}
.goa-pay-settle-body {
padding: 0 0.75rem 0.75rem;
border-top: 1px solid rgba(148, 163, 184, 0.25);
}
.goa-pay-settle-note {
margin-top: 0.55rem;
}
/* Settlement payto QR — plain box (not wallet pay / no logo / no blue ring) */
.goa-pay-payto-qr {
width: fit-content;
max-width: 160px;
margin: 0.35rem auto 0.55rem;
padding: 0.45rem 0.5rem 0.5rem;
border-radius: 10px;
background: #fff;
border: 1px dashed rgba(100, 116, 139, 0.75);
box-sizing: border-box;
}
.goa-pay-payto-qr__caption {
font-size: 0.62rem;
font-weight: 750;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #64748b;
text-align: center;
margin: 0 0 0.35rem;
line-height: 1.2;
}
.goa-pay-payto-qr__inner {
line-height: 0;
min-height: 0;
}
.goa-pay-payto-qr__inner img {
display: block !important;
margin: 0 auto !important;
background: #fff;
image-rendering: pixelated;
}
.goa-pay-sub {
margin: 0 0 0.55rem;
font-size: 0.75rem;
line-height: 1.35;
color: #b8a8c9;
font-weight: 500;
text-align: center;
}
.goa-pay-hint {
margin: 0 0 0.75rem;
font-size: 0.82rem;
color: #b8a8c9;
}
.goa-pay-hint.err {
color: #fca5a5;
}
.goa-pay-cta {
display: block;
text-decoration: none;
font-weight: 750;
padding: 0.65rem 1rem;
border-radius: 11px;
background: linear-gradient(135deg, #a78bfa, #6366f1);
color: #0f0a1a;
margin-bottom: 0.75rem;
}
.goa-pay-cta:hover {
filter: brightness(1.06);
}
.goa-pay-cta.disabled {
opacity: 0.45;
pointer-events: none;
}
.goa-pay-label {
margin: 0.55rem 0 0.2rem;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #b8a8c9;
}
.goa-pay-line {
margin: 0 0 0.35rem;
text-align: center;
font-size: 0.88rem;
font-weight: 650;
}
.goa-pay-link {
color: #93c5fd;
text-decoration: none;
white-space: nowrap;
}
.goa-pay-link:hover {
text-decoration: underline;
}
.goa-pay-link.disabled {
opacity: 0.45;
pointer-events: none;
}
.goa-pay-sep {
margin: 0 0.4rem;
color: #b8a8c9;
font-weight: 500;
}
.goa-pay-close {
display: block;
width: 100%;
margin-top: 0.85rem;
border: 1px solid rgba(167, 139, 250, 0.45);
background: transparent;
color: #c4b5fd;
font-weight: 650;
padding: 0.5rem;
border-radius: 10px;
cursor: pointer;
}
/* Bank (gold/teal) overrides */
.goa-pay-modal.bank-theme .goa-pay-card {
background: rgba(42, 32, 24, 0.98);
border-color: rgba(232, 168, 56, 0.4);
}
.goa-pay-modal.bank-theme .goa-pay-amount {
color: #5eead4;
}
.goa-pay-modal.bank-theme .goa-pay-cta {
background: linear-gradient(135deg, #5eead4, #0d9488);
color: #042f2e;
}
.goa-pay-modal.bank-theme .goa-pay-link {
color: #5eead4;
}
.goa-pay-modal.bank-theme .goa-pay-x,
.goa-pay-modal.bank-theme .goa-pay-close {
color: #e8c878;
border-color: rgba(232, 168, 56, 0.35);
}
.goa-pay-modal.bank-theme .goa-pay-settle {
border-color: rgba(232, 168, 56, 0.3);
background: rgba(0, 0, 0, 0.28);
}
.goa-pay-modal.bank-theme .goa-pay-settle-toggle {
color: #e8c878;
}
.goa-pay-modal.bank-theme .goa-pay-settle-body {
border-top-color: rgba(232, 168, 56, 0.25);
}

View file

@ -0,0 +1,410 @@
/**
* GOA shop pay popup public only (no merchant secrets).
* Flow: POST templates/{id} taler://pay/… + payto links.
*
* QR display matches taler-merchant-webui QR_Taler
* (@gnu-taler/web-util QR.tsx): animated #0042B3 conic ring + qr-logo.png.
* Uses qrcode-generator (same lib as webui) via global QRCode if present,
* else falls back to canvas from qrcode.min.js (davidshimjs).
*/
(function () {
var MERCHANT_HOST = "taler.hacktivism.ch";
/* goa-shop: dedicated instance with fixed-order product templates */
var INSTANCE = "goa-shop";
var SHOP_PAYTO =
"payto://x-taler-bank/bank.hacktivism.ch/goa-shop?receiver-name=GOA%20Shop";
var SHOP_PAYTO_HTTPS = "https://bank.hacktivism.ch/webui/";
function introBase() {
var b = document.querySelector("base");
return b && b.href ? b.href : "/intro/";
}
function logoSrc() {
/* PNG: Android often drops SVG wordmark fill inheritance (missing "taler") */
return introBase() + "qr-logo.png";
}
function templateHttps(productId) {
return (
"https://" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/templates/" +
encodeURIComponent(productId)
);
}
function payTemplateUri(productId) {
return (
"taler://pay-template/" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/" +
encodeURIComponent(productId)
);
}
function ensureModal() {
var m = document.getElementById("goa-pay-modal");
if (m) return m;
m = document.createElement("div");
m.id = "goa-pay-modal";
m.className = "goa-pay-modal";
m.setAttribute("role", "dialog");
m.setAttribute("aria-modal", "true");
m.setAttribute("aria-labelledby", "goa-pay-title");
m.hidden = true;
m.innerHTML =
'<div class="goa-pay-card">' +
' <button type="button" class="goa-pay-x" id="goa-pay-x" aria-label="Close">×</button>' +
' <h3 id="goa-pay-title">Pay with Taler</h3>' +
' <p class="goa-pay-amount" id="goa-pay-amount"></p>' +
' <p class="goa-pay-hint" id="goa-pay-status">Preparing payment…</p>' +
' <p class="goa-pay-label">Wallet payment</p>' +
' <p class="goa-pay-sub">Scan with the GNU Taler Wallet to pay this product</p>' +
' <div class="goa-pay-taler-qr" id="goa-pay-qr-pay-wrap">' +
' <div class="goa-pay-taler-qr__inner" id="goa-pay-qr-pay"></div>' +
' <img class="goa-pay-taler-qr__logo" id="goa-pay-qr-pay-logo" alt="" />' +
" </div>" +
' <a class="goa-pay-payload" id="goa-pay-qr-payload" href="#">…</a>' +
' <a class="goa-pay-cta" id="goa-pay-open" href="#">Open payment link →</a>' +
' <p class="goa-pay-line">' +
' <a class="goa-pay-link" id="goa-pay-uri" href="#">taler:// URI</a>' +
' <span class="goa-pay-sep">·</span>' +
' <a class="goa-pay-link" id="goa-pay-uri-https" href="#" target="_blank" rel="noopener">HTTPS</a>' +
" </p>" +
' <div class="goa-pay-settle" id="goa-pay-settle">' +
' <button type="button" class="goa-pay-settle-toggle" id="goa-pay-settle-toggle" aria-expanded="false" aria-controls="goa-pay-settle-body">' +
' <span class="goa-pay-settle-toggle-text">Settlement account (payto) · not a wallet pay QR</span>' +
' <span class="goa-pay-settle-chevron" aria-hidden="true">▸</span>' +
" </button>" +
' <div class="goa-pay-settle-body" id="goa-pay-settle-body" hidden>' +
' <p class="goa-pay-sub goa-pay-settle-note">' +
" This is the merchants bank account address (payto://). " +
" The exchange wires funds here after a wallet payment. " +
" Do not scan this to buy the product — use the wallet QR above." +
" </p>" +
' <div class="goa-pay-payto-qr" id="goa-pay-qr-payto-wrap">' +
' <div class="goa-pay-payto-qr__caption">Settlement only</div>' +
' <div class="goa-pay-payto-qr__inner" id="goa-pay-qr-payto"></div>' +
" </div>" +
' <a class="goa-pay-payload" id="goa-pay-payto-payload" href="#">…</a>' +
' <p class="goa-pay-line">' +
' <a class="goa-pay-link" id="goa-pay-payto" href="#">payto:// URI</a>' +
' <span class="goa-pay-sep">·</span>' +
' <a class="goa-pay-link" id="goa-pay-payto-https" href="#" target="_blank" rel="noopener">HTTPS</a>' +
" </p>" +
" </div>" +
" </div>" +
' <button type="button" class="goa-pay-close" id="goa-pay-close">Close</button>' +
"</div>";
document.body.appendChild(m);
function close() {
m.classList.remove("open");
m.hidden = true;
// collapse settlement on close
var body = document.getElementById("goa-pay-settle-body");
var btn = document.getElementById("goa-pay-settle-toggle");
if (body) body.hidden = true;
if (btn) {
btn.setAttribute("aria-expanded", "false");
m.classList.remove("settle-open");
}
}
m.addEventListener("click", function (e) {
if (e.target === m) close();
});
document.getElementById("goa-pay-x").onclick = close;
document.getElementById("goa-pay-close").onclick = close;
document.getElementById("goa-pay-settle-toggle").onclick = function () {
var body = document.getElementById("goa-pay-settle-body");
var btn = document.getElementById("goa-pay-settle-toggle");
var open = body.hidden;
body.hidden = !open;
btn.setAttribute("aria-expanded", open ? "true" : "false");
m.classList.toggle("settle-open", open);
if (open) {
// paint payto QR when first expanded
renderQr(document.getElementById("goa-pay-qr-payto"), SHOP_PAYTO, 140);
var pp = document.getElementById("goa-pay-payto-payload");
if (pp) {
pp.href = SHOP_PAYTO;
pp.textContent = SHOP_PAYTO;
}
}
};
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && m.classList.contains("open")) close();
});
return m;
}
/**
* Render URI as a real <img> PNG (not a live canvas).
* davidshimjs paints canvas then often hides it for an img; if the modal is
* still display:none, layout collapses and you only see the blue ring.
* We always encode off-DOM and inject a fixed-size image.
*/
function renderQr(hostEl, text, size) {
if (!hostEl) return;
hostEl.innerHTML = "";
size = size || 220;
if (!text) {
hostEl.innerHTML = '<p class="goa-pay-hint">…</p>';
return;
}
if (typeof QRCode === "undefined") {
hostEl.innerHTML =
'<p class="goa-pay-hint err">QR library missing (qrcode.min.js)</p>';
return;
}
var level =
QRCode.CorrectLevel && QRCode.CorrectLevel.M != null
? QRCode.CorrectLevel.M
: QRCode.CorrectLevel && QRCode.CorrectLevel.L != null
? QRCode.CorrectLevel.L
: 1;
var scratch = document.createElement("div");
scratch.setAttribute("aria-hidden", "true");
scratch.style.cssText =
"position:fixed;left:-9999px;top:0;width:" +
size +
"px;height:" +
size +
"px;overflow:hidden;opacity:0;pointer-events:none";
document.body.appendChild(scratch);
var dataUrl = "";
try {
new QRCode(scratch, {
text: String(text),
width: size,
height: size,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: level,
});
var canvas = scratch.querySelector("canvas");
var libImg = scratch.querySelector("img");
if (canvas && canvas.width > 0) {
try {
dataUrl = canvas.toDataURL("image/png");
} catch (e1) {}
}
if (!dataUrl && libImg && libImg.src && libImg.src.indexOf("data:") === 0) {
dataUrl = libImg.src;
}
} catch (err) {
dataUrl = "";
}
if (scratch.parentNode) scratch.parentNode.removeChild(scratch);
if (!dataUrl) {
hostEl.innerHTML =
'<p class="goa-pay-hint err">QR encode failed</p>';
return;
}
var img = document.createElement("img");
img.alt = "QR code";
img.width = size;
img.height = size;
img.src = dataUrl;
img.style.display = "block";
img.style.width = size + "px";
img.style.height = size + "px";
img.style.maxWidth = "100%";
img.style.margin = "0 auto";
img.style.background = "#fff";
hostEl.appendChild(img);
}
function setLogo(imgEl) {
if (!imgEl) return;
imgEl.alt = "Taler";
imgEl.width = 100;
imgEl.height = 50;
imgEl.decoding = "async";
imgEl.src = logoSrc();
imgEl.onerror = function () {
imgEl.src = introBase() + "qr-logo.svg";
};
imgEl.style.display = "";
}
function normalizePayUri(uri) {
if (!uri) return "";
return String(uri)
.replace(/taler\.hacktivism\.ch:443/g, "taler.hacktivism.ch")
.replace(/:443\//g, "/")
.replace(/:443\?/g, "?");
}
/** Public: template → order → taler_pay_uri (no secrets). */
function createPayUri(productId) {
var url = templateHttps(productId);
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
cache: "no-store",
})
.then(function (r) {
if (!r.ok) throw new Error("template POST HTTP " + r.status);
return r.json();
})
.then(function (created) {
var oid = created.order_id;
var tok = created.token;
if (!oid || !tok) throw new Error("no order_id/token");
var statusUrl =
"https://" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/orders/" +
encodeURIComponent(oid) +
"?token=" +
encodeURIComponent(tok);
return fetch(statusUrl, { cache: "no-store" }).then(function (r) {
return r.text().then(function (t) {
var pay = "";
try {
var d = JSON.parse(t);
pay = d.taler_pay_uri || "";
} catch (e) {}
if (!pay) {
pay =
"taler://pay/" +
MERCHANT_HOST +
"/instances/" +
INSTANCE +
"/" +
oid +
"/?c=" +
tok;
}
return {
taler_pay_uri: normalizePayUri(pay),
order_id: oid,
token: tok,
status_https: statusUrl.replace(
"taler.hacktivism.ch:443",
"taler.hacktivism.ch"
),
template_https: url,
};
});
});
});
}
function showPay(productId, name, amount) {
var m = ensureModal();
if (document.body && document.body.getAttribute("data-shop-theme") === "bank") {
m.classList.add("bank-theme");
} else {
m.classList.remove("bank-theme");
}
document.getElementById("goa-pay-title").textContent = name || productId;
document.getElementById("goa-pay-amount").textContent = amount || "";
var status = document.getElementById("goa-pay-status");
status.textContent = "Preparing payment…";
status.className = "goa-pay-hint";
var open = document.getElementById("goa-pay-open");
open.removeAttribute("href");
open.classList.add("disabled");
open.onclick = null;
var payLink = document.getElementById("goa-pay-uri");
payLink.removeAttribute("href");
payLink.classList.add("disabled");
payLink.textContent = "taler:// URI";
document.getElementById("goa-pay-uri-https").href = templateHttps(productId);
document.getElementById("goa-pay-uri-https").textContent = "HTTPS";
var paytoLink = document.getElementById("goa-pay-payto");
paytoLink.href = SHOP_PAYTO;
paytoLink.textContent = "payto:// URI";
document.getElementById("goa-pay-payto-https").href = SHOP_PAYTO_HTTPS;
document.getElementById("goa-pay-payto-https").textContent = "HTTPS";
setLogo(document.getElementById("goa-pay-qr-pay-logo"));
// Reset settlement panel (collapsed until user expands)
var settleBody = document.getElementById("goa-pay-settle-body");
var settleBtn = document.getElementById("goa-pay-settle-toggle");
if (settleBody) settleBody.hidden = true;
if (settleBtn) settleBtn.setAttribute("aria-expanded", "false");
m.classList.remove("settle-open");
var paytoHost = document.getElementById("goa-pay-qr-payto");
if (paytoHost) paytoHost.innerHTML = "";
// Open first so layout exists, then paint wallet QR only
m.hidden = false;
m.classList.add("open");
var tpl = payTemplateUri(productId);
renderQr(
document.getElementById("goa-pay-qr-pay"),
tpl,
220
);
var payload = document.getElementById("goa-pay-qr-payload");
if (payload) {
payload.href = tpl;
payload.textContent = tpl;
}
createPayUri(productId)
.then(function (info) {
var pay = info.taler_pay_uri;
open.href = pay;
open.classList.remove("disabled");
open.textContent = "Open payment link →";
payLink.href = pay;
payLink.classList.remove("disabled");
payLink.textContent = "taler:// URI";
var uh = document.getElementById("goa-pay-uri-https");
uh.href = info.status_https || info.template_https;
uh.textContent = "HTTPS";
// Live unpaid taler://pay (fresh order each open)
renderQr(document.getElementById("goa-pay-qr-pay"), pay, 220);
if (payload) {
payload.href = pay;
payload.textContent = pay;
}
status.textContent = "Ready — scan in wallet";
})
.catch(function (err) {
status.textContent =
"Payment setup failed: " + (err && err.message ? err.message : err);
status.className = "goa-pay-hint err";
open.textContent = "Retry";
open.onclick = function (e) {
e.preventDefault();
showPay(productId, name, amount);
};
});
}
function bind() {
document.querySelectorAll("[data-product]").forEach(function (el) {
el.addEventListener("click", function (e) {
e.preventDefault();
var id = el.getAttribute("data-product");
var nameEl = el.querySelector(".name");
var priceEl = el.querySelector(".price");
showPay(
id,
nameEl ? nameEl.textContent.trim() : id,
priceEl ? priceEl.textContent.trim() : ""
);
});
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bind);
} else {
bind();
}
})();

25
configs/ports.md Normal file
View file

@ -0,0 +1,25 @@
# Port map (see also host/overview/services.md)
| Port | Role |
|------|------|
| 22 | sshd (WAN often via VeciGate 23235) |
| 80 | systemd `http-proxy.socket` → Caddy **9000** |
| 443 | systemd `https-proxy.socket` → Caddy **9001** |
| 9000 | Caddy HTTP + ACME webroot |
| 9001 | Caddy HTTPS (vhosts) |
| 9010 | podman `taler-hacktivism` (merchant) |
| 9011 | podman **`taler-hacktivism-exchange-ansible`** (exchange) |
| 9012 | podman `taler-hacktivism-bank` (libeufin-bank) |
| 90139015 | 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`** |
| **9200** | podman **Forgejo git-SSH** (host-direct; not Caddy) |
| 90909092 | 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 vhosts on **9001** → 127.0.0.1:{90109015, 90209025, 90909092}.
Git SSH needs separate NAT/firewall **9200/tcp** if exposed to WAN.

16
configs/prime/README.md Normal file
View file

@ -0,0 +1,16 @@
# Prime — Jellyfin + qBittorrent
| Item | Value |
|------|--------|
| Live | `/home/hernani/koopa-prime/` |
| Compose | `compose.yml` (this dir; **no secret values**) |
| Jellyfin | **9022** → Caddy `prime.hacktivism.ch` |
| qBittorrent | **9023** → Caddy `bt.hacktivism.ch` |
| Topic | `2026/2026-07-09--koopa-prime-jellyfin.md` |
`WEBUI_PASSWORD` is `${WEBUI_PASSWORD}` in the mirror — live value only in
`koopa-admin-secrets/…/koopa-prime/` / host env (not admin-log).
```bash
cd ~/koopa-prime && podman-compose up -d
```

47
configs/prime/compose.yml Normal file
View file

@ -0,0 +1,47 @@
# koopa-prime — Jellyfin + qBittorrent
# Media source: /home/hernani/Downloads
services:
jellyfin:
image: docker.io/linuxserver/jellyfin:10.10.7
container_name: koopa-prime-jellyfin
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Zurich
- JELLYFIN_PublishedServerUrl=https://prime.hacktivism.ch
volumes:
- ./config/jellyfin:/config
- ./cache:/cache
- /home/hernani/Downloads:/media/downloads:ro
ports:
- "9022:8096"
labels:
org.hacktivism.service: jellyfin
org.hacktivism.host_port: "9022"
org.hacktivism.site: prime.hacktivism.ch
org.hacktivism.managed_by: koopa-admin
qbittorrent:
image: docker.io/linuxserver/qbittorrent:5.0.4
container_name: koopa-prime-qbittorrent
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Zurich
- WEBUI_PORT=8080
- WEBUI_PASSWORD=${WEBUI_PASSWORD}
- TORRENTING_PORT=6881
volumes:
- ./config/qbittorrent:/config
- /home/hernani/Downloads:/downloads
ports:
- "9023:8080"
- "6881:6881"
- "6881:6881/udp"
labels:
org.hacktivism.service: qbittorrent
org.hacktivism.host_port: "9023"
org.hacktivism.site: bt.hacktivism.ch
org.hacktivism.managed_by: koopa-admin

View file

@ -0,0 +1,39 @@
<!-- Include CSS: goa-stats.css JS: goa-stats.js -->
<section class="stats" id="stats" aria-labelledby="stats-title" hidden>
<h2 id="stats-title">GOA flow</h2>
<div class="stats-grid">
<div class="stat">
<span class="k">Accounts</span>
<span class="v teal" id="st-accounts"></span>
</div>
<div class="stat">
<span class="k">Bank credits</span>
<span class="v teal" id="st-incoming"></span>
</div>
<div class="stat">
<span class="k">Withdraws</span>
<span class="v" id="st-withdraw"></span>
</div>
<div class="stat">
<span class="k">Unique wallets</span>
<span class="v teal" id="st-wallets"></span>
</div>
<div class="stat">
<span class="k">24h</span>
<span class="v" id="st-24h"></span>
</div>
<div class="stat">
<span class="k">7d</span>
<span class="v" id="st-7d"></span>
</div>
<div class="stat wide">
<span class="k">Recent Withdraws</span>
<ul class="wd-list" id="st-recent" aria-live="polite">
<li class="meta"></li>
</ul>
</div>
</div>
<p class="stats-foot" id="st-foot">
<a href="https://bank.hacktivism.ch/intro/">bank.hacktivism.ch</a>
</p>
</section>

View file

@ -0,0 +1,86 @@
/* Shared GOA flow stats (used on bank / exchange / merchant landings) */
.stats {
background: rgba(18, 28, 28, 0.55);
border: 1px solid rgba(62, 207, 191, 0.28);
border-radius: 16px;
padding: 1.1rem 1.15rem 1.2rem;
margin: 0 0 1.5rem;
}
.stats h2 {
margin: 0 0 0.85rem;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted, #c9b8a0);
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.65rem 0.85rem;
}
@media (min-width: 420px) {
.stats-grid { grid-template-columns: 1fr 1fr 1fr; }
}
.stat {
background: rgba(0, 0, 0, 0.22);
border-radius: 10px;
padding: 0.55rem 0.65rem;
min-width: 0;
}
.stat .k {
display: block;
font-size: 0.68rem;
color: var(--muted, #c9b8a0);
letter-spacing: 0.04em;
text-transform: uppercase;
margin-bottom: 0.2rem;
text-align: center;
}
.stat .v {
display: block;
font-size: 1.05rem;
font-weight: 750;
color: #f0d090;
word-break: break-word;
font-variant-numeric: tabular-nums;
text-align: center;
}
.stat .v.teal { color: #5eead4; }
.stat.wide { grid-column: 1 / -1; }
.wd-list {
list-style: none;
margin: 0.15rem 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.wd-list li {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.25rem 0.75rem;
font-size: 0.88rem;
font-weight: 650;
color: #f0d090;
font-variant-numeric: tabular-nums;
border-bottom: 1px solid rgba(201, 184, 160, 0.12);
padding-bottom: 0.3rem;
}
.wd-list li:last-child { border-bottom: none; padding-bottom: 0; }
.wd-list .amt { color: #5eead4; font-weight: 750; }
.wd-list .meta {
color: var(--muted, #c9b8a0);
font-size: 0.78rem;
font-weight: 600;
}
.stats-foot {
margin: 0.75rem 0 0;
font-size: 0.72rem;
color: var(--muted, #c9b8a0);
text-align: center;
}
.stats-foot a { color: #e8c878; }
.stats-foot.err { color: #f0a090; }

130
configs/shared/goa-stats.js Normal file
View file

@ -0,0 +1,130 @@
/* Load GOA flow stats from bank landing stats.json (CORS on bank). */
(function () {
var STATS_URL = "https://bank.hacktivism.ch/intro/stats.json";
var BANK_INTRO = "https://bank.hacktivism.ch/intro/";
function fmtCest(unix, fallback) {
var base = "";
if (fallback) {
base = String(fallback).replace(/\s*(CEST|CET|UTC|Z)\s*$/i, "").trim();
} else if (unix != null && unix !== "") {
try {
base = new Intl.DateTimeFormat("de-CH", {
timeZone: "Europe/Zurich",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false
}).format(new Date(Number(unix) * 1000));
} catch (e) {
base = "";
}
}
return base ? base + " Europe/Zurich" : "";
}
function set(id, v) {
var el = document.getElementById(id);
if (el) el.textContent = v == null || v === "" ? "—" : String(v);
}
function showFallback(msg) {
var box = document.getElementById("stats");
var foot = document.getElementById("st-foot");
if (box) box.hidden = false;
if (foot) {
foot.hidden = false;
foot.className = "stats-foot err";
foot.innerHTML =
(msg || "Stats offline") +
' · <a href="' + BANK_INTRO + '">bank.hacktivism.ch</a>';
}
}
function loadStats() {
var box = document.getElementById("stats");
var foot = document.getElementById("st-foot");
if (!box) return;
box.hidden = false;
// Same-origin first (bank landing), else public bank URL
var urls = [];
if (location.hostname.indexOf("bank.hacktivism") !== -1) {
urls.push("stats.json");
}
urls.push(STATS_URL);
function tryNext(i) {
if (i >= urls.length) {
showFallback("No stats");
return;
}
fetch(urls[i], { cache: "no-store", mode: "cors" })
.then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then(function (d) {
if (!d || !d.ok) throw new Error("bad stats");
var w = d.withdraws || {};
var h24 = w.last_24h || {};
var h7 = w.last_7d || {};
var ba = d.bank_accounts || {};
var wl = d.wallets || {};
var flow = d.flow || {};
var fin = flow.incoming || {};
var fwd = flow.withdraw || {};
set("st-accounts", ba.total != null ? ba.total : "—");
set("st-wallets", wl.unique_reserves != null ? wl.unique_reserves : "—");
set("st-incoming", fin.amount || flow.total_in || "—");
set("st-withdraw", fwd.amount || w.total_amount || "—");
set("st-24h", h24.amount || "GOA:0");
set("st-7d", h7.amount || "GOA:0");
var list = document.getElementById("st-recent");
if (list) {
list.innerHTML = "";
var rows = (d.recent_withdraws || []).slice(0, 3);
if (!rows.length) {
var empty = document.createElement("li");
empty.className = "meta";
empty.textContent = "—";
list.appendChild(empty);
} else {
rows.forEach(function (row) {
var li = document.createElement("li");
var amt = document.createElement("span");
amt.className = "amt";
amt.textContent = row.amount || "?";
var meta = document.createElement("span");
meta.className = "meta";
meta.textContent = fmtCest(row.at_unix, row.at || row.at_iso) || "";
li.appendChild(amt);
li.appendChild(meta);
list.appendChild(li);
});
}
}
if (foot) {
foot.hidden = false;
foot.className = "stats-foot";
foot.innerHTML =
'Source: <a href="' + BANK_INTRO + '">bank.hacktivism.ch</a>' +
(d.generated_at_human ? " · " + d.generated_at_human : "");
}
})
.catch(function () {
tryNext(i + 1);
});
}
tryNext(0);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadStats);
} else {
loadStats();
}
})();

View file

@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile

View file

@ -0,0 +1,11 @@
[Unit]
Description=Forward incoming HTTP :80 to Caddy on 127.0.0.1:9000
[Socket]
ListenStream=0.0.0.0:80
ListenStream=[::]:80
BindIPv6Only=yes
Service=https-proxy@9000.service
[Install]
WantedBy=sockets.target

View file

@ -0,0 +1,11 @@
[Unit]
Description=Forward incoming HTTPS :443 to Caddy on 127.0.0.1:9001
[Socket]
ListenStream=0.0.0.0:443
ListenStream=[::]:443
BindIPv6Only=yes
Service=https-proxy@9001.service
[Install]
WantedBy=sockets.target

View file

@ -0,0 +1,5 @@
[Unit]
Description=Proxy for incoming HTTPS traffic to port %i
[Service]
ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:%i

View file

@ -0,0 +1,26 @@
FROM docker.io/library/debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -yqq && \
apt-get install -yqq \
ansible \
cron \
git \
locales \
openssh-server \
python3 \
python3-debian \
systemd \
whois # mkpasswd provided by whois package
RUN mkdir -p /etc/ansible/facts.d
#####################################################################
## WARNING: THIS ALLOWS FOR COMPLETELY UNAUTHENTICATED SSH SESSIONS #
####### FOR TESTING ENVIRONMENT ONLY! ###############################
RUN echo "root:$(mkpasswd -s </dev/null)" | chpasswd -e
RUN sed -i'' -e's/^#PermitRootLogin prohibit-password$/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -i'' -e's/^#PasswordAuthentication yes$/PasswordAuthentication yes/' /etc/ssh/sshd_config \
&& sed -i'' -e's/^#PermitEmptyPasswords no$/PermitEmptyPasswords yes/' /etc/ssh/sshd_config \
&& sed -i'' -e's/^UsePAM yes/UsePAM no/' /etc/ssh/sshd_config

View file

@ -0,0 +1,33 @@
# Exchange on koopa — Ansible-managed container
**Live state reflected from host koopa** (not a full upstream clone of ansible-taler-exchange).
| Item | Live on koopa |
|------|----------------|
| Container name | **`taler-hacktivism-exchange-ansible`** |
| Image | `localhost/taler-hacktivism-exchange-ansible:landing` (build tag `:base`) |
| Ports | **9011** (API), **9014** (landing/intro) |
| Site | `https://exchange.hacktivism.ch` |
| Tree | `/home/hernani/ansible-taler-exchange/` |
| Secrets | `inventories/host_vars/.../secrets.yml`**not** in admin-log |
## Files here (mirrors / excerpts)
| File | Role |
|------|------|
| `run-container-koopa.sh` | Build image, run systemd container `--name taler-hacktivism-exchange-ansible` |
| `deploy-hacktivism-goa.sh` | `ansible-playbook` limit that host + public/secrets vars |
| `Containerfile` | Debian + systemd base for the container |
| `public.yml` | Non-secret host_vars for hacktivism GOA |
| `inventories-default.snippet` | Inventory lines for the koopa container host |
## Ops
```bash
cd ~/ansible-taler-exchange
./run-container-koopa.sh # recreate container if needed
./deploy-hacktivism-goa.sh # apply ansible (needs secrets.yml + vault)
podman ps --filter name=taler-hacktivism-exchange-ansible
```
Full host inventory of all services: **`host/overview/LIVE.md`**.

View file

@ -0,0 +1,25 @@
#!/bin/sh
# Deploy event-currency GOA exchange (regional-currency flow + systemd).
# Prerequisites on koopa:
# 1) ./run-container-koopa.sh # systemd container, port 9011
# 2) this script
# Inventory: ansible_connection=podman
set -eu
cd "$(dirname "$0")"
export PATH="${HOME}/venv-ansible/bin:${PATH}"
export ANSIBLE_HOST_KEY_CHECKING=False
if [ ! -f vault_pass.txt ]; then
echo unused > vault_pass.txt
fi
# Refuse obvious non-systemd targets early
if ! podman exec taler-hacktivism-exchange-ansible cat /proc/1/comm 2>/dev/null | grep -qx systemd; then
echo "error: container PID1 is not systemd. Run ./run-container-koopa.sh first." >&2
exit 1
fi
exec ansible-playbook -v \
--limit taler-hacktivism-exchange-ansible \
-e @inventories/host_vars/taler-hacktivism-exchange-ansible/public.yml \
-e @inventories/host_vars/taler-hacktivism-exchange-ansible/secrets.yml \
playbooks/setup-hacktivism-goa.yml

View file

@ -0,0 +1,6 @@
# From live ~/ansible-taler-exchange/inventories/default (koopa lines only)
# hacktivism GOA exchange: podman container on koopa (API host:9011; ansible via podman)
taler-hacktivism-exchange-ansible ansible_connection=podman ansible_user=root
[testing]
taler-hacktivism-exchange-ansible

View file

@ -0,0 +1,31 @@
---
# Event currency (GOA) — aligned with regional-currency without fiat.
# Target: Caddy → host :9011 → container exchange.
domain_name: "hacktivism.ch"
exchange_domain: "exchange.hacktivism.ch"
TARGET_HOST_NAME: "taler-hacktivism-exchange-ansible"
taler_repo_suites: trixie
# regional-currency style flags (systemd required)
require_systemd: true
do_conversion: false
# greenfield with new offline key: set true after offline key exists in container
do_offline: false
exchange_account_id: "1"
use_static_goa_coins: true
exchange_wire_method: x-taler-bank
CURRENCY: GOA
CURRENCY_ROUND_UNIT: "GOA:0.00000001"
EXCHANGE_BASE_URL: "https://exchange.hacktivism.ch/"
EXCHANGE_HTTP_PORT: 9011
EXCHANGE_MASTER_PUB: TW6K5FXF81VYCAH0YWYX0SX98KBBSJ42VX27WAX01FTFH400QG10
EXCHANGE_TERMS_ETAG: "no-terms-v0"
EXCHANGE_PP_ETAG: "no-privacy-v0"
EXCHANGE_BANK_ACCOUNT_PAYTO: "payto://x-taler-bank/bank.hacktivism.ch/exchange?receiver-name=GOA%20Exchange"
EXCHANGE_WIRE_GATEWAY_URL: "https://bank.hacktivism.ch/accounts/exchange/taler-wire-gateway/"

View file

@ -0,0 +1,45 @@
#!/bin/bash
# Recreate taler-hacktivism-exchange-ansible with systemd (regional-currency style).
# Image: Containerfile (debian + systemd + ssh). Port: only 9011.
# Ansible: ansible_connection=podman → systemctl inside container.
set -euo pipefail
NAME=taler-hacktivism-exchange-ansible
IMAGE=localhost/taler-hacktivism-exchange-ansible:base
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
podman build -f Containerfile -t "$IMAGE"
echo "Stopping/removing existing $NAME (if any)..."
podman rm -f "$NAME" 2>/dev/null || true
podman run -d \
--name "$NAME" \
--hostname "$NAME" \
--network pasta \
--systemd=always \
-p 9011:9011 \
--label org.hacktivism.service=taler-exchange \
--label org.hacktivism.host_port=9011 \
--label org.hacktivism.site=exchange.hacktivism.ch \
--label org.hacktivism.currency=GOA \
--label org.hacktivism.managed_by=ansible-taler-exchange \
--label org.hacktivism.init=systemd \
"$IMAGE" \
/usr/sbin/init
echo "waiting for systemd..."
for i in $(seq 1 30); do
if podman exec "$NAME" systemctl is-system-running 2>/dev/null | grep -Eq 'running|degraded'; then
break
fi
sleep 1
done
podman ps --filter "name=$NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}"
echo -n "PID1: "
podman exec "$NAME" cat /proc/1/comm
echo -n "systemd: "
podman exec "$NAME" systemctl is-system-running || true
echo "Next: ./deploy-hacktivism-goa.sh"

View file

@ -0,0 +1,44 @@
# Exchange config (inside ansible container)
**Live container:** `taler-hacktivism-exchange-ansible`
**Image:** `localhost/taler-hacktivism-exchange-ansible:landing`
**Deploy tree on host:** `~/ansible-taler-exchange/` → see **`configs/taler-exchange-ansible/`**
This directory mirrors **exchange software config** (overrides, coins, terms), not the Ansible wrapper itself.
Container: **`taler-hacktivism-exchange-ansible`** · image tags `localhost/taler-hacktivism-exchange-ansible-live:9011` (live) and optional snapshot
Host port: **9011** (pasta) · public: **https://exchange.hacktivism.ch/** (Caddy → 9011)
**Snapshot + restore (local tar, no registry):**
→ [`2026/2026-07-09--exchange-snapshot-and-restore.md`](../../2026/2026-07-09--exchange-snapshot-and-restore.md)
## Terms of service (wallets)
Wallets require a working `/terms` (and usually `/privacy`) with a
`Taler-Terms-Version` header. If missing, many apps hang on accept /
`isPending` during the first withdraw QR.
This site uses **minimal stubs** — no formal legal ToS:
| Config | Value |
|--------|--------|
| `TERMS_ETAG` | `no-terms-v0` |
| `PRIVACY_ETAG` | `no-privacy-v0` |
| files | `/var/lib/taler-exchange/terms/en/no-terms-v0.{html,txt,md}` etc. |
Install / refresh inside the exchange container as root:
```bash
/usr/local/bin/install_no_terms.sh
# ensure exchange-overrides.conf has TERMS_ETAG / PRIVACY_ETAG (see above)
runuser -u taler-exchange-httpd -- /usr/local/bin/start_exchange.sh --restart
curl -sS -D- http://127.0.0.1:9011/terms | head
```
Sources: `configs/taler-exchange/terms-src/` and
`scripts/taler-exchange/install_no_terms.sh`.
## Secrets
`ATTRIBUTE_ENCRYPTION_KEY` and wire/DB credentials are **not** in this tree.
See repo-root `SECRETS.md` and `koopa-admin-secrets/containers/taler-exchange/secrets/`.

Some files were not shown because too many files have changed in this diff Show more