Add Docker and environment configuration files for AdventureLog

- Introduced .dockerignore to exclude unnecessary files from Docker context.
- Added .env.aio.example for minimal configuration of the AdventureLog All-in-One setup.
- Updated .env.example to include optional SITE_URL and GUNICORN_WORKERS settings.
- Enhanced deploy.sh script for improved deployment flexibility and backup options.
- Updated docker-compose files to use PostGIS 16-3.5 and added health checks for services.
- Created docker-compose.aio.yml for All-in-One deployment configuration.
- Improved health checks and service dependencies in docker-compose.dev.yml and docker-compose.yml.
- Added GitHub workflows for building and pushing Docker images, including smoke tests for AIO setup.
This commit is contained in:
Sean Morley
2026-06-06 18:54:25 -04:00
parent bd6addfab5
commit 7f6bf1390a
102 changed files with 6580 additions and 1940 deletions

78
scripts/backup.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
# Backup AdventureLog database, media volume, and environment file.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$SCRIPT_DIR"
COMPOSE_FILE="${COMPOSE_FILE:-}"
BACKUP_DIR="${BACKUP_DIR:-backups}"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
if [[ -z "$COMPOSE_FILE" ]]; then
if [[ -f .env.aio ]] && [[ ! -f .env ]]; then
COMPOSE_FILE="docker-compose.aio.yml"
elif [[ -f .env.aio ]] && [[ -f docker-compose.aio.yml ]] && [[ "${ADVENTURELOG_COMPOSE:-}" == "aio" ]]; then
COMPOSE_FILE="docker-compose.aio.yml"
else
COMPOSE_FILE="docker-compose.yml"
fi
fi
case "$COMPOSE_FILE" in
*docker-compose.aio.yml*)
ENV_FILE=".env.aio"
;;
*)
ENV_FILE=".env"
;;
esac
COMPOSE=(docker compose -f "$COMPOSE_FILE")
if [[ -f "$ENV_FILE" ]]; then
COMPOSE=(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE")
fi
DEST="$BACKUP_DIR/$TIMESTAMP"
mkdir -p "$DEST"
resolve_compose_volume() {
local suffix="$1"
"${COMPOSE[@]}" volume ls -q | grep "${suffix}$" | head -n 1
}
MEDIA_VOLUME="$(resolve_compose_volume "adventurelog_media")"
if [[ -z "$MEDIA_VOLUME" ]]; then
MEDIA_VOLUME="$(resolve_compose_volume "media")"
fi
echo "Backing up AdventureLog to $DEST"
if [[ -f "$ENV_FILE" ]]; then
cp "$ENV_FILE" "$DEST/"
fi
POSTGRES_USER="${POSTGRES_USER:-adventure}"
POSTGRES_DB="${POSTGRES_DB:-database}"
if [[ -f "$ENV_FILE" ]]; then
# shellcheck disable=SC1090
set -a
source "$ENV_FILE"
set +a
fi
echo "Dumping database..."
"${COMPOSE[@]}" exec -T db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "$DEST/database.sql"
echo "Archiving media volume..."
if [[ -z "$MEDIA_VOLUME" ]]; then
echo "WARNING: media volume not found; skipping media backup" >&2
else
docker run --rm \
-v "${MEDIA_VOLUME}:/data:ro" \
-v "$DEST:/backup" \
alpine:3.21 \
tar czf /backup/media.tar.gz -C /data .
fi
echo "Backup complete: $DEST"

118
scripts/install/lib/aio-config.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/bin/bash
# AIO configuration wizard and .env.aio writer.
set -euo pipefail
prompt_aio_core_config() {
print_step_header 2 "$WIZARD_TOTAL" "Core configuration (AIO)"
log_info "All-in-One uses a single URL and port."
echo ""
local default_site="http://localhost:8015"
while true; do
SITE_URL="$(prompt_with_default "Public site URL" "$default_site")"
if validate_url "$SITE_URL"; then
break
fi
log_error "Enter a valid http:// or https:// URL"
done
SITE_URL="${SITE_URL%/}"
HOST_PORT="$(extract_port_from_url "$SITE_URL" "8015")"
local port_override
port_override="$(tui_input "Host port to bind [${HOST_PORT}]" "$HOST_PORT")"
HOST_PORT="${port_override:-$HOST_PORT}"
log_success "Site: $SITE_URL (port $HOST_PORT)"
POSTGRES_PASSWORD="$(generate_secure_password 32)"
log_success "Generated secure database password"
if tui_confirm "Change default admin credentials (admin/admin)?" "y"; then
DJANGO_ADMIN_USERNAME="$(prompt_with_default "Admin username" "admin")"
DJANGO_ADMIN_PASSWORD="$(generate_secure_password 24)"
DJANGO_ADMIN_EMAIL="$(prompt_with_default "Admin email" "admin@example.com")"
else
DJANGO_ADMIN_USERNAME="admin"
DJANGO_ADMIN_PASSWORD="admin"
DJANGO_ADMIN_EMAIL="admin@example.com"
log_warning "Using default admin/admin — change after first login"
fi
echo ""
}
write_env_aio() {
local target="$ENV_FILE"
{
echo "# AdventureLog All-in-One — generated by installer"
echo "POSTGRES_PASSWORD=$(format_env_value "$POSTGRES_PASSWORD")"
echo "SITE_URL=$(format_env_value "$SITE_URL")"
echo "HOST_PORT=$(format_env_value "$HOST_PORT")"
echo "DJANGO_ADMIN_USERNAME=$(format_env_value "$DJANGO_ADMIN_USERNAME")"
echo "DJANGO_ADMIN_PASSWORD=$(format_env_value "$DJANGO_ADMIN_PASSWORD")"
echo "DJANGO_ADMIN_EMAIL=$(format_env_value "$DJANGO_ADMIN_EMAIL")"
} > "$target"
save_optional_env_to_file "$target"
log_success "Wrote $target"
}
show_aio_review() {
local opt_count=0
[[ -v OPTIONAL_ENV_LINES ]] && opt_count="${#OPTIONAL_ENV_LINES[@]}"
print_step_header 5 "$WIZARD_TOTAL" "Review configuration"
print_summary_row "Setup" "All-in-One (AIO)"
print_summary_row "Site URL" "$SITE_URL"
print_summary_row "Host port" "$HOST_PORT"
print_summary_row "PostGIS image" "$POSTGIS_IMAGE"
print_summary_row "Admin user" "$DJANGO_ADMIN_USERNAME"
print_summary_row "Optional vars" "$opt_count configured"
echo ""
if ! tui_confirm "Proceed with installation?" "y"; then
log_info "Installation cancelled."
exit 0
fi
}
save_credentials_file() {
if [[ "$DRY_RUN" == true ]]; then
return 0
fi
if ! tui_confirm "Save credentials to credentials.txt (mode 600)?" "y"; then
return 0
fi
cat > credentials.txt << EOF
AdventureLog credentials — $(date)
Site URL: ${SITE_URL}
Admin username: ${DJANGO_ADMIN_USERNAME}
Admin password: ${DJANGO_ADMIN_PASSWORD}
Database password: ${POSTGRES_PASSWORD}
EOF
chmod 600 credentials.txt
log_success "Saved credentials.txt (keep this secure)"
}
print_aio_success() {
print_success_banner
log_success "Installation completed!"
echo ""
echo -e "${BOLD}Access:${NC} ${CYAN}${SITE_URL}${NC}"
echo ""
echo -e "${BOLD}Admin:${NC} ${GREEN}${DJANGO_ADMIN_USERNAME}${NC} / ${GREEN}${DJANGO_ADMIN_PASSWORD}${NC}"
echo ""
echo -e "${BOLD}Config:${NC} $(pwd)/${ENV_FILE}"
echo ""
echo -e "${BOLD}Management:${NC}"
echo -e " Re-run installer: curl -sSL https://get.adventurelog.app | bash"
echo -e " Update: bash deploy.sh --backup"
echo -e " Logs: docker logs ${LOG_CONTAINER} -f"
echo -e " Stop: docker compose -f ${COMPOSE_FILE} down"
echo ""
}
run_aio_install_wizard() {
SETUP_TYPE="aio"
resolve_compose_settings
WIZARD_TOTAL=7
prompt_aio_core_config
run_port_checks
run_optional_features_wizard
write_postgis_override
show_aio_review
}

182
scripts/install/lib/checks.sh Executable file
View File

@@ -0,0 +1,182 @@
#!/bin/bash
# System preflight checks.
set -euo pipefail
detect_system_arch() {
local raw
raw="$(uname -m)"
case "$raw" in
aarch64|arm64|armv7l|armv8*)
SYSTEM_ARCH="arm"
USE_ARM_POSTGIS=true
POSTGIS_IMAGE="imresamu/postgis:16-3.5-alpine"
;;
x86_64|amd64)
SYSTEM_ARCH="amd64"
USE_ARM_POSTGIS=false
POSTGIS_IMAGE="postgis/postgis:16-3.5"
;;
*)
SYSTEM_ARCH="$raw"
USE_ARM_POSTGIS=false
POSTGIS_IMAGE="postgis/postgis:16-3.5"
;;
esac
}
verify_postgis_image() {
local image="$1"
if docker manifest inspect "$image" &>/dev/null; then
return 0
fi
log_warning "Could not verify manifest for $image"
return 1
}
write_postgis_override() {
local target="${1:-docker-compose.override.yml}"
if [[ "$USE_ARM_POSTGIS" != true ]]; then
return 0
fi
log_info "ARM architecture detected — using PostGIS image: $POSTGIS_IMAGE"
cat > "$target" << EOF
# Auto-generated by AdventureLog installer for ARM hosts
services:
db:
image: ${POSTGIS_IMAGE}
EOF
log_success "Wrote $target"
}
check_dependencies() {
log_info "Checking system dependencies..."
local missing=()
command -v curl &>/dev/null || missing+=("curl")
command -v docker &>/dev/null || missing+=("docker")
if ! docker compose version &>/dev/null 2>&1 && ! command -v docker-compose &>/dev/null; then
missing+=("docker-compose")
fi
if [[ ${#missing[@]} -gt 0 ]]; then
log_error "Missing: ${missing[*]}"
echo "Install Docker: https://docs.docker.com/get-docker/"
exit 1
fi
log_success "Dependencies OK"
}
check_docker_status() {
if [[ "${DRY_RUN:-false}" == true ]]; then
log_info "[dry-run] Skipping Docker daemon check"
return 0
fi
log_info "Checking Docker daemon..."
if ! docker info &>/dev/null; then
log_error "Docker is not running"
exit 1
fi
if ! docker info 2>/dev/null | grep -q "Username:" && [[ "$(id -u)" -ne 0 ]]; then
if ! groups 2>/dev/null | grep -q docker; then
log_warning "User not in docker group — you may need sudo for docker commands"
fi
fi
log_success "Docker is running"
}
check_memory() {
local avail_kb avail_mb
if [[ -r /proc/meminfo ]]; then
avail_kb="$(awk '/MemAvailable/ {print $2}' /proc/meminfo)"
if [[ -n "$avail_kb" && "$avail_kb" =~ ^[0-9]+$ ]]; then
avail_mb=$((avail_kb / 1024))
if (( avail_mb < 2048 )); then
log_warning "Available RAM is ~${avail_mb}MB — first boot needs ~2GB for world data import"
if tui_confirm "Enable SKIP_WORLD_DATA=1 to skip geography import on first boot?" "y"; then
SKIP_WORLD_DATA="1"
append_env_line "SKIP_WORLD_DATA=1"
fi
fi
fi
elif [[ "$OSTYPE" == "darwin"* ]]; then
log_info "Could not auto-detect RAM on macOS — ensure at least 2GB is allocated to Docker"
fi
}
check_disk_space() {
local target="${1:-.}"
local check_path="$target"
if [[ ! -e "$check_path" ]]; then
check_path="."
fi
local avail
if command -v df &>/dev/null; then
avail="$(df -BG "$check_path" 2>/dev/null | awk 'NR==2 {gsub(/G/,"",$4); print $4}')" || avail=""
if [[ -n "$avail" && "$avail" =~ ^[0-9]+$ ]] && (( avail < 5 )); then
log_warning "Less than 5GB free disk space in $check_path"
fi
fi
}
check_port_in_use() {
local port="$1"
local name="$2"
local in_use=false
if command -v ss &>/dev/null; then
ss -ln 2>/dev/null | grep -q ":${port} " && in_use=true
elif command -v netstat &>/dev/null; then
netstat -ln 2>/dev/null | grep -q ":${port} " && in_use=true
fi
if [[ "$in_use" == true ]]; then
log_warning "Port $port ($name) appears to be in use"
tui_confirm "Continue anyway?" "n" || exit 0
fi
}
detect_existing_install() {
local dir="${1:-$INSTALL_DIR}"
if [[ -f "$dir/.env.aio" ]] || [[ -f "$dir/.env" ]]; then
return 0
fi
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qE '^(adventurelog-aio|adventurelog-frontend|adventurelog-backend)$'; then
return 0
fi
return 1
}
detect_setup_type_from_install() {
local dir="${1:-$INSTALL_DIR}"
if [[ -f "$dir/.env.aio" ]]; then
SETUP_TYPE="aio"
elif [[ -f "$dir/.env" ]]; then
SETUP_TYPE="standard"
fi
resolve_compose_settings
}
run_preflight() {
print_step_header 1 "$WIZARD_TOTAL" "System checks"
check_dependencies
check_docker_status
detect_system_arch
log_info "Architecture: $SYSTEM_ARCH (${POSTGIS_IMAGE})"
check_memory
check_disk_space "$INSTALL_DIR"
}
run_port_checks() {
if [[ "$SETUP_TYPE" == "aio" ]]; then
check_port_in_use "$HOST_PORT" "AdventureLog"
else
check_port_in_use "$FRONTEND_PORT" "frontend"
check_port_in_use "$BACKEND_PORT" "backend"
fi
}
prompt_postgis_override() {
if [[ "$USE_ARM_POSTGIS" == true ]]; then
return 0
fi
if tui_confirm "Use custom PostGIS image? (default: $POSTGIS_IMAGE)" "n"; then
POSTGIS_IMAGE="$(prompt_with_default "PostGIS image" "$POSTGIS_IMAGE")"
USE_ARM_POSTGIS=true
fi
}

204
scripts/install/lib/common.sh Executable file
View File

@@ -0,0 +1,204 @@
#!/bin/bash
# Shared globals and utilities for AdventureLog installer.
set -euo pipefail
APP_NAME="AdventureLog"
INSTALL_DIR="${INSTALL_DIR:-./adventurelog}"
ADVENTURELOG_REF="${ADVENTURELOG_REF:-main}"
GITHUB_RAW="https://raw.githubusercontent.com/seanmorley15/AdventureLog/${ADVENTURELOG_REF}"
DOCS_BASE="https://adventurelog.app/docs/configuration"
# Setup type: aio | standard
SETUP_TYPE="${SETUP_TYPE:-aio}"
DRY_RUN="${DRY_RUN:-false}"
FORCE_INSTALL="${FORCE_INSTALL:-false}"
# AIO configuration
declare -g SITE_URL=""
declare -g HOST_PORT=""
declare -g POSTGRES_PASSWORD=""
declare -g DJANGO_ADMIN_USERNAME="admin"
declare -g DJANGO_ADMIN_PASSWORD=""
declare -g DJANGO_ADMIN_EMAIL="admin@example.com"
declare -g SKIP_WORLD_DATA=""
# Standard configuration
declare -g FRONTEND_ORIGIN=""
declare -g BACKEND_URL=""
declare -g FRONTEND_PORT=""
declare -g BACKEND_PORT=""
# Platform
declare -g SYSTEM_ARCH=""
declare -g POSTGIS_IMAGE=""
declare -g USE_ARM_POSTGIS=false
# Wizard state
declare -g WIZARD_STEP=0
declare -g WIZARD_TOTAL=7
declare -g REPO_ROOT="${REPO_ROOT:-}"
# Compose
declare -g COMPOSE_FILE=""
declare -g ENV_FILE=""
declare -g LOG_CONTAINER=""
resolve_compose_settings() {
if [[ "$SETUP_TYPE" == "aio" ]]; then
COMPOSE_FILE="docker-compose.aio.yml"
ENV_FILE=".env.aio"
LOG_CONTAINER="adventurelog-aio"
else
COMPOSE_FILE="docker-compose.yml"
ENV_FILE=".env"
LOG_CONTAINER="adventurelog-backend"
fi
}
generate_secure_password() {
local length=${1:-24}
if [[ ! -r "/dev/urandom" ]]; then
echo "ERROR: /dev/urandom not readable" >&2
return 1
fi
if command -v tr &>/dev/null; then
LC_ALL=C tr -dc 'A-Za-z0-9!#$%&*+-=?@^_' </dev/urandom 2>/dev/null | head -c "$length" 2>/dev/null
return 0
fi
if command -v openssl &>/dev/null; then
openssl rand -base64 32 | tr -d "=+/" | cut -c1-"$length"
return 0
fi
echo "ERROR: No suitable random generation method found" >&2
return 1
}
validate_url() {
local url="$1"
[[ $url =~ ^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$ ]]
}
extract_port_from_url() {
local url="$1"
local default_port="$2"
if [[ $url =~ :([0-9]+)(/|$) ]]; then
echo "${BASH_REMATCH[1]}"
elif [[ $url =~ ^https:// ]]; then
echo "443"
elif [[ $url =~ ^http:// ]]; then
echo "${default_port:-80}"
else
echo "$default_port"
fi
}
get_compose_cmd() {
if docker compose version &>/dev/null 2>&1; then
echo "docker compose"
else
echo "docker-compose"
fi
}
format_env_value() {
local val="$1"
if [[ "$val" =~ ^[A-Za-z0-9_.,/@^+-]+$ ]]; then
printf '%s' "$val"
else
local escaped="${val//\\/\\\\}"
escaped="${escaped//\"/\\\"}"
printf '"%s"' "$escaped"
fi
}
compose_args() {
local cmd
cmd="$(get_compose_cmd)"
if [[ -f "$ENV_FILE" ]]; then
# shellcheck disable=SC2086
echo $cmd --env-file "$ENV_FILE" -f "$COMPOSE_FILE"
else
# shellcheck disable=SC2086
echo $cmd -f "$COMPOSE_FILE"
fi
}
run_compose() {
local cmd
cmd="$(get_compose_cmd)"
if [[ -f "$ENV_FILE" ]]; then
# shellcheck disable=SC2086
$cmd --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
else
# shellcheck disable=SC2086
$cmd -f "$COMPOSE_FILE" "$@"
fi
}
detect_install_lib_dir() {
local script_path="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}"
local candidate
if [[ -n "${INSTALLER_LIB_DIR:-}" && -f "${INSTALLER_LIB_DIR}/ui.sh" ]]; then
echo "$INSTALLER_LIB_DIR"
return 0
fi
candidate="$(cd "$(dirname "$script_path")" && pwd)"
if [[ -f "$candidate/ui.sh" ]]; then
echo "$candidate"
return 0
fi
candidate="$(cd "$(dirname "$script_path")/../.." && pwd)/scripts/install/lib"
if [[ -f "$candidate/ui.sh" ]]; then
echo "$candidate"
return 0
fi
if [[ -f "./adventurelog/scripts/install/lib/ui.sh" ]]; then
echo "$(cd "./adventurelog/scripts/install/lib" && pwd)"
return 0
fi
return 1
}
append_env_line() {
local line="$1"
local key="${line%%=*}"
local value="${line#*=}"
if [[ "$key" != "$line" ]]; then
OPTIONAL_ENV_LINES+=("${key}=$(format_env_value "$value")")
else
OPTIONAL_ENV_LINES+=("$line")
fi
}
save_optional_env_to_file() {
local target="$1"
local count=0
[[ -v OPTIONAL_ENV_LINES ]] && count="${#OPTIONAL_ENV_LINES[@]}"
if (( count == 0 )); then
return 0
fi
{
echo ""
echo "# Optional features configured by AdventureLog installer"
for line in "${OPTIONAL_ENV_LINES[@]}"; do
echo "$line"
done
} >> "$target"
}
load_existing_env() {
local file="$1"
if [[ ! -f "$file" ]]; then
return 0
fi
# shellcheck disable=SC1090
set -a
source "$file"
set +a
SITE_URL="${SITE_URL:-}"
HOST_PORT="${HOST_PORT:-}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}"
DJANGO_ADMIN_USERNAME="${DJANGO_ADMIN_USERNAME:-admin}"
DJANGO_ADMIN_PASSWORD="${DJANGO_ADMIN_PASSWORD:-admin}"
DJANGO_ADMIN_EMAIL="${DJANGO_ADMIN_EMAIL:-admin@example.com}"
}

118
scripts/install/lib/deploy.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/bin/bash
# Deploy containers and wait for health.
set -euo pipefail
validate_env_file() {
if [[ ! -f "scripts/validate-env.sh" ]]; then
log_warning "validate-env.sh not found — skipping validation"
return 0
fi
log_info "Validating $ENV_FILE ..."
if ! bash scripts/validate-env.sh "$ENV_FILE"; then
log_error "Environment validation failed"
exit 1
fi
log_success "Environment validated"
}
print_pull_failure_help() {
echo "" >&2
log_error "Failed to pull Docker images."
echo "" >&2
echo "Common fixes:" >&2
echo " • Check your internet connection" >&2
echo " • If you see 'denied' from ghcr.io, wait a moment and retry" >&2
echo " • Try: docker login ghcr.io (only if using a private registry token)" >&2
echo " • Manual pull: docker compose --env-file ${ENV_FILE} -f ${COMPOSE_FILE} pull" >&2
echo "" >&2
echo "To build from source instead, clone the full AdventureLog repo and run:" >&2
echo " docker compose -f docker-compose.aio.yml build && docker compose -f docker-compose.aio.yml up -d" >&2
echo "" >&2
}
start_services() {
if [[ "$DRY_RUN" == true ]]; then
log_info "[dry-run] Would run: run_compose pull && run_compose up -d"
return 0
fi
log_info "Pulling Docker images (this may take a few minutes)..."
echo "" >&2
if ! run_compose pull; then
print_pull_failure_help
exit 1
fi
echo "" >&2
log_success "Images pulled"
log_info "Starting containers..."
if run_compose up -d --remove-orphans --wait 2>/dev/null; then
log_success "Containers started"
elif run_compose up -d --remove-orphans; then
log_success "Containers started (without --wait)"
else
log_error "Failed to start containers"
run_compose ps || true
exit 1
fi
}
wait_for_health() {
local url="${1:-$SITE_URL}"
local max_attempts="${2:-300}"
local attempt=1
local health_url="${url%/}/health"
if [[ "$DRY_RUN" == true ]]; then
log_info "[dry-run] Would wait for $health_url"
return 0
fi
log_info "Waiting for AdventureLog (up to $((max_attempts / 6)) minutes on first boot)..."
while (( attempt <= max_attempts )); do
if curl -fsS -o /dev/null "$health_url" 2>/dev/null; then
echo "" >&2
log_success "Health check passed: $health_url"
return 0
fi
local pct=$(( attempt * 100 / max_attempts ))
local frame='⠋'
case $(( attempt % 4 )) in
1) frame='⠙' ;;
2) frame='⠹' ;;
3) frame='⠸' ;;
esac
printf "\r ${CYAN}${frame}${NC} Waiting for ${health_url} ${DIM}(${pct}%%)${NC} " >&2
sleep 2
((attempt++)) || true
done
printf "\r\033[K" >&2
log_warning "Health check timed out — service may still be starting (first boot imports world data)"
run_compose ps || true
log_info "Recent logs:"
docker logs "$LOG_CONTAINER" --tail 30 2>&1 || true
}
cleanup_on_failure() {
if [[ -f ".env.aio.backup" ]]; then
mv .env.aio.backup .env.aio 2>/dev/null || true
fi
if [[ -f ".env.backup" ]]; then
mv .env.backup .env 2>/dev/null || true
fi
if [[ "$DRY_RUN" != true ]] && command -v docker &>/dev/null; then
run_compose down --remove-orphans 2>/dev/null || true
fi
}
run_deploy_phase() {
print_step_header 6 "$WIZARD_TOTAL" "Deploy"
validate_env_file
start_services
print_step_header 7 "$WIZARD_TOTAL" "Health check"
if [[ "$SETUP_TYPE" == "aio" ]]; then
wait_for_health "$SITE_URL" 300
else
wait_for_health "$FRONTEND_ORIGIN" 300
fi
}

104
scripts/install/lib/download.sh Executable file
View File

@@ -0,0 +1,104 @@
#!/bin/bash
# Download installer files from GitHub.
set -euo pipefail
INSTALLER_LIB_FILES=(
common.sh ui.sh tui.sh checks.sh download.sh deploy.sh
aio-config.sh standard-config.sh manage.sh features.sh
)
download_file() {
local url="$1"
local dest="$2"
local relpath="${url#*AdventureLog/${ADVENTURELOG_REF}/}"
if [[ -n "${REPO_ROOT:-}" && -f "${REPO_ROOT}/${relpath}" ]]; then
mkdir -p "$(dirname "$dest")"
cp "${REPO_ROOT}/${relpath}" "$dest"
return 0
fi
if [[ "$DRY_RUN" == true ]]; then
log_info "[dry-run] Would download $url -> $dest"
return 0
fi
mkdir -p "$(dirname "$dest")"
if ! curl -fsSL --connect-timeout 15 --max-time 60 "$url" -o "$dest"; then
log_error "Failed to download $url"
return 1
fi
}
bootstrap_installer_libs() {
local dest_dir="$1"
log_info "Downloading installer libraries..."
mkdir -p "$dest_dir"
local f
for f in "${INSTALLER_LIB_FILES[@]}"; do
download_file "${GITHUB_RAW}/scripts/install/lib/${f}" "${dest_dir}/${f}"
done
log_success "Installer libraries ready"
}
copy_local_installer_libs() {
local src="$1"
local dest="$2"
if [[ "$DRY_RUN" == true ]]; then
log_info "[dry-run] Would copy installer libs to $dest"
return 0
fi
mkdir -p "$dest"
local f
for f in "${INSTALLER_LIB_FILES[@]}"; do
if [[ -f "$src/$f" ]]; then
cp "$src/$f" "$dest/$f"
chmod +x "$dest/$f" 2>/dev/null || true
fi
done
}
download_aio_toolkit() {
log_info "Downloading AIO deployment files..."
download_file "${GITHUB_RAW}/docker-compose.aio.yml" "docker-compose.aio.yml"
download_file "${GITHUB_RAW}/.env.aio.example" ".env.aio.example"
download_file "${GITHUB_RAW}/deploy.sh" "deploy.sh"
download_file "${GITHUB_RAW}/scripts/validate-env.sh" "scripts/validate-env.sh"
download_file "${GITHUB_RAW}/scripts/backup.sh" "scripts/backup.sh"
download_file "${GITHUB_RAW}/scripts/restore.sh" "scripts/restore.sh"
download_file "${GITHUB_RAW}/docker-compose.override.example.yml" "docker-compose.override.example.yml" || true
chmod +x deploy.sh scripts/validate-env.sh scripts/backup.sh scripts/restore.sh 2>/dev/null || true
if [[ -d "$(dirname "${BASH_SOURCE[0]}")" ]]; then
copy_local_installer_libs "$(dirname "${BASH_SOURCE[0]}")" "scripts/install/lib"
fi
log_success "AIO toolkit downloaded"
}
download_standard_toolkit() {
log_info "Downloading standard deployment files..."
download_file "${GITHUB_RAW}/docker-compose.yml" "docker-compose.yml"
download_file "${GITHUB_RAW}/.env.example" ".env.example"
download_file "${GITHUB_RAW}/deploy.sh" "deploy.sh"
download_file "${GITHUB_RAW}/scripts/validate-env.sh" "scripts/validate-env.sh"
download_file "${GITHUB_RAW}/scripts/backup.sh" "scripts/backup.sh"
download_file "${GITHUB_RAW}/scripts/restore.sh" "scripts/restore.sh"
download_file "${GITHUB_RAW}/docker-compose.override.example.yml" "docker-compose.override.example.yml" || true
chmod +x deploy.sh scripts/validate-env.sh scripts/backup.sh scripts/restore.sh 2>/dev/null || true
if [[ -d "$(dirname "${BASH_SOURCE[0]}")" ]]; then
copy_local_installer_libs "$(dirname "${BASH_SOURCE[0]}")" "scripts/install/lib"
fi
log_success "Standard toolkit downloaded"
}
ensure_install_directory() {
local dir="$INSTALL_DIR"
log_info "Install directory: $dir"
if [[ -d "$dir" ]] && [[ "$FORCE_INSTALL" != true ]]; then
if detect_existing_install "$dir"; then
return 0
fi
log_warning "Directory exists"
tui_confirm "Continue and update files in existing directory?" "y" || exit 0
fi
if [[ "$DRY_RUN" != true ]] || [[ ! -d "$dir" ]]; then
mkdir -p "$dir"
fi
cd "$dir" || exit 1
}

153
scripts/install/lib/features.sh Executable file
View File

@@ -0,0 +1,153 @@
#!/bin/bash
# Optional feature configuration wizard modules.
set -euo pipefail
feature_section_header() {
local name="$1"
local doc_slug="$2"
if declare -f tui_print_box &>/dev/null; then
tui_print_box "Configure: ${name}" "Documentation: ${DOCS_BASE}/${doc_slug}.html"
else
echo ""
log_header "Configure: $name"
log_info "Docs: ${DOCS_BASE}/${doc_slug}.html"
echo ""
fi
}
configure_registration_auth() {
feature_section_header "Registration & Auth" "disable_registration"
if tui_confirm "Disable new user registration?" "n"; then
append_env_line "DISABLE_REGISTRATION=True"
local msg
msg="$(tui_input "Registration disabled message (optional)" "Registration is disabled for this instance of AdventureLog.")"
[[ -n "$msg" ]] && append_env_line "DISABLE_REGISTRATION_MESSAGE=${msg}"
fi
if tui_confirm "Allow social signup when registration is disabled?" "n"; then
append_env_line "SOCIALACCOUNT_ALLOW_SIGNUP=True"
fi
log_warning "Social-only login: do not enable unless SSO is already configured."
log_info "SSO can be set up after AdventureLog starts — Django admin → Social applications."
if tui_confirm "Force social-only login (disable password login)?" "n"; then
append_env_line "FORCE_SOCIALACCOUNT_LOGIN=True"
fi
if tui_confirm "Configure email verification for new accounts?" "n"; then
local verify
if ! verify="$(tui_choose "Email verification level" "optional" "mandatory")"; then
verify="optional"
fi
append_env_line "ACCOUNT_EMAIL_VERIFICATION=${verify}"
fi
}
configure_email() {
feature_section_header "Email" "email"
append_env_line "EMAIL_BACKEND=email"
append_env_line "EMAIL_HOST=$(prompt_with_default "SMTP host" "smtp.gmail.com")"
if tui_confirm "Use TLS?" "y"; then
append_env_line "EMAIL_USE_TLS=True"
append_env_line "EMAIL_PORT=587"
else
append_env_line "EMAIL_USE_TLS=False"
append_env_line "EMAIL_USE_SSL=True"
append_env_line "EMAIL_PORT=465"
fi
append_env_line "EMAIL_HOST_USER=$(prompt_with_default "SMTP username" "")"
append_env_line "EMAIL_HOST_PASSWORD=$(tui_password "SMTP password")"
append_env_line "DEFAULT_FROM_EMAIL=$(prompt_with_default "From email" "noreply@example.com")"
}
configure_s3() {
feature_section_header "S3 Media Storage" "s3_storage"
log_warning "Choose storage at install time — migrating later is difficult."
append_env_line "MEDIA_STORAGE=s3"
append_env_line "AWS_ACCESS_KEY_ID=$(prompt_with_default "AWS access key ID" "")"
append_env_line "AWS_SECRET_ACCESS_KEY=$(tui_password "AWS secret access key")"
append_env_line "AWS_STORAGE_BUCKET_NAME=$(prompt_with_default "Bucket name" "")"
local endpoint
endpoint="$(tui_input "S3 endpoint URL (empty for AWS)" "")"
[[ -n "$endpoint" ]] && append_env_line "AWS_S3_ENDPOINT_URL=${endpoint}"
append_env_line "AWS_S3_REGION_NAME=$(prompt_with_default "Region" "auto")"
append_env_line "AWS_S3_ADDRESSING_STYLE=path"
append_env_line "AWS_S3_SIGNATURE_VERSION=s3v4"
local custom_domain
custom_domain="$(tui_input "Custom CDN domain (optional)" "")"
[[ -n "$custom_domain" ]] && append_env_line "AWS_S3_CUSTOM_DOMAIN=${custom_domain}"
}
configure_google_maps() {
feature_section_header "Google Maps" "google_maps_integration"
local key
key="$(prompt_with_default "Google Maps API key" "")"
[[ -n "$key" ]] && append_env_line "GOOGLE_MAPS_API_KEY=${key}"
}
configure_strava() {
feature_section_header "Strava Integration" "strava_integration"
append_env_line "STRAVA_CLIENT_ID=$(prompt_with_default "Strava client ID" "")"
append_env_line "STRAVA_CLIENT_SECRET=$(tui_password "Strava client secret")"
}
configure_analytics() {
feature_section_header "Umami Analytics" "analytics"
append_env_line "PUBLIC_UMAMI_SRC=$(prompt_with_default "Umami script URL" "https://cloud.umami.is/script.js")"
append_env_line "PUBLIC_UMAMI_WEBSITE_ID=$(prompt_with_default "Umami website ID" "")"
}
configure_performance() {
feature_section_header "Performance & Rate Limits" "advanced_configuration"
local workers
workers="$(prompt_with_default "Gunicorn workers (1 for small hosts)" "2")"
append_env_line "GUNICORN_WORKERS=${workers}"
if tui_confirm "Enable rate limits (recommended for production)?" "n"; then
append_env_line "ENABLE_RATE_LIMITS=True"
fi
}
configure_debug() {
feature_section_header "Debug mode" "advanced_configuration"
if tui_confirm "Enable DEBUG mode? (not for production)" "n"; then
append_env_line "DEBUG=True"
fi
}
run_optional_features_wizard() {
print_step_header 4 "$WIZARD_TOTAL" "Optional features"
if [[ "${DRY_RUN:-false}" == true ]]; then
log_info "[dry-run] Skipping optional features"
return 0
fi
log_info "Configure optional integrations, or pick Done when finished."
echo ""
local groups=(
"Registration & Auth"
"Email"
"S3 Media Storage"
"Google Maps"
"Strava"
"Umami Analytics"
"Performance"
"Debug mode"
"Done — continue"
)
local choice
while true; do
if ! choice="$(tui_choose "Optional features" "${groups[@]}")"; then
break
fi
case "$choice" in
"Registration & Auth") configure_registration_auth ;;
"Email") configure_email ;;
"S3 Media Storage") configure_s3 ;;
"Google Maps") configure_google_maps ;;
"Strava") configure_strava ;;
"Umami Analytics") configure_analytics ;;
"Performance") configure_performance ;;
"Debug mode") configure_debug ;;
"Done — continue"|*) break ;;
esac
done
if [[ -n "$SKIP_WORLD_DATA" ]]; then
append_env_line "SKIP_WORLD_DATA=1"
fi
}

