diff --git a/Makefile b/Makefile index dccec42..e7ce5ba 100644 --- a/Makefile +++ b/Makefile @@ -104,7 +104,7 @@ start: @echo "Starting Personal Internet Cell..." @docker network inspect cell-network >/dev/null 2>&1 || \ docker network create --driver bridge --subnet "$${CELL_NETWORK:-172.20.0.0/16}" cell-network - PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build + @PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build --quiet-pull @echo "Services started. Check status with 'make status'" stop: @@ -145,7 +145,7 @@ update: @echo "Rebuilding and restarting services..." @docker network inspect cell-network >/dev/null 2>&1 || \ docker network create --driver bridge --subnet "$${CELL_NETWORK:-172.20.0.0/16}" cell-network - PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build + @PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build --quiet-pull @echo "Update complete. Run 'make status' to verify." reinstall: @@ -229,7 +229,7 @@ start-core: @echo "Starting core services (caddy, dns, wireguard, api, webui)..." @docker network inspect cell-network >/dev/null 2>&1 || \ docker network create --driver bridge --subnet "$${CELL_NETWORK:-172.20.0.0/16}" cell-network - PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build + @PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build --quiet-pull @echo "Core services started. Run 'make start' to also bring up optional services." start-dns: diff --git a/docker-compose.services.yml b/docker-compose.services.yml index 0189ff2..c178d8e 100644 --- a/docker-compose.services.yml +++ b/docker-compose.services.yml @@ -1,4 +1,3 @@ -version: '3.3' services: {} networks: cell-network: diff --git a/docker-compose.yml b/docker-compose.yml index 6c47469..e12aa96 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.3' - services: # Reverse Proxy - Caddy for routing all .cell traffic caddy: diff --git a/install.sh b/install.sh index 78039ce..247777e 100755 --- a/install.sh +++ b/install.sh @@ -42,19 +42,31 @@ API_HEALTH_URL="http://127.0.0.1:3000/health" API_HEALTH_TIMEOUT=60 WEBUI_PORT=8081 FORCE=0 +PIC_DEBUG="${PIC_DEBUG:-0}" # Parse flags for arg in "$@"; do case "$arg" in --force) FORCE=1 ;; + --debug) PIC_DEBUG=1 ;; *) echo "Unknown argument: $arg" >&2 - echo "Usage: $0 [--force]" >&2 + echo "Usage: $0 [--force] [--debug]" >&2 exit 1 ;; esac done +# --------------------------------------------------------------------------- +# Log file — /var/log/pic-install.log when writable (root via sudo), else /tmp +# --------------------------------------------------------------------------- +if touch /var/log/pic-install.log 2>/dev/null; then + LOGFILE="/var/log/pic-install.log" +else + LOGFILE="${TMPDIR:-/tmp}/pic-install.log" +fi +: > "$LOGFILE" # truncate / create + # --------------------------------------------------------------------------- # Color output # --------------------------------------------------------------------------- @@ -77,7 +89,68 @@ log_ok() { printf " ${GREEN}✓${RESET} %s\n" "$1"; } log_warn() { printf " ${YELLOW}⚠${RESET} %s\n" "$1"; } log_error() { printf "\n${RED}${BOLD}ERROR:${RESET}${RED} %s${RESET}\n" "$1" >&2; } -die() { log_error "$1"; exit 1; } +die() { + log_error "$1" + if [ "$PIC_DEBUG" -eq 0 ]; then + printf "\n${YELLOW}Last output (full log: %s):${RESET}\n" "$LOGFILE" >&2 + tail -n 30 "$LOGFILE" | sed 's/^/ /' >&2 + fi + exit 1 +} + +# --------------------------------------------------------------------------- +# run_step [args...] +# +# Default mode: redirect stdout+stderr to LOGFILE; print a single "in +# progress" line then overwrite it with a checkmark on success. On failure +# print the last 30 log lines and die. +# +# Debug mode (PIC_DEBUG=1): tee output to LOGFILE AND stdout (indented), +# print the done line at the end. +# +# TERM safety: when stdout is not a TTY the \r trick does not work, so we +# fall back to a plain two-line "... / done" style. +# --------------------------------------------------------------------------- +_IS_TTY=0 +[ -t 1 ] && _IS_TTY=1 + +run_step() { + local label_running="$1" + local label_done="$2" + shift 2 + # "$@" is the command to run + + if [ "$PIC_DEBUG" -eq 1 ]; then + printf " → %s\n" "$label_running" + # set -o pipefail: the pipeline below fails if "$@" fails, regardless + # of tee's or sed's exit code. + { "$@" 2>&1 | tee -a "$LOGFILE" | sed 's/^/ /'; } || \ + die "Command failed. See $LOGFILE for details." + log_ok "$label_done" + return + fi + + # Default (quiet) mode + if [ "$_IS_TTY" -eq 1 ]; then + printf " → %s..." "$label_running" + else + printf " → %s...\n" "$label_running" + fi + + local exit_code=0 + "$@" >> "$LOGFILE" 2>&1 || exit_code=$? + + if [ "$exit_code" -ne 0 ]; then + [ "$_IS_TTY" -eq 1 ] && printf "\n" + die "Step failed: $label_running" + fi + + if [ "$_IS_TTY" -eq 1 ]; then + printf "\r ${GREEN}✓${RESET} %-60s\n" "$label_done" + else + printf " ${GREEN}✓${RESET} %s\n" "$label_done" + fi +} TOTAL_STEPS=7 @@ -88,6 +161,9 @@ if ! command -v sudo >/dev/null 2>&1; then die "sudo is required. Install it and ensure your user has sudo access." fi +printf " Full log: %s\n" "$LOGFILE" +[ "$PIC_DEBUG" -eq 1 ] && printf " ${YELLOW}Debug mode enabled — verbose output active${RESET}\n" + # --------------------------------------------------------------------------- # Idempotency guard # --------------------------------------------------------------------------- @@ -145,60 +221,72 @@ log_ok "Detected OS: ${OS_ID} (package manager: ${PKG_MANAGER})" # --------------------------------------------------------------------------- log_step 2 "Installing dependencies..." +_install_deps() { + case "$PKG_MANAGER" in + + apt) + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update -qq + sudo apt-get install -y -qq git curl make docker.io docker-compose-plugin || true + + if ! docker compose version >/dev/null 2>&1; then + sudo apt-get install -y -qq docker-compose || true + fi + + # Ensure host clock is synchronised before DDNS/TOTP registration. + sudo apt-get install -y -qq chrony || true + if sudo systemctl enable --now chrony >/dev/null 2>&1; then + : # NTP enabled + elif sudo systemctl enable --now chronyd >/dev/null 2>&1; then + : # NTP enabled + fi + ;; + + dnf) + sudo dnf install -y -q git curl make docker || true + sudo systemctl enable --now docker >/dev/null 2>&1 || true + + if ! docker compose version >/dev/null 2>&1; then + sudo dnf install -y -q docker-compose-plugin || true + fi + + sudo dnf install -y -q chrony || true + sudo systemctl enable --now chronyd >/dev/null 2>&1 || true + ;; + + apk) + sudo apk add --quiet git curl make docker docker-cli-compose || true + sudo rc-update add docker default >/dev/null 2>&1 || true + sudo service docker start >/dev/null 2>&1 || true + + sudo apk add --quiet chrony || true + sudo rc-update add chronyd default >/dev/null 2>&1 || true + sudo service chronyd start >/dev/null 2>&1 || true + ;; + + esac +} + +run_step "Installing system packages" "System packages installed" _install_deps + +# Report NTP status (informational, outside the noisy run_step) case "$PKG_MANAGER" in - apt) - export DEBIAN_FRONTEND=noninteractive - sudo apt-get update -qq - sudo apt-get install -y -qq git curl make docker.io docker-compose-plugin 2>&1 \ - | grep -v "^$" | sed 's/^/ /' || true - - if ! docker compose version >/dev/null 2>&1; then - log_warn "docker-compose-plugin not available; falling back to standalone docker-compose" - sudo apt-get install -y -qq docker-compose 2>&1 | grep -v "^$" | sed 's/^/ /' || true - fi - - # Ensure host clock is synchronised before DDNS/TOTP registration. - # chrony is preferred; the service name differs by distro (chrony on Debian, chronyd on some Ubuntu). - sudo apt-get install -y -qq chrony 2>&1 | grep -v "^$" | sed 's/^/ /' || true - if sudo systemctl enable --now chrony >/dev/null 2>&1; then - log_ok "Host NTP (chrony) enabled and started" - elif sudo systemctl enable --now chronyd >/dev/null 2>&1; then - log_ok "Host NTP (chronyd) enabled and started" + if sudo systemctl is-active --quiet chrony 2>/dev/null || \ + sudo systemctl is-active --quiet chronyd 2>/dev/null; then + log_ok "Host NTP (chrony) is running" else log_warn "Could not start chrony — verify host clock is accurate before running the setup wizard" fi ;; - - dnf) - sudo dnf install -y -q git curl make docker 2>&1 | sed 's/^/ /' || true - - sudo systemctl enable --now docker >/dev/null 2>&1 || true - - if ! docker compose version >/dev/null 2>&1; then - log_warn "docker compose plugin not found; installing docker-compose-plugin..." - sudo dnf install -y -q docker-compose-plugin 2>&1 | sed 's/^/ /' || true + dnf|apk) + if sudo systemctl is-active --quiet chronyd 2>/dev/null || \ + sudo service chronyd status >/dev/null 2>&1; then + log_ok "Host NTP (chronyd) is running" + else + log_warn "Could not start chronyd — verify host clock is accurate before running the setup wizard" fi - - sudo dnf install -y -q chrony 2>&1 | sed 's/^/ /' || true - sudo systemctl enable --now chronyd >/dev/null 2>&1 \ - && log_ok "Host NTP (chronyd) enabled and started" \ - || log_warn "Could not start chronyd — verify host clock is accurate before running the setup wizard" ;; - - apk) - sudo apk add --quiet git curl make docker docker-cli-compose 2>&1 | sed 's/^/ /' || true - - sudo rc-update add docker default >/dev/null 2>&1 || true - sudo service docker start >/dev/null 2>&1 || true - - sudo apk add --quiet chrony 2>&1 | sed 's/^/ /' || true - sudo rc-update add chronyd default >/dev/null 2>&1 || true - sudo service chronyd start >/dev/null 2>&1 \ - && log_ok "Host NTP (chronyd) enabled and started" \ - || log_warn "Could not start chronyd — verify host clock is accurate before running the setup wizard" - ;; - esac # Final sanity checks @@ -253,14 +341,14 @@ log_step 4 "Setting up repository at ${PIC_DIR}..." if [ -d "${PIC_DIR}/.git" ]; then log_warn "Repository already cloned — running git pull" - git -C "$PIC_DIR" pull --ff-only 2>&1 | sed 's/^/ /' - log_ok "Repository updated" + run_step "Updating repository" "Repository updated" \ + git -C "$PIC_DIR" pull --ff-only elif [ -d "$PIC_DIR" ] && [ "$(ls -A "$PIC_DIR" 2>/dev/null)" ]; then die "${PIC_DIR} exists and is not empty and is not a git repo. Aborting to avoid data loss." else mkdir -p "$(dirname "$PIC_DIR")" - git clone "$PIC_REPO" "$PIC_DIR" 2>&1 | sed 's/^/ /' - log_ok "Repository cloned to ${PIC_DIR}" + run_step "Cloning repository" "Repository cloned to ${PIC_DIR}" \ + git clone "$PIC_REPO" "$PIC_DIR" fi sudo git config --system --add safe.directory "$PIC_DIR" 2>/dev/null || true @@ -268,12 +356,28 @@ sudo git config --system --add safe.directory "$PIC_DIR" 2>/dev/null || true # --------------------------------------------------------------------------- # Step 5 — Run make install # --------------------------------------------------------------------------- -log_step 5 "Running 'make install'..." +log_step 5 "Generating configuration..." cd "$PIC_DIR" -if ! make install 2>&1 | sed 's/^/ /'; then - die "'make install' failed. Check the output above." +# run_step routes all output to LOGFILE. After it returns we scan LOGFILE +# for the admin password banner (printed once by setup_cell.py) and relay it +# to the user — it must never be silently buried in the log. +# We record the log byte-offset before the step so we only scan new output. +_LOG_OFFSET_BEFORE="$(wc -c < "$LOGFILE" 2>/dev/null || echo 0)" + +run_step "Generating configuration" "Configuration generated" make install + +# Extract only the lines added by this step. +_NEW_LOG="$(tail -c +"$(( _LOG_OFFSET_BEFORE + 1 ))" "$LOGFILE" 2>/dev/null || true)" + +# Relay admin password banner if present. +if printf '%s\n' "$_NEW_LOG" | grep -qiE "(ADMIN PASSWORD|shown once)"; then + printf "\n" + printf '%s\n' "$_NEW_LOG" \ + | awk '/ADMIN PASSWORD|shown once|={6}/{found=1} found{print} found && /^[[:space:]]*$/{exit}' \ + | sed 's/^/ /' + printf "\n" fi log_ok "'make install' complete" @@ -285,9 +389,10 @@ log_step 6 "Starting core services..." cd "$PIC_DIR" -if ! make start-core 2>&1 | sed 's/^/ /'; then - die "'make start-core' failed. Check the output above." -fi +run_step \ + "Downloading container images (first run can take a few minutes)" \ + "Container images ready" \ + make start-core log_ok "Core services started"