diff --git a/VERSION b/VERSION
index d615fd0..81c871d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.9.4
+1.10.0
diff --git a/VERSIONS.md b/VERSIONS.md
index 4a9b77a..35fa01e 100644
--- a/VERSIONS.md
+++ b/VERSIONS.md
@@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre
| Tag | Date (UTC) | Notes |
|-----|------------|--------|
+| **v1.10.0** | 2026-07-19 | **Feature:** aptdeploy **container overview board** — inventory + per-pod matrix (state, merchant, httpd, ldd, OK/ERROR) in log and HTML sticky/top (`#container-overview`). |
| **v1.9.4** | 2026-07-19 | **Fix:** restore **taler-monitoring-aptdeploy(+_err)** as first-class GOA mon page (catalog + monpages path allow + surface docs). Mail/mattermost stay folded into surface. |
| **v1.9.3** | 2026-07-19 | **Bugfix (suite):** monpages merchant **code 21** always ERROR (bare no longer soft-WARN); apply hints use absolute `/home/hernani/koopa-caddy/…`; ship `host-agent/apply-monitoring-live.sh` (smoke array, bare check); fix ROOT-ON-KOOPA wrong `redir /path/ 302` examples. |
| **v1.9.2** | 2026-07-19 | **Bugfix:** Caddy bare-path redir footgun — inside `handle` use `redir * /path/ 302` (not `redir /path/ 302`, which becomes `Location: 302` and bare URLs fall through to merchant **code 21**). Snippet + ROOT-APPLY: absolute apply path (`/home/hernani/koopa-caddy/…`, not `~` as root). |
diff --git a/check_apt_deploy.sh b/check_apt_deploy.sh
index 5240e82..54d5704 100755
--- a/check_apt_deploy.sh
+++ b/check_apt_deploy.sh
@@ -9,6 +9,9 @@
# koopa-taler-deploy-test-apt-src-trixie-upgrade
# koopa-taler-deploy-test-apt-src-trixie-testing-upgrade
#
+# Emits a compact container board (board · key=value) for the HTML sticky/top
+# overview: which pods are active and which fail (httpd / ldd / …).
+#
# Env:
# APT_DEPLOY_CONTAINERS "name:suite:mode …" mode=fresh|upgrade (optional, default fresh)
# APT_DEPLOY_SKIP=1
@@ -44,6 +47,58 @@ ok "podman" "$("$PODMAN_BIN" --version 2>/dev/null | head -1)"
info "host" "$(hostname 2>/dev/null || echo unknown) · whoami=$(whoami)"
fail_any=0
+# board rows for end overview (human table + machine board · lines)
+BOARD_ROWS=()
+
+_short_name() {
+ # koopa-taler-deploy-test-apt-src-trixie-testing-upgrade → trixie-testing-upgrade
+ local n="$1"
+ n="${n#koopa-taler-deploy-test-apt-src-}"
+ printf '%s' "$n"
+}
+
+# Record one container for the top-of-page board (HTML parses "board · key=value").
+board_record() {
+ local short="$1" name="$2" suite="$3" mode="$4" state="$5"
+ local merchant="$6" httpd="$7" ldd="$8" verdict="$9" note="${10:-}"
+ local kv
+ kv="short=${short} name=${name} suite=${suite} mode=${mode} state=${state}"
+ kv+=" merchant=${merchant} httpd=${httpd} ldd=${ldd} verdict=${verdict}"
+ # note: no spaces (HTML/parser splits on whitespace)
+ if [ -n "$note" ]; then
+ note_safe=$(printf '%s' "$note" | tr ' ' '_' | tr -d '|')
+ kv+=" note=${note_safe}"
+ fi
+ info "board" "$kv"
+ BOARD_ROWS+=("$short|$suite|$mode|$state|$merchant|$httpd|$ldd|$verdict|$note")
+}
+
+print_board_table() {
+ section "aptdeploy · container overview (active / errors)"
+ info "board-table" "| short | suite | mode | podman | merchant | httpd | ldd | verdict | note |"
+ info "board-table" "|-------|-------|------|--------|----------|-------|-----|---------|------|"
+ local r short suite mode state merchant httpd ldd verdict note
+ local n_ok=0 n_err=0 n_miss=0
+ for r in "${BOARD_ROWS[@]+"${BOARD_ROWS[@]}"}"; do
+ IFS='|' read -r short suite mode state merchant httpd ldd verdict note <<<"$r"
+ info "board-table" "| ${short} | ${suite} | ${mode} | ${state} | ${merchant} | ${httpd} | ${ldd} | ${verdict} | ${note} |"
+ case "$verdict" in
+ OK|ok) n_ok=$((n_ok + 1)) ;;
+ MISSING|missing) n_miss=$((n_miss + 1)) ;;
+ *) n_err=$((n_err + 1)) ;;
+ esac
+ done
+ info "board-summary" "containers=${#BOARD_ROWS[@]} ok=${n_ok} error=${n_err} missing=${n_miss}"
+ # one-line human skim (INFO/OK only — do not re-emit ERROR, details already counted)
+ for r in "${BOARD_ROWS[@]+"${BOARD_ROWS[@]}"}"; do
+ IFS='|' read -r short suite mode state merchant httpd ldd verdict note <<<"$r"
+ if [ "$verdict" = "OK" ]; then
+ ok "overview" "${short}: ${state} · merchant=${merchant} · httpd=${httpd} · ldd=${ldd}"
+ else
+ info "overview" "${short}: ${verdict} · state=${state} merchant=${merchant} httpd=${httpd} ldd=${ldd}${note:+ · ${note}}"
+ fi
+ done
+}
print_pkg_table() {
local name=$1
@@ -58,13 +113,18 @@ print_pkg_table() {
done
' 2>/dev/null || echo "| (query failed) | |")
info "pkg-table" "$name"
- # emit as plain lines so HTML log keeps the table
while IFS= read -r line; do
[ -n "$line" ] || continue
info "pkg" "$line"
done <<<"$rows"
}
+# merchant package version only (for board)
+_merchant_ver() {
+ local name=$1
+ "$PODMAN_BIN" exec "$name" dpkg-query -W -f='${Version}' taler-merchant 2>/dev/null || echo "?"
+}
+
check_ldd_deep() {
local name=$1
local out
@@ -95,7 +155,7 @@ check_ldd_deep() {
check_systemd() {
local name=$1
- local out active_target active_httpd enabled
+ local out active_httpd
out=$("$PODMAN_BIN" exec "$name" bash -lc '
set +e
echo "system: $(systemctl is-system-running 2>&1)"
@@ -115,19 +175,19 @@ check_systemd() {
done <<<"$out"
active_httpd=$(echo "$out" | sed -n 's/^after_start_httpd: //p' | tail -1)
+ # export for caller board
+ APT_LAST_HTTPD="${active_httpd:-?}"
if [ "$active_httpd" = "active" ]; then
ok "systemd httpd" "taler-merchant-httpd active after start taler-merchant.target"
return 0
fi
- # soft if target not shipped as enabled in container — still ERROR for deploy smoke
err "systemd httpd" "taler-merchant-httpd not active" "after_start_httpd=${active_httpd:-?} · see systemd lines"
return 1
}
check_merchant_basics() {
local name=$1
- local out code
- # unix socket or curl localhost if httpd listens
+ local out
out=$("$PODMAN_BIN" exec "$name" bash -lc '
set +e
echo "=== binaries ==="
@@ -135,7 +195,6 @@ check_merchant_basics() {
echo "=== --version ==="
taler-merchant-httpd --version 2>&1
echo "=== config probe ==="
- # try common paths without TLS
for u in \
http://127.0.0.1:9966/config \
http://127.0.0.1:8081/config \
@@ -165,10 +224,6 @@ check_merchant_basics() {
err "httpd" "shared library error on --version" "see basics/ldd"
return 1
fi
- if ! echo "$out" | grep -q 'taler-merchant-httpd --version\|v\.\|taler-merchant-httpd'; then
- # version line varies; success if exit was ok — check via separate exec
- :
- fi
set +e
"$PODMAN_BIN" exec "$name" taler-merchant-httpd --version >/dev/null 2>&1
@@ -181,7 +236,6 @@ check_merchant_basics() {
return 1
fi
- # /config is nice-to-have if stack is fully configured
if echo "$out" | grep -qE 'curl .* -> 200'; then
ok "merchant /config" "HTTP 200 (local)"
else
@@ -192,7 +246,10 @@ check_merchant_basics() {
check_one() {
local name="$1" expect_suite="$2" mode="${3:-fresh}"
- local st suite_line pkgs
+ local st short merchant="-" httpd="-" ldd="-" verdict="OK" note="" c_fail=0
+
+ short="$(_short_name "$name")"
+ APT_LAST_HTTPD="-"
# group ids: aptdeploy.trixie-01 / aptdeploy.trixie-testing-upgrade-01
set_group "${expect_suite}${mode:+-$mode}"
@@ -200,12 +257,14 @@ check_one() {
if ! "$PODMAN_BIN" container exists "$name" 2>/dev/null; then
err "container" "$name missing" "run ensure-apt-deploy-test-containers.sh"
+ board_record "$short" "$name" "$expect_suite" "$mode" "missing" "-" "-" "-" "MISSING" "ensure containers"
fail_any=1
return
fi
st=$("$PODMAN_BIN" inspect -f '{{.State.Status}}' "$name" 2>/dev/null || echo unknown)
if [ "$st" != "running" ]; then
err "container" "$name not running" "status=$st"
+ board_record "$short" "$name" "$expect_suite" "$mode" "$st" "-" "-" "-" "ERROR" "not running"
fail_any=1
return
fi
@@ -217,31 +276,47 @@ check_one() {
ok "apt suite" "$expect_suite"
else
err "apt suite" "expected $expect_suite" "got: ${suite_line:-empty}"
- fail_any=1
+ c_fail=1
+ note="suite mismatch"
fi
+ merchant="$(_merchant_ver "$name")"
print_pkg_table "$name"
if ! check_merchant_basics "$name"; then
- fail_any=1
- check_ldd_deep "$name" || fail_any=1
+ c_fail=1
+ if ! check_ldd_deep "$name"; then
+ ldd="FAIL"
+ c_fail=1
+ else
+ ldd="OK"
+ fi
else
- # still show ldd summary as info when ok
ldd_out=$("$PODMAN_BIN" exec "$name" bash -lc \
'ldd "$(command -v taler-merchant-httpd)" 2>&1 | grep -E "libtalerutil|not found" || true' 2>/dev/null || true)
if echo "$ldd_out" | grep -q 'not found'; then
err "libtalerutil" "not found" "$ldd_out"
check_ldd_deep "$name" || true
- fail_any=1
+ ldd="FAIL"
+ c_fail=1
else
+ ldd="OK"
info "libtalerutil" "$(echo "$ldd_out" | tr '\n' ' ')"
fi
fi
if ! check_systemd "$name"; then
- fail_any=1
- check_ldd_deep "$name" || true
+ c_fail=1
+ httpd="${APT_LAST_HTTPD:-FAIL}"
+ # deeper ldd if httpd failed
+ if [ "$ldd" != "FAIL" ]; then
+ check_ldd_deep "$name" || { ldd="FAIL"; c_fail=1; }
+ fi
+ else
+ httpd="active"
fi
+ # normalize httpd cell
+ [ -z "$httpd" ] || [ "$httpd" = "-" ] && httpd="${APT_LAST_HTTPD:-?}"
# Disk free inside this deploy-test container (+ host once, first container only)
if [ "${_APT_DEPLOY_HOST_DISK_DONE:-0}" != "1" ]; then
@@ -251,12 +326,39 @@ check_one() {
fi
set_group disk
if ! mon_disk_check_podman "$name" "$PODMAN_BIN"; then
- fail_any=1
+ c_fail=1
+ note="${note:+$note; }disk"
fi
+
+ if [ "$c_fail" -ne 0 ]; then
+ verdict="ERROR"
+ fail_any=1
+ else
+ verdict="OK"
+ fi
+ board_record "$short" "$name" "$expect_suite" "$mode" "running" "$merchant" "$httpd" "$ldd" "$verdict" "$note"
}
+# --- inventory first (what exists before checks) ---
+section "aptdeploy · inventory (podman)"
+info "inventory" "expected tracks: trixie/trixie-testing × fresh/upgrade (4 containers)"
+for entry in $LIST; do
+ cname=${entry%%:*}
+ rest=${entry#*:}
+ case "$rest" in
+ *:*) suite=${rest%%:*}; mode=${rest#*:} ;;
+ *) suite=$rest; mode=fresh ;;
+ esac
+ short="$(_short_name "$cname")"
+ if "$PODMAN_BIN" container exists "$cname" 2>/dev/null; then
+ st=$("$PODMAN_BIN" inspect -f '{{.State.Status}}' "$cname" 2>/dev/null || echo unknown)
+ ok "inventory" "${short}: ${st} (${suite}/${mode})"
+ else
+ warn "inventory" "${short}: missing (${suite}/${mode})" "will ERROR in check"
+ fi
+done
+
for entry in $LIST; do
- # name:suite or name:suite:mode
cname=${entry%%:*}
rest=${entry#*:}
case "$rest" in
@@ -272,6 +374,9 @@ for entry in $LIST; do
check_one "$cname" "$suite" "$mode"
done
+# Compact matrix + overview ERROR lines (jump targets for HTML)
+print_board_table
+
summary
if [ "$fail_any" -ne 0 ]; then
exit 1
diff --git a/site-gen/console_to_html.py b/site-gen/console_to_html.py
index f708d31..9db83b0 100755
--- a/site-gen/console_to_html.py
+++ b/site-gen/console_to_html.py
@@ -53,6 +53,9 @@ def ui(lang: str, key: str, **kwargs) -> str:
"apt_i5": "Checks: packages, taler-merchant-httpd --version / ldd, systemd unit, optional local /config probe — not public nginx/HTTPS",
"apt_i6": "HTML published only on taler.hacktivism.ch (this page host: {host})",
"apt_i7": "Timer: taler-monitoring-aptdeploy.timer (4h)",
+ "apt_i8": "Top board: per-container podman state, merchant version, httpd, ldd, OK/ERROR (see CONTAINER OVERVIEW)",
+ "apt_board_title": "Container overview — active pods & failures",
+ "apt_board_empty": "No board · lines in log (re-run aptdeploy with suite ≥ v1.10.0).",
"focus_bank": "Libeufin bank public HTTPS + container inside checks",
"focus_exchange": "Exchange public HTTPS + container inside checks",
"focus_merchant": "Merchant backend / SPA public HTTPS + container inside checks",
@@ -125,6 +128,9 @@ def ui(lang: str, key: str, **kwargs) -> str:
"apt_i5": "Contrôles : paquets, taler-merchant-httpd --version / ldd, unité systemd, sonde locale /config optionnelle — pas de nginx/HTTPS public",
"apt_i6": "HTML publié uniquement sur taler.hacktivism.ch (hôte de cette page : {host})",
"apt_i7": "Minuterie : taler-monitoring-aptdeploy.timer (4 h)",
+ "apt_i8": "Tableau haut : état podman, version merchant, httpd, ldd, OK/ERROR par conteneur",
+ "apt_board_title": "Vue conteneurs — pods actifs et erreurs",
+ "apt_board_empty": "Pas de lignes board · dans le journal (relancer aptdeploy suite ≥ v1.10.0).",
"focus_bank": "Banque Libeufin HTTPS public + contrôles inside conteneur",
"focus_exchange": "Exchange HTTPS public + contrôles inside conteneur",
"focus_merchant": "Backend marchand / SPA HTTPS public + contrôles inside conteneur",
@@ -210,6 +216,90 @@ def is_run_timeout_text(text: str) -> bool:
return "RUN.TIMEOUT" in u or "RUN_TIMEOUT" in u or "RUN TIMEOUT" in u
+def extract_aptdeploy_board(log_text: str) -> list[dict[str, str]]:
+ """Parse `board · short=… suite=… verdict=…` lines from check_apt_deploy."""
+ rows: list[dict[str, str]] = []
+ seen: set[str] = set()
+ for ln in strip_ansi(log_text or "").splitlines():
+ if "board ·" not in ln and " board · " not in ln:
+ # also match "board ·" after tid
+ if not re.search(r"\bboard\b.*\bshort=", ln):
+ continue
+ # detail after last "board ·" or "board "
+ m = re.search(r"\bboard\b\s*·\s*(.+)$", ln)
+ if not m:
+ m = re.search(r"\bboard\b\s+(.+)$", ln)
+ if not m:
+ continue
+ detail = m.group(1).strip()
+ if "short=" not in detail:
+ continue
+ rec: dict[str, str] = {}
+ for part in detail.split():
+ if "=" in part:
+ k, v = part.split("=", 1)
+ rec[k] = v
+ short = rec.get("short", "")
+ if not short or short in seen:
+ continue
+ seen.add(short)
+ rows.append(rec)
+ return rows
+
+
+def aptdeploy_board_html(log_text: str, lang: str = "en") -> str:
+ """Compact HTML table: which containers are active and which fail."""
+ rows = extract_aptdeploy_board(log_text)
+ title = ui(lang, "apt_board_title")
+ if not rows:
+ return (
+ f' {html.escape(ui(lang, 'apt_board_empty'))}{html.escape(title)}
\n"
+ f"
{html.escape(summary)}
\n' + f'