80
scripts/install/lib/main.sh Executable file
View File

@@ -0,0 +1,80 @@
#!/bin/bash
# Main install orchestration.
set -euo pipefail
choose_setup_type() {
if [[ "${DRY_RUN:-false}" == true ]]; then
SETUP_TYPE="aio"
resolve_compose_settings
return 0
fi
print_step_header 2 7 "Choose setup type"
local choice
if ! choice="$(tui_choose "Select installation type" \
"All-in-One (recommended — single URL, minimal config)" \
"Standard (advanced — separate frontend and backend)")"; then
choice="All-in-One (recommended — single URL, minimal config)"
fi
case "$choice" in
Standard*)
SETUP_TYPE="standard"
;;
*)
SETUP_TYPE="aio"
;;
esac
resolve_compose_settings
log_info "Selected: $SETUP_TYPE"
}
run_fresh_install() {
print_screen "Starting installation — $(date)"
init_tui
run_preflight
if [[ -z "${SETUP_TYPE:-}" ]] || [[ "$SETUP_TYPE" == "aio" && -z "${SITE_URL:-}" ]]; then
choose_setup_type
fi
ensure_install_directory
if [[ "$SETUP_TYPE" == "aio" ]]; then
download_aio_toolkit
run_aio_install_wizard
write_env_aio
else
download_standard_toolkit
run_standard_install_wizard
write_env_standard
fi
trap 'cleanup_on_failure; print_failure_message; exit 1' ERR
run_deploy_phase
save_credentials_file
if [[ "$SETUP_TYPE" == "aio" ]]; then
print_aio_success
else
print_standard_success
fi
}
run_installer_main() {
if [[ "$FORCE_INSTALL" != true ]]; then
if [[ -d "$INSTALL_DIR" ]] && detect_existing_install "$INSTALL_DIR"; then
init_tui
if [[ "${ADVENTURELOG_MANAGE:-}" == "1" ]] || tui_confirm "Existing AdventureLog install detected. Open management menu?" "y"; then
INSTALL_DIR="$(cd "$INSTALL_DIR" && pwd)"
run_management_menu
return 0
fi
fi
if [[ "${ADVENTURELOG_MANAGE:-}" == "1" ]]; then
init_tui
run_management_menu
return 0
fi
fi
run_fresh_install
}

