diff --git a/scripts/01_system_preparation.sh b/scripts/01_system_preparation.sh index cc05fcc..8662850 100755 --- a/scripts/01_system_preparation.sh +++ b/scripts/01_system_preparation.sh @@ -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 diff --git a/scripts/02_install_docker.sh b/scripts/02_install_docker.sh index 978deca..54e381f 100755 --- a/scripts/02_install_docker.sh +++ b/scripts/02_install_docker.sh @@ -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 diff --git a/scripts/03_generate_secrets.sh b/scripts/03_generate_secrets.sh index e686c3e..2813bf2 100644 --- a/scripts/03_generate_secrets.sh +++ b/scripts/03_generate_secrets.sh @@ -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 --- diff --git a/scripts/04_wizard.sh b/scripts/04_wizard.sh index 88dc861..b9ded2d 100755 --- a/scripts/04_wizard.sh +++ b/scripts/04_wizard.sh @@ -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 diff --git a/scripts/05_configure_services.sh b/scripts/05_configure_services.sh index 6d1719c..01f94f6 100644 --- a/scripts/05_configure_services.sh +++ b/scripts/05_configure_services.sh @@ -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 diff --git a/scripts/06_run_services.sh b/scripts/06_run_services.sh index 4f08174..6586526 100755 --- a/scripts/06_run_services.sh +++ b/scripts/06_run_services.sh @@ -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" diff --git a/scripts/07_final_report.sh b/scripts/07_final_report.sh index 30773b5..6f6fdee 100644 --- a/scripts/07_final_report.sh +++ b/scripts/07_final_report.sh @@ -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:-}" -echo " Password: ${WELCOME_PASSWORD:-}" -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:-}" +print_credential "Password" "${WELCOME_PASSWORD:-}" +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 "" diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 0257615..a6bc1df 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -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 diff --git a/scripts/install.sh b/scripts/install.sh index c47e699..7c0e8e6 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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 \ No newline at end of file diff --git a/scripts/update_preview.sh b/scripts/update_preview.sh index 0f27289..798aee1 100755 --- a/scripts/update_preview.sh +++ b/scripts/update_preview.sh @@ -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 "" diff --git a/scripts/utils.sh b/scripts/utils.sh index a5dcbc2..119def1 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -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" }