docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable
This commit is contained in:
commit
96961f23f5
268 changed files with 24161 additions and 0 deletions
33
scripts/castopod/README.md
Normal file
33
scripts/castopod/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Castopod ops scripts (koopa)
|
||||
|
||||
Live stack: `/home/hernani/koopa-castopod/` (podman-compose).
|
||||
These scripts live in admin-log and should be **copied** to the host when changed.
|
||||
|
||||
|
||||
## Usage (on koopa as `hernani`)
|
||||
|
||||
```bash
|
||||
# copy once
|
||||
mkdir -p ~/koopa-castopod/bin
|
||||
cp /path/to/koopa-admin-log/scripts/castopod/{lib.sh,status.sh,up.sh,install-systemd.sh,apply-branding.sh} \
|
||||
~/koopa-castopod/bin/
|
||||
chmod +x ~/koopa-castopod/bin/*.sh
|
||||
|
||||
~/koopa-castopod/bin/status.sh
|
||||
~/koopa-castopod/bin/up.sh
|
||||
~/koopa-castopod/bin/install-systemd.sh # boot units
|
||||
~/koopa-castopod/bin/apply-branding.sh # logo + theme + optional overlay
|
||||
```
|
||||
|
||||
Env overrides: `CP_MAX_TIME`, `CP_PULL_TIMEOUT`, `CP_HEALTH_TRIES`, `CP_BASEURL`,
|
||||
`CP_SITE_NAME`, `CP_SITE_DESCRIPTION`, `CP_THEME` (default `amber`), `CP_APPLY_OVERLAY` (default `1`).
|
||||
|
||||
Passwords stay in `~/koopa-castopod/.env` and `users.env` (mode 600) — never in admin-log.
|
||||
|
||||
## Official branding docs
|
||||
|
||||
Instance settings (name, description, site icon, six accent themes):
|
||||
|
||||
https://docs.castopod.org/main/en/user-guide/instance/settings/
|
||||
|
||||
See also `configs/castopod/README.md` and `2026/2026-07-13--castopod-boot-branding.md`.
|
||||
137
scripts/castopod/apply-branding.sh
Executable file
137
scripts/castopod/apply-branding.sh
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env bash
|
||||
# Apply hacktivism branding to Castopod (official settings + optional CSS overlay).
|
||||
#
|
||||
# Official docs (instance settings — site name/description/icon + 6 accent themes):
|
||||
# https://docs.castopod.org/main/en/user-guide/instance/settings/
|
||||
# Docker/compose background:
|
||||
# https://docs.castopod.org/main/en/getting-started/docker/
|
||||
#
|
||||
# Logo: same magician mark as git.hacktivism.ch / bonfire.hacktivism.ch
|
||||
# (configs/castopod/assets/img/logo.png — ≥512×512 required by Castopod).
|
||||
# Official accent closest to gold: "amber" (among pine/crimson/lake/amber/jacaranda/onyx).
|
||||
# Optional warm-dark CSS: NOT official — see configs/castopod/assets/css/theme-hacktivism-overlay.css
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${KOOPA_CASTOPOD_ROOT:-$HOME/koopa-castopod}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || true)"
|
||||
# shellcheck source=lib.sh
|
||||
source "${SCRIPT_DIR}/lib.sh"
|
||||
|
||||
: "${CP_BASEURL:=https://castopod.hacktivism.ch}"
|
||||
: "${CP_ADMIN_GATEWAY:=cp-admin}"
|
||||
: "${CP_SITE_NAME:=hacktivism castopod}"
|
||||
: "${CP_SITE_DESCRIPTION:=Castopod on hacktivism.ch — FOSS Airwaves and free-culture podcasting.}"
|
||||
: "${CP_THEME:=amber}"
|
||||
: "${CP_APPLY_OVERLAY:=1}"
|
||||
|
||||
BRANDING_DST="${ROOT}/branding"
|
||||
ICON_SRC=""
|
||||
CSS_SRC=""
|
||||
|
||||
if [[ -d "${REPO_ROOT}/configs/castopod/assets/img" ]]; then
|
||||
ICON_SRC="${REPO_ROOT}/configs/castopod/assets/img/logo.png"
|
||||
CSS_SRC="${REPO_ROOT}/configs/castopod/assets/css/theme-hacktivism-overlay.css"
|
||||
elif [[ -f "${ROOT}/branding/logo.png" ]]; then
|
||||
ICON_SRC="${ROOT}/branding/logo.png"
|
||||
CSS_SRC="${ROOT}/branding/theme-hacktivism-overlay.css"
|
||||
else
|
||||
echo "missing logo.png (run from koopa-admin-log checkout or copy assets to ${ROOT}/branding/)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d -m 755 "${BRANDING_DST}"
|
||||
if [[ "$(realpath "${ICON_SRC}" 2>/dev/null || echo "${ICON_SRC}")" != "$(realpath "${BRANDING_DST}/logo.png" 2>/dev/null || echo "${BRANDING_DST}/logo.png")" ]]; then
|
||||
install -m 644 "${ICON_SRC}" "${BRANDING_DST}/logo.png"
|
||||
fi
|
||||
if [[ -n "${CSS_SRC}" && -f "${CSS_SRC}" ]]; then
|
||||
if [[ "$(realpath "${CSS_SRC}" 2>/dev/null || echo "${CSS_SRC}")" != "$(realpath "${BRANDING_DST}/theme-hacktivism-overlay.css" 2>/dev/null || true)" ]]; then
|
||||
install -m 644 "${CSS_SRC}" "${BRANDING_DST}/theme-hacktivism-overlay.css"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Mount overlay into public assets if compose branding volume is present
|
||||
if [[ -d "${ROOT}/branding" ]]; then
|
||||
# Ensure host path for compose bind exists before recreate
|
||||
true
|
||||
fi
|
||||
|
||||
COOKIE=$(mktemp /tmp/cp-brand-cj.XXXXXX)
|
||||
trap 'rm -f "$COOKIE"' EXIT
|
||||
|
||||
echo "== admin login =="
|
||||
cp_admin_login "$COOKIE"
|
||||
|
||||
csrf_from() {
|
||||
local url=$1
|
||||
local html
|
||||
html=$(cp_curl -c "$COOKIE" -b "$COOKIE" "$url")
|
||||
# multiple possible CSRF field names
|
||||
printf '%s' "$html" | sed -n 's/.*name="csrf_test_name" value="\([^"]*\)".*/\1/p' | head -1
|
||||
}
|
||||
|
||||
echo "== official: general settings (site name/description/icon) =="
|
||||
# UI/docs: https://docs.castopod.org/main/en/user-guide/instance/settings/
|
||||
# POST route: /{admin}/settings/instance (SettingsController::attemptInstanceEdit)
|
||||
GEN_GET="${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings"
|
||||
GEN_POST="${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings/instance"
|
||||
csrf=$(csrf_from "$GEN_GET")
|
||||
[[ -n "$csrf" ]] || { echo "ERROR: no CSRF on settings general" >&2; exit 1; }
|
||||
|
||||
code=$(cp_curl -c "$COOKIE" -b "$COOKIE" -o /tmp/cp-gen.out -w '%{http_code}' \
|
||||
-X POST "${GEN_POST}" \
|
||||
-F "csrf_test_name=${csrf}" \
|
||||
-F "site_name=${CP_SITE_NAME}" \
|
||||
-F "site_description=${CP_SITE_DESCRIPTION}" \
|
||||
-F "site_icon=@${BRANDING_DST}/logo.png;type=image/png")
|
||||
echo "general POST → HTTP $code"
|
||||
# accept redirect
|
||||
if [[ "$code" != "303" && "$code" != "302" && "$code" != "200" ]]; then
|
||||
echo "WARN: unexpected status; body:" >&2
|
||||
head -c 400 /tmp/cp-gen.out >&2 || true
|
||||
fi
|
||||
|
||||
echo "== official: theme accent (${CP_THEME}) =="
|
||||
THEME_URL="${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings/theme"
|
||||
csrf=$(csrf_from "$THEME_URL")
|
||||
[[ -n "$csrf" ]] || { echo "ERROR: no CSRF on settings theme" >&2; exit 1; }
|
||||
|
||||
code=$(cp_curl -c "$COOKIE" -b "$COOKIE" -o /tmp/cp-theme.out -w '%{http_code}' \
|
||||
-X POST "${THEME_URL}" \
|
||||
--data-urlencode "csrf_test_name=${csrf}" \
|
||||
--data-urlencode "theme=${CP_THEME}")
|
||||
echo "theme POST → HTTP $code"
|
||||
|
||||
if [[ "${CP_APPLY_OVERLAY}" == "1" && -f "${BRANDING_DST}/theme-hacktivism-overlay.css" ]]; then
|
||||
echo "== optional: warm-dark CSS overlay (not official) =="
|
||||
# Serve from container public path via bind (compose) or copy into media volume
|
||||
podman cp "${BRANDING_DST}/theme-hacktivism-overlay.css" \
|
||||
koopa-castopod:/app/public/assets/hacktivism-overlay.css 2>/dev/null || true
|
||||
|
||||
# Idempotent inject before </head> in public layouts (re-run after image upgrade)
|
||||
# Themes are root-owned in the image — patch as root (re-run after image upgrade).
|
||||
# shellcheck disable=SC2016
|
||||
podman exec -u 0 koopa-castopod sh -c '
|
||||
LINK="<!-- hacktivism-overlay --><link rel=\"stylesheet\" href=\"/assets/hacktivism-overlay.css\">"
|
||||
for f in /app/themes/cp_app/home.php \
|
||||
/app/themes/cp_app/podcast/_layout.php \
|
||||
/app/themes/cp_app/episode/_layout.php \
|
||||
/app/themes/cp_app/pages/_layout.php \
|
||||
/app/themes/cp_app/embed.php; do
|
||||
[ -f "$f" ] || continue
|
||||
if grep -q "hacktivism-overlay" "$f"; then
|
||||
echo "already: $f"
|
||||
continue
|
||||
fi
|
||||
awk -v link="$LINK" "{ if (\$0 ~ /<\\/head>/ && !done) { print link; done=1 } print }" "$f" > "$f.tmp" \
|
||||
&& mv "$f.tmp" "$f" \
|
||||
&& echo "patched: $f"
|
||||
done
|
||||
' || echo "WARN: layout patch skipped (container missing?)"
|
||||
fi
|
||||
|
||||
# Clear page cache so theme/name show up
|
||||
podman exec koopa-castopod sh -c 'rm -rf /app/writable/cache/* 2>/dev/null || true' || true
|
||||
|
||||
echo "Done. Hard-reload https://castopod.hacktivism.ch/"
|
||||
echo "Official UI also: ${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings (and /settings/theme)"
|
||||
41
scripts/castopod/install-systemd.sh
Executable file
41
scripts/castopod/install-systemd.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install Castopod user systemd units on koopa (boot via linger, like Bonfire).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
UNIT_DIR="${HOME}/.config/systemd/user"
|
||||
BIN_DIR="${HOME}/koopa-castopod/bin"
|
||||
|
||||
mkdir -p "${UNIT_DIR}" "${BIN_DIR}"
|
||||
|
||||
for f in lib.sh status.sh up.sh apply-branding.sh; do
|
||||
if [[ -f "${ROOT}/scripts/castopod/${f}" ]]; then
|
||||
install -m 755 "${ROOT}/scripts/castopod/${f}" "${BIN_DIR}/${f}"
|
||||
fi
|
||||
done
|
||||
|
||||
cp "${ROOT}/configs/castopod/container-koopa-castopod-mariadb.service" "${UNIT_DIR}/"
|
||||
cp "${ROOT}/configs/castopod/container-koopa-castopod-redis.service" "${UNIT_DIR}/"
|
||||
cp "${ROOT}/configs/castopod/container-koopa-castopod.service" "${UNIT_DIR}/"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable \
|
||||
container-koopa-castopod-mariadb.service \
|
||||
container-koopa-castopod-redis.service \
|
||||
container-koopa-castopod.service
|
||||
|
||||
if ! podman ps --format '{{.Names}}' | grep -qx koopa-castopod; then
|
||||
(cd "${HOME}/koopa-castopod" && set -a && source .env && set +a && podman-compose up -d)
|
||||
fi
|
||||
|
||||
systemctl --user start \
|
||||
container-koopa-castopod-mariadb.service \
|
||||
container-koopa-castopod-redis.service \
|
||||
container-koopa-castopod.service
|
||||
|
||||
systemctl --user --no-pager status \
|
||||
container-koopa-castopod-mariadb.service \
|
||||
container-koopa-castopod-redis.service \
|
||||
container-koopa-castopod.service
|
||||
|
||||
echo "OK. Needs: loginctl enable-linger hernani (already set on koopa)."
|
||||
echo "Public: https://castopod.hacktivism.ch/"
|
||||
116
scripts/castopod/lib.sh
Normal file
116
scripts/castopod/lib.sh
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# shellcheck shell=bash
|
||||
# Shared helpers for Castopod ops on koopa — fail fast, never hang forever.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- defaults (override via env) ---
|
||||
: "${CP_BASEURL:=https://castopod.hacktivism.ch}"
|
||||
: "${CP_COMPOSE_DIR:=/home/hernani/koopa-castopod}"
|
||||
: "${CP_CONNECT_TIMEOUT:=10}" # seconds — TCP connect
|
||||
: "${CP_MAX_TIME:=120}" # seconds — whole HTTP transfer
|
||||
: "${CP_PULL_TIMEOUT:=600}" # seconds — image pull
|
||||
: "${CP_HEALTH_TRIES:=30}" # health poll attempts
|
||||
: "${CP_HEALTH_SLEEP:=5}" # seconds between polls
|
||||
|
||||
# curl that cannot hang forever
|
||||
cp_curl() {
|
||||
curl -sS \
|
||||
--connect-timeout "${CP_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CP_MAX_TIME}" \
|
||||
--retry 2 \
|
||||
--retry-delay 2 \
|
||||
--retry-connrefused \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# HTTP status only (no hang)
|
||||
cp_http_code() {
|
||||
local url=$1
|
||||
cp_curl -o /dev/null -w '%{http_code}' "$url" || echo "000"
|
||||
}
|
||||
|
||||
# Run command with hard wall-clock limit (GNU coreutils timeout)
|
||||
cp_timeout() {
|
||||
local secs=$1
|
||||
shift
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout --signal=TERM --kill-after=15s "${secs}" "$@"
|
||||
else
|
||||
# fallback: no timeout binary — still run, but warn
|
||||
echo "WARN: timeout(1) missing; running without wall limit: $*" >&2
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Non-interactive answer stream for spark / CLI that prompts [y,n] or passwords.
|
||||
# Usage: cp_yes | podman exec -i … php spark …
|
||||
# Prefer SQL for activate when possible (spark prompts hang without TTY).
|
||||
cp_yes() {
|
||||
# enough y's for a few prompts; never block waiting for input
|
||||
yes y 2>/dev/null | head -n 20
|
||||
}
|
||||
|
||||
# Poll until URL returns expected code or give up
|
||||
cp_wait_http() {
|
||||
local url=$1
|
||||
local want=${2:-200}
|
||||
local i code
|
||||
for ((i = 1; i <= CP_HEALTH_TRIES; i++)); do
|
||||
code=$(cp_http_code "$url")
|
||||
echo "health try $i/${CP_HEALTH_TRIES}: $url → $code"
|
||||
if [[ "$code" == "$want" || "$code" =~ ^[23] ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep "${CP_HEALTH_SLEEP}"
|
||||
done
|
||||
echo "ERROR: $url still not healthy after ${CP_HEALTH_TRIES} tries (last=$code)" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# MariaDB password from compose .env (no hang if missing)
|
||||
cp_mysql_password() {
|
||||
local envf="${CP_COMPOSE_DIR}/.env"
|
||||
[[ -f "$envf" ]] || { echo "ERROR: missing $envf" >&2; return 1; }
|
||||
# shellcheck disable=SC1090
|
||||
grep -E '^MYSQL_PASSWORD=' "$envf" | head -1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
cp_mysql() {
|
||||
local pw
|
||||
pw=$(cp_mysql_password)
|
||||
podman exec koopa-castopod-mariadb \
|
||||
mariadb -ucastopod -p"$pw" castopod "$@"
|
||||
}
|
||||
|
||||
# Activate shield users without spark interactive prompt
|
||||
cp_activate_users() {
|
||||
cp_mysql -e "UPDATE cp_users SET active=1 WHERE username IN ('admin','ngi');"
|
||||
}
|
||||
|
||||
# Session login for admin UI automation (uses users.env)
|
||||
cp_admin_login() {
|
||||
local cookie=${1:-/tmp/cp-cj}
|
||||
local envf="${CP_COMPOSE_DIR}/users.env"
|
||||
[[ -f "$envf" ]] || { echo "ERROR: missing $envf" >&2; return 1; }
|
||||
# shellcheck disable=SC1090
|
||||
source "$envf"
|
||||
[[ -n "${ADMIN_PW:-}" ]] || { echo "ERROR: ADMIN_PW empty" >&2; return 1; }
|
||||
|
||||
rm -f "$cookie"
|
||||
cp_curl -c "$cookie" -b "$cookie" "${CP_BASEURL}/cp-auth/login" -o /tmp/cp-login.html
|
||||
local csrf
|
||||
csrf=$(sed -n 's/.*name="csrf_test_name" value="\([^"]*\)".*/\1/p' /tmp/cp-login.html | head -1)
|
||||
[[ -n "$csrf" ]] || { echo "ERROR: no CSRF on login page" >&2; return 1; }
|
||||
|
||||
local code
|
||||
code=$(cp_curl -c "$cookie" -b "$cookie" -o /dev/null -w '%{http_code}' \
|
||||
-X POST "${CP_BASEURL}/cp-auth/login" \
|
||||
--data-urlencode "csrf_test_name=${csrf}" \
|
||||
--data-urlencode "email=admin@castopod.hacktivism.ch" \
|
||||
--data-urlencode "password=${ADMIN_PW}")
|
||||
# 303 see other is success
|
||||
if [[ "$code" != "303" && "$code" != "302" && "$code" != "200" ]]; then
|
||||
echo "ERROR: login HTTP $code" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
21
scripts/castopod/status.sh
Normal file
21
scripts/castopod/status.sh
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
# Quick Castopod health — bounded network, no hangs.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=lib.sh
|
||||
source "${ROOT}/lib.sh"
|
||||
|
||||
echo "== podman =="
|
||||
podman ps --filter name=koopa-castopod --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' || true
|
||||
|
||||
echo "== local backend :9020 =="
|
||||
code=$(cp_http_code "http://127.0.0.1:9020/health" || true)
|
||||
echo "local health → ${code:-000} (307 redirect to public is OK)"
|
||||
|
||||
echo "== public =="
|
||||
# Note: do not put bare @url in curl -w; @ means "read file" in some curl contexts.
|
||||
for path in "/" "/@foss" "/@foss/feed.xml" "/@foss/episodes/four-freedoms"; do
|
||||
url="${CP_BASEURL}${path}"
|
||||
code=$(cp_http_code "$url")
|
||||
printf ' %s → %s\n' "$url" "$code"
|
||||
done
|
||||
32
scripts/castopod/up.sh
Normal file
32
scripts/castopod/up.sh
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
# Start Castopod stack with hard timeouts on pull/up (no infinite hang).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=lib.sh
|
||||
source "${ROOT}/lib.sh"
|
||||
|
||||
cd "${CP_COMPOSE_DIR}"
|
||||
|
||||
echo "== pull (max ${CP_PULL_TIMEOUT}s) =="
|
||||
cp_timeout "${CP_PULL_TIMEOUT}" podman-compose pull
|
||||
|
||||
echo "== up =="
|
||||
cp_timeout 180 podman-compose up -d
|
||||
|
||||
echo "== wait for app on :9020 =="
|
||||
# Castopod often 307 from /health to https — accept 2xx/3xx
|
||||
ok=0
|
||||
for ((i = 1; i <= CP_HEALTH_TRIES; i++)); do
|
||||
code=$(cp_http_code "http://127.0.0.1:9020/" || true)
|
||||
echo "try $i: local / → $code"
|
||||
if [[ "$code" =~ ^[23] ]]; then
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep "${CP_HEALTH_SLEEP}"
|
||||
done
|
||||
[[ "$ok" -eq 1 ]] || { echo "ERROR: app not answering on 9020"; podman-compose ps; exit 1; }
|
||||
|
||||
echo "== public via Caddy =="
|
||||
cp_wait_http "${CP_BASEURL}/" || true
|
||||
echo "done."
|
||||
Loading…
Add table
Add a link
Reference in a new issue