184
scripts/install/lib/manage.sh Executable file
View File

@@ -0,0 +1,184 @@
#!/bin/bash
# Management mode for existing installs.
set -euo pipefail
enter_install_dir() {
if [[ -d "$INSTALL_DIR" ]]; then
cd "$INSTALL_DIR" || exit 1
fi
detect_setup_type_from_install "."
resolve_compose_settings
load_existing_env "$ENV_FILE"
}
mgmt_check_status() {
local compose health_url
compose="$(compose_args)"
echo ""
log_header "Status"
$compose ps 2>/dev/null || docker ps -a --filter "name=adventurelog"
echo ""
if [[ "$SETUP_TYPE" == "aio" ]]; then
health_url="${SITE_URL:-http://localhost:8015}/health"
else
health_url="${FRONTEND_ORIGIN:-http://localhost:8015}/health"
fi
if curl -fsS -o /dev/null "$health_url" 2>/dev/null; then
log_success "Health OK: $health_url"
else
log_warning "Health check failed: $health_url"
fi
tui_press_enter
}
mgmt_update() {
local backup_flag=""
if tui_confirm "Create backup before update?" "y"; then
backup_flag="--backup"
fi
if [[ -f deploy.sh ]]; then
# shellcheck disable=SC2086
COMPOSE_FILE="$COMPOSE_FILE" bash deploy.sh $backup_flag
else
local compose
compose="$(compose_args)"
$compose pull
$compose up -d --wait 2>/dev/null || $compose up -d
fi
log_success "Update complete"
tui_press_enter
}
mgmt_reconfigure() {
log_info "Reconfigure optional features and core settings"
if [[ -f "$ENV_FILE" ]]; then
cp "$ENV_FILE" "${ENV_FILE}.backup.$(date +%Y%m%d-%H%M%S)"
fi
OPTIONAL_ENV_LINES=()
if [[ "$SETUP_TYPE" == "aio" ]]; then
prompt_aio_core_config
run_optional_features_wizard
write_env_aio
else
prompt_standard_core_config
run_optional_features_wizard
write_env_standard
fi
validate_env_file
if tui_confirm "Restart services to apply changes?" "y"; then
local compose
compose="$(compose_args)"
$compose up -d --remove-orphans
fi
log_success "Configuration updated"
tui_press_enter
}
mgmt_backup() {
if [[ -f scripts/backup.sh ]]; then
COMPOSE_FILE="$COMPOSE_FILE" bash scripts/backup.sh
else
log_error "scripts/backup.sh not found"
fi
tui_press_enter
}
mgmt_restore() {
local backups dir choice
if [[ ! -d backups ]]; then
log_error "No backups/ directory found"
tui_press_enter
return
fi
mapfile -t backups < <(find backups -mindepth 1 -maxdepth 1 -type d | sort -r)
if [[ ${#backups[@]} -eq 0 ]]; then
log_error "No backups found"
tui_press_enter
return
fi
echo "Available backups:"
local i=1
for dir in "${backups[@]}"; do
echo " [$i] $dir"
((i++)) || true
done
read -r -p "Select backup number: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#backups[@]} )); then
dir="${backups[$((choice - 1))]}"
if tui_confirm "Restore from $dir? This will overwrite current data." "n"; then
COMPOSE_FILE="$COMPOSE_FILE" bash scripts/restore.sh "$dir"
fi
else
log_error "Invalid selection"
fi
tui_press_enter
}
mgmt_logs() {
if docker logs "$LOG_CONTAINER" --tail 50 2>/dev/null; then
echo ""
if tui_confirm "Follow logs (Ctrl+C to stop)?" "n"; then
docker logs "$LOG_CONTAINER" --follow
fi
else
local compose
compose="$(compose_args)"
$compose logs --tail 50
fi
}
mgmt_restart() {
local compose
compose="$(compose_args)"
$compose restart
log_success "Services restarted"
tui_press_enter
}
mgmt_uninstall() {
if ! tui_confirm "Stop containers and remove volumes? This deletes all data." "n"; then
return
fi
local compose
compose="$(compose_args)"
$compose down -v --remove-orphans
if tui_confirm "Also delete install directory $(pwd)?" "n"; then
local install_path
install_path="$(pwd)"
cd .. || exit 1
rm -rf "$install_path"
fi
log_success "Uninstalled"
}
run_management_menu() {
enter_install_dir
print_screen "AdventureLog Manager — $(pwd)"
log_info "Setup: $SETUP_TYPE | Compose: $COMPOSE_FILE"
echo ""
while true; do
local choice
choice="$(tui_choose "What would you like to do?" \
"Check status & health" \
"Update to latest images" \
"Edit configuration" \
"Backup now" \
"Restore from backup" \
"View logs" \
"Restart services" \
"Uninstall" \
"Exit")" || break
case "$choice" in
"Check status & health") mgmt_check_status ;;
"Update to latest images") mgmt_update ;;
"Edit configuration") mgmt_reconfigure ;;
"Backup now") mgmt_backup ;;
"Restore from backup") mgmt_restore ;;
"View logs") mgmt_logs ;;
"Restart services") mgmt_restart ;;
"Uninstall") mgmt_uninstall; break ;;
"Exit"|*) break ;;
esac
print_screen "AdventureLog Manager"
done
}

