Files
Anthias/bin/collect_debug.sh
Viktor Petersson 7acda8ca8d fix(server): serve remote-hosted image/video previews from their source URL (#3211)
The asset preview and download endpoints decided whether to redirect to
the source URL or stream a local file by mimetype (webpage/streaming
only). A remote-hosted image is stored mimetype=image with an http(s)
URI, so it fell through to the local-file lookup, found no file on disk,
and 302'd to the home page — a blank preview in the management UI.

Dispatch on the URI scheme instead: any http(s) asset redirects to its
source, and only filesystem-path assets are served from disk. rtsp/rtmp
streams and unknown schemes still fall back to the home page as before.

Also fix the DB section of collect_debug.sh: its host-sqlite3-missing
fallback imported the pre-#2818 module path (anthias_app.models), so the
bundle carried a Python traceback instead of any asset data. The server
container already ships sqlite3 and bind-mounts the DB, so run the same
read-only queries there, and surface is_reachable — the field that
distinguishes a failed reachability probe from a working one when
triaging "web content doesn't display" reports.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 06:23:20 +01:00

537 lines
22 KiB
Bash
Executable File

#!/bin/bash
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- sh-basic-offset: 4 -*-
# collect_debug.sh — gather everything needed to debug a faulty Anthias
# installation into a single, shareable archive.
#
# Pulls host/system info, Docker + compose state, per-container logs
# (journald-tagged in production), the Anthias config and database
# overview, an ffprobe of every video asset, network reachability,
# Redis health, and Raspberry Pi specifics (throttling, temperature).
#
# A final PII-scrub pass redacts IP/MAC addresses, email addresses,
# URL-embedded credentials, the device hostname, and the secrets in
# anthias.conf from every file before the archive is built — so the
# bundle is safe to attach to a public GitHub issue or forum post.
#
# Safe to run on a live device — it is read-only and never touches the
# running stack. Output is a tarball under the user's home directory
# (or wherever --output points).
#
# Usage:
# bin/collect_debug.sh [--output DIR] [--lines N] [--no-archive]
#
# --output DIR Where to write the report dir/archive (default: $HOME)
# --lines N Log lines to capture per container (default: 2000)
# --no-archive Leave the report directory in place, skip the tarball
# -h, --help Show this help
set -uo pipefail
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
LOG_LINES=2000
MAKE_ARCHIVE=1
OUTPUT_BASE=""
usage() {
cat <<'EOF'
collect_debug.sh — collect a redacted debug bundle for a faulty Anthias install.
Usage:
bin/collect_debug.sh [--output DIR] [--lines N] [--no-archive]
--output DIR Where to write the report dir/archive (default: $HOME)
--lines N Log lines to capture per container (default: 2000)
--no-archive Leave the report directory in place, skip the tarball
-h, --help Show this help
A final pass redacts IP/MAC addresses, emails, URL credentials, the
device hostname, and anthias.conf secrets from every file before the
archive is built.
EOF
exit "${1:-0}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--output)
OUTPUT_BASE="${2:-}"
shift 2
;;
--lines)
LOG_LINES="${2:-2000}"
shift 2
;;
--no-archive)
MAKE_ARCHIVE=0
shift
;;
-h|--help)
usage 0
;;
*)
echo "Unknown argument: $1" >&2
usage 1
;;
esac
done
# ---------------------------------------------------------------------------
# Environment discovery
# ---------------------------------------------------------------------------
# Resolve the operator's home dir without trusting $HOME (this may run
# under sudo, where $HOME points at /root). Mirrors migrate_legacy_paths.sh.
RUN_USER="${SUDO_USER:-${USER:-$(id -un)}}"
USER_HOME="$(getent passwd "$RUN_USER" 2>/dev/null | cut -d: -f6)"
USER_HOME="${USER_HOME:-/home/${RUN_USER}}"
# The repo / compose dir and the config dir, with legacy fallbacks.
ANTHIAS_DIR="${USER_HOME}/anthias"
[[ -d "$ANTHIAS_DIR" ]] || ANTHIAS_DIR="${USER_HOME}/screenly"
CONFIG_DIR="${USER_HOME}/.anthias"
[[ -d "$CONFIG_DIR" ]] || CONFIG_DIR="${USER_HOME}/.screenly"
COMPOSE_FILE="${ANTHIAS_DIR}/docker-compose.yml"
SSL_OVERRIDE="${ANTHIAS_DIR}/docker-compose.ssl.override.yml"
# docker compose may need sudo (the install adds the user to the docker
# group, but a freshly-installed session might not have picked it up).
DOCKER=(docker)
if ! docker info >/dev/null 2>&1; then
if sudo -n docker info >/dev/null 2>&1; then
DOCKER=(sudo docker)
elif sudo docker info >/dev/null 2>&1; then
DOCKER=(sudo docker)
fi
fi
COMPOSE_ARGS=(compose)
if [[ -f "$COMPOSE_FILE" ]]; then
COMPOSE_ARGS+=(-f "$COMPOSE_FILE")
[[ -f "$SSL_OVERRIDE" ]] && COMPOSE_ARGS+=(-f "$SSL_OVERRIDE")
fi
# Production tags the containers on the journald driver; capture by tag
# as well as by container so we get logs even if the compose file moved.
CONTAINER_TAGS=(anthias-server anthias-viewer anthias-celery anthias-redis anthias-caddy)
# Captured up front so the PII-scrub pass can redact this device's
# hostname (it appears in journald prefixes, `uname -a`, and the README).
HOSTNAME_VAL="$(hostname 2>/dev/null || true)"
# ---------------------------------------------------------------------------
# Output layout
# ---------------------------------------------------------------------------
OUTPUT_BASE="${OUTPUT_BASE:-$USER_HOME}"
STAMP="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo unknown)"
REPORT_NAME="anthias-debug-${STAMP}"
REPORT_DIR="${OUTPUT_BASE}/${REPORT_NAME}"
LOG_DIR="${REPORT_DIR}/logs"
mkdir -p "$LOG_DIR" || {
echo "Could not create report directory under ${OUTPUT_BASE}" >&2
exit 1
}
# Run a command, label it, and tee both stdout and stderr into a file.
# Never aborts the script — a missing tool just records the error.
section() {
local title="$1" outfile="$2"
shift 2
{
echo "########################################################"
echo "# ${title}"
echo "# \$ $*"
echo "########################################################"
"$@" 2>&1
echo
} >>"$outfile" 2>&1
}
note() { echo " - $1"; }
# Final-pass PII redaction over every file in the report. Runs once, just
# before archiving, so it covers logs, configs, ffprobe output and the
# README alike. Order matters: MAC addresses are redacted before the
# IPv6 rule (a MAC otherwise looks like a 6-group IPv6). 127.0.0.1 and
# 0.0.0.0 are deliberately preserved — they're non-identifying constants
# that keep bind/route lines readable.
scrub_pii() {
local f hostname_re
while IFS= read -r -d '' f; do
# Shield the keep-list constants from the IPv4 rule below.
sed -i \
-e 's/127\.0\.0\.1/__KEEP_LOOPBACK__/g' \
-e 's/\b0\.0\.0\.0\b/__KEEP_ANY__/g' \
"$f"
sed -i -E \
-e 's#(://)[^/@[:space:]]+@#\1<redacted-credentials>@#g' \
-e 's#[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}#<redacted-email>#g' \
-e 's#([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}#<redacted-mac>#g' \
-e 's#([0-9A-Fa-f]{1,4}:){4,}[0-9A-Fa-f]{1,4}#<redacted-ipv6>#g' \
-e 's#\b([0-9]{1,3}\.){3}[0-9]{1,3}\b#<redacted-ip>#g' \
"$f"
sed -i \
-e 's/__KEEP_LOOPBACK__/127.0.0.1/g' \
-e 's/__KEEP_ANY__/0.0.0.0/g' \
"$f"
# The device hostname is freeform text, so redact the literal
# value wherever it lands. Escape regex metacharacters first.
if [[ -n "$HOSTNAME_VAL" && "${#HOSTNAME_VAL}" -ge 3 ]]; then
hostname_re="$(printf '%s' "$HOSTNAME_VAL" | sed 's/[][\.*^$/]/\\&/g')"
sed -i "s/${hostname_re}/<redacted-hostname>/g" "$f"
fi
done < <(find "$REPORT_DIR" -type f -print0)
}
echo "Collecting Anthias debug bundle..."
echo " user : ${RUN_USER}"
echo " anthias dir : ${ANTHIAS_DIR}"
echo " config dir : ${CONFIG_DIR}"
echo " output : ${REPORT_DIR}"
echo
# ---------------------------------------------------------------------------
# 1. System / host
# ---------------------------------------------------------------------------
SYS="${REPORT_DIR}/system.txt"
note "system & hardware"
section "Date / uptime" "$SYS" date
section "Uptime" "$SYS" uptime
section "OS release" "$SYS" cat /etc/os-release
section "Kernel / arch" "$SYS" uname -a
section "CPU / arch detail" "$SYS" lscpu
section "Memory" "$SYS" free -h
section "Disk usage" "$SYS" df -h
section "Inode usage" "$SYS" df -ih
section "Top memory consumers" "$SYS" sh -c 'ps -eo pid,ppid,user,%cpu,%mem,rss,comm --sort=-%mem | head -20'
section "Mounts" "$SYS" mount
# Raspberry Pi specifics — model, firmware, throttling, temperature.
note "raspberry pi specifics (if present)"
PI="${REPORT_DIR}/raspberry-pi.txt"
section "Device model" "$PI" cat /proc/device-tree/model
section "Throttling status" "$PI" vcgencmd get_throttled
section "Core temperature" "$PI" vcgencmd measure_temp
section "Firmware version" "$PI" vcgencmd version
section "Memory split" "$PI" vcgencmd get_mem arm
section "config.txt" "$PI" sh -c 'cat /boot/firmware/config.txt 2>/dev/null || cat /boot/config.txt 2>/dev/null'
# ---------------------------------------------------------------------------
# 2. Docker / compose state
# ---------------------------------------------------------------------------
note "docker engine & compose state"
DOCK="${REPORT_DIR}/docker.txt"
section "Docker version" "$DOCK" "${DOCKER[@]}" version
section "Docker info" "$DOCK" "${DOCKER[@]}" info
section "All containers" "$DOCK" "${DOCKER[@]}" ps -a
section "Images" "$DOCK" "${DOCKER[@]}" images
section "Volumes" "$DOCK" "${DOCKER[@]}" volume ls
section "Disk usage (docker)" "$DOCK" "${DOCKER[@]}" system df
if [[ -f "$COMPOSE_FILE" ]]; then
section "Compose ps" "$DOCK" "${DOCKER[@]}" "${COMPOSE_ARGS[@]}" ps -a
section "Compose config (resolved)" "$DOCK" "${DOCKER[@]}" "${COMPOSE_ARGS[@]}" config
else
echo "No docker-compose.yml found at ${COMPOSE_FILE}" >>"$DOCK"
fi
# Per-container resource snapshot (one-shot, never blocks).
section "Container stats" "$DOCK" "${DOCKER[@]}" stats --no-stream
# Inspect each running Anthias container for restart counts / OOM / exit codes.
INSPECT="${REPORT_DIR}/container-inspect.txt"
note "container inspect (restarts, OOM, exit codes)"
for tag in "${CONTAINER_TAGS[@]}"; do
cid="$("${DOCKER[@]}" ps -aq --filter "name=${tag}" 2>/dev/null | head -1)"
[[ -z "$cid" ]] && continue
section "Inspect ${tag} (state)" "$INSPECT" \
"${DOCKER[@]}" inspect --format \
'Status={{.State.Status}} Running={{.State.Running}} RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Error={{.State.Error}} StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' \
"$cid"
done
# ---------------------------------------------------------------------------
# 3. Container logs
# ---------------------------------------------------------------------------
note "container logs (${LOG_LINES} lines each)"
# Prefer compose logs (works regardless of logging driver); fall back to
# journald-by-tag (production driver) and finally `docker logs` by name.
if [[ -f "$COMPOSE_FILE" ]]; then
"${DOCKER[@]}" "${COMPOSE_ARGS[@]}" logs --no-color --tail "$LOG_LINES" \
>"${LOG_DIR}/compose-logs.txt" 2>&1 || true
fi
for tag in "${CONTAINER_TAGS[@]}"; do
dest="${LOG_DIR}/${tag}.txt"
# journald (production logging driver tags each service).
if command -v journalctl >/dev/null 2>&1; then
journalctl -t "$tag" -n "$LOG_LINES" --no-pager >"$dest" 2>/dev/null
[[ -s "$dest" ]] && continue
fi
# Fall back to `docker logs` by container name.
cid="$("${DOCKER[@]}" ps -aq --filter "name=${tag}" 2>/dev/null | head -1)"
if [[ -n "$cid" ]]; then
"${DOCKER[@]}" logs --tail "$LOG_LINES" "$cid" >"$dest" 2>&1 || true
fi
[[ -s "$dest" ]] || rm -f "$dest"
done
# Host-side Docker daemon log — explains failures to pull / start at all.
if command -v journalctl >/dev/null 2>&1; then
journalctl -u docker --no-pager -n 500 \
>"${LOG_DIR}/docker-daemon.txt" 2>/dev/null || true
fi
# Kernel ring buffer — OOM kills, USB/SD I/O errors, DRM/display faults.
note "kernel log (dmesg)"
if command -v dmesg >/dev/null 2>&1; then
{ dmesg -T 2>/dev/null || sudo dmesg -T 2>/dev/null || dmesg 2>/dev/null; } \
>"${LOG_DIR}/dmesg.txt" 2>&1 || true
fi
# ---------------------------------------------------------------------------
# 4. Anthias config & database
# ---------------------------------------------------------------------------
note "anthias config (credentials redacted)"
CONF="${REPORT_DIR}/anthias-config.txt"
{
echo "Config dir: ${CONFIG_DIR}"
echo
if [[ -f "${CONFIG_DIR}/anthias.conf" ]]; then
echo "=== anthias.conf (passwords/keys redacted) ==="
# Blank out anything that looks like a secret while keeping keys
# visible so the operator can confirm a setting exists.
sed -E 's/^([[:space:]]*(password|user|secret|token|key|cert|private)[^=]*=).*/\1 <redacted>/I' \
"${CONFIG_DIR}/anthias.conf"
else
echo "No anthias.conf found at ${CONFIG_DIR}/anthias.conf"
fi
} >"$CONF" 2>&1
section "Config dir listing" "$CONF" ls -la "$CONFIG_DIR"
section "Backups listing" "$CONF" ls -la "${CONFIG_DIR}/backups"
# Database overview — size, integrity, asset counts. Read-only queries
# against a copy so we never touch the live DB / WAL.
note "database overview"
DB="${REPORT_DIR}/database.txt"
DB_FILE="${CONFIG_DIR}/anthias.db"
[[ -f "$DB_FILE" ]] || DB_FILE="${CONFIG_DIR}/screenly.db"
if [[ -f "$DB_FILE" ]]; then
section "Database file" "$DB" ls -la "$DB_FILE"
# `is_reachable` is the single most useful column for triaging a
# "web content doesn't display" report: the server marks a remote
# asset unreachable when its reachability probe fails, and the
# viewer then skips it. Keep it in the inventory on both paths.
integrity_q='PRAGMA integrity_check;'
journal_q='PRAGMA journal_mode;'
tables_q='.tables'
count_q='SELECT count(*) AS total, sum(is_enabled) AS enabled, sum(NOT is_reachable) AS unreachable FROM assets;'
# Asset inventory without leaking full URLs of private content.
inventory_q="SELECT substr(asset_id,1,8), substr(name,1,40), mimetype, is_enabled, is_reachable, substr(uri,1,60) FROM assets;"
if command -v sqlite3 >/dev/null 2>&1; then
section "Integrity check" "$DB" sqlite3 "file:${DB_FILE}?mode=ro" "$integrity_q"
section "Journal mode" "$DB" sqlite3 "file:${DB_FILE}?mode=ro" "$journal_q"
section "Tables" "$DB" sqlite3 "file:${DB_FILE}?mode=ro" "$tables_q"
section "Asset count" "$DB" sqlite3 "file:${DB_FILE}?mode=ro" "$count_q"
section "Assets (truncated)" "$DB" sqlite3 "file:${DB_FILE}?mode=ro" "$inventory_q"
else
echo "sqlite3 not installed on host — DB inspected via the server container" >>"$DB"
# Fall back to the sqlite3 inside the server container. The host
# config dir is bind-mounted at /data/.anthias, so the same DB
# file is reachable there under its original basename.
cid="$("${DOCKER[@]}" ps -q --filter "name=anthias-server" 2>/dev/null | head -1)"
if [[ -n "$cid" ]]; then
c_db="file:/data/.anthias/$(basename "$DB_FILE")?mode=ro"
section "Integrity check (in container)" "$DB" \
"${DOCKER[@]}" exec "$cid" sqlite3 "$c_db" "$integrity_q"
section "Journal mode (in container)" "$DB" \
"${DOCKER[@]}" exec "$cid" sqlite3 "$c_db" "$journal_q"
section "Asset count (in container)" "$DB" \
"${DOCKER[@]}" exec "$cid" sqlite3 "$c_db" "$count_q"
section "Assets (truncated, in container)" "$DB" \
"${DOCKER[@]}" exec "$cid" sqlite3 "$c_db" "$inventory_q"
else
echo "server container not running — cannot inspect DB" >>"$DB"
fi
fi
else
echo "No database found under ${CONFIG_DIR}" >"$DB"
fi
# Assets directory — confirm media is actually on disk and how big it is.
section "Assets directory" "$DB" sh -c "ls -la '${USER_HOME}/anthias_assets' 2>/dev/null | head -50; echo; du -sh '${USER_HOME}/anthias_assets' 2>/dev/null"
# ---------------------------------------------------------------------------
# 4b. Video assets — ffprobe every video the player would try to play
# ---------------------------------------------------------------------------
# "Video won't play" is the most common bad-install symptom, so probe
# each video asset exactly the way processing.py does. The asset list
# comes straight from the live ORM (so local files resolve to their
# in-container /data/anthias_assets/<id> path and remote assets keep
# their http/rtsp URL), and ffprobe runs inside the server/celery image
# where it's guaranteed to exist — no host ffmpeg required.
note "video assets (ffprobe)"
VID="${REPORT_DIR}/video-assets.txt"
SERVER_CID="$("${DOCKER[@]}" ps -q --filter "name=anthias-server" 2>/dev/null | head -1)"
[[ -n "$SERVER_CID" ]] || \
SERVER_CID="$("${DOCKER[@]}" ps -q --filter "name=anthias-celery" 2>/dev/null | head -1)"
{
echo "ffprobe -v error -show_format -show_streams -print_format json <uri>"
echo
if [[ -z "$SERVER_CID" ]]; then
echo "anthias-server/celery container not running — cannot enumerate or probe video assets."
else
# One uri per line from the ORM (mimetype == 'video'). Use a
# bare django.setup() rather than `manage shell -c` — the latter
# prints an "N objects imported automatically" banner that would
# contaminate the uri list.
mapfile -t VIDEO_URIS < <(
"${DOCKER[@]}" exec "$SERVER_CID" python -c \
'import os, django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "anthias_server.django_project.settings")
django.setup()
from anthias_server.app.models import Asset
for a in Asset.objects.filter(mimetype="video"):
print(a.uri or "")' 2>/dev/null
)
if [[ ${#VIDEO_URIS[@]} -eq 0 ]]; then
echo "No video assets found in the database."
else
echo "Probing ${#VIDEO_URIS[@]} video asset(s)."
echo
fi
for uri in "${VIDEO_URIS[@]}"; do
[[ -z "$uri" ]] && continue
echo "================================================================"
echo "URI: ${uri}"
echo "----------------------------------------------------------------"
# `timeout` caps a hung probe on an unreachable RTSP/stream URL.
"${DOCKER[@]}" exec "$SERVER_CID" timeout 30 ffprobe \
-v error -show_format -show_streams -print_format json "$uri" 2>&1 \
|| echo "(ffprobe failed, timed out, or file missing)"
echo
done
fi
} >"$VID" 2>&1
# ---------------------------------------------------------------------------
# 5. Network & service reachability
# ---------------------------------------------------------------------------
note "network & reachability"
NET="${REPORT_DIR}/network.txt"
section "Interfaces" "$NET" sh -c 'ip addr 2>/dev/null || ifconfig 2>/dev/null'
section "Routes" "$NET" sh -c 'ip route 2>/dev/null || route -n 2>/dev/null'
section "DNS config" "$NET" cat /etc/resolv.conf
section "Listening sockets" "$NET" sh -c 'ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null'
# Local web UI reachability.
section "Local web UI (HTTP)" "$NET" sh -c \
'curl -sS -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" --max-time 10 http://localhost/ || echo "unreachable"'
# Internet reachability (image registry / asset downloads).
section "Internet (ghcr.io)" "$NET" sh -c \
'curl -sS -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" --max-time 10 https://ghcr.io/ || echo "unreachable"'
section "DNS resolution" "$NET" sh -c \
'getent hosts ghcr.io || nslookup ghcr.io 2>/dev/null || echo "resolution failed"'
# Redis health — broker / channel layer / viewer bus all live here.
note "redis health"
REDIS="${REPORT_DIR}/redis.txt"
cid="$("${DOCKER[@]}" ps -q --filter "name=anthias-redis" 2>/dev/null | head -1)"
if [[ -n "$cid" ]]; then
section "Redis PING" "$REDIS" "${DOCKER[@]}" exec "$cid" redis-cli ping
section "Redis INFO server" "$REDIS" "${DOCKER[@]}" exec "$cid" redis-cli info server
section "Redis INFO memory" "$REDIS" "${DOCKER[@]}" exec "$cid" redis-cli info memory
section "Redis INFO persistence" "$REDIS" "${DOCKER[@]}" exec "$cid" redis-cli info persistence
section "Redis keyspace" "$REDIS" "${DOCKER[@]}" exec "$cid" redis-cli info keyspace
else
echo "No anthias-redis container running" >"$REDIS"
fi
# ---------------------------------------------------------------------------
# 6. Git / version metadata
# ---------------------------------------------------------------------------
note "version metadata"
VER="${REPORT_DIR}/version.txt"
if [[ -d "${ANTHIAS_DIR}/.git" ]]; then
section "Git describe" "$VER" git -C "$ANTHIAS_DIR" describe --tags --always
section "Git log (last 10)" "$VER" git -C "$ANTHIAS_DIR" log --oneline -10
section "Git status" "$VER" git -C "$ANTHIAS_DIR" status -sb
fi
section "Image tags in use" "$VER" sh -c \
"grep -E 'image:|DOCKER_TAG|DEVICE_TYPE' '${COMPOSE_FILE}' 2>/dev/null; cat '${ANTHIAS_DIR}/.env' 2>/dev/null"
# ---------------------------------------------------------------------------
# 7. Wrap up
# ---------------------------------------------------------------------------
# Top-level summary so a reader knows the report's shape at a glance.
{
echo "Anthias debug report"
echo "Generated : $(date 2>/dev/null)"
echo "User : ${RUN_USER}"
echo "Host : $(hostname 2>/dev/null)"
echo "Anthias : ${ANTHIAS_DIR}"
echo "Config : ${CONFIG_DIR}"
echo
echo "Files in this report:"
( cd "$REPORT_DIR" && find . -type f | sort )
} >"${REPORT_DIR}/README.txt" 2>&1
# Redact PII across the whole bundle as the very last step before it
# leaves this machine.
note "redacting PII (IP/MAC/email/credentials/hostname)"
scrub_pii
# Fix ownership when invoked under sudo so the operator can read/delete it.
if [[ -n "${SUDO_USER:-}" ]]; then
chown -R "${SUDO_USER}:${SUDO_USER}" "$REPORT_DIR" 2>/dev/null || true
fi
echo
if [[ "$MAKE_ARCHIVE" -eq 1 ]]; then
ARCHIVE="${OUTPUT_BASE}/${REPORT_NAME}.tar.gz"
if tar -czf "$ARCHIVE" -C "$OUTPUT_BASE" "$REPORT_NAME" 2>/dev/null; then
rm -rf "$REPORT_DIR"
[[ -n "${SUDO_USER:-}" ]] && chown "${SUDO_USER}:${SUDO_USER}" "$ARCHIVE" 2>/dev/null || true
echo "Debug bundle written to:"
echo " ${ARCHIVE}"
echo
echo "Attach this file to your GitHub issue or forum post."
echo "PII (IP/MAC/email/credentials/hostname) and anthias.conf"
echo "secrets were redacted; skim the bundle before sharing."
else
echo "Archiving failed; the uncompressed report is at:"
echo " ${REPORT_DIR}"
fi
else
echo "Debug report written to:"
echo " ${REPORT_DIR}"
fi