Files
n8n-install/scripts/generate_welcome_page.sh
Yury Kossakovsky 366865ad4c feat: add welcome page dashboard for post-install credentials
replace terminal-based final report with web-based welcome page that
displays service credentials, hostnames, and quick start guide.

- add welcome/index.html with tailwind css and dark mode support
- add welcome/app.js with service metadata and password toggle/copy
- add scripts/generate_welcome_page.sh to generate data.json from env
- simplify 07_final_report.sh to show welcome page url and make commands
- add welcome page basic auth credentials to caddy and secret generation
- update add-new-service documentation with new welcome page steps
2025-12-11 17:09:42 -07:00

484 lines
13 KiB
Bash
Executable File

#!/bin/bash
# Generate data.json for the welcome page with active services and credentials
set -e
# Source the utilities file
source "$(dirname "$0")/utils.sh"
# Get the directory where the script resides
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )"
ENV_FILE="$PROJECT_ROOT/.env"
OUTPUT_FILE="$PROJECT_ROOT/welcome/data.json"
# Check if .env file exists
if [ ! -f "$ENV_FILE" ]; then
log_error "The .env file ('$ENV_FILE') was not found."
exit 1
fi
# Ensure welcome directory exists
mkdir -p "$PROJECT_ROOT/welcome"
# Remove existing data.json if it exists (always regenerate)
if [ -f "$OUTPUT_FILE" ]; then
rm -f "$OUTPUT_FILE"
fi
# Load environment variables from .env file
set -a
source "$ENV_FILE"
set +a
# Function to check if a profile is active
is_profile_active() {
local profile_to_check="$1"
if [ -z "$COMPOSE_PROFILES" ]; then
return 1
fi
if [[ ",$COMPOSE_PROFILES," == *",$profile_to_check,"* ]]; then
return 0
else
return 1
fi
}
# Function to escape JSON strings
json_escape() {
local str="$1"
# Escape backslashes, double quotes, and control characters
printf '%s' "$str" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r'
}
# Start building JSON
GENERATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Build services object (key-based)
SERVICES_JSON=""
# n8n
if is_profile_active "n8n"; then
N8N_WORKER_COUNT_VAL="${N8N_WORKER_COUNT:-1}"
SERVICES_JSON+='"n8n": {
"hostname": "'"$(json_escape "$N8N_HOSTNAME")"'",
"credentials": {
"note": "Use the email you provided during installation"
},
"extra": {
"workers": "'"$N8N_WORKER_COUNT_VAL"'"
}
},'
fi
# Flowise
if is_profile_active "flowise"; then
SERVICES_JSON+='"flowise": {
"hostname": "'"$(json_escape "$FLOWISE_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$FLOWISE_USERNAME")"'",
"password": "'"$(json_escape "$FLOWISE_PASSWORD")"'"
}
},'
fi
# Open WebUI
if is_profile_active "open-webui"; then
SERVICES_JSON+='"open-webui": {
"hostname": "'"$(json_escape "$WEBUI_HOSTNAME")"'",
"credentials": {
"note": "Create account on first login"
}
},'
fi
# Grafana (monitoring)
if is_profile_active "monitoring"; then
SERVICES_JSON+='"grafana": {
"hostname": "'"$(json_escape "$GRAFANA_HOSTNAME")"'",
"credentials": {
"username": "admin",
"password": "'"$(json_escape "$GRAFANA_ADMIN_PASSWORD")"'"
}
},'
SERVICES_JSON+='"prometheus": {
"hostname": "'"$(json_escape "$PROMETHEUS_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$PROMETHEUS_USERNAME")"'",
"password": "'"$(json_escape "$PROMETHEUS_PASSWORD")"'"
}
},'
fi
# Portainer
if is_profile_active "portainer"; then
SERVICES_JSON+='"portainer": {
"hostname": "'"$(json_escape "$PORTAINER_HOSTNAME")"'",
"credentials": {
"note": "Create admin account on first login"
}
},'
fi
# Postgresus
if is_profile_active "postgresus"; then
SERVICES_JSON+='"postgresus": {
"hostname": "'"$(json_escape "$POSTGRESUS_HOSTNAME")"'",
"credentials": {
"note": "Uses PostgreSQL credentials from .env"
},
"extra": {
"pg_host": "postgres",
"pg_port": "'"${POSTGRES_PORT:-5432}"'",
"pg_user": "'"$(json_escape "${POSTGRES_USER:-postgres}")"'",
"pg_password": "'"$(json_escape "$POSTGRES_PASSWORD")"'",
"pg_db": "'"$(json_escape "${POSTGRES_DB:-postgres}")"'"
}
},'
fi
# Langfuse
if is_profile_active "langfuse"; then
SERVICES_JSON+='"langfuse": {
"hostname": "'"$(json_escape "$LANGFUSE_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$LANGFUSE_INIT_USER_EMAIL")"'",
"password": "'"$(json_escape "$LANGFUSE_INIT_USER_PASSWORD")"'"
}
},'
fi
# Supabase
if is_profile_active "supabase"; then
SERVICES_JSON+='"supabase": {
"hostname": "'"$(json_escape "$SUPABASE_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$DASHBOARD_USERNAME")"'",
"password": "'"$(json_escape "$DASHBOARD_PASSWORD")"'"
},
"extra": {
"internal_api": "http://kong:8000",
"service_role_key": "'"$(json_escape "$SERVICE_ROLE_KEY")"'"
}
},'
fi
# Dify
if is_profile_active "dify"; then
SERVICES_JSON+='"dify": {
"hostname": "'"$(json_escape "$DIFY_HOSTNAME")"'",
"credentials": {
"note": "Create account on first login"
},
"extra": {
"api_endpoint": "https://'"$(json_escape "$DIFY_HOSTNAME")"'/v1",
"internal_api": "http://dify-api:5001"
}
},'
fi
# Qdrant
if is_profile_active "qdrant"; then
SERVICES_JSON+='"qdrant": {
"hostname": "'"$(json_escape "$QDRANT_HOSTNAME")"'",
"credentials": {
"api_key": "'"$(json_escape "$QDRANT_API_KEY")"'"
},
"extra": {
"dashboard": "https://'"$(json_escape "$QDRANT_HOSTNAME")"'/dashboard",
"internal_api": "http://qdrant:6333"
}
},'
fi
# Weaviate
if is_profile_active "weaviate"; then
SERVICES_JSON+='"weaviate": {
"hostname": "'"$(json_escape "$WEAVIATE_HOSTNAME")"'",
"credentials": {
"api_key": "'"$(json_escape "$WEAVIATE_API_KEY")"'",
"username": "'"$(json_escape "$WEAVIATE_USERNAME")"'"
}
},'
fi
# Neo4j
if is_profile_active "neo4j"; then
SERVICES_JSON+='"neo4j": {
"hostname": "'"$(json_escape "$NEO4J_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$NEO4J_AUTH_USERNAME")"'",
"password": "'"$(json_escape "$NEO4J_AUTH_PASSWORD")"'"
},
"extra": {
"bolt_port": "7687"
}
},'
fi
# SearXNG
if is_profile_active "searxng"; then
SERVICES_JSON+='"searxng": {
"hostname": "'"$(json_escape "$SEARXNG_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$SEARXNG_USERNAME")"'",
"password": "'"$(json_escape "$SEARXNG_PASSWORD")"'"
}
},'
fi
# RAGApp
if is_profile_active "ragapp"; then
SERVICES_JSON+='"ragapp": {
"hostname": "'"$(json_escape "$RAGAPP_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$RAGAPP_USERNAME")"'",
"password": "'"$(json_escape "$RAGAPP_PASSWORD")"'"
},
"extra": {
"admin": "https://'"$(json_escape "$RAGAPP_HOSTNAME")"'/admin",
"docs": "https://'"$(json_escape "$RAGAPP_HOSTNAME")"'/docs",
"internal_api": "http://ragapp:8000"
}
},'
fi
# RAGFlow
if is_profile_active "ragflow"; then
SERVICES_JSON+='"ragflow": {
"hostname": "'"$(json_escape "$RAGFLOW_HOSTNAME")"'",
"credentials": {
"note": "Create account on first login"
},
"extra": {
"internal_api": "http://ragflow:80"
}
},'
fi
# LightRAG
if is_profile_active "lightrag"; then
SERVICES_JSON+='"lightrag": {
"hostname": "'"$(json_escape "$LIGHTRAG_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$LIGHTRAG_USERNAME")"'",
"password": "'"$(json_escape "$LIGHTRAG_PASSWORD")"'",
"api_key": "'"$(json_escape "$LIGHTRAG_API_KEY")"'"
},
"extra": {
"docs": "https://'"$(json_escape "$LIGHTRAG_HOSTNAME")"'/docs",
"internal_api": "http://lightrag:9621"
}
},'
fi
# Letta
if is_profile_active "letta"; then
SERVICES_JSON+='"letta": {
"hostname": "'"$(json_escape "$LETTA_HOSTNAME")"'",
"credentials": {
"api_key": "'"$(json_escape "$LETTA_SERVER_PASSWORD")"'"
}
},'
fi
# ComfyUI
if is_profile_active "comfyui"; then
SERVICES_JSON+='"comfyui": {
"hostname": "'"$(json_escape "$COMFYUI_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$COMFYUI_USERNAME")"'",
"password": "'"$(json_escape "$COMFYUI_PASSWORD")"'"
}
},'
fi
# LibreTranslate
if is_profile_active "libretranslate"; then
SERVICES_JSON+='"libretranslate": {
"hostname": "'"$(json_escape "$LT_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$LT_USERNAME")"'",
"password": "'"$(json_escape "$LT_PASSWORD")"'"
},
"extra": {
"internal_api": "http://libretranslate:5000"
}
},'
fi
# Docling
if is_profile_active "docling"; then
SERVICES_JSON+='"docling": {
"hostname": "'"$(json_escape "$DOCLING_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$DOCLING_USERNAME")"'",
"password": "'"$(json_escape "$DOCLING_PASSWORD")"'"
},
"extra": {
"ui": "https://'"$(json_escape "$DOCLING_HOSTNAME")"'/ui",
"docs": "https://'"$(json_escape "$DOCLING_HOSTNAME")"'/docs",
"internal_api": "http://docling:5001"
}
},'
fi
# PaddleOCR
if is_profile_active "paddleocr"; then
SERVICES_JSON+='"paddleocr": {
"hostname": "'"$(json_escape "$PADDLEOCR_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$PADDLEOCR_USERNAME")"'",
"password": "'"$(json_escape "$PADDLEOCR_PASSWORD")"'"
},
"extra": {
"internal_api": "http://paddleocr:8080"
}
},'
fi
# Postiz
if is_profile_active "postiz"; then
SERVICES_JSON+='"postiz": {
"hostname": "'"$(json_escape "$POSTIZ_HOSTNAME")"'",
"credentials": {
"note": "Create account on first login"
},
"extra": {
"internal_api": "http://postiz:5000"
}
},'
fi
# WAHA
if is_profile_active "waha"; then
SERVICES_JSON+='"waha": {
"hostname": "'"$(json_escape "$WAHA_HOSTNAME")"'",
"credentials": {
"username": "'"$(json_escape "$WAHA_DASHBOARD_USERNAME")"'",
"password": "'"$(json_escape "$WAHA_DASHBOARD_PASSWORD")"'",
"api_key": "'"$(json_escape "$WAHA_API_KEY_PLAIN")"'"
},
"extra": {
"dashboard": "https://'"$(json_escape "$WAHA_HOSTNAME")"'/dashboard",
"swagger_user": "'"$(json_escape "$WHATSAPP_SWAGGER_USERNAME")"'",
"swagger_pass": "'"$(json_escape "$WHATSAPP_SWAGGER_PASSWORD")"'",
"internal_api": "http://waha:3000"
}
},'
fi
# Crawl4AI (internal only)
if is_profile_active "crawl4ai"; then
SERVICES_JSON+='"crawl4ai": {
"hostname": null,
"credentials": {
"note": "Internal service only"
},
"extra": {
"internal_api": "http://crawl4ai:11235"
}
},'
fi
# Gotenberg (internal only)
if is_profile_active "gotenberg"; then
SERVICES_JSON+='"gotenberg": {
"hostname": null,
"credentials": {
"note": "Internal service only"
},
"extra": {
"internal_api": "http://gotenberg:3000",
"docs": "https://gotenberg.dev/docs"
}
},'
fi
# Ollama (internal only)
if is_profile_active "cpu" || is_profile_active "gpu-nvidia" || is_profile_active "gpu-amd"; then
SERVICES_JSON+='"ollama": {
"hostname": null,
"credentials": {
"note": "Internal service only"
},
"extra": {
"internal_api": "http://ollama:11434"
}
},'
fi
# Redis/Valkey (internal only, shown if n8n or langfuse active)
if is_profile_active "n8n" || is_profile_active "langfuse"; then
SERVICES_JSON+='"redis": {
"hostname": null,
"credentials": {
"password": "'"$(json_escape "$REDIS_AUTH")"'"
},
"extra": {
"internal_host": "'"${REDIS_HOST:-redis}"'",
"internal_port": "'"${REDIS_PORT:-6379}"'"
}
},'
fi
# PostgreSQL (internal only, shown if n8n or langfuse active)
if is_profile_active "n8n" || is_profile_active "langfuse"; then
SERVICES_JSON+='"postgres": {
"hostname": null,
"credentials": {
"username": "'"$(json_escape "${POSTGRES_USER:-postgres}")"'",
"password": "'"$(json_escape "$POSTGRES_PASSWORD")"'"
},
"extra": {
"internal_host": "postgres",
"internal_port": "'"${POSTGRES_PORT:-5432}"'",
"database": "'"$(json_escape "${POSTGRES_DB:-postgres}")"'"
}
},'
fi
# Python Runner (internal only)
if is_profile_active "python-runner"; then
SERVICES_JSON+='"python-runner": {
"hostname": null,
"credentials": {
"note": "Internal service only"
},
"extra": {
"logs_command": "docker compose -p localai logs -f python-runner"
}
},'
fi
# Cloudflare Tunnel
if is_profile_active "cloudflare-tunnel"; then
SERVICES_JSON+='"cloudflare-tunnel": {
"hostname": null,
"credentials": {
"note": "Zero-trust access via Cloudflare network"
},
"extra": {
"recommendation": "Close ports 80, 443, 7687 in your VPS firewall after confirming tunnel connectivity"
}
},'
fi
# Remove trailing comma from services JSON
SERVICES_JSON=$(echo "$SERVICES_JSON" | sed 's/,$//')
# Write final JSON
cat > "$OUTPUT_FILE" << EOF
{
"domain": "$(json_escape "$USER_DOMAIN_NAME")",
"generated_at": "$GENERATED_AT",
"services": {
$SERVICES_JSON
}
}
EOF
log_success "Welcome page data generated at: $OUTPUT_FILE"
log_info "Access it at: https://${WELCOME_HOSTNAME:-welcome.${USER_DOMAIN_NAME}}"