View File

@@ -0,0 +1,123 @@
#!/bin/bash
# Standard multi-container configuration wizard.
set -euo pipefail
prompt_standard_core_config() {
print_step_header 2 "$WIZARD_TOTAL" "Core configuration (Standard)"
log_info "Standard setup uses separate frontend and backend URLs."
echo ""
local default_frontend="http://localhost:8015"
while true; do
FRONTEND_ORIGIN="$(prompt_with_default "Frontend URL" "$default_frontend")"
if validate_url "$FRONTEND_ORIGIN"; then
FRONTEND_PORT="$(extract_port_from_url "$FRONTEND_ORIGIN" "8015")"
break
fi
log_error "Invalid frontend URL"
done
local default_backend="http://localhost:8016"
while true; do
BACKEND_URL="$(prompt_with_default "Backend URL" "$default_backend")"
if validate_url "$BACKEND_URL"; then
BACKEND_PORT="$(extract_port_from_url "$BACKEND_URL" "8016")"
break
fi
log_error "Invalid backend URL"
done
POSTGRES_PASSWORD="$(generate_secure_password 32)"
if tui_confirm "Change default admin credentials (admin/admin)?" "y"; then
DJANGO_ADMIN_USERNAME="$(prompt_with_default "Admin username" "admin")"
DJANGO_ADMIN_PASSWORD="$(generate_secure_password 24)"
DJANGO_ADMIN_EMAIL="$(prompt_with_default "Admin email" "admin@example.com")"
else
DJANGO_ADMIN_USERNAME="admin"
DJANGO_ADMIN_PASSWORD="admin"
DJANGO_ADMIN_EMAIL="admin@example.com"
fi
log_success "Frontend: $FRONTEND_ORIGIN Backend: $BACKEND_URL"
echo ""
}
write_env_standard() {
local target="$ENV_FILE"
local secret_key
secret_key="$(generate_secure_password 50)"
if [[ -f .env.example ]]; then
cp .env.example "$target"
local tmp="${target}.tmp"
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
POSTGRES_PASSWORD=*) echo "POSTGRES_PASSWORD=$(format_env_value "$POSTGRES_PASSWORD")" ;;
DJANGO_ADMIN_PASSWORD=*) echo "DJANGO_ADMIN_PASSWORD=$(format_env_value "$DJANGO_ADMIN_PASSWORD")" ;;
DJANGO_ADMIN_USERNAME=*) echo "DJANGO_ADMIN_USERNAME=$(format_env_value "$DJANGO_ADMIN_USERNAME")" ;;
DJANGO_ADMIN_EMAIL=*) echo "DJANGO_ADMIN_EMAIL=$(format_env_value "$DJANGO_ADMIN_EMAIL")" ;;
SECRET_KEY=*) echo "SECRET_KEY=$(format_env_value "$secret_key")" ;;
ORIGIN=*) echo "ORIGIN=$(format_env_value "$FRONTEND_ORIGIN")" ;;
PUBLIC_URL=*) echo "PUBLIC_URL=$(format_env_value "$BACKEND_URL")" ;;
CSRF_TRUSTED_ORIGINS=*) echo "CSRF_TRUSTED_ORIGINS=$(format_env_value "${FRONTEND_ORIGIN},${BACKEND_URL}")" ;;
FRONTEND_URL=*) echo "FRONTEND_URL=$(format_env_value "$FRONTEND_ORIGIN")" ;;
FRONTEND_PORT=*) echo "FRONTEND_PORT=$(format_env_value "$FRONTEND_PORT")" ;;
BACKEND_PORT=*) echo "BACKEND_PORT=$(format_env_value "$BACKEND_PORT")" ;;
*) echo "$line" ;;
esac
done < "$target" > "$tmp"
mv "$tmp" "$target"
else
{
echo "PUBLIC_SERVER_URL=http://server:8000"
echo "ORIGIN=$(format_env_value "$FRONTEND_ORIGIN")"
echo "BODY_SIZE_LIMIT=Infinity"
echo "FRONTEND_PORT=$(format_env_value "$FRONTEND_PORT")"
echo "PGHOST=db"
echo "POSTGRES_DB=database"
echo "POSTGRES_USER=adventure"
echo "POSTGRES_PASSWORD=$(format_env_value "$POSTGRES_PASSWORD")"
echo "SECRET_KEY=$(format_env_value "$secret_key")"
echo "DJANGO_ADMIN_USERNAME=$(format_env_value "$DJANGO_ADMIN_USERNAME")"
echo "DJANGO_ADMIN_PASSWORD=$(format_env_value "$DJANGO_ADMIN_PASSWORD")"
echo "DJANGO_ADMIN_EMAIL=$(format_env_value "$DJANGO_ADMIN_EMAIL")"
echo "PUBLIC_URL=$(format_env_value "$BACKEND_URL")"
echo "CSRF_TRUSTED_ORIGINS=$(format_env_value "${FRONTEND_ORIGIN},${BACKEND_URL}")"
echo "DEBUG=False"
echo "FRONTEND_URL=$(format_env_value "$FRONTEND_ORIGIN")"
echo "BACKEND_PORT=$(format_env_value "$BACKEND_PORT")"
} > "$target"
fi
save_optional_env_to_file "$target"
log_success "Wrote $target"
}
show_standard_review() {
print_step_header 5 "$WIZARD_TOTAL" "Review configuration"
print_summary_row "Setup" "Standard (multi-container)"
print_summary_row "Frontend" "$FRONTEND_ORIGIN"
print_summary_row "Backend" "$BACKEND_URL"
print_summary_row "PostGIS image" "$POSTGIS_IMAGE"
echo ""
tui_confirm "Proceed with installation?" "y" || exit 0
}
print_standard_success() {
print_success_banner
log_success "Installation completed!"
echo -e "${BOLD}Frontend:${NC} ${CYAN}${FRONTEND_ORIGIN}${NC}"
echo -e "${BOLD}Backend:${NC} ${CYAN}${BACKEND_URL}${NC}"
echo -e "${BOLD}Admin:${NC} ${GREEN}${DJANGO_ADMIN_USERNAME}${NC} / ${GREEN}${DJANGO_ADMIN_PASSWORD}${NC}"
echo ""
echo -e "Update: bash deploy.sh --backup"
echo ""
}
run_standard_install_wizard() {
SETUP_TYPE="standard"
resolve_compose_settings
WIZARD_TOTAL=7
prompt_standard_core_config
run_port_checks
run_optional_features_wizard
write_postgis_override
show_standard_review
}

433
scripts/install/lib/tui.sh Executable file
View File

@@ -0,0 +1,433 @@
#!/bin/bash
# Cross-distro TUI: full gum (real terminals) | styled bash + gum cosmetics (IDE) | plain fallback.
set -euo pipefail
TUI_BACKEND="bash"
GUM_STYLE=false
has_gum() {
command -v gum &>/dev/null
}
# Full gum choose/confirm/input uses alt-screen — broken in IDE integrated terminals.
terminal_supports_gum_interactive() {
has_gum || return 1
[[ -t 0 && -t 1 ]] || return 1
[[ -n "${TERM:-}" && "${TERM:-}" != "dumb" ]] || return 1
case "${TERM_PROGRAM:-}" in
vscode|Visual\ Studio\ Code) return 1 ;;
esac
[[ -n "${VSCODE_GIT_IPC_HANDLE:-}" ]] && return 1
[[ -n "${CURSOR_TRACE_ID:-}" ]] && return 1
[[ -n "${CURSOR_AGENT:-}" ]] && return 1
return 0
}
detect_tui_backend() {
if [[ -n "${ADVENTURELOG_TUI:-}" ]]; then
TUI_BACKEND="$ADVENTURELOG_TUI"
return 0
fi
if terminal_supports_gum_interactive; then
TUI_BACKEND="gum"
elif has_gum; then
TUI_BACKEND="styled"
elif command -v whiptail &>/dev/null; then
TUI_BACKEND="whiptail"
elif command -v dialog &>/dev/null; then
TUI_BACKEND="dialog"
else
TUI_BACKEND="bash"
fi
}
offer_gum_install() {
if has_gum; then
return 0
fi
tui_print_box "Enhanced UI" "Install gum for styled menus, spinners, and progress bars."
if ! tui_confirm_bash "Install gum now?" "y"; then
log_info "Continuing with built-in styled UI."
return 1
fi
if [[ "$OSTYPE" == "darwin"* ]] && command -v brew &>/dev/null; then
log_info "Installing gum via Homebrew..."
brew install gum
elif command -v apt-get &>/dev/null; then
log_info "Installing gum via apt (may require sudo)..."
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list >/dev/null
sudo apt-get update -qq
sudo apt-get install -y gum
else
log_warning "Automatic gum install not supported on this OS."
log_info "See https://github.com/charmbracelet/gum#installation"
return 1
fi
has_gum
}
init_tui() {
GUM_STYLE=false
TUI_BACKEND="bash"
if has_gum; then
GUM_STYLE=true
fi
if [[ "$DRY_RUN" == true ]] || [[ "${ADVENTURELOG_SKIP_GUM:-}" == "1" ]]; then
TUI_BACKEND="bash"
return 0
fi
if ! has_gum && [[ "${ADVENTURELOG_SKIP_GUM:-}" != "1" ]]; then
offer_gum_install && GUM_STYLE=true
fi
detect_tui_backend
case "$TUI_BACKEND" in
gum) log_info "UI mode: interactive gum" ;;
styled) log_info "UI mode: styled terminal (IDE-safe)" ;;
*) log_info "UI mode: classic text" ;;
esac
}
# ── Visual helpers (safe in all terminals) ──────────────────────────────────
tui_styled() {
local fg="${1:-212}"
shift
if [[ "$GUM_STYLE" == true ]]; then
gum style --foreground "$fg" "$@"
else
echo -e "${PURPLE}${BOLD}$*${NC}"
fi
}
tui_print_box() {
local title="$1"
local body="$2"
echo "" >&2
if [[ "$GUM_STYLE" == true ]]; then
gum join --align left --vertical \
"$(gum style --border rounded --border-foreground 212 --padding "0 1" --bold "$title")" \
"$(gum style --border rounded --border-foreground 240 --padding "0 1" "$body")" >&2
else
local width=58
echo -e "${CYAN}$(printf '─%.0s' $(seq 1 "$width"))${NC}" >&2
echo -e "${CYAN}${NC} ${BOLD}${title}${NC}" >&2
echo -e "${CYAN}$(printf '─%.0s' $(seq 1 "$width"))${NC}" >&2
while IFS= read -r line; do
echo -e "${CYAN}${NC} ${line}" >&2
done <<< "$body"
echo -e "${CYAN}$(printf '─%.0s' $(seq 1 "$width"))${NC}" >&2
fi
echo "" >&2
}
tui_progress_bar() {
local step="$1"
local total="$2"
local label="$3"
local width=24
local filled=$(( step * width / total ))
local empty=$(( width - filled ))
local bar
bar="$(printf '█%.0s' $(seq 1 "$filled" 2>/dev/null || true))$(printf '░%.0s' $(seq 1 "$empty" 2>/dev/null || true))"
if [[ "$GUM_STYLE" == true ]]; then
if ! gum join --horizontal \
"$(gum style --foreground 212 "[${bar}]")" \
"$(gum style --bold " Step ${step}/${total}")" \
"$(gum style --foreground 245 " ${label}")" >&2 2>/dev/null; then
echo -e "${CYAN}[${bar}]${NC} ${BOLD}Step ${step}/${total}${NC} ${DIM}${label}${NC}" >&2
fi
else
echo -e "${CYAN}[${bar}]${NC} ${BOLD}Step ${step}/${total}${NC} ${DIM}${label}${NC}" >&2
fi
echo "" >&2
}
# ── Prompts ─────────────────────────────────────────────────────────────────
tui_confirm_bash() {
local prompt="$1"
local default="${2:-n}"
local hint="[y/N]"
[[ "$default" == "y" ]] && hint="[Y/n]"
if [[ "$TUI_BACKEND" == "styled" ]] || [[ "$TUI_BACKEND" == "gum" ]]; then
tui_print_box "Confirm" "$prompt"
fi
echo "" >&2
read -r -p "$(echo -e "${BOLD}?${NC} ${prompt} ${DIM}${hint}${NC}: ")" reply
reply="${reply:-$default}"
[[ "$reply" =~ ^[Yy] ]]
}
tui_confirm() {
local prompt="$1"
local default="${2:-n}"
if [[ "${DRY_RUN:-false}" == true ]]; then
[[ "$default" == "y" ]]
return
fi
case "$TUI_BACKEND" in
gum)
local gum_rc=0
if [[ "$default" == "y" ]]; then
gum confirm "$prompt" --affirmative "Yes" --negative "No" --default=true 2>/dev/null || gum_rc=$?
else
gum confirm "$prompt" --affirmative "Yes" --negative "No" --default=false 2>/dev/null || gum_rc=$?
fi
if [[ $gum_rc -eq 0 ]]; then return 0; fi
if [[ $gum_rc -eq 1 ]]; then return 1; fi
tui_confirm_bash "$prompt" "$default"
;;
whiptail)
if whiptail --yesno "$prompt" 12 70; then return 0; else return 1; fi
;;
dialog)
if dialog --yesno "$prompt" 12 70; then return 0; else return 1; fi
;;
*)
tui_confirm_bash "$prompt" "$default"
;;
esac
}
tui_input_bash() {
local prompt="$1"
local default="${2:-}"
local value
if [[ "$TUI_BACKEND" == "styled" ]]; then
tui_print_box "Input" "$prompt"
fi
echo "" >&2
if [[ -n "$default" ]]; then
read -r -p "$(echo -e "${BOLD}${NC} ${prompt} ${DIM}[${default}]${NC}: ")" value
echo "${value:-$default}"
else
read -r -p "$(echo -e "${BOLD}${NC} ${prompt}: ")" value
echo "$value"
fi
}
tui_input() {
local prompt="$1"
local default="${2:-}"
if [[ "${DRY_RUN:-false}" == true ]]; then
echo "$default"
return 0
fi
local value
case "$TUI_BACKEND" in
gum)
if [[ -n "$default" ]]; then
value="$(gum input --placeholder "$default" --prompt "$prompt " --value "$default" --width 60 2>/dev/null || true)"
else
value="$(gum input --prompt "$prompt " --width 60 2>/dev/null || true)"
fi
if [[ -n "$value" ]]; then
echo "$value"
return 0
fi
tui_input_bash "$prompt" "$default"
;;
whiptail)
whiptail --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3
;;
dialog)
dialog --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3
;;
*)
tui_input_bash "$prompt" "$default"
;;
esac
}
tui_password() {
local prompt="$1"
if [[ "${DRY_RUN:-false}" == true ]]; then
echo "dry-run-password"
return 0
fi
local value
case "$TUI_BACKEND" in
gum)
value="$(gum input --password --prompt "$prompt " --width 60 2>/dev/null || true)"
if [[ -n "$value" ]]; then
echo "$value"
return 0
fi
;&
*)
if [[ "$TUI_BACKEND" == "styled" ]]; then
tui_print_box "Secret" "$prompt"
fi
echo "" >&2
read -r -s -p "$(echo -e "${BOLD}🔒${NC} ${prompt}: ")" value
echo "" >&2
echo "$value"
;;
esac
}
tui_choose_styled() {
local prompt="$1"
shift
local options=("$@")
local choice i default=1
tui_print_box "$prompt" "Enter the number for your choice."
echo "" >&2
i=1
for opt in "${options[@]}"; do
if (( i == default )); then
if [[ "$GUM_STYLE" == true ]]; then
echo " $(gum style --foreground 212 --bold " ${i}") $(gum style --bold "$opt")" >&2
else
echo -e " ${CYAN}${BOLD} ${i}${NC} ${BOLD}${opt}${NC}" >&2
fi
else
echo -e " ${DIM}${i}${NC} ${opt}" >&2
fi
((i++)) || true
done
echo "" >&2
read -r -p "$(echo -e "${BOLD}${NC} Choice ${DIM}[${default}]${NC}: ")" choice
choice="${choice:-$default}"
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#options[@]} )); then
echo "${options[$((choice - 1))]}"
return 0
fi
return 1
}
tui_choose() {
local prompt="$1"
shift
local options=("$@")
local choice result
if [[ "${DRY_RUN:-false}" == true ]]; then
echo "${options[0]}"
return 0
fi
case "$TUI_BACKEND" in
gum)
if result="$(gum choose --header "$prompt" --cursor " " --selected.foreground "212" "${options[@]}" 2>/dev/null)" && [[ -n "$result" ]]; then
echo "$result"
return 0
fi
tui_choose_styled "$prompt" "${options[@]}"
;;
styled|bash)
tui_choose_styled "$prompt" "${options[@]}"
;;
whiptail|dialog)
local menu_args=()
local idx=1
for opt in "${options[@]}"; do
menu_args+=("$idx" "$opt")
((idx++)) || true
done
local cmd="whiptail"
[[ "$TUI_BACKEND" == "dialog" ]] && cmd="dialog"
choice=$($cmd --menu "$prompt" 20 78 12 "${menu_args[@]}" 3>&1 1>&2 2>&3) || return 1
echo "${options[$((choice - 1))]}"
;;
esac
}
tui_spinner_bash() {
local title="$1"
shift
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
local tmp_out tmp_err
tmp_out="$(mktemp)"
tmp_err="$(mktemp)"
("$@" >"$tmp_out" 2>"$tmp_err") &
local pid=$!
while kill -0 "$pid" 2>/dev/null; do
printf "\r ${CYAN}${frames[$i]}${NC} ${title}" >&2
i=$(( (i + 1) % ${#frames[@]} ))
sleep 0.1
done
wait "$pid"
local rc=$?
printf "\r\033[K" >&2
if [[ $rc -eq 0 ]]; then
log_success "$title" >&2
else
log_error "$title failed" >&2
cat "$tmp_err" >&2
fi
cat "$tmp_out"
rm -f "$tmp_out" "$tmp_err"
return $rc
}
tui_spinner() {
local title="$1"
shift
case "$TUI_BACKEND" in
gum)
gum spin --spinner dot --spinner.foreground "212" --title "$title" -- "$@" 2>/dev/null || {
tui_spinner_bash "$title" "$@"
}
;;
styled|bash)
tui_spinner_bash "$title" "$@"
;;
*)
log_info "$title"
"$@"
;;
esac
}
tui_wait_progress() {
local message="$1"
local max="$2"
local attempt=0
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while (( attempt < max )); do
local pct=$(( attempt * 100 / max ))
local frame="${frames[$i]}"
printf "\r ${CYAN}${frame}${NC} ${message} ${DIM}(${pct}%%)${NC} " >&2
i=$(( (i + 1) % ${#frames[@]} ))
sleep 2
((attempt++)) || true
done
printf "\r\033[K" >&2
}
tui_press_enter() {
local msg="${1:-Press Enter to continue...}"
echo "" >&2
if [[ "$GUM_STYLE" == true ]]; then
gum style --foreground 245 "$msg" >&2
else
echo -e "${DIM}${msg}${NC}" >&2
fi
read -r -p "" _
}
prompt_with_default() {
local prompt="$1"
local default="$2"
local validator="${3:-}"
local value
while true; do
value="$(tui_input "$prompt" "$default")"
value="${value:-$default}"
if [[ -z "$validator" ]] || "$validator" "$value"; then
echo "$value"
return 0
fi
log_error "Invalid value. Please try again."
done
}
prompt_yes_no() {
local prompt="$1"
local default="${2:-n}"
tui_confirm "$prompt" "$default"
}

129
scripts/install/lib/ui.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/bin/bash
# UI helpers: colors, banners, logging, step headers.
set -euo pipefail
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly MAGENTA='\033[0;35m'
readonly BOLD='\033[1m'
readonly DIM='\033[2m'
readonly NC='\033[0m'
log_info() { echo -e "${BLUE} $1${NC}"; }
log_success() { echo -e "${GREEN}$1${NC}"; }
log_warning() { echo -e "${YELLOW}$1${NC}"; }
log_error() { echo -e "${RED}$1${NC}"; }
log_header() { echo -e "${PURPLE}${BOLD}$1${NC}"; }
print_banner() {
if declare -f has_gum &>/dev/null && has_gum && [[ "${GUM_STYLE:-false}" == true ]]; then
gum style \
--border double \
--border-foreground 212 \
--align center \
--width 65 \
--padding "1 2" \
--bold \
"ADVENTURELOG" \
"" \
"Installer & Manager" \
"The Ultimate Travel Companion" 2>/dev/null || print_banner_fallback
return
fi
print_banner_fallback
}
print_banner_fallback() {
cat << 'EOF'
╔═════════════════════════════════════════════════════════════════════════╗
║ ║
║ A D V E N T U R E L O G I N S T A L L E R ║
║ ║
║ The Ultimate Travel Companion ║
╚═════════════════════════════════════════════════════════════════════════╝
EOF
}
print_manager_banner() {
cat << 'EOF'
╔═════════════════════════════════════════════════════════════════════════╗
║ ║
║ A D V E N T U R E L O G M A N A G E R ║
║ ║
╚═════════════════════════════════════════════════════════════════════════╝
EOF
}
print_screen() {
local title="${1:-}"
if [[ -t 1 ]] && [[ -n "${TERM:-}" ]] && [[ "$TERM" != "dumb" ]]; then
clear 2>/dev/null || true
fi
echo ""
print_banner
echo ""
if [[ -n "$title" ]]; then
log_header "$title"
echo ""
fi
}
print_step_header() {
local step="$1"
local total="$2"
local label="$3"
WIZARD_STEP="$step"
WIZARD_TOTAL="$total"
echo "" >&2
if declare -f tui_progress_bar &>/dev/null; then
tui_progress_bar "$step" "$total" "$label"
else
log_header "Step ${step}/${total}: ${label}"
echo -e "${DIM}$(printf '─%.0s' {1..60})${NC}"
echo ""
fi
}
print_divider() {
echo -e "${DIM}$(printf '─%.0s' {1..60})${NC}"
}
print_summary_row() {
local key="$1"
local value="$2"
if [[ "${GUM_STYLE:-false}" == true ]] && has_gum; then
gum join --horizontal \
"$(gum style --width 22 --bold "$key")" \
"$(gum style "$value")" 2>/dev/null || \
printf " ${BOLD}%-22s${NC} %s\n" "$key" "$value"
else
printf " ${BOLD}%-22s${NC} %s\n" "$key" "$value"
fi
}
print_success_banner() {
echo ""
cat << 'EOF'
╔════════════════════════════════════════════════════════════════════════════╗
║ A D V E N T U R E L O G I S R E A D Y F O R L A U N C H! ║
╚════════════════════════════════════════════════════════════════════════════╝
EOF
echo ""
}
print_failure_message() {
echo ""
log_error "Operation failed!"
echo ""
echo "Troubleshooting:"
echo " 1. docker info"
echo " 2. docker compose -f $COMPOSE_FILE ps"
echo " 3. docker compose -f $COMPOSE_FILE logs"
echo " 4. bash scripts/validate-env.sh $ENV_FILE"
echo ""
echo "Support: https://github.com/seanmorley15/AdventureLog"
}

107
scripts/restore.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/bin/bash
# Restore AdventureLog from a backup created by scripts/backup.sh.
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <backup-directory>" >&2
echo "Example: $0 backups/20260101-120000" >&2
exit 1
fi
BACKUP_DIR="$1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$SCRIPT_DIR"
if [[ ! -d "$BACKUP_DIR" ]]; then
echo "ERROR: Backup directory not found: $BACKUP_DIR" >&2
exit 1
fi
COMPOSE_FILE="${COMPOSE_FILE:-}"
if [[ -z "$COMPOSE_FILE" ]]; then
if [[ -f .env.aio ]] && [[ ! -f .env ]]; then
COMPOSE_FILE="docker-compose.aio.yml"
elif [[ -f .env.aio ]] && [[ -f docker-compose.aio.yml ]] && [[ "${ADVENTURELOG_COMPOSE:-}" == "aio" ]]; then
COMPOSE_FILE="docker-compose.aio.yml"
else
COMPOSE_FILE="docker-compose.yml"
fi
fi
ENV_FILE="${ENV_FILE:-}"
case "$COMPOSE_FILE" in
*docker-compose.aio.yml*)
ENV_FILE="${ENV_FILE:-.env.aio}"
;;
*)
ENV_FILE="${ENV_FILE:-.env}"
;;
esac
if [[ -f "$ENV_FILE" ]]; then
COMPOSE=(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE")
else
COMPOSE=(docker compose -f "$COMPOSE_FILE")
fi
resolve_compose_volume() {
local suffix="$1"
"${COMPOSE[@]}" volume ls -q | grep "${suffix}$" | head -n 1
}
MEDIA_VOLUME="$(resolve_compose_volume "adventurelog_media")"
if [[ -z "$MEDIA_VOLUME" ]]; then
MEDIA_VOLUME="$(resolve_compose_volume "media")"
fi
POSTGRES_USER="${POSTGRES_USER:-adventure}"
POSTGRES_DB="${POSTGRES_DB:-database}"
if [[ -f .env ]]; then
# shellcheck disable=SC1090
set -a
source .env
set +a
elif [[ -f .env.aio ]]; then
# shellcheck disable=SC1090
set -a
source .env.aio
set +a
fi
echo "Stopping AdventureLog containers..."
"${COMPOSE[@]}" down
if [[ -f "$BACKUP_DIR/.env" ]]; then
cp "$BACKUP_DIR/.env" .env
elif [[ -f "$BACKUP_DIR/.env.aio" ]]; then
cp "$BACKUP_DIR/.env.aio" .env.aio
fi
echo "Starting database..."
"${COMPOSE[@]}" up -d db
sleep 5
if [[ -f "$BACKUP_DIR/database.sql" ]]; then
echo "Restoring database..."
"${COMPOSE[@]}" exec -T db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
cat "$BACKUP_DIR/database.sql" | "${COMPOSE[@]}" exec -T db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"
fi
if [[ -f "$BACKUP_DIR/media.tar.gz" ]]; then
if [[ -z "$MEDIA_VOLUME" ]]; then
echo "ERROR: media volume not found; cannot restore media" >&2
exit 1
fi
echo "Restoring media volume..."
docker run --rm \
-v "${MEDIA_VOLUME}:/data" \
-v "$(realpath "$BACKUP_DIR"):/backup:ro" \
alpine:3.21 \
sh -c "rm -rf /data/* && tar xzf /backup/media.tar.gz -C /data"
fi
echo "Starting AdventureLog..."
"${COMPOSE[@]}" up -d
echo "Restore complete."

144
scripts/validate-env.sh Executable file
View File

@@ -0,0 +1,144 @@
#!/bin/bash
# Validate AdventureLog .env configuration for common self-hosting mistakes.
set -uo pipefail
ENV_FILE="${1:-.env}"
WARNINGS=0
ERRORS=0
warn() {
echo "WARNING: $1" >&2
WARNINGS=$((WARNINGS + 1))
}
error() {
echo "ERROR: $1" >&2
ERRORS=$((ERRORS + 1))
}
if [[ ! -f "$ENV_FILE" ]]; then
error "Environment file not found: $ENV_FILE"
exit 1
fi
# shellcheck disable=SC1090
set -a
source "$ENV_FILE"
set +a
echo "Validating $ENV_FILE ..."
IS_AIO_ENV=false
if [[ "$ENV_FILE" == *".env.aio"* ]] || [[ "$(basename "$ENV_FILE")" == ".env.aio" ]]; then
IS_AIO_ENV=true
fi
if [[ "$IS_AIO_ENV" == true ]]; then
if [[ -z "${POSTGRES_PASSWORD:-}" ]]; then
error "POSTGRES_PASSWORD is required in $ENV_FILE"
fi
if [[ "${POSTGRES_PASSWORD:-changeme123}" == "changeme123" ]]; then
warn "Default POSTGRES_PASSWORD detected — change this for production"
fi
if [[ -n "${SITE_URL:-}" ]] && [[ "$SITE_URL" != http://* ]] && [[ "$SITE_URL" != https://* ]]; then
warn "SITE_URL should start with http:// or https://"
fi
if [[ "${DJANGO_ADMIN_PASSWORD:-admin}" == "admin" ]] && [[ "${DJANGO_ADMIN_USERNAME:-admin}" == "admin" ]]; then
warn "Default admin credentials (admin/admin) detected — change for production"
fi
if [[ "${MEDIA_STORAGE:-local}" == "s3" ]]; then
if [[ -z "${AWS_ACCESS_KEY_ID:-}" ]] || [[ -z "${AWS_SECRET_ACCESS_KEY:-}" ]] || [[ -z "${AWS_STORAGE_BUCKET_NAME:-}" ]]; then
error "MEDIA_STORAGE=s3 requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_STORAGE_BUCKET_NAME"
fi
fi
if [[ "${EMAIL_BACKEND:-}" == "email" ]]; then
if [[ -z "${EMAIL_HOST:-}" ]] || [[ -z "${EMAIL_HOST_USER:-}" ]] || [[ -z "${EMAIL_HOST_PASSWORD:-}" ]]; then
error "EMAIL_BACKEND=email requires EMAIL_HOST, EMAIL_HOST_USER, and EMAIL_HOST_PASSWORD"
fi
fi
if [[ "${DEBUG:-False}" == "True" ]] || [[ "${DEBUG:-False}" == "true" ]]; then
warn "DEBUG is enabled — disable for production"
fi
echo ""
if [[ $ERRORS -gt 0 ]]; then
echo "Validation failed: $ERRORS error(s), $WARNINGS warning(s)"
exit 1
fi
if [[ $WARNINGS -gt 0 ]]; then
echo "Validation passed with $WARNINGS warning(s)"
exit 0
fi
echo "Validation passed"
exit 0
fi
if [[ -z "${PUBLIC_SERVER_URL:-}" ]]; then
warn "PUBLIC_SERVER_URL is not set (default http://server:8000 is expected in Docker)"
elif [[ "$PUBLIC_SERVER_URL" == *"localhost:8016"* ]] || [[ "$PUBLIC_SERVER_URL" == *"127.0.0.1:8016"* ]]; then
error "PUBLIC_SERVER_URL points at the host-mapped backend port. Use http://server:8000 inside Docker."
elif [[ "$PUBLIC_SERVER_URL" != *":8000"* ]] && [[ "$PUBLIC_SERVER_URL" != "http://server:8000" ]]; then
warn "PUBLIC_SERVER_URL is '$PUBLIC_SERVER_URL' — standard Docker installs should use http://server:8000"
fi
if [[ -n "${SITE_URL:-}" ]]; then
echo "SITE_URL is set — individual URL vars will be derived when not explicitly set."
fi
if [[ -z "${CSRF_TRUSTED_ORIGINS:-}" ]] && [[ -z "${SITE_URL:-}" ]]; then
warn "CSRF_TRUSTED_ORIGINS is not set and SITE_URL is not set"
fi
if [[ -n "${CSRF_TRUSTED_ORIGINS:-}" ]] && [[ -z "${SITE_URL:-}" ]]; then
IFS=',' read -ra ORIGINS <<< "$CSRF_TRUSTED_ORIGINS"
if [[ -n "${ORIGIN:-}" ]] && [[ ! "$CSRF_TRUSTED_ORIGINS" == *"$ORIGIN"* ]]; then
warn "CSRF_TRUSTED_ORIGINS does not include ORIGIN ($ORIGIN)"
fi
if [[ -n "${FRONTEND_URL:-}" ]] && [[ ! "$CSRF_TRUSTED_ORIGINS" == *"$FRONTEND_URL"* ]]; then
warn "CSRF_TRUSTED_ORIGINS does not include FRONTEND_URL ($FRONTEND_URL)"
fi
if [[ -n "${PUBLIC_URL:-}" ]] && [[ ! "$CSRF_TRUSTED_ORIGINS" == *"$PUBLIC_URL"* ]]; then
warn "CSRF_TRUSTED_ORIGINS does not include PUBLIC_URL ($PUBLIC_URL)"
fi
fi
if [[ -n "${PUBLIC_URL:-}" ]] && [[ -n "${FRONTEND_URL:-}" ]]; then
public_scheme="${PUBLIC_URL%%://*}"
frontend_scheme="${FRONTEND_URL%%://*}"
if [[ "$public_scheme" != "$frontend_scheme" ]]; then
warn "PUBLIC_URL ($public_scheme) and FRONTEND_URL ($frontend_scheme) use different schemes"
fi
fi
if [[ -n "${FRONTEND_PORT:-}" ]] && [[ -n "${ORIGIN:-}" ]]; then
if [[ "$ORIGIN" == *":$FRONTEND_PORT"* ]] || [[ "$ORIGIN" != *":"* && "$FRONTEND_PORT" == "80" ]]; then
:
else
warn "ORIGIN ($ORIGIN) may not match FRONTEND_PORT ($FRONTEND_PORT)"
fi
fi
if [[ -n "${BACKEND_PORT:-}" ]] && [[ -n "${PUBLIC_URL:-}" ]]; then
if [[ "$PUBLIC_URL" == *":$BACKEND_PORT"* ]] || [[ "$PUBLIC_URL" != *":"* && "$BACKEND_PORT" == "80" ]]; then
:
else
warn "PUBLIC_URL ($PUBLIC_URL) may not match BACKEND_PORT ($BACKEND_PORT)"
fi
fi
if [[ "${POSTGRES_PASSWORD:-changeme123}" == "changeme123" ]] || [[ "${SECRET_KEY:-changeme123}" == "changeme123" ]]; then
warn "Default POSTGRES_PASSWORD or SECRET_KEY detected — change these for production"
fi
echo ""
if [[ $ERRORS -gt 0 ]]; then
echo "Validation failed: $ERRORS error(s), $WARNINGS warning(s)"
exit 1
fi
if [[ $WARNINGS -gt 0 ]]; then
echo "Validation passed with $WARNINGS warning(s)"
exit 0
fi
echo "Validation passed"
exit 0