refactor(utils): improve whiptail dialogs and logging utilities

- add adaptive terminal sizing for all whiptail dialogs (wt_get_size)
- add new wrapper functions: wt_checklist, wt_radiolist, wt_menu
- add safe parser wt_parse_choices to replace eval
- improve NEWT_COLORS theme with better contrast (brightgreen/brightcyan)
- add new logging functions: log_header, log_subheader, log_divider, log_box
- add spinner animation utilities for progress indication
- expand color palette with bright variants and text styles
- update 04_wizard.sh to use new whiptail wrappers
This commit is contained in:
Yury Kossakovsky
2025-12-12 13:43:20 -07:00
parent 7d922c4e67
commit 04e70cee2d
11 changed files with 423 additions and 205 deletions

View File

@@ -21,10 +21,12 @@ init_paths
export DEBIAN_FRONTEND=noninteractive
# System Update
log_subheader "System Update"
log_info "Updating package list and upgrading the system..."
apt update -y && apt upgrade -y
# Installing Basic Utilities
log_subheader "Installing Utilities"
log_info "Installing standard CLI tools..."
apt install -y \
htop git curl make unzip ufw fail2ban python3 psmisc whiptail \
@@ -32,7 +34,8 @@ apt install -y \
debian-keyring debian-archive-keyring apt-transport-https python3-pip python3-dotenv python3-yaml
# Configuring Firewall (UFW)
log_info "Configuring firewall (UFW)..."
log_subheader "Firewall (UFW)"
log_info "Configuring firewall..."
echo "y" | ufw reset
ufw --force enable
ufw default deny incoming
@@ -44,7 +47,8 @@ ufw reload
ufw status
# Configuring Fail2Ban
log_info "Enabling brute-force protection (Fail2Ban)..."
log_subheader "Fail2Ban"
log_info "Enabling brute-force protection..."
systemctl enable fail2ban
sleep 1
systemctl start fail2ban
@@ -54,12 +58,14 @@ sleep 1
fail2ban-client status sshd
# Automatic Security Updates
log_subheader "Security Updates"
log_info "Enabling automatic security updates..."
apt install -y unattended-upgrades
# Automatic confirmation for dpkg-reconfigure
echo "y" | dpkg-reconfigure --priority=low unattended-upgrades
# Configure vm.max_map_count for Elasticsearch (required for RAGFlow)
log_subheader "Kernel Parameters"
log_info "Configuring vm.max_map_count for Elasticsearch..."
CURRENT_VALUE=$(sysctl -n vm.max_map_count 2>/dev/null || echo "0")
if [[ "$CURRENT_VALUE" -lt 262144 ]]; then

View File

@@ -21,7 +21,6 @@ source "$(dirname "$0")/utils.sh"
# 1. Preparing the environment
export DEBIAN_FRONTEND=noninteractive
APT_OPTIONS="-o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef -y"
log_info "Preparing Docker installation..."
# Configuration for apt retry logic
APT_RETRY_COUNT=10
@@ -62,6 +61,7 @@ run_apt_with_retry() {
# Check if Docker is already installed
log_subheader "Docker Check"
if command -v docker &> /dev/null; then
log_info "Docker is already installed."
docker --version
@@ -92,6 +92,7 @@ if command -v docker &> /dev/null; then
fi
# 2. Updating and installing dependencies
log_subheader "Dependencies"
log_info "Installing necessary dependencies..."
run_apt_with_retry update -qq
run_apt_with_retry install -qq $APT_OPTIONS \
@@ -101,19 +102,21 @@ run_apt_with_retry install -qq $APT_OPTIONS \
lsb-release || { log_error "Failed to install dependencies."; exit 1; }
# 3. Adding Docker's GPG key
log_subheader "Docker Repository"
log_info "Adding Docker's GPG key..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
# 4. Adding the Docker repository
log_info "Adding the official Docker repository..."
log_info "Adding the official Docker APT repository..."
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. Installing Docker and Docker Compose
log_subheader "Docker Installation"
log_info "Installing Docker Engine and Compose Plugin..."
run_apt_with_retry update -qq
run_apt_with_retry install -qq $APT_OPTIONS \
@@ -124,7 +127,7 @@ run_apt_with_retry install -qq $APT_OPTIONS \
docker-compose-plugin || { log_error "Failed to install Docker packages."; exit 1; }
# 6. Adding the user to the Docker group
# Use SUDO_USER to get the original user who invoked sudo
log_subheader "User Configuration"
ORIGINAL_USER=${SUDO_USER:-$(whoami)}
log_info "Adding user '$ORIGINAL_USER' to the docker group..."
if id "$ORIGINAL_USER" &>/dev/null; then

View File

@@ -118,6 +118,8 @@ if [ -f "$OUTPUT_FILE" ]; then
fi
# Install Caddy
log_subheader "Installing Caddy"
log_info "Adding Caddy repository and installing..."
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt install -y caddy
@@ -126,7 +128,9 @@ apt install -y caddy
require_command "caddy" "Caddy installation failed. Please check the installation logs above."
require_whiptail
# Prompt for the domain name
log_subheader "Domain Configuration"
DOMAIN="" # Initialize DOMAIN variable
# Try to get domain from existing .env file first
@@ -162,6 +166,7 @@ else
fi
# Prompt for user email
log_subheader "Email Configuration"
if [[ -z "${existing_env_vars[LETSENCRYPT_EMAIL]}" ]]; then
wt_msg "Email Required" "Please enter your email address. It will be used for logins and Let's Encrypt SSL."
fi
@@ -190,6 +195,7 @@ fi
log_subheader "Secret Generation"
log_info "Generating secrets and creating .env file..."
# --- Helper Functions ---

View File

@@ -98,18 +98,16 @@ while [ $idx -lt ${#base_services_data[@]} ]; do
idx=$((idx + 2))
done
# Use whiptail to display the checklist
num_services=$(( ${#services[@]} / 3 ))
CHOICES=$(whiptail --title "Service Selection Wizard" --checklist \
"Choose the services you want to deploy.\nUse ARROW KEYS to navigate, SPACEBAR to select/deselect, ENTER to confirm." 32 90 $num_services \
"${services[@]}" \
3>&1 1>&2 2>&3)
# Use whiptail to display the checklist (with adaptive sizing)
CHOICES=$(wt_checklist "Service Selection Wizard" \
"Choose the services you want to deploy.\nUse ARROW KEYS to navigate, SPACEBAR to select/deselect, ENTER to confirm." \
"${services[@]}")
exitstatus=$?
# Restore original DEBIAN_FRONTEND
restore_debian_frontend
# Exit if user pressed Cancel or Esc
exitstatus=$?
if [ $exitstatus -ne 0 ]; then
log_info "Service selection cancelled by user. Exiting wizard."
log_info "No changes made to service profiles. Default services will be used."
@@ -124,10 +122,9 @@ ollama_selected=0
ollama_profile=""
if [ -n "$CHOICES" ]; then
# Whiptail returns a string like "tag1" "tag2" "tag3"
# We need to remove quotes and convert to an array
# Parse whiptail output safely (without eval)
temp_choices=()
eval "temp_choices=($CHOICES)"
wt_parse_choices "$CHOICES" temp_choices
for choice in "${temp_choices[@]}"; do
if [ "$choice" == "ollama" ]; then
@@ -141,11 +138,11 @@ fi
# Enforce mutual exclusivity between Dify and Supabase (compact)
if printf '%s\n' "${selected_profiles[@]}" | grep -qx "dify" && \
printf '%s\n' "${selected_profiles[@]}" | grep -qx "supabase"; then
CHOSEN_EXCLUSIVE=$(whiptail --title "Conflict: Dify and Supabase" --default-item "supabase" --radiolist \
"Dify and Supabase are mutually exclusive. Choose which one to keep." 15 78 2 \
CHOSEN_EXCLUSIVE=$(wt_radiolist "Conflict: Dify and Supabase" \
"Dify and Supabase are mutually exclusive. Choose which one to keep." \
"supabase" \
"dify" "Keep Dify (AI App Platform)" OFF \
"supabase" "Keep Supabase (Backend as a Service)" ON \
3>&1 1>&2 2>&3)
"supabase" "Keep Supabase (Backend as a Service)" ON)
[ -z "$CHOSEN_EXCLUSIVE" ] && CHOSEN_EXCLUSIVE="supabase"
to_remove=$([ "$CHOSEN_EXCLUSIVE" = "dify" ] && echo "supabase" || echo "dify")
@@ -187,10 +184,10 @@ if [ $ollama_selected -eq 1 ]; then
"gpu-nvidia" "NVIDIA GPU (Requires NVIDIA drivers & CUDA)" "$ollama_hw_on_gpu_nvidia"
"gpu-amd" "AMD GPU (Requires ROCm drivers)" "$ollama_hw_on_gpu_amd"
)
CHOSEN_OLLAMA_PROFILE=$(whiptail --title "Ollama Hardware Profile" --default-item "$default_ollama_hardware" --radiolist \
"Choose the hardware profile for Ollama. This will be added to your Docker Compose profiles." 15 78 3 \
"${ollama_hardware_options[@]}" \
3>&1 1>&2 2>&3)
CHOSEN_OLLAMA_PROFILE=$(wt_radiolist "Ollama Hardware Profile" \
"Choose the hardware profile for Ollama. This will be added to your Docker Compose profiles." \
"$default_ollama_hardware" \
"${ollama_hardware_options[@]}")
ollama_exitstatus=$?
if [ $ollama_exitstatus -eq 0 ] && [ -n "$CHOSEN_OLLAMA_PROFILE" ]; then
@@ -209,19 +206,19 @@ if [ ${#selected_profiles[@]} -eq 0 ]; then
log_info "No optional services selected."
COMPOSE_PROFILES_VALUE=""
else
log_info "You have selected the following service profiles to be deployed:"
log_info "Selected service profiles:"
# Join the array into a comma-separated string
COMPOSE_PROFILES_VALUE=$(IFS=,; echo "${selected_profiles[*]}")
for profile in "${selected_profiles[@]}"; do
# Check if the current profile is an Ollama hardware profile that was chosen
if [[ "$profile" == "cpu" || "$profile" == "gpu-nvidia" || "$profile" == "gpu-amd" ]]; then
if [ "$profile" == "$ollama_profile" ]; then # ollama_profile stores the CHOSEN_OLLAMA_PROFILE from this wizard run
echo " - Ollama ($profile profile)"
else # This handles a (highly unlikely) non-Ollama service named "cpu", "gpu-nvidia", or "gpu-amd"
echo " - $profile"
if [ "$profile" == "$ollama_profile" ]; then
echo -e " ${GREEN}*${NC} Ollama ($profile profile)"
else
echo -e " ${GREEN}*${NC} $profile"
fi
else
echo " - $profile"
echo -e " ${GREEN}*${NC} $profile"
fi
done
fi

View File

@@ -27,12 +27,10 @@ init_paths
# Ensure .env exists
ensure_file_exists "$ENV_FILE"
log_info "Configuring service options in .env..."
# ----------------------------------------------------------------
# Prompt for OpenAI API key (optional) using .env value as source of truth
# ----------------------------------------------------------------
log_subheader "OpenAI API Key"
EXISTING_OPENAI_API_KEY="$(read_env_var OPENAI_API_KEY)"
OPENAI_API_KEY=""
if [[ -z "$EXISTING_OPENAI_API_KEY" ]]; then
@@ -50,6 +48,7 @@ fi
# ----------------------------------------------------------------
# Logic for n8n workflow import (RUN_N8N_IMPORT)
# ----------------------------------------------------------------
log_subheader "n8n Workflow Import"
final_run_n8n_import_decision="false"
require_whiptail
if wt_yesno "Import n8n Workflows" "Import ~300 ready-made n8n workflows now? This can take ~30 minutes." "no"; then
@@ -65,7 +64,7 @@ write_env_var "RUN_N8N_IMPORT" "$final_run_n8n_import_decision"
# ----------------------------------------------------------------
# Prompt for number of n8n workers
# ----------------------------------------------------------------
log_info "Configuring n8n worker count..."
log_subheader "n8n Worker Configuration"
EXISTING_N8N_WORKER_COUNT="$(read_env_var N8N_WORKER_COUNT)"
require_whiptail
if [[ -n "$EXISTING_N8N_WORKER_COUNT" ]]; then
@@ -126,12 +125,12 @@ bash "$SCRIPT_DIR/generate_n8n_workers.sh"
# ----------------------------------------------------------------
# Cloudflare Tunnel Token (if cloudflare-tunnel profile is active)
# ----------------------------------------------------------------
# If Cloudflare Tunnel is selected (based on COMPOSE_PROFILES), prompt for the token and write to .env
COMPOSE_PROFILES_VALUE="$(read_env_var COMPOSE_PROFILES)"
# Set COMPOSE_PROFILES for is_profile_active to work
COMPOSE_PROFILES="$COMPOSE_PROFILES_VALUE"
if is_profile_active "cloudflare-tunnel"; then
log_subheader "Cloudflare Tunnel"
existing_cf_token="$(read_env_var CLOUDFLARE_TUNNEL_TOKEN)"
if [ -n "$existing_cf_token" ]; then

View File

@@ -26,6 +26,7 @@ init_paths
cd "$PROJECT_ROOT"
# Check required files
log_subheader "Pre-flight Checks"
require_file "$ENV_FILE" ".env file not found in project root."
require_file "$PROJECT_ROOT/docker-compose.yml" "docker-compose.yml file not found in project root."
require_file "$PROJECT_ROOT/Caddyfile" "Caddyfile not found in project root. Reverse proxy might not work."
@@ -43,6 +44,7 @@ if [ ! -x "$PROJECT_ROOT/start_services.py" ]; then
chmod +x "$PROJECT_ROOT/start_services.py"
fi
log_subheader "Starting Services"
log_info "Launching services using start_services.py..."
# Execute start_services.py
"$PROJECT_ROOT/start_services.py"

View File

@@ -32,54 +32,73 @@ if [ -f "$SCRIPT_DIR/generate_welcome_page.sh" ]; then
bash "$SCRIPT_DIR/generate_welcome_page.sh" || log_warning "Failed to generate welcome page"
fi
echo
echo "======================================================================="
echo " Installation Complete!"
echo "======================================================================="
echo
# Helper function to print a divider line
print_line() {
echo -e "${DIM}${GREEN}$(printf '%.0s-' {1..70})${NC}"
}
# --- Welcome Page ---
echo "================================= Welcome Page =========================="
echo
echo "All your service credentials are available on the Welcome Page:"
echo
echo " URL: https://${WELCOME_HOSTNAME:-welcome.${USER_DOMAIN_NAME}}"
echo " Username: ${WELCOME_USERNAME:-<not_set>}"
echo " Password: ${WELCOME_PASSWORD:-<not_set>}"
echo
echo "The Welcome Page displays:"
echo " - All installed services with their hostnames"
echo " - Login credentials (username/password/API keys)"
echo " - Internal URLs for service-to-service communication"
echo
# Helper function to print a credential row
print_credential() {
local label="$1"
local value="$2"
printf " ${CYAN}%-12s${NC} ${WHITE}%s${NC}\n" "$label:" "$value"
}
# --- Next Steps ---
echo "======================================================================="
echo " Next Steps"
echo "======================================================================="
echo
echo "1. Visit your Welcome Page to view all service credentials"
echo " https://${WELCOME_HOSTNAME:-welcome.${USER_DOMAIN_NAME}}"
echo
echo "2. Store the Welcome Page credentials securely"
echo
echo "3. Configure services as needed:"
# Helper function to print section header
print_section() {
local title="$1"
echo ""
echo -e "${BOLD}${BRIGHT_GREEN} $title${NC}"
echo -e " ${DIM}$(printf '%.0s-' {1..40})${NC}"
}
# Clear screen for clean presentation
clear
# Header
log_box "Installation Complete"
# --- Welcome Page Section ---
print_section "Welcome Page"
echo ""
echo -e " ${WHITE}All your service credentials are available here:${NC}"
echo ""
print_credential "URL" "https://${WELCOME_HOSTNAME:-welcome.${USER_DOMAIN_NAME}}"
print_credential "Username" "${WELCOME_USERNAME:-<not_set>}"
print_credential "Password" "${WELCOME_PASSWORD:-<not_set>}"
echo ""
echo -e " ${DIM}The Welcome Page shows all installed services with their${NC}"
echo -e " ${DIM}hostnames, credentials, and internal URLs.${NC}"
# --- Next Steps Section ---
print_section "Next Steps"
echo ""
echo -e " ${WHITE}1.${NC} Visit your Welcome Page to view all credentials"
echo -e " ${CYAN}https://${WELCOME_HOSTNAME:-welcome.${USER_DOMAIN_NAME}}${NC}"
echo ""
echo -e " ${WHITE}2.${NC} Store the Welcome Page credentials securely"
echo ""
echo -e " ${WHITE}3.${NC} Configure services as needed:"
if is_profile_active "n8n"; then
echo " - n8n: Complete first-run setup with your email"
echo -e " ${GREEN}*${NC} ${WHITE}n8n${NC}: Complete first-run setup with your email"
fi
if is_profile_active "portainer"; then
echo " - Portainer: Create admin account on first login"
echo -e " ${GREEN}*${NC} ${WHITE}Portainer${NC}: Create admin account on first login"
fi
if is_profile_active "flowise"; then
echo " - Flowise: Register and create your account"
echo -e " ${GREEN}*${NC} ${WHITE}Flowise${NC}: Register and create your account"
fi
if is_profile_active "open-webui"; then
echo " - Open WebUI: Register your account"
echo -e " ${GREEN}*${NC} ${WHITE}Open WebUI${NC}: Register your account"
fi
echo
echo "4. Run 'make doctor' if you experience any issues"
echo
echo "======================================================================="
echo
log_info "Thank you for using n8n-install!"
echo
echo ""
echo -e " ${WHITE}4.${NC} Run ${CYAN}make doctor${NC} if you experience any issues"
# --- Footer ---
echo ""
print_line
echo ""
echo -e " ${BRIGHT_GREEN}Thank you for using n8n-install!${NC}"
echo ""
print_line
echo ""

View File

@@ -28,15 +28,11 @@ count_error() {
ERRORS=$((ERRORS + 1))
}
echo ""
echo "========================================"
echo " n8n-install System Diagnostics"
echo "========================================"
echo ""
# Header
log_box "n8n-install System Diagnostics"
# Check if .env file exists
echo "Configuration:"
echo "--------------"
log_subheader "Configuration"
if [ -f "$ENV_FILE" ]; then
count_ok ".env file exists"
@@ -64,16 +60,12 @@ if [ -f "$ENV_FILE" ]; then
fi
else
count_error ".env file not found at $ENV_FILE"
echo ""
echo "Run 'make install' to set up the environment."
print_info "Run 'make install' to set up the environment."
exit 1
fi
echo ""
# Check Docker
echo "Docker:"
echo "-------"
log_subheader "Docker"
if command -v docker &> /dev/null; then
count_ok "Docker is installed"
@@ -93,11 +85,8 @@ else
count_warning "Docker Compose is not available"
fi
echo ""
# Check disk space
echo "Disk Space:"
echo "-----------"
log_subheader "Disk Space"
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
DISK_AVAIL=$(df -h / | awk 'NR==2 {print $4}')
@@ -116,11 +105,8 @@ if [ -n "$DOCKER_DISK" ]; then
print_info "Docker using: $DOCKER_DISK"
fi
echo ""
# Check memory
echo "Memory:"
echo "-------"
log_subheader "Memory"
if command -v free &> /dev/null; then
MEM_TOTAL=$(free -h | awk '/^Mem:/ {print $2}')
@@ -139,11 +125,8 @@ else
print_info "Memory info not available (free command not found)"
fi
echo ""
# Check containers
echo "Containers:"
echo "-----------"
log_subheader "Containers"
RUNNING=$(docker ps -q 2>/dev/null | wc -l)
TOTAL=$(docker ps -aq 2>/dev/null | wc -l)
@@ -181,11 +164,8 @@ else
count_ok "No unhealthy containers"
fi
echo ""
# Check DNS resolution
echo "DNS Resolution:"
echo "---------------"
log_subheader "DNS Resolution"
check_dns() {
local hostname="$1"
@@ -212,11 +192,8 @@ else
print_info "Skipping DNS checks (no domain configured)"
fi
echo ""
# Check SSL (Caddy)
echo "SSL/Caddy:"
echo "----------"
log_subheader "SSL/Caddy"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "caddy"; then
count_ok "Caddy container is running"
@@ -231,11 +208,8 @@ else
count_warning "Caddy container is not running"
fi
echo ""
# Check key services
echo "Key Services:"
echo "-------------"
log_subheader "Key Services"
check_service() {
local container="$1"
@@ -263,26 +237,24 @@ if is_profile_active "monitoring"; then
check_service "prometheus" "9090"
fi
echo ""
# Summary
echo "========================================"
echo " Summary"
echo "========================================"
log_box "Summary"
echo ""
echo -e " ${GREEN}OK:${NC} $OK"
echo -e " ${YELLOW}Warnings:${NC} $WARNINGS"
echo -e " ${RED}Errors:${NC} $ERRORS"
echo -e " ${GREEN}OK:${NC} ${BOLD}$OK${NC}"
echo -e " ${YELLOW}Warnings:${NC} ${BOLD}$WARNINGS${NC}"
echo -e " ${RED}Errors:${NC} ${BOLD}$ERRORS${NC}"
echo ""
if [ $ERRORS -gt 0 ]; then
echo -e "${RED}Some issues were found. Please review the errors above.${NC}"
echo -e " ${BG_RED}${WHITE} ISSUES FOUND ${NC}"
echo -e " ${RED}Please review the errors above and take action.${NC}"
exit 1
elif [ $WARNINGS -gt 0 ]; then
echo -e "${YELLOW}System is mostly healthy with some warnings.${NC}"
echo -e " ${BG_YELLOW}${WHITE} MOSTLY HEALTHY ${NC}"
echo -e " ${YELLOW}System is functional with some warnings.${NC}"
exit 0
else
echo -e "${GREEN}System is healthy!${NC}"
echo -e " ${BG_GREEN}${WHITE} HEALTHY ${NC}"
echo -e " ${GREEN}All checks passed successfully!${NC}"
exit 0
fi

View File

@@ -97,42 +97,42 @@ fi
# Run installation steps sequentially using their full paths
log_header "STEP 1: System Preparation"
show_step 1 7 "System Preparation"
bash "$SCRIPT_DIR/01_system_preparation.sh" || { log_error "System Preparation failed"; exit 1; }
log_success "System preparation complete!"
log_header "STEP 2: Installing Docker"
show_step 2 7 "Installing Docker"
bash "$SCRIPT_DIR/02_install_docker.sh" || { log_error "Docker Installation failed"; exit 1; }
log_success "Docker installation complete!"
log_header "STEP 3: Generating Secrets and Configuration"
show_step 3 7 "Generating Secrets and Configuration"
bash "$SCRIPT_DIR/03_generate_secrets.sh" || { log_error "Secret/Config Generation failed"; exit 1; }
log_success "Secret/Config Generation complete!"
log_header "STEP 4: Running Service Selection Wizard"
show_step 4 7 "Running Service Selection Wizard"
bash "$SCRIPT_DIR/04_wizard.sh" || { log_error "Service Selection Wizard failed"; exit 1; }
log_success "Service Selection Wizard complete!"
log_header "STEP 5: Configure Services"
show_step 5 7 "Configure Services"
bash "$SCRIPT_DIR/05_configure_services.sh" || { log_error "Configure Services failed"; exit 1; }
log_success "Configure Services complete!"
log_header "STEP 6: Running Services"
show_step 6 7 "Running Services"
bash "$SCRIPT_DIR/06_run_services.sh" || { log_error "Running Services failed"; exit 1; }
log_success "Running Services complete!"
log_header "STEP 7: Generating Final Report"
show_step 7 7 "Generating Final Report"
# --- Installation Summary ---
log_info "Installation Summary. The following steps were performed by the scripts:"
log_success "- System updated and basic utilities installed"
log_success "- Firewall (UFW) configured and enabled"
log_success "- Fail2Ban activated for brute-force protection"
log_success "- Automatic security updates enabled"
log_success "- Docker and Docker Compose installed"
log_success "- '.env' generated with secure passwords and secrets"
log_success "- Services launched via Docker Compose"
log_info "Installation Summary:"
echo -e " ${GREEN}*${NC} System updated and basic utilities installed"
echo -e " ${GREEN}*${NC} Firewall (UFW) configured and enabled"
echo -e " ${GREEN}*${NC} Fail2Ban activated for brute-force protection"
echo -e " ${GREEN}*${NC} Automatic security updates enabled"
echo -e " ${GREEN}*${NC} Docker and Docker Compose installed"
echo -e " ${GREEN}*${NC} '.env' generated with secure passwords and secrets"
echo -e " ${GREEN}*${NC} Services launched via Docker Compose"
bash "$SCRIPT_DIR/07_final_report.sh" || { log_error "Final Report Generation failed"; exit 1; }
log_success "Final Report Generation complete!"
log_success "Installation complete!"
exit 0

View File

@@ -12,12 +12,9 @@ init_paths
# Load environment variables
load_env || exit 1
log_box "Update Preview (Dry Run)"
echo ""
echo "========================================"
echo " Update Preview (Dry Run)"
echo "========================================"
echo ""
echo "Checking for available updates..."
echo -e " ${CYAN}Checking for available updates...${NC}"
echo ""
# Function to get local image digest
@@ -74,108 +71,84 @@ log_info "Scanning images from docker-compose.yml..."
echo ""
# Core services (always checked)
echo "Core Services:"
echo "--------------"
log_subheader "Core Services"
check_image_update "postgres" "postgres:${POSTGRES_VERSION:-17}-alpine"
check_image_update "redis" "valkey/valkey:8-alpine"
check_image_update "caddy" "caddy:2-alpine"
echo ""
# Check n8n if profile is active
if is_profile_active "n8n"; then
echo "n8n Services:"
echo "-------------"
log_subheader "n8n Services"
check_image_update "n8n" "docker.n8n.io/n8nio/n8n:${N8N_VERSION:-latest}"
check_image_update "n8n-runner" "n8nio/runners:${N8N_VERSION:-latest}"
echo ""
fi
# Check monitoring if profile is active
if is_profile_active "monitoring"; then
echo "Monitoring Services:"
echo "--------------------"
log_subheader "Monitoring Services"
check_image_update "grafana" "grafana/grafana:latest"
check_image_update "prometheus" "prom/prometheus:latest"
check_image_update "node-exporter" "prom/node-exporter:latest"
check_image_update "cadvisor" "gcr.io/cadvisor/cadvisor:latest"
echo ""
fi
# Check other common services
if is_profile_active "flowise"; then
echo "Flowise:"
echo "--------"
log_subheader "Flowise"
check_image_update "flowise" "flowiseai/flowise:latest"
echo ""
fi
if is_profile_active "open-webui"; then
echo "Open WebUI:"
echo "-----------"
log_subheader "Open WebUI"
check_image_update "open-webui" "ghcr.io/open-webui/open-webui:main"
echo ""
fi
if is_profile_active "portainer"; then
echo "Portainer:"
echo "----------"
log_subheader "Portainer"
check_image_update "portainer" "portainer/portainer-ce:latest"
echo ""
fi
if is_profile_active "langfuse"; then
echo "Langfuse:"
echo "---------"
log_subheader "Langfuse"
check_image_update "langfuse-web" "langfuse/langfuse:latest"
check_image_update "langfuse-worker" "langfuse/langfuse-worker:latest"
echo ""
fi
if is_profile_active "cpu" || is_profile_active "gpu-nvidia" || is_profile_active "gpu-amd"; then
echo "Ollama:"
echo "-------"
log_subheader "Ollama"
check_image_update "ollama" "ollama/ollama:latest"
echo ""
fi
if is_profile_active "qdrant"; then
echo "Qdrant:"
echo "-------"
log_subheader "Qdrant"
check_image_update "qdrant" "qdrant/qdrant:latest"
echo ""
fi
if is_profile_active "searxng"; then
echo "SearXNG:"
echo "--------"
log_subheader "SearXNG"
check_image_update "searxng" "searxng/searxng:latest"
echo ""
fi
if is_profile_active "postgresus"; then
echo "Postgresus:"
echo "-----------"
log_subheader "Postgresus"
check_image_update "postgresus" "ghcr.io/postgresus/postgresus:latest"
echo ""
fi
# Summary
echo "========================================"
echo " Summary"
echo "========================================"
log_divider
echo ""
if [ $UPDATES_AVAILABLE -gt 0 ]; then
echo -e "${GREEN}$UPDATES_AVAILABLE update(s) available.${NC}"
echo -e " ${BRIGHT_GREEN}$UPDATES_AVAILABLE update(s) available!${NC}"
echo ""
echo "To apply updates, run:"
echo " make update"
echo -e " ${WHITE}To apply updates, run:${NC}"
echo -e " ${CYAN}make update${NC}"
echo ""
echo "Or manually:"
echo " docker compose -p localai pull"
echo " docker compose -p localai up -d"
echo -e " ${DIM}Or manually:${NC}"
echo -e " ${DIM}docker compose -p localai pull${NC}"
echo -e " ${DIM}docker compose -p localai up -d${NC}"
else
echo -e "${GREEN}All images are up to date!${NC}"
echo -e " ${BRIGHT_GREEN}All images are up to date!${NC}"
fi
echo ""

View File

@@ -21,6 +21,39 @@
#=============================================================================
DOMAIN_PLACEHOLDER="yourdomain.com"
#=============================================================================
# WHIPTAIL THEME (NEWT_COLORS)
#=============================================================================
# Dark theme with green/cyan accents on black background
# Format: element=foreground,background
# Colors: black, red, green, yellow, blue, magenta, cyan, white
# Prefix with "bright" for bright variants (e.g., brightgreen)
export NEWT_COLORS='
root=white,black
border=brightgreen,black
window=white,black
shadow=black,black
title=brightgreen,black
button=black,brightgreen
actbutton=black,brightcyan
compactbutton=white,black
checkbox=brightgreen,black
actcheckbox=black,brightgreen
entry=brightcyan,black
disentry=gray,black
label=white,black
listbox=white,black
actlistbox=black,brightgreen
sellistbox=brightcyan,black
actsellistbox=black,brightcyan
textbox=white,black
acttextbox=brightgreen,black
emptyscale=black,black
fullscale=brightgreen,black
helpline=brightcyan,black
roottext=brightgreen,black
'
#=============================================================================
# PATH INITIALIZATION
#=============================================================================
@@ -65,21 +98,77 @@ log_error() {
}
# Display a header for major sections
# Usage: log_header "Section Title"
log_header() {
local message="$1"
local width=60
local padding=$(( (width - ${#message} - 2) / 2 ))
local pad_left=$(printf '%*s' "$padding" '' | tr ' ' '=')
local pad_right=$(printf '%*s' "$((width - ${#message} - 2 - padding))" '' | tr ' ' '=')
echo ""
echo ""
echo "========== $message =========="
echo -e "${BRIGHT_GREEN}${pad_left}${NC} ${BOLD}${WHITE}${message}${NC} ${BRIGHT_GREEN}${pad_right}${NC}"
}
# Display a sub-header for sections
# Usage: log_subheader "Sub Section"
log_subheader() {
local message="$1"
echo ""
echo -e "${CYAN}--- ${message} ---${NC}"
}
# Display a divider line
# Usage: log_divider
log_divider() {
echo ""
echo -e "${DIM}${GREEN}$(printf '%.0s-' {1..60})${NC}"
}
# Display text in a box (for important messages)
# Usage: log_box "Important message"
log_box() {
local message="$1"
local len=${#message}
local border=$(printf '%*s' "$((len + 4))" '' | tr ' ' '=')
echo ""
echo -e "${BRIGHT_GREEN}+${border}+${NC}"
echo -e "${BRIGHT_GREEN}|${NC} ${BOLD}${WHITE}${message}${NC} ${BRIGHT_GREEN}|${NC}"
echo -e "${BRIGHT_GREEN}+${border}+${NC}"
}
#=============================================================================
# COLOR OUTPUT (for diagnostics and previews)
#=============================================================================
# Basic colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
WHITE='\033[1;37m'
GRAY='\033[0;90m'
NC='\033[0m' # No Color / Reset
# Text styles
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
# Bright colors
BRIGHT_RED='\033[1;31m'
BRIGHT_GREEN='\033[1;32m'
BRIGHT_YELLOW='\033[1;33m'
BRIGHT_BLUE='\033[1;34m'
BRIGHT_CYAN='\033[1;36m'
# Background colors (for badges/labels)
BG_RED='\033[41m'
BG_GREEN='\033[42m'
BG_YELLOW='\033[43m'
print_ok() {
echo ""
@@ -101,6 +190,85 @@ print_info() {
echo -e " ${BLUE}[INFO]${NC} $1"
}
#=============================================================================
# PROGRESS INDICATORS
#=============================================================================
# Spinner animation frames
SPINNER_FRAMES=('|' '/' '-' '\')
SPINNER_PID=""
# Start spinner with message
# Usage: start_spinner "Loading..."
start_spinner() {
local message="$1"
local i=0
# Don't start if not in terminal or already running
[[ ! -t 1 ]] && return
[[ -n "$SPINNER_PID" ]] && return
(
while true; do
printf "\r ${GREEN}${SPINNER_FRAMES[$i]}${NC} ${message} "
i=$(( (i + 1) % 4 ))
sleep 0.1
done
) &
SPINNER_PID=$!
disown
}
# Stop spinner and clear line
# Usage: stop_spinner
stop_spinner() {
[[ -z "$SPINNER_PID" ]] && return
kill "$SPINNER_PID" 2>/dev/null
wait "$SPINNER_PID" 2>/dev/null
SPINNER_PID=""
# Clear the spinner line
printf "\r%*s\r" 80 ""
}
# Show step progress (e.g., Step 3/7)
# Usage: show_step 3 7 "Installing Docker"
show_step() {
local current=$1
local total=$2
local description="$3"
echo ""
echo -e "${BRIGHT_GREEN}[${current}/${total}]${NC} ${BOLD}${description}${NC}"
echo -e "${DIM}$(printf '%.0s.' {1..50})${NC}"
}
# Show a simple progress bar
# Usage: show_progress 50 100 "Downloading"
show_progress() {
local current=$1
local total=$2
local label="${3:-Progress}"
local width=40
local percent=$(( current * 100 / total ))
local filled=$(( current * width / total ))
local empty=$(( width - filled ))
local bar_filled=$(printf '%*s' "$filled" '' | tr ' ' '#')
local bar_empty=$(printf '%*s' "$empty" '' | tr ' ' '-')
printf "\r ${label}: ${GREEN}[${bar_filled}${GRAY}${bar_empty}${GREEN}]${NC} ${WHITE}%3d%%${NC}" "$percent"
}
# Complete progress bar with message
# Usage: complete_progress "Download complete"
complete_progress() {
local message="${1:-Done}"
printf "\r%*s\r" 80 ""
echo -e " ${GREEN}[OK]${NC} ${message}"
}
#=============================================================================
# ENVIRONMENT MANAGEMENT
#=============================================================================
@@ -308,57 +476,130 @@ require_whiptail() {
fi
}
# Input box
# Get adaptive terminal size for whiptail dialogs
# Usage: eval "$(wt_get_size)"
# Sets: WT_HEIGHT, WT_WIDTH, WT_LIST_HEIGHT
wt_get_size() {
local term_lines term_cols
term_lines=$(tput lines 2>/dev/null || echo 24)
term_cols=$(tput cols 2>/dev/null || echo 80)
# Calculate dimensions with margins
local height=$((term_lines - 4))
local width=$((term_cols - 4))
# Apply min/max constraints
[[ $height -lt 10 ]] && height=10
[[ $height -gt 40 ]] && height=40
[[ $width -lt 60 ]] && width=60
[[ $width -gt 120 ]] && width=120
# List height for checklists/menus (leave room for title, prompt, buttons)
local list_height=$((height - 8))
[[ $list_height -lt 5 ]] && list_height=5
echo "WT_HEIGHT=$height WT_WIDTH=$width WT_LIST_HEIGHT=$list_height"
}
# Input box with adaptive sizing
# Usage: result=$(wt_input "Title" "Prompt" "default")
# Returns 0 on OK, 1 on Cancel
wt_input() {
local title="$1"
local prompt="$2"
local default_value="$3"
eval "$(wt_get_size)"
local result
result=$(whiptail --title "$title" --inputbox "$prompt" 15 80 "$default_value" 3>&1 1>&2 2>&3)
result=$(whiptail --title "$title" --inputbox "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$default_value" 3>&1 1>&2 2>&3)
local status=$?
if [ $status -ne 0 ]; then
return 1
fi
[[ $status -ne 0 ]] && return 1
echo "$result"
return 0
}
# Password box
# Password box with adaptive sizing
# Usage: result=$(wt_password "Title" "Prompt")
# Returns 0 on OK, 1 on Cancel
wt_password() {
local title="$1"
local prompt="$2"
eval "$(wt_get_size)"
local result
result=$(whiptail --title "$title" --passwordbox "$prompt" 15 80 3>&1 1>&2 2>&3)
result=$(whiptail --title "$title" --passwordbox "$prompt" "$WT_HEIGHT" "$WT_WIDTH" 3>&1 1>&2 2>&3)
local status=$?
if [ $status -ne 0 ]; then
return 1
fi
[[ $status -ne 0 ]] && return 1
echo "$result"
return 0
}
# Yes/No box
# Yes/No box with adaptive sizing
# Usage: wt_yesno "Title" "Prompt" "default" (default: yes|no)
# Returns 0 for Yes, 1 for No/Cancel
wt_yesno() {
local title="$1"
local prompt="$2"
local default_choice="$3"
eval "$(wt_get_size)"
local height=$((WT_HEIGHT < 12 ? WT_HEIGHT : 12))
if [ "$default_choice" = "yes" ]; then
whiptail --title "$title" --yesno "$prompt" 10 80
whiptail --title "$title" --yesno "$prompt" "$height" "$WT_WIDTH"
else
whiptail --title "$title" --defaultno --yesno "$prompt" 10 80
whiptail --title "$title" --defaultno --yesno "$prompt" "$height" "$WT_WIDTH"
fi
}
# Message box
# Message box with adaptive sizing
# Usage: wt_msg "Title" "Message"
wt_msg() {
local title="$1"
local message="$2"
whiptail --title "$title" --msgbox "$message" 10 80
eval "$(wt_get_size)"
local height=$((WT_HEIGHT < 12 ? WT_HEIGHT : 12))
whiptail --title "$title" --msgbox "$message" "$height" "$WT_WIDTH"
}
# Checklist (multiple selection) with adaptive sizing
# Usage: result=$(wt_checklist "Title" "Prompt" "tag1" "desc1" "ON" "tag2" "desc2" "OFF" ...)
# Returns: space-separated quoted tags, e.g., "tag1" "tag2"
wt_checklist() {
local title="$1"
local prompt="$2"
shift 2
eval "$(wt_get_size)"
whiptail --title "$title" --checklist "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$WT_LIST_HEIGHT" "$@" 3>&1 1>&2 2>&3
}
# Radiolist (single selection) with adaptive sizing
# Usage: result=$(wt_radiolist "Title" "Prompt" "default_item" "tag1" "desc1" "ON" ...)
# Returns: selected tag
wt_radiolist() {
local title="$1"
local prompt="$2"
local default_item="$3"
shift 3
eval "$(wt_get_size)"
whiptail --title "$title" --default-item "$default_item" --radiolist "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$WT_LIST_HEIGHT" "$@" 3>&1 1>&2 2>&3
}
# Menu (item selection) with adaptive sizing
# Usage: result=$(wt_menu "Title" "Prompt" "tag1" "desc1" "tag2" "desc2" ...)
# Returns: selected tag
wt_menu() {
local title="$1"
local prompt="$2"
shift 2
eval "$(wt_get_size)"
whiptail --title "$title" --menu "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$WT_LIST_HEIGHT" "$@" 3>&1 1>&2 2>&3
}
# Safe parser for whiptail checklist results (replaces eval)
# Usage: wt_parse_choices "$CHOICES" result_array
# Parses quoted output like: "tag1" "tag2" "tag3"
wt_parse_choices() {
local choices="$1"
local -n arr="$2"
arr=()
# Remove quotes and split by spaces
local cleaned="${choices//\"/}"
read -ra arr <<< "$cleaned"
}