From 366865ad4c034acdf9810555c60c7d63cfdb5819 Mon Sep 17 00:00:00 2001 From: Yury Kossakovsky Date: Thu, 11 Dec 2025 17:09:42 -0700 Subject: [PATCH] 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 --- .cursor/rules/add-new-service.mdc | 56 ++- .env.example | 10 + .gitignore | 3 +- Caddyfile | 10 + docker-compose.yml | 4 + scripts/03_generate_secrets.sh | 19 +- scripts/07_final_report.sh | 470 +++--------------------- scripts/generate_welcome_page.sh | 483 +++++++++++++++++++++++++ welcome/app.js | 582 ++++++++++++++++++++++++++++++ welcome/index.html | 139 +++++++ 10 files changed, 1344 insertions(+), 432 deletions(-) create mode 100755 scripts/generate_welcome_page.sh create mode 100644 welcome/app.js create mode 100644 welcome/index.html diff --git a/.cursor/rules/add-new-service.mdc b/.cursor/rules/add-new-service.mdc index cba191e..b3bd014 100644 --- a/.cursor/rules/add-new-service.mdc +++ b/.cursor/rules/add-new-service.mdc @@ -3,7 +3,7 @@ alwaysApply: false --- # Guide: Adding a New Service to n8n-install -This document shows how to add a new optional service (behind Docker Compose profiles) and wire it into the installer, Caddy, and final report. +This document shows how to add a new optional service (behind Docker Compose profiles) and wire it into the installer, Caddy, and Welcome Page. Use a short lowercase slug for your service, e.g., `myservice`. @@ -110,24 +110,43 @@ _update_or_add_env_var "MYSERVICE_PASSWORD_HASH" "$FINAL_MYSERVICE_HASH" "myservice" "MyService (Short description)" ``` -## 6) scripts/07_final_report.sh -- Add a block that prints discovered URLs/credentials: +## 6) scripts/generate_welcome_page.sh +- Add a block that generates JSON data for the Welcome Page: ```bash +# MyService if is_profile_active "myservice"; then - echo - echo "================================= MyService ===========================" - echo - echo "Host: ${MYSERVICE_HOSTNAME:-}" - # Only print credentials if Caddy basic auth is enabled for this service - echo "User: ${MYSERVICE_USERNAME:-}" - echo "Password: ${MYSERVICE_PASSWORD:-}" - echo "API (external via Caddy): https://${MYSERVICE_HOSTNAME:-}" - echo "API (internal): http://myservice:8080" - echo "Docs: " + SERVICES_JSON+='"myservice": { + "hostname": "'"$(json_escape "$MYSERVICE_HOSTNAME")"'", + "credentials": { + "username": "'"$(json_escape "$MYSERVICE_USERNAME")"'", + "password": "'"$(json_escape "$MYSERVICE_PASSWORD")"'" + }, + "extra": { + "internal_api": "http://myservice:8080", + "docs": "https://example.com/docs" + } + },' fi ``` -## 7) README.md +Notes: +- Use `"hostname": null` for internal-only services (no external access) +- Use `"credentials": { "note": "..." }` for services without username/password +- Add useful info to `extra` (internal_api, docs, dashboard URLs, etc.) + +## 7) welcome/app.js +- Add service metadata to `SERVICE_METADATA` object: +```javascript +'myservice': { + name: 'MyService', + description: 'Short description of what it does', + icon: 'MS', // 2-letter abbreviation + color: 'bg-blue-500', // Tailwind color class + category: 'tools' // ai, database, monitoring, tools, infra, automation +}, +``` + +## 8) README.md - Add a short, one-line description under "What's Included", linking to your service docs/homepage. ```md ✅ [**MyService**](https://example.com) - One-line description of what it provides. @@ -137,14 +156,14 @@ fi - **MyService:** `myservice.yourdomain.com` (Brief description) ``` -## 8) Ask about Basic Auth (important) +## 9) Ask about Basic Auth (important) When adding any new public-facing service, explicitly ask the user whether they want to protect the service with Basic Auth via Caddy. If yes, add: - Credentials section to `.env.example` - Secret generation in `scripts/03_generate_secrets.sh` - `basic_auth` in `Caddyfile` - Pass the username/hash through `docker-compose.yml` `caddy.environment` -## 9) Verify and apply +## 10) Verify and apply - Regenerate secrets to populate new variables: ```bash bash scripts/03_generate_secrets.sh @@ -161,12 +180,13 @@ docker compose -p localai logs -f --tail=200 myservice | cat docker compose -p localai logs -f --tail=200 caddy | cat ``` -## 10) Quick checklist +## 11) Quick checklist - [ ] Service added to `docker-compose.yml` with a profile (no external ports exposed) - [ ] Hostname and (optional) credentials added to `.env.example` - [ ] Secret + hash generation added to `scripts/03_generate_secrets.sh` - [ ] Exposed via `Caddyfile` with `reverse_proxy` (+ `basic_auth` if desired) - [ ] Service selectable in `scripts/04_wizard.sh` -- [ ] Listed with URLs/credentials in `scripts/07_final_report.sh` +- [ ] Service data added to `scripts/generate_welcome_page.sh` +- [ ] Service metadata added to `welcome/app.js` (`SERVICE_METADATA`) - [ ] One-line description added to `README.md` diff --git a/.env.example b/.env.example index 5d82b62..ac49aaa 100644 --- a/.env.example +++ b/.env.example @@ -166,6 +166,16 @@ SUPABASE_HOSTNAME=supabase.yourdomain.com WAHA_HOSTNAME=waha.yourdomain.com WEAVIATE_HOSTNAME=weaviate.yourdomain.com WEBUI_HOSTNAME=webui.yourdomain.com +WELCOME_HOSTNAME=welcome.yourdomain.com + +############ +# [required] +# Welcome Page credentials (for Caddy basic auth) +############ + +WELCOME_USERNAME= +WELCOME_PASSWORD= +WELCOME_PASSWORD_HASH= # Everything below this point is optional. # Default values will suffice unless you need more features/customization. diff --git a/.gitignore b/.gitignore index a88e24a..2548634 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ supabase/ dify/ volumes/ docker-compose.override.yml -docker-compose.n8n-workers.yml \ No newline at end of file +docker-compose.n8n-workers.yml +welcome/data.json \ No newline at end of file diff --git a/Caddyfile b/Caddyfile index 17e1f52..65c2c5b 100644 --- a/Caddyfile +++ b/Caddyfile @@ -148,6 +148,16 @@ https://{$NEO4J_HOSTNAME}:7687 { reverse_proxy docling:5001 } +# Welcome Page (Post-install dashboard) +{$WELCOME_HOSTNAME} { + basic_auth { + {$WELCOME_USERNAME} {$WELCOME_PASSWORD_HASH} + } + root * /srv/welcome + file_server + try_files {path} /index.html +} + import /etc/caddy/addons/*.conf # # SearXNG diff --git a/docker-compose.yml b/docker-compose.yml index eeb8efc..5572983 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -264,6 +264,7 @@ services: volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./caddy-addon:/etc/caddy/addons:ro + - ./welcome:/srv/welcome:ro - caddy-data:/data:rw - caddy-config:/config:rw environment: @@ -306,6 +307,9 @@ services: - SUPABASE_HOSTNAME=${SUPABASE_HOSTNAME} - WEAVIATE_HOSTNAME=${WEAVIATE_HOSTNAME} - WEBUI_HOSTNAME=${WEBUI_HOSTNAME} + - WELCOME_HOSTNAME=${WELCOME_HOSTNAME} + - WELCOME_PASSWORD_HASH=${WELCOME_PASSWORD_HASH} + - WELCOME_USERNAME=${WELCOME_USERNAME} cap_drop: - ALL cap_add: diff --git a/scripts/03_generate_secrets.sh b/scripts/03_generate_secrets.sh index 9ffa9cd..00ecb4f 100644 --- a/scripts/03_generate_secrets.sh +++ b/scripts/03_generate_secrets.sh @@ -62,6 +62,7 @@ declare -A VARS_TO_GENERATE=( ["VAULT_ENC_KEY"]="alphanum:32" ["WAHA_DASHBOARD_PASSWORD"]="password:32" ["WEAVIATE_API_KEY"]="secret:48" # API Key for Weaviate service (36 bytes -> 48 chars base64) + ["WELCOME_PASSWORD"]="password:32" # Welcome page basic auth password ["WHATSAPP_SWAGGER_PASSWORD"]="password:32" ) @@ -272,6 +273,7 @@ generated_values["LIGHTRAG_USERNAME"]="$USER_EMAIL" # Set LightRAG username for generated_values["WAHA_DASHBOARD_USERNAME"]="$USER_EMAIL" # WAHA dashboard username default generated_values["WHATSAPP_SWAGGER_USERNAME"]="$USER_EMAIL" # WAHA swagger username default generated_values["DOCLING_USERNAME"]="$USER_EMAIL" # Set Docling username for Caddy +generated_values["WELCOME_USERNAME"]="$USER_EMAIL" # Set Welcome page username for Caddy # Create a temporary file for processing @@ -299,6 +301,7 @@ found_vars["DOCLING_USERNAME"]=0 found_vars["LT_USERNAME"]=0 found_vars["LIGHTRAG_USERNAME"]=0 found_vars["WAHA_DASHBOARD_USERNAME"]=0 +found_vars["WELCOME_USERNAME"]=0 found_vars["WHATSAPP_SWAGGER_USERNAME"]=0 # Read template, substitute domain, generate initial values @@ -346,7 +349,7 @@ while IFS= read -r line || [[ -n "$line" ]]; do # This 'else' block is for lines from template not covered by existing values or VARS_TO_GENERATE. # Check if it is one of the user input vars - these are handled by found_vars later if not in template. is_user_input_var=0 # Reset for each line - user_input_vars=("FLOWISE_USERNAME" "DASHBOARD_USERNAME" "LETSENCRYPT_EMAIL" "RUN_N8N_IMPORT" "PROMETHEUS_USERNAME" "SEARXNG_USERNAME" "OPENAI_API_KEY" "LANGFUSE_INIT_USER_EMAIL" "N8N_WORKER_COUNT" "WEAVIATE_USERNAME" "NEO4J_AUTH_USERNAME" "COMFYUI_USERNAME" "RAGAPP_USERNAME" "PADDLEOCR_USERNAME" "LT_USERNAME" "LIGHTRAG_USERNAME" "WAHA_DASHBOARD_USERNAME" "WHATSAPP_SWAGGER_USERNAME") + user_input_vars=("FLOWISE_USERNAME" "DASHBOARD_USERNAME" "LETSENCRYPT_EMAIL" "RUN_N8N_IMPORT" "PROMETHEUS_USERNAME" "SEARXNG_USERNAME" "OPENAI_API_KEY" "LANGFUSE_INIT_USER_EMAIL" "N8N_WORKER_COUNT" "WEAVIATE_USERNAME" "NEO4J_AUTH_USERNAME" "COMFYUI_USERNAME" "RAGAPP_USERNAME" "PADDLEOCR_USERNAME" "LT_USERNAME" "LIGHTRAG_USERNAME" "WAHA_DASHBOARD_USERNAME" "WELCOME_USERNAME" "WHATSAPP_SWAGGER_USERNAME") for uivar in "${user_input_vars[@]}"; do if [[ "$varName" == "$uivar" ]]; then is_user_input_var=1 @@ -428,7 +431,7 @@ if [[ -z "${generated_values[SERVICE_ROLE_KEY]}" ]]; then fi # Add any custom variables that weren't found in the template -for var in "FLOWISE_USERNAME" "DASHBOARD_USERNAME" "LETSENCRYPT_EMAIL" "RUN_N8N_IMPORT" "OPENAI_API_KEY" "PROMETHEUS_USERNAME" "SEARXNG_USERNAME" "LANGFUSE_INIT_USER_EMAIL" "N8N_WORKER_COUNT" "WEAVIATE_USERNAME" "NEO4J_AUTH_USERNAME" "COMFYUI_USERNAME" "RAGAPP_USERNAME" "PADDLEOCR_USERNAME" "LT_USERNAME" "LIGHTRAG_USERNAME" "WAHA_DASHBOARD_USERNAME" "WHATSAPP_SWAGGER_USERNAME" "DOCLING_USERNAME"; do +for var in "FLOWISE_USERNAME" "DASHBOARD_USERNAME" "LETSENCRYPT_EMAIL" "RUN_N8N_IMPORT" "OPENAI_API_KEY" "PROMETHEUS_USERNAME" "SEARXNG_USERNAME" "LANGFUSE_INIT_USER_EMAIL" "N8N_WORKER_COUNT" "WEAVIATE_USERNAME" "NEO4J_AUTH_USERNAME" "COMFYUI_USERNAME" "RAGAPP_USERNAME" "PADDLEOCR_USERNAME" "LT_USERNAME" "LIGHTRAG_USERNAME" "WAHA_DASHBOARD_USERNAME" "WELCOME_USERNAME" "WHATSAPP_SWAGGER_USERNAME" "DOCLING_USERNAME"; do if [[ ${found_vars["$var"]} -eq 0 && -v generated_values["$var"] ]]; then # Before appending, check if it's already in TMP_ENV_FILE to avoid duplicates if ! grep -q -E "^${var}=" "$TMP_ENV_FILE"; then @@ -618,6 +621,18 @@ if [[ -z "$FINAL_DOCLING_HASH" && -n "$DOCLING_PLAIN_PASS" ]]; then fi _update_or_add_env_var "DOCLING_PASSWORD_HASH" "$FINAL_DOCLING_HASH" +# --- WELCOME PAGE --- +WELCOME_PLAIN_PASS="${generated_values["WELCOME_PASSWORD"]}" +FINAL_WELCOME_HASH="${generated_values[WELCOME_PASSWORD_HASH]}" +if [[ -z "$FINAL_WELCOME_HASH" && -n "$WELCOME_PLAIN_PASS" ]]; then + NEW_HASH=$(_generate_and_get_hash "$WELCOME_PLAIN_PASS") + if [[ -n "$NEW_HASH" ]]; then + FINAL_WELCOME_HASH="$NEW_HASH" + generated_values["WELCOME_PASSWORD_HASH"]="$NEW_HASH" + fi +fi +_update_or_add_env_var "WELCOME_PASSWORD_HASH" "$FINAL_WELCOME_HASH" + if [ $? -eq 0 ]; then # This $? reflects the status of the last mv command from the last _update_or_add_env_var call. # For now, assuming if we reached here and mv was fine, primary operations were okay. echo ".env file generated successfully in the project root ($OUTPUT_FILE)." diff --git a/scripts/07_final_report.sh b/scripts/07_final_report.sh index aeab431..073acfd 100644 --- a/scripts/07_final_report.sh +++ b/scripts/07_final_report.sh @@ -17,439 +17,87 @@ if [ ! -f "$ENV_FILE" ]; then fi # Load environment variables from .env file -# Use set -a to export all variables read from the file set -a source "$ENV_FILE" set +a +# Generate welcome page data +if [ -f "$SCRIPT_DIR/generate_welcome_page.sh" ]; then + log_info "Generating welcome page..." + bash "$SCRIPT_DIR/generate_welcome_page.sh" || log_warning "Failed to generate welcome page" +fi + # Function to check if a profile is active is_profile_active() { local profile_to_check="$1" - # COMPOSE_PROFILES is sourced from .env and will be available here if [ -z "$COMPOSE_PROFILES" ]; then - return 1 # Not active if COMPOSE_PROFILES is empty or not set + return 1 fi - # Check if the profile_to_check is in the comma-separated list - # Adding commas at the beginning and end of both strings handles edge cases - # (e.g., single profile, profile being a substring of another) if [[ ",$COMPOSE_PROFILES," == *",$profile_to_check,"* ]]; then - return 0 # Active + return 0 else - return 1 # Not active + return 1 fi } -# --- Service Access Credentials --- - -# Display credentials, checking if variables exist -echo -log_info "Service Access Credentials. Save this information securely!" -# Display credentials, checking if variables exist - -if is_profile_active "n8n"; then - echo - echo "================================= n8n =================================" - echo - echo "Host: ${N8N_HOSTNAME:-}" - N8N_WORKER_COUNT_VAL="${N8N_WORKER_COUNT:-1}" - echo "Workers: $N8N_WORKER_COUNT_VAL (each with dedicated task runner sidecar)" -fi - -if is_profile_active "open-webui"; then - echo - echo "================================= WebUI ===============================" - echo - echo "Host: ${WEBUI_HOSTNAME:-}" -fi - -if is_profile_active "flowise"; then - echo - echo "================================= Flowise =============================" - echo - echo "Host: ${FLOWISE_HOSTNAME:-}" - echo "User: ${FLOWISE_USERNAME:-}" - echo "Password: ${FLOWISE_PASSWORD:-}" -fi - -if is_profile_active "dify"; then - echo - echo "================================= Dify =================================" - echo - echo "Host: ${DIFY_HOSTNAME:-}" - echo "Description: AI Application Development Platform with LLMOps" - echo - echo "API Access:" - echo " - Web Interface: https://${DIFY_HOSTNAME:-}" - echo " - API Endpoint: https://${DIFY_HOSTNAME:-}/v1" - echo " - Internal API: http://dify-api:5001" -fi - -if is_profile_active "supabase"; then - echo - echo "================================= Supabase ============================" - echo - echo "External Host (via Caddy): ${SUPABASE_HOSTNAME:-}" - echo "Studio User: ${DASHBOARD_USERNAME:-}" - echo "Studio Password: ${DASHBOARD_PASSWORD:-}" - echo - echo "Internal API Gateway: http://kong:8000" - echo "Service Role Secret: ${SERVICE_ROLE_KEY:-}" -fi - -if is_profile_active "langfuse"; then - echo - echo "================================= Langfuse ============================" - echo - echo "Host: ${LANGFUSE_HOSTNAME:-}" - echo "User: ${LANGFUSE_INIT_USER_EMAIL:-}" - echo "Password: ${LANGFUSE_INIT_USER_PASSWORD:-}" -fi - -if is_profile_active "monitoring"; then - echo - echo "================================= Grafana =============================" - echo - echo "Host: ${GRAFANA_HOSTNAME:-}" - echo "User: admin" - echo "Password: ${GRAFANA_ADMIN_PASSWORD:-}" - echo - echo "================================= Prometheus ==========================" - echo - echo "Host: ${PROMETHEUS_HOSTNAME:-}" - echo "User: ${PROMETHEUS_USERNAME:-}" - echo "Password: ${PROMETHEUS_PASSWORD:-}" -fi - -if is_profile_active "searxng"; then - echo - echo "================================= Searxng =============================" - echo - echo "Host: ${SEARXNG_HOSTNAME:-}" - echo "User: ${SEARXNG_USERNAME:-}" - echo "Password: ${SEARXNG_PASSWORD:-}" -fi - -if is_profile_active "portainer"; then - echo - echo "================================= Portainer ===========================" - echo - echo "Host: ${PORTAINER_HOSTNAME:-}" - echo "(Note: On first login, Portainer will prompt to set up an admin user.)" -fi - -if is_profile_active "postiz"; then - echo - echo "================================= Postiz ==============================" - echo - echo "Host: ${POSTIZ_HOSTNAME:-}" - echo "Internal Access (e.g., from n8n): http://postiz:5000" -fi - -if is_profile_active "postgresus"; then - echo - echo "================================= Postgresus ==========================" - echo - echo "Host: ${POSTGRESUS_HOSTNAME:-}" - echo "UI (external via Caddy): https://${POSTGRESUS_HOSTNAME:-}" - echo "UI (internal): http://postgresus:4005" - echo "------ Backup Target (internal PostgreSQL) ------" - echo "PG version: ${POSTGRES_VERSION:-17}" - echo "Host: postgres" - echo "Port: ${POSTGRES_PORT:-5432}" - echo "Username: ${POSTGRES_USER:-postgres}" - echo "Password: ${POSTGRES_PASSWORD:-}" - echo "DB name: ${POSTGRES_DB:-postgres}" - echo "Use HTTPS: false" -fi - -if is_profile_active "ragapp"; then - echo - echo "================================= RAGApp ==============================" - echo - echo "Host: ${RAGAPP_HOSTNAME:-}" - echo "Internal Access (e.g., from n8n): http://ragapp:8000" - echo "User: ${RAGAPP_USERNAME:-}" - echo "Password: ${RAGAPP_PASSWORD:-}" - echo "Admin: https://${RAGAPP_HOSTNAME:-}/admin" - echo "API Docs: https://${RAGAPP_HOSTNAME:-}/docs" -fi - -if is_profile_active "ragflow"; then - echo - echo "================================= RAGFlow =============================" - echo - echo "Host: ${RAGFLOW_HOSTNAME:-}" - echo "API (external via Caddy): https://${RAGFLOW_HOSTNAME:-}" - echo "API (internal): http://ragflow:80" - echo "Note: Uses built-in authentication (login/registration available in web UI)" -fi - -if is_profile_active "comfyui"; then - echo - echo "================================= ComfyUI =============================" - echo - echo "Host: ${COMFYUI_HOSTNAME:-}" - echo "User: ${COMFYUI_USERNAME:-}" - echo "Password: ${COMFYUI_PASSWORD:-}" -fi - -if is_profile_active "libretranslate"; then - echo - echo "================================= LibreTranslate ===========================" - echo - echo "Host: ${LT_HOSTNAME:-}" - echo "User: ${LT_USERNAME:-}" - echo "Password: ${LT_PASSWORD:-}" - echo "API (external via Caddy): https://${LT_HOSTNAME:-}" - echo "API (internal): http://libretranslate:5000" - echo "Docs: https://github.com/LibreTranslate/LibreTranslate" -fi - -if is_profile_active "qdrant"; then - echo - echo "================================= Qdrant ==============================" - echo - echo "Dashboard: https://${QDRANT_HOSTNAME:-}/dashboard" - echo "Host: https://${QDRANT_HOSTNAME:-}" - echo "API Key: ${QDRANT_API_KEY:-}" - echo "Internal REST API Access (e.g., from backend): http://qdrant:6333" -fi - -if is_profile_active "crawl4ai"; then - echo - echo "================================= Crawl4AI ============================" - echo - echo "Internal Access (e.g., from n8n): http://crawl4ai:11235" - echo "(Note: Not exposed externally via Caddy by default)" -fi - -if is_profile_active "docling"; then - echo - echo "================================= Docling =============================" - echo - echo "Web UI: https://${DOCLING_HOSTNAME:-}/ui" - echo "API Docs: https://${DOCLING_HOSTNAME:-}/docs" - echo - echo "Credentials (Caddy Basic Auth):" - echo "User: ${DOCLING_USERNAME:-}" - echo "Password: ${DOCLING_PASSWORD:-}" - echo - echo "API Endpoints:" - echo "External (via Caddy): https://${DOCLING_HOSTNAME:-}" - echo "Internal (from n8n): http://docling:5001" - echo - echo "VLM Pipeline (Vision Language Model):" - echo " 1. Load VLM model in Ollama via Open WebUI -> Settings -> Models" - echo " Example: granite3.2-vision:2b" - echo - echo " 2. API request with VLM pipeline:" - echo ' curl -X POST "https://'"${DOCLING_HOSTNAME:-}"'/v1/convert/source" \' - echo ' -H "Content-Type: application/json" \' - echo ' -u "'"${DOCLING_USERNAME:-}"':'"${DOCLING_PASSWORD:-}"'" \' - echo " -d '{" - echo ' "source": "https://arxiv.org/pdf/2501.17887",' - echo ' "options": {' - echo ' "pipeline": "vlm",' - echo ' "vlm_pipeline_model_api": {' - echo ' "url": "http://ollama:11434/v1/chat/completions",' - echo ' "params": {"model": "granite3.2-vision:2b"},' - echo ' "prompt": "Convert this page to docling.",' - echo ' "timeout": 300' - echo " }" - echo " }" - echo " }'" -fi - -if is_profile_active "gotenberg"; then - echo - echo "================================= Gotenberg ============================" - echo - echo "Internal Access (e.g., from n8n): http://gotenberg:3000" - echo "API Documentation: https://gotenberg.dev/docs" - echo - echo "Common API Endpoints:" - echo " HTML to PDF: POST /forms/chromium/convert/html" - echo " URL to PDF: POST /forms/chromium/convert/url" - echo " Markdown to PDF: POST /forms/chromium/convert/markdown" - echo " Office to PDF: POST /forms/libreoffice/convert" -fi - -if is_profile_active "waha"; then - echo - echo "============================== WAHA (WhatsApp HTTP API) ==============================" - echo - echo "Dashboard: https://${WAHA_HOSTNAME:-}/dashboard" - echo "Swagger: https://${WAHA_HOSTNAME:-}" - echo "Internal: http://waha:3000" - echo - echo "Dashboard User: ${WAHA_DASHBOARD_USERNAME:-}" - echo "Dashboard Pass: ${WAHA_DASHBOARD_PASSWORD:-}" - echo "Swagger User: ${WHATSAPP_SWAGGER_USERNAME:-}" - echo "Swagger Pass: ${WHATSAPP_SWAGGER_PASSWORD:-}" - echo "API key (plain): ${WAHA_API_KEY_PLAIN:-}" -fi - -if is_profile_active "paddleocr"; then - echo - echo "================================= PaddleOCR ===========================" - echo - echo "Host: ${PADDLEOCR_HOSTNAME:-}" - echo "User: ${PADDLEOCR_USERNAME:-}" - echo "Password: ${PADDLEOCR_PASSWORD:-}" - echo "API (external via Caddy): https://${PADDLEOCR_HOSTNAME:-}" - echo "API (internal): http://paddleocr:8080" - echo "Docs: https://paddleocr.a2.fyi/docs" - echo "Notes: PaddleX Basic Serving (CPU), pipeline=OCR" -fi - -if is_profile_active "python-runner"; then - echo - echo "================================= Python Runner ========================" - echo - echo "Internal Container DNS: python-runner" - echo "Mounted Code Directory: ./python-runner (host) -> /app (container)" - echo "Entry File: /app/main.py" - echo "(Note: Internal-only service with no exposed ports; view output via logs)" - echo "Logs: docker compose -p localai logs -f python-runner" -fi - -if is_profile_active "n8n" || is_profile_active "langfuse"; then - echo - echo "================================= Redis (Valkey) ======================" - echo - echo "Internal Host: ${REDIS_HOST:-redis}" - echo "Internal Port: ${REDIS_PORT:-6379}" - echo "Password: ${REDIS_AUTH:-}" - echo "(Note: Primarily for internal service communication, not exposed externally by default)" -fi - -if is_profile_active "letta"; then - echo - echo "================================= Letta ================================" - echo - echo "Host: ${LETTA_HOSTNAME:-}" - echo "Authorization: Bearer ${LETTA_SERVER_PASSWORD}" -fi - -if is_profile_active "lightrag"; then - echo - echo "================================= LightRAG =============================" - echo - echo "Host: ${LIGHTRAG_HOSTNAME:-}" - echo "Web UI: https://${LIGHTRAG_HOSTNAME:-}" - echo "Internal Access (e.g., from n8n): http://lightrag:9621" - echo "" - echo "Authentication (Web UI):" - echo " User: ${LIGHTRAG_USERNAME:-}" - echo " Password: ${LIGHTRAG_PASSWORD:-}" - echo "" - echo "API Access:" - echo " API Key: ${LIGHTRAG_API_KEY:-}" - echo " API Docs: https://${LIGHTRAG_HOSTNAME:-}/docs" - echo " Ollama-compatible: https://${LIGHTRAG_HOSTNAME:-}/v1/chat/completions" - echo "" - echo "Configuration:" - echo " LLM: Ollama (qwen2.5:32b) at http://ollama:11434" - echo " Embeddings: Ollama (bge-m3:latest) at http://ollama:11434" - echo " Storage: Flexible (JSON/PostgreSQL/Neo4j based on installed services)" - echo "" - echo "Note: Requires Ollama to be installed and running for LLM and embeddings." - echo " Upload documents via /app/data/inputs volume or Web UI." -fi - -if is_profile_active "cpu" || is_profile_active "gpu-nvidia" || is_profile_active "gpu-amd"; then - echo - echo "================================= Ollama ==============================" - echo - echo "Internal Access (e.g., from n8n, Open WebUI): http://ollama:11434" - echo "(Note: Ollama runs with the selected profile: cpu, gpu-nvidia, or gpu-amd)" -fi - -if is_profile_active "weaviate"; then - echo - echo "================================= Weaviate ============================" - echo - echo "Host: ${WEAVIATE_HOSTNAME:-}" - echo "Admin User (for Weaviate RBAC): ${WEAVIATE_USERNAME:-}" - echo "Weaviate API Key: ${WEAVIATE_API_KEY:-}" -fi - -if is_profile_active "neo4j"; then - echo - echo "================================= Neo4j ==================================" - echo - echo "Web UI Host: https://${NEO4J_HOSTNAME:-}" - echo "Bolt Port (for drivers): 7687 (e.g., neo4j://\\${NEO4J_HOSTNAME:-}:7687)" - echo "User (for Web UI & API): ${NEO4J_AUTH_USERNAME:-}" - echo "Password (for Web UI & API): ${NEO4J_AUTH_PASSWORD:-}" - echo - echo "HTTP API Access (e.g., for N8N):" - echo " Authentication: Basic (use User/Password above)" - echo " Cypher API Endpoint (POST): https://\\${NEO4J_HOSTNAME:-}/db/neo4j/tx/commit" - echo " Authorization Header Value (for 'Authorization: Basic '): \$(echo -n \"${NEO4J_AUTH_USERNAME:-neo4j}:${NEO4J_AUTH_PASSWORD}\" | base64)" -fi - -# Standalone PostgreSQL (used by n8n, Langfuse, etc.) -# Check if n8n or langfuse is active, as they use this PostgreSQL instance. -# The Supabase section already details its own internal Postgres. -if is_profile_active "n8n" || is_profile_active "langfuse"; then - # Check if Supabase is NOT active, to avoid confusion with Supabase's Postgres if both are present - # However, the main POSTGRES_PASSWORD is used by this standalone instance. - # Supabase has its own environment variables for its internal Postgres if configured differently, - # but the current docker-compose.yml uses the main POSTGRES_PASSWORD for langfuse's postgres dependency too. - # For clarity, we will label this distinctly. - echo - echo "==================== Standalone PostgreSQL (for n8n, Langfuse, etc.) =====================" - echo - echo "Host: postgres" - echo "Port: ${POSTGRES_PORT:-5432}" - echo "Database: ${POSTGRES_DB:-postgres}" # This is typically 'postgres' or 'n8n' for n8n, and 'langfuse' for langfuse, but refers to the service. - echo "User: ${POSTGRES_USER:-postgres}" - echo "Password: ${POSTGRES_PASSWORD:-}" - echo "(Note: This is the PostgreSQL instance used by services like n8n and Langfuse.)" - echo "(It is separate from Supabase's internal PostgreSQL if Supabase is also enabled.)" -fi - echo echo "=======================================================================" +echo " Installation Complete!" +echo "=======================================================================" echo -# --- Update Script Info (Placeholder) --- -log_info "To update the services, run the 'update.sh' script: bash ./scripts/update.sh" +# --- Make Commands --- +echo "================================= Make Commands =========================" +echo +echo " make logs View logs (all services)" +echo " make logs s= View logs for specific service" +echo " make status Show container status" +echo " make monitor Live CPU/memory monitoring" +echo " make restarts Show restart count per container" +echo +echo " make update Update system and services" +echo " make update-preview Preview available updates (dry-run)" +echo " make doctor Run system diagnostics" +echo " make clean Remove unused Docker resources" +echo +echo " make switch-beta Switch to beta (develop branch)" +echo " make switch-stable Switch to stable (main branch)" +echo -# ============================================ -# Cloudflare Tunnel Security Notice -# ============================================ -if is_profile_active "cloudflare-tunnel"; then - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "🔒 CLOUDFLARE TUNNEL SECURITY" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "✅ Cloudflare Tunnel is configured and running!" - echo "" - echo "Your services are accessible through Cloudflare's secure network." - echo "All traffic is encrypted and routed through the tunnel." - echo "" - echo "🛡️ RECOMMENDED SECURITY ENHANCEMENT:" - echo " For maximum security, close the following ports in your VPS firewall:" - echo " • Port 80 (HTTP)" - echo " • Port 443 (HTTPS)" - echo " • Port 7687 (Neo4j Bolt)" - echo "" - echo " ⚠️ Only close ports AFTER confirming tunnel connectivity!" - echo "" -fi +# --- 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 +# --- Next Steps --- +echo "=======================================================================" +echo " Next Steps" +echo "=======================================================================" echo -echo "======================================================================" +echo "1. Visit your Welcome Page to view all service credentials" +echo " https://${WELCOME_HOSTNAME:-welcome.${USER_DOMAIN_NAME}}" echo -echo "Next Steps:" -echo "1. Review the credentials above and store them safely." -echo "2. Access the services via their respective URLs (check \`docker compose ps\` if needed)." -echo "3. Configure services as needed (e.g., first-run setup for n8n)." +echo "2. Store the Welcome Page credentials securely" +echo " (Username: ${WELCOME_USERNAME:-})" echo -echo "======================================================================" +echo "3. Configure services as needed:" +echo " - n8n: Complete first-run setup with your email" +echo " - Portainer: Create admin account on first login" +echo " - Open WebUI: Register your account" echo -log_info "Thank you for using this repository!" +echo "4. Run 'make doctor' if you experience any issues" +echo +echo "=======================================================================" +echo +log_info "Thank you for using n8n-install!" echo diff --git a/scripts/generate_welcome_page.sh b/scripts/generate_welcome_page.sh new file mode 100755 index 0000000..e4d7406 --- /dev/null +++ b/scripts/generate_welcome_page.sh @@ -0,0 +1,483 @@ +#!/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}}" diff --git a/welcome/app.js b/welcome/app.js new file mode 100644 index 0000000..a8ea342 --- /dev/null +++ b/welcome/app.js @@ -0,0 +1,582 @@ +/** + * n8n-install Welcome Page + * Dynamic rendering of services and credentials from data.json + */ + +(function() { + 'use strict'; + + // Service metadata - hardcoded info about each service + const SERVICE_METADATA = { + 'n8n': { + name: 'n8n', + description: 'Workflow Automation', + icon: 'n8n', + color: 'bg-orange-500', + category: 'automation' + }, + 'flowise': { + name: 'Flowise', + description: 'AI Agent Builder', + icon: 'FL', + color: 'bg-blue-500', + category: 'ai' + }, + 'open-webui': { + name: 'Open WebUI', + description: 'ChatGPT-like Interface', + icon: 'AI', + color: 'bg-green-500', + category: 'ai' + }, + 'grafana': { + name: 'Grafana', + description: 'Monitoring Dashboard', + icon: 'GF', + color: 'bg-orange-600', + category: 'monitoring' + }, + 'prometheus': { + name: 'Prometheus', + description: 'Metrics Collection', + icon: 'PM', + color: 'bg-red-500', + category: 'monitoring' + }, + 'portainer': { + name: 'Portainer', + description: 'Docker Management UI', + icon: 'PT', + color: 'bg-cyan-500', + category: 'infra' + }, + 'postgresus': { + name: 'Postgresus', + description: 'PostgreSQL Backups & Monitoring', + icon: 'PG', + color: 'bg-blue-600', + category: 'database' + }, + 'langfuse': { + name: 'Langfuse', + description: 'AI Observability', + icon: 'LF', + color: 'bg-violet-500', + category: 'ai' + }, + 'supabase': { + name: 'Supabase', + description: 'Backend as a Service', + icon: 'SB', + color: 'bg-emerald-500', + category: 'database' + }, + 'dify': { + name: 'Dify', + description: 'AI Application Platform', + icon: 'DF', + color: 'bg-indigo-500', + category: 'ai' + }, + 'qdrant': { + name: 'Qdrant', + description: 'Vector Database', + icon: 'QD', + color: 'bg-purple-500', + category: 'database' + }, + 'weaviate': { + name: 'Weaviate', + description: 'Vector Database', + icon: 'WV', + color: 'bg-green-600', + category: 'database' + }, + 'neo4j': { + name: 'Neo4j', + description: 'Graph Database', + icon: 'N4', + color: 'bg-blue-700', + category: 'database' + }, + 'searxng': { + name: 'SearXNG', + description: 'Private Metasearch Engine', + icon: 'SX', + color: 'bg-teal-500', + category: 'tools' + }, + 'ragapp': { + name: 'RAGApp', + description: 'RAG UI & API', + icon: 'RA', + color: 'bg-amber-500', + category: 'ai' + }, + 'ragflow': { + name: 'RAGFlow', + description: 'Document Understanding RAG', + icon: 'RF', + color: 'bg-rose-500', + category: 'ai' + }, + 'lightrag': { + name: 'LightRAG', + description: 'Graph-based RAG', + icon: 'LR', + color: 'bg-lime-600', + category: 'ai' + }, + 'letta': { + name: 'Letta', + description: 'Agent Server & SDK', + icon: 'LT', + color: 'bg-fuchsia-500', + category: 'ai' + }, + 'comfyui': { + name: 'ComfyUI', + description: 'Stable Diffusion UI', + icon: 'CU', + color: 'bg-pink-500', + category: 'ai' + }, + 'libretranslate': { + name: 'LibreTranslate', + description: 'Translation API', + icon: 'TR', + color: 'bg-sky-500', + category: 'tools' + }, + 'docling': { + name: 'Docling', + description: 'Document Converter', + icon: 'DL', + color: 'bg-stone-500', + category: 'tools' + }, + 'paddleocr': { + name: 'PaddleOCR', + description: 'OCR API Server', + icon: 'OC', + color: 'bg-yellow-600', + category: 'tools' + }, + 'postiz': { + name: 'Postiz', + description: 'Social Publishing Platform', + icon: 'PZ', + color: 'bg-violet-600', + category: 'tools' + }, + 'waha': { + name: 'WAHA', + description: 'WhatsApp HTTP API', + icon: 'WA', + color: 'bg-green-700', + category: 'tools' + }, + 'crawl4ai': { + name: 'Crawl4AI', + description: 'Web Crawler for AI', + icon: 'C4', + color: 'bg-gray-600', + category: 'tools' + }, + 'gotenberg': { + name: 'Gotenberg', + description: 'PDF Generator API', + icon: 'GT', + color: 'bg-red-600', + category: 'tools' + }, + 'ollama': { + name: 'Ollama', + description: 'Local LLM Runner', + icon: 'OL', + color: 'bg-gray-700', + category: 'ai' + }, + 'redis': { + name: 'Redis (Valkey)', + description: 'In-Memory Data Store', + icon: 'RD', + color: 'bg-red-700', + category: 'infra' + }, + 'postgres': { + name: 'PostgreSQL', + description: 'Relational Database', + icon: 'PG', + color: 'bg-blue-800', + category: 'infra' + }, + 'python-runner': { + name: 'Python Runner', + description: 'Custom Python Scripts', + icon: 'PY', + color: 'bg-yellow-500', + category: 'tools' + }, + 'cloudflare-tunnel': { + name: 'Cloudflare Tunnel', + description: 'Zero-Trust Network Access', + icon: 'CF', + color: 'bg-orange-500', + category: 'infra' + } + }; + + // DOM Elements + const servicesContainer = document.getElementById('services-container'); + const quickstartContainer = document.getElementById('quickstart-container'); + const domainInfo = document.getElementById('domain-info'); + const errorToast = document.getElementById('error-toast'); + const errorMessage = document.getElementById('error-message'); + + /** + * Show error toast + */ + function showError(message) { + errorMessage.textContent = message; + errorToast.classList.remove('hidden'); + setTimeout(() => { + errorToast.classList.remove('translate-y-20', 'opacity-0'); + }, 10); + + setTimeout(() => { + errorToast.classList.add('translate-y-20', 'opacity-0'); + setTimeout(() => errorToast.classList.add('hidden'), 300); + }, 5000); + } + + /** + * Create password field with toggle and copy buttons + */ + function createPasswordField(password) { + const container = document.createElement('div'); + container.className = 'flex items-center gap-1'; + + const passwordSpan = document.createElement('span'); + passwordSpan.className = 'font-mono text-sm select-all'; + passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); + passwordSpan.dataset.password = password; + passwordSpan.dataset.hidden = 'true'; + + // Toggle visibility button (eye icon) + const toggleBtn = document.createElement('button'); + toggleBtn.className = 'p-1 rounded hover:bg-gray-200 dark:hover:bg-slate-600 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500'; + toggleBtn.innerHTML = ` + + + + + `; + toggleBtn.title = 'Hold to reveal'; + + // Show password on mouse down, hide on mouse up/leave + const showPassword = () => { + passwordSpan.textContent = passwordSpan.dataset.password; + passwordSpan.dataset.hidden = 'false'; + }; + const hidePassword = () => { + passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); + passwordSpan.dataset.hidden = 'true'; + }; + + toggleBtn.addEventListener('mousedown', showPassword); + toggleBtn.addEventListener('mouseup', hidePassword); + toggleBtn.addEventListener('mouseleave', hidePassword); + toggleBtn.addEventListener('touchstart', showPassword); + toggleBtn.addEventListener('touchend', hidePassword); + + // Copy button + const copyBtn = document.createElement('button'); + copyBtn.className = 'p-1 rounded hover:bg-gray-200 dark:hover:bg-slate-600 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500'; + copyBtn.innerHTML = ` + + + + + `; + copyBtn.title = 'Copy to clipboard'; + + copyBtn.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(password); + // Show checkmark + const copyIcon = copyBtn.querySelector('.copy-icon'); + const checkIcon = copyBtn.querySelector('.check-icon'); + copyIcon.classList.add('hidden'); + checkIcon.classList.remove('hidden'); + // Revert after 2 seconds + setTimeout(() => { + copyIcon.classList.remove('hidden'); + checkIcon.classList.add('hidden'); + }, 2000); + } catch (err) { + console.error('Failed to copy:', err); + } + }); + + container.appendChild(passwordSpan); + container.appendChild(toggleBtn); + container.appendChild(copyBtn); + + return container; + } + + /** + * Render a single service card + */ + function renderServiceCard(key, serviceData) { + const metadata = SERVICE_METADATA[key] || { + name: key, + description: '', + icon: key.substring(0, 2).toUpperCase(), + color: 'bg-slate-500' + }; + + const card = document.createElement('div'); + card.className = 'bg-white dark:bg-slate-800 rounded-xl border border-gray-200 dark:border-slate-700 p-5 hover:shadow-lg transition-shadow'; + + // Build credentials section + let credentialsHtml = ''; + if (serviceData.credentials) { + const creds = serviceData.credentials; + + if (creds.note) { + credentialsHtml = ` +
+

${escapeHtml(creds.note)}

+
+ `; + } else { + let fields = []; + if (creds.username) { + fields.push(` +
+ Username: + ${escapeHtml(creds.username)} +
+ `); + } + if (creds.password) { + fields.push(` +
+ Password: +
+ `); + } + if (creds.api_key) { + fields.push(` +
+ API Key: +
+ `); + } + + if (fields.length > 0) { + credentialsHtml = ` +
+ ${fields.join('')} +
+ `; + } + } + } + + // Build extra info section (internal URLs, etc.) + let extraHtml = ''; + if (serviceData.extra) { + const extraItems = []; + const extra = serviceData.extra; + + if (extra.internal_api) { + extraItems.push(`Internal: ${escapeHtml(extra.internal_api)}`); + } + if (extra.workers) { + extraItems.push(`Workers: ${escapeHtml(extra.workers)}`); + } + if (extra.recommendation) { + extraItems.push(`${escapeHtml(extra.recommendation)}`); + } + + if (extraItems.length > 0) { + extraHtml = `
${extraItems.join('')}
`; + } + } + + card.innerHTML = ` +
+
+ ${metadata.icon} +
+
+

${escapeHtml(metadata.name)}

+

${escapeHtml(metadata.description)}

+ ${serviceData.hostname ? ` + + ${escapeHtml(serviceData.hostname)} + + + + + ` : 'Internal service'} + ${extraHtml} +
+
+ ${credentialsHtml} + `; + + // Add password fields after card is created + if (serviceData.credentials) { + const creds = serviceData.credentials; + + setTimeout(() => { + if (creds.password) { + const pwdContainer = card.querySelector(`#pwd-${key}`); + if (pwdContainer) { + pwdContainer.appendChild(createPasswordField(creds.password)); + } + } + if (creds.api_key) { + const apiContainer = card.querySelector(`#api-${key}`); + if (apiContainer) { + apiContainer.appendChild(createPasswordField(creds.api_key)); + } + } + }, 0); + } + + return card; + } + + /** + * Render all services + */ + function renderServices(services) { + servicesContainer.innerHTML = ''; + + if (!services || Object.keys(services).length === 0) { + servicesContainer.innerHTML = ` +
+

No services configured. Run the installer to set up services.

+
+ `; + return; + } + + // Sort services: external first (with hostname), then internal + const sortedKeys = Object.keys(services).sort((a, b) => { + const aHasHostname = services[a].hostname ? 1 : 0; + const bHasHostname = services[b].hostname ? 1 : 0; + return bHasHostname - aHasHostname; + }); + + sortedKeys.forEach(key => { + servicesContainer.appendChild(renderServiceCard(key, services[key])); + }); + } + + /** + * Render quick start steps + */ + function renderQuickStart(steps) { + quickstartContainer.innerHTML = ''; + + if (!steps || steps.length === 0) { + // Default steps if none provided + steps = [ + { step: 1, title: 'Log into n8n', description: 'Use the email you provided during installation' }, + { step: 2, title: 'Create your first workflow', description: 'Start with a Manual Trigger + HTTP Request nodes' }, + { step: 3, title: 'Explore community workflows', description: 'Check imported workflows for 300+ examples' }, + { step: 4, title: 'Monitor your system', description: 'Use Grafana to track performance' } + ]; + } + + steps.forEach(item => { + const stepEl = document.createElement('div'); + stepEl.className = 'flex items-start gap-4 p-4 bg-white dark:bg-slate-800 rounded-xl border border-gray-200 dark:border-slate-700'; + + stepEl.innerHTML = ` +
+ ${item.step} +
+
+

${escapeHtml(item.title)}

+

${escapeHtml(item.description)}

+
+ `; + + quickstartContainer.appendChild(stepEl); + }); + } + + /** + * Escape HTML to prevent XSS + */ + function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Load data and render page + */ + async function init() { + try { + const response = await fetch('data.json'); + + if (!response.ok) { + throw new Error(`Failed to load data (${response.status})`); + } + + const data = await response.json(); + + // Update domain info + if (data.domain) { + domainInfo.textContent = `Domain: ${data.domain}`; + } + if (data.generated_at) { + const date = new Date(data.generated_at); + domainInfo.textContent += ` | Generated: ${date.toLocaleString()}`; + } + + // Render services + renderServices(data.services); + + // Render quick start + renderQuickStart(data.quick_start); + + } catch (error) { + console.error('Error loading data:', error); + + // Show error in UI + servicesContainer.innerHTML = ` +
+ + + +

Unable to load service data

+

Make sure the installation completed successfully and data.json was generated.

+
+ `; + + // Still render default quick start + renderQuickStart(null); + } + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/welcome/index.html b/welcome/index.html new file mode 100644 index 0000000..81334aa --- /dev/null +++ b/welcome/index.html @@ -0,0 +1,139 @@ + + + + + + Welcome to n8n-install + + + + + +
+ +
+

+ Welcome to n8n-install +

+

+ Your self-hosted automation platform is ready +

+

+
+ + +
+

+ + + + Your Services +

+
+ +
+
+
+
+
+ + +
+

+ + + + Quick Start +

+
+ +
+
+
+
+ + +
+

+ + + + Documentation +

+ +
+ + + +
+ + + + + + +