#!/usr/bin/env bash # ═══════════════════════════════════════════════════════════════════ # Teris Suite — Script d'installation on-premise BOULANGER-PROOF # US-46.3 — EPIC 46 — Domain Sovereignty # Brevet Baroni / Leroy B1→B9 © 2026 # # Ce script part d'un Ubuntu nu et installe TOUT. # Caddy tourne en Docker (dans le compose) — pas en systemd. # # 4 cas de déploiement : # Cas 1/2/4 (standard) : # sudo ./install.sh --tenant X --teris-domain X --tls-email X \ # --studio-url X --studio-key X --keycloak-url X --keycloak-realm X # # Cas 3 (full self-hosted, TerisStudio inclus) : # sudo ./install.sh --selfhosted --tenant X --teris-domain X --tls-email X \ # --terisstudio-domain studio.X --keycloak-url X --keycloak-realm X \ # --llm-provider anthropic --anthropic-key sk-ant-xxx # # Idempotent — relancer ne casse rien. # ═══════════════════════════════════════════════════════════════════ set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' TOTAL_STEPS=9; CURRENT_STEP=0 # ── Variables d'entrée ─────────────────────────────────────────── ONBOARDING_TOKEN="" STUDIO_API_URL="" TENANT_ID=""; TERIS_DOMAIN=""; TLS_EMAIL="" STUDIO_URL=""; STUDIO_KEY="" KEYCLOAK_BASE_URL=""; KEYCLOAK_REALM="" VISTA_PORT="8002"; BOOTSTRAP_DOMAIN=""; TERIS_VERSION="latest" INSTALL_DIR=""; GITHUB_REPO=""; REGISTRY_URL=""; REGISTRY_USER="" # Selfhosted (cas 3) SELFHOSTED=false TERISSTUDIO_DOMAIN="" LLM_PROVIDER=""; ANTHROPIC_KEY=""; OLLAMA_URL=""; MISTRAL_KEY="" LLM_MODEL="" # Générées TC_POSTGRES_PASSWORD=""; ISOTERIS_DB_PASSWORD=""; ENGINE_DB_PASSWORD="" TC_API_KEY=""; ENGINE_API_KEY=""; VISTA_ADMIN_KEY="" TERISSTUDIO_POSTGRES_PASSWORD=""; TERISSTUDIO_API_KEY="" CREDENTIALS_DIR=""; CREDENTIALS_FILE=""; ENV_FILE="" # ── Fonctions utilitaires ──────────────────────────────────────── log() { echo -e " $*"; } success() { echo -e " ${GREEN}✅${NC} $*"; } warn() { echo -e " ${YELLOW}⚠️${NC} $*"; } fail() { echo -e " ${RED}❌${NC} $*" >&2; exit 1; } step() { CURRENT_STEP=$((CURRENT_STEP+1)); echo "" echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${NC}" echo -e "${BOLD}${CYAN} Étape ${CURRENT_STEP}/${TOTAL_STEPS} — $1${NC}" echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${NC}"; echo ""; } banner() { echo -e "\n${BOLD}${BLUE}" echo " ╔═══════════════════════════════════════════════════════╗" echo " ║ T E R I S S U I T E — I N S T A L L ║" echo " ║ Brevet Baroni / Leroy B1→B9 © 2026 ║" echo " ║ Votre infrastructure. Vos domaines. ║" echo " ╚═══════════════════════════════════════════════════════╝" echo -e "${NC}"; } # ═══════════════════════════════════════════════════════════════════ # PARSING # ═══════════════════════════════════════════════════════════════════ show_help() { cat < --teris-domain --tls-email \\ --studio-url --studio-key --keycloak-url --keycloak-realm ${BOLD}Mode self-hosted (cas 3 — TerisStudio inclus) :${NC} sudo ./install.sh --selfhosted --tenant --teris-domain --tls-email \\ --terisstudio-domain --keycloak-url --keycloak-realm \\ --llm-provider anthropic --anthropic-key ${BOLD}Paramètres obligatoires (tous les modes) :${NC} --tenant Identifiant organisation --teris-domain Domaine racine --tls-email Email Let's Encrypt --keycloak-url URL Keycloak IAM --keycloak-realm Realm Keycloak ${BOLD}Paramètres obligatoires (standard uniquement) :${NC} --studio-url URL TerisStudio SaaS --studio-key Clé API TerisStudio ${BOLD}Paramètres obligatoires (selfhosted uniquement) :${NC} --selfhosted Active le mode self-hosted --terisstudio-domain Domaine TerisStudio --llm-provider anthropic | ollama | mistral --anthropic-key Clé API Anthropic --ollama-url URL Ollama (si provider=ollama) --mistral-key Clé Mistral (si mistral) ${BOLD}Optionnels :${NC} --vista-port (8002) --version (latest) --install-dir (~\/teris-infra) --repo --registry-url --registry-user --bootstrap-domain --llm-model EOF exit 0; } parse_args() { # ── EPIC 47 — Détection mode token ────────────────────────────── # Si le premier argument contient un "." et pas de "--" → c'est un JWT # Usage : install.sh TOKEN STUDIO_URL if [[ $# -ge 1 && "$1" != --* && "$1" == *.* ]]; then ONBOARDING_TOKEN="$1"; shift [[ $# -ge 1 && "$1" != --* ]] && STUDIO_API_URL="$1" && shift return fi while [[ $# -gt 0 ]]; do case $1 in --tenant) TENANT_ID="$2"; shift 2;; --teris-domain) TERIS_DOMAIN="$2"; shift 2;; --tls-email) TLS_EMAIL="$2"; shift 2;; --studio-url) STUDIO_URL="$2"; shift 2;; --studio-key) STUDIO_KEY="$2"; shift 2;; --keycloak-url) KEYCLOAK_BASE_URL="$2"; shift 2;; --keycloak-realm) KEYCLOAK_REALM="$2"; shift 2;; --vista-port) VISTA_PORT="$2"; shift 2;; --selfhosted) SELFHOSTED=true; shift;; --terisstudio-domain) TERISSTUDIO_DOMAIN="$2"; shift 2;; --llm-provider) LLM_PROVIDER="$2"; shift 2;; --anthropic-key) ANTHROPIC_KEY="$2"; shift 2;; --ollama-url) OLLAMA_URL="$2"; shift 2;; --mistral-key) MISTRAL_KEY="$2"; shift 2;; --llm-model) LLM_MODEL="$2"; shift 2;; --bootstrap-domain) BOOTSTRAP_DOMAIN="$2"; shift 2;; --version) TERIS_VERSION="$2"; shift 2;; --install-dir) INSTALL_DIR="$2"; shift 2;; --repo) GITHUB_REPO="$2"; shift 2;; --registry-url) REGISTRY_URL="$2"; shift 2;; --registry-user) REGISTRY_USER="$2"; shift 2;; --help|-h) show_help;; *) fail "Argument inconnu : $1";; esac; done; } validate_args() { local e=0 m="" # ── Obligatoires dans tous les modes ──────────────────────────── [[ -z "$TENANT_ID" ]] && m+=" --tenant\n" && e=$((e+1)) [[ -z "$TERIS_DOMAIN" ]] && m+=" --teris-domain\n" && e=$((e+1)) [[ -z "$TLS_EMAIL" ]] && m+=" --tls-email\n" && e=$((e+1)) [[ -z "$KEYCLOAK_BASE_URL" ]] && m+=" --keycloak-url\n" && e=$((e+1)) [[ -z "$KEYCLOAK_REALM" ]] && m+=" --keycloak-realm\n" && e=$((e+1)) if [[ "$SELFHOSTED" == "true" ]]; then # ── Obligatoires en selfhosted ────────────────────────────── [[ -z "$TERISSTUDIO_DOMAIN" ]] && m+=" --terisstudio-domain\n" && e=$((e+1)) [[ -z "$LLM_PROVIDER" ]] && m+=" --llm-provider (anthropic|ollama|mistral)\n" && e=$((e+1)) if [[ "$LLM_PROVIDER" == "anthropic" && -z "$ANTHROPIC_KEY" ]]; then m+=" --anthropic-key (requis avec --llm-provider anthropic)\n"; e=$((e+1)); fi if [[ "$LLM_PROVIDER" == "ollama" && -z "$OLLAMA_URL" ]]; then m+=" --ollama-url (requis avec --llm-provider ollama)\n"; e=$((e+1)); fi if [[ "$LLM_PROVIDER" == "mistral" && -z "$MISTRAL_KEY" ]]; then m+=" --mistral-key (requis avec --llm-provider mistral)\n"; e=$((e+1)); fi # Dériver STUDIO_URL et STUDIO_KEY automatiquement STUDIO_URL="http://terisstudio-api:8003" else # ── Obligatoires en standard ──────────────────────────────── [[ -z "$STUDIO_URL" ]] && m+=" --studio-url\n" && e=$((e+1)) [[ -z "$STUDIO_KEY" ]] && m+=" --studio-key\n" && e=$((e+1)) fi if [[ $e -gt 0 ]]; then local mode="standard"; [[ "$SELFHOSTED" == "true" ]] && mode="selfhosted" echo -e "\n${RED}${BOLD} Mode ${mode} — il manque ${e} paramètre(s) :${NC}\n${RED}${m}${NC}" echo " Lancez ./install.sh --help"; exit 1 fi [[ -z "$INSTALL_DIR" ]] && INSTALL_DIR="${HOME}/teris-infra" # ── Défaut LLM model ─────────────────────────────────────────── if [[ "$SELFHOSTED" == "true" && -z "$LLM_MODEL" ]]; then case "$LLM_PROVIDER" in anthropic) LLM_MODEL="claude-sonnet-4-20250514";; ollama) LLM_MODEL="llama3.1:8b";; mistral) LLM_MODEL="mistral-small-latest";; esac fi } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 0 — Mode token : récupérer la config depuis TerisStudio # ═══════════════════════════════════════════════════════════════════ fetch_token_config() { [[ -z "$ONBOARDING_TOKEN" ]] && return step "Récupération de la configuration" [[ -z "$STUDIO_API_URL" ]] && fail "Usage : install.sh TOKEN STUDIO_URL" # Détecter l'IP publique du serveur local server_ip server_ip=$(curl -sf --max-time 5 https://api.ipify.org 2>/dev/null \ || curl -sf --max-time 5 https://ifconfig.me 2>/dev/null \ || hostname -I | awk '{print $1}') log "IP publique détectée : ${BOLD}${server_ip}${NC}" # Appeler TerisStudio pour récupérer la config log "Appel ${STUDIO_API_URL}/onboarding/config/..." local response response=$(curl -sf --max-time 30 \ "${STUDIO_API_URL}/onboarding/config/${ONBOARDING_TOKEN}?server_ip=${server_ip}" \ 2>/dev/null) \ || fail "Impossible de contacter TerisStudio. Vérifiez l'URL et le token." # Vérifier que la réponse est du JSON valide echo "$response" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null \ || fail "Réponse invalide de TerisStudio." # Extraire les variables depuis le JSON local cfg; cfg=$(echo "$response" | python3 -c " import sys, json r = json.load(sys.stdin) c = r['config'] s = c['secrets'] d = c['subdomains'] k = c['keycloak'] t = c['terisstudio'] db = c['databases'] print(f\"TENANT_ID={r['tenant_slug']}\") print(f\"TERIS_DOMAIN={d['teris_domain']}\") print(f\"STUDIO_URL={t['url']}\") print(f\"STUDIO_KEY={t['api_key']}\") print(f\"KEYCLOAK_BASE_URL={k['base_url']}\") print(f\"KEYCLOAK_REALM={k['realm']}\") print(f\"TLS_EMAIL=tls@{d['teris_domain']}\") print(f\"TC_POSTGRES_PASSWORD={s['tc_postgres_password']}\") print(f\"ISOTERIS_DB_PASSWORD={s['isoteris_db_password']}\") print(f\"ENGINE_DB_PASSWORD={s['engine_db_password']}\") print(f\"TC_API_KEY={s['tc_api_key']}\") print(f\"ENGINE_API_KEY={s['engine_api_key']}\") print(f\"VISTA_ADMIN_KEY={s['vista_admin_key']}\") print(f\"MODE={r['mode']}\") sh = c.get('selfhosted', {}) if sh: print(f\"TERISSTUDIO_POSTGRES_PASSWORD={sh['terisstudio_postgres_password']}\") print(f\"TERISSTUDIO_API_KEY={sh['terisstudio_api_key']}\") ") || fail "Impossible de parser la configuration." # Charger les variables eval "$cfg" # Mode selfhosted si on-premise ou hybrid [[ "$MODE" == "onpremise" || "$MODE" == "hybrid" ]] && SELFHOSTED=true # Stocker le token et l'IP pour le callback export _ONBOARDING_TOKEN="$ONBOARDING_TOKEN" export _ONBOARDING_CALLBACK_URL="${STUDIO_API_URL}/onboarding/callback/${ONBOARDING_TOKEN}" export _SERVER_IP="$server_ip" success "Configuration reçue pour ${BOLD}${TENANT_ID}${NC} (${MODE})" log "Domaine : ${TERIS_DOMAIN}" log "Mode : ${MODE}" } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 1 — Vérifications système # ═══════════════════════════════════════════════════════════════════ check_system() { step "Vérifications système" [[ $EUID -ne 0 ]] && fail "Ce script doit être lancé en root.\n Relancez avec : sudo ./install.sh ..." success "Exécution en root" if [[ "$SELFHOSTED" == "true" ]]; then log "Mode : ${BOLD}SELF-HOSTED${NC} (TerisStudio inclus — cas 3)" else log "Mode : ${BOLD}STANDARD${NC} (TerisStudio SaaS — cas 1/2/4)" fi if [[ -f /etc/os-release ]]; then source /etc/os-release [[ "$ID" == "ubuntu" ]] && success "Système : Ubuntu ${VERSION_ID}" || warn "Système : ${PRETTY_NAME:-inconnu}" fi local arch; arch=$(uname -m) [[ "$arch" != "x86_64" && "$arch" != "aarch64" ]] && fail "Architecture non supportée : $arch" success "Architecture : $arch" local ram_mb; ram_mb=$(free -m | awk '/^Mem:/{print $2}') local ram_min=3500; [[ "$SELFHOSTED" == "true" ]] && ram_min=7500 [[ $ram_mb -lt $ram_min ]] && fail "RAM insuffisante : ${ram_mb} Mo (minimum : $((ram_min/1000)) Go)" success "RAM : ${ram_mb} Mo" local disk_gb; disk_gb=$(df -BG / | awk 'NR==2{print $4}' | tr -d 'G') [[ $disk_gb -lt 10 ]] && fail "Espace disque insuffisant : ${disk_gb} Go (minimum : 10 Go)" success "Espace disque : ${disk_gb} Go libres" for port in 80 443; do ss -tlnp 2>/dev/null | grep -q ":${port} " && warn "Port ${port} déjà utilisé" done if command -v caddy &>/dev/null && systemctl is-active --quiet caddy 2>/dev/null; then warn "Caddy systemd actif — conflit avec Docker Caddy (ports 80/443)" warn "Arrêtez-le : systemctl stop caddy && systemctl disable caddy" fi } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 2 — Prérequis # ═══════════════════════════════════════════════════════════════════ install_prerequisites() { step "Installation des prérequis" apt-get update -qq 2>/dev/null local pkgs="" for p in curl git openssl python3 jq ca-certificates gnupg dnsutils; do command -v "$p" &>/dev/null || pkgs+=" $p" done [[ -n "$pkgs" ]] && apt-get install -y -qq $pkgs 2>/dev/null success "Paquets système OK" if ! command -v docker &>/dev/null; then log "Installation de Docker..."; curl -fsSL https://get.docker.com | sh 2>/dev/null systemctl enable docker; systemctl start docker; success "Docker installé" else success "Docker $(docker --version | awk '{print $3}' | tr -d ',')"; fi docker compose version &>/dev/null || fail "Docker Compose plugin manquant" success "Docker Compose $(docker compose version --short 2>/dev/null)" if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow 80/tcp >/dev/null 2>&1 || true; ufw allow 443/tcp >/dev/null 2>&1 || true success "Ports 80/443 ouverts dans UFW" fi } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 3 — Sources # ═══════════════════════════════════════════════════════════════════ fetch_sources() { step "Récupération des fichiers" if [[ -d "$INSTALL_DIR/.git" ]]; then cd "$INSTALL_DIR" git pull origin main 2>/dev/null || warn "git pull échoué — fichiers locaux" success "Sources mises à jour : $INSTALL_DIR" elif [[ -n "$GITHUB_REPO" ]]; then git clone "$GITHUB_REPO" "$INSTALL_DIR" 2>/dev/null cd "$INSTALL_DIR"; success "Sources téléchargées : $INSTALL_DIR" else local sd; sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "${sd}/docker-compose.on-premise.yml" ]]; then INSTALL_DIR="${sd}"; cd "$INSTALL_DIR" elif [[ -f "./docker-compose.on-premise.yml" ]]; then INSTALL_DIR="$(pwd)"; cd "$INSTALL_DIR" else fail "Fichiers introuvables. Utilisez --repo ou lancez depuis ~/teris-infra"; fi success "Répertoire : $INSTALL_DIR" fi for f in docker-compose.on-premise.yml Caddyfile.on-premise; do [[ ! -f "${INSTALL_DIR}/${f}" ]] && fail "Fichier manquant : ${f}"; done success "Fichiers essentiels présents" # Créer conf.d/ pour import optionnel Caddy mkdir -p "${INSTALL_DIR}/conf.d" # Si selfhosted, copier Caddyfile.terisstudio dans conf.d/ if [[ "$SELFHOSTED" == "true" ]]; then if [[ -f "${INSTALL_DIR}/Caddyfile.terisstudio" ]]; then cp "${INSTALL_DIR}/Caddyfile.terisstudio" "${INSTALL_DIR}/conf.d/terisstudio.caddy" success "Caddyfile.terisstudio activé dans conf.d/" else warn "Caddyfile.terisstudio introuvable — TerisStudio ne sera pas exposé publiquement" fi fi ENV_FILE="${INSTALL_DIR}/.env" CREDENTIALS_DIR="${INSTALL_DIR}/.credentials" CREDENTIALS_FILE="${CREDENTIALS_DIR}/credentials_${TENANT_ID}.txt" } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 4 — Génération .env + credentials # ═══════════════════════════════════════════════════════════════════ generate_env() { step "Génération de la configuration" [[ -f "$ENV_FILE" ]] && cp "$ENV_FILE" "${ENV_FILE}.backup.$(date +%Y%m%d_%H%M%S)" && warn ".env sauvegardé" # ── Secrets communs ───────────────────────────────────────────── TC_POSTGRES_PASSWORD=$(openssl rand -hex 16) ISOTERIS_DB_PASSWORD=$(openssl rand -hex 16) ENGINE_DB_PASSWORD=$(openssl rand -hex 16) TC_API_KEY=$(openssl rand -hex 24) ENGINE_API_KEY=$(openssl rand -hex 24) VISTA_ADMIN_KEY=$(openssl rand -hex 24) # ── Secrets selfhosted ────────────────────────────────────────── if [[ "$SELFHOSTED" == "true" ]]; then TERISSTUDIO_POSTGRES_PASSWORD=$(openssl rand -hex 16) TERISSTUDIO_API_KEY=$(openssl rand -hex 24) STUDIO_KEY="$TERISSTUDIO_API_KEY" # Auto-référence interne success "8 secrets générés (dont 2 TerisStudio)" else success "6 secrets générés" fi # ── Sous-domaines dérivés ─────────────────────────────────────── local ENGINE_DOMAIN="api.${TERIS_DOMAIN}" local VISTA_DOMAIN="api.terisvista.${TERIS_DOMAIN}" local BRAIN_DOMAIN="brain.${TERIS_DOMAIN}" local MONITOR_DOMAIN="monitor.${TERIS_DOMAIN}" local NTFY_DOMAIN="ntfy.${TERIS_DOMAIN}" local MQTT_DOMAIN="mqtt.${TERIS_DOMAIN}" # ── TERISSTUDIO_URL auto-dérivé en selfhosted ────────────────── if [[ "$SELFHOSTED" == "true" ]]; then STUDIO_URL="https://${TERISSTUDIO_DOMAIN}" fi # ── Écriture .env ─────────────────────────────────────────────── cat > "$ENV_FILE" <> "$ENV_FILE" < "$CREDENTIALS_FILE" </dev/null || curl -sf --max-time 5 https://ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}') log "IP publique : ${BOLD}${ip}${NC}"; echo "" local ed vd bd; ed=$(grep '^ENGINE_DOMAIN=' "$ENV_FILE"|cut -d= -f2) vd=$(grep '^VISTA_DOMAIN=' "$ENV_FILE"|cut -d= -f2) bd=$(grep '^BRAIN_DOMAIN=' "$ENV_FILE"|cut -d= -f2) local domains=("$ed" "$vd" "$bd") [[ "$SELFHOSTED" == "true" ]] && domains+=("$TERISSTUDIO_DOMAIN") local fail_count=0 for d in "${domains[@]}"; do local r; r=$(dig +short "$d" 2>/dev/null|head -1) if [[ "$r" == "$ip" ]]; then success "${d} → ${r}" else warn "${d} → ${r:-non résolu}"; fail_count=$((fail_count+1)); fi done echo "" if [[ $fail_count -gt 0 ]]; then warn "${fail_count} sous-domaine(s) non configuré(s)" echo -e " ${BOLD}Ajoutez les enregistrements DNS :${NC}" for d in "${domains[@]}"; do echo " ${d} → A → ${ip}"; done echo ""; log "Caddy réessaiera automatiquement. L'installation continue."; echo "" else success "DNS OK"; fi } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 6 — Registry Docker # ═══════════════════════════════════════════════════════════════════ docker_registry_login() { step "Registry Docker" if [[ -n "${GITHUB_TOKEN:-}" && -n "$REGISTRY_URL" ]]; then echo "$GITHUB_TOKEN" | docker login "$REGISTRY_URL" -u "${REGISTRY_USER:-deploy}" --password-stdin 2>/dev/null success "Connecté à ${REGISTRY_URL}" else log "Pas de token fourni. Si images privées : export GITHUB_TOKEN=xxx"; fi } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 7 — Démarrage # ═══════════════════════════════════════════════════════════════════ start_services() { step "Démarrage de la Teris Suite" cd "$INSTALL_DIR" local compose="docker compose -f docker-compose.on-premise.yml" [[ "$SELFHOSTED" == "true" ]] && compose+=" --profile selfhosted" log "Téléchargement des images Docker..." $compose pull 2>&1 || fail "Échec du pull — vérifiez connexion et registry" success "Images téléchargées" log "Démarrage des containers..." $compose up -d 2>&1; success "Containers lancés" log "Attente du démarrage (60s)..." local w=0; while [[ $w -lt 60 ]]; do sleep 10; w=$((w+10)); echo -ne " ⏳ ${w}s...\r"; done echo " " # ── tf_ro ─────────────────────────────────────────────────────── log "Création rôle tf_ro..." local u p d; u=$(grep '^ISOTERIS_DB_USER=' "$ENV_FILE"|cut -d= -f2); u="${u:-teris}" d=$(grep '^ISOTERIS_DB=' "$ENV_FILE"|cut -d= -f2); d="${d:-teris_db}" p=$(grep '^ISOTERIS_DB_PASSWORD=' "$ENV_FILE"|cut -d= -f2) docker exec isoteris-db psql -U "$u" -d "$d" -c \ "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='tf_ro') THEN CREATE ROLE tf_ro LOGIN PASSWORD '${p}'; END IF; END \$\$; GRANT CONNECT ON DATABASE ${d} TO tf_ro; GRANT USAGE ON SCHEMA public TO tf_ro; GRANT SELECT ON ALL TABLES IN SCHEMA public TO tf_ro; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO tf_ro;" \ 2>/dev/null && success "Rôle tf_ro créé" || warn "tf_ro : création échouée" } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 8 — Health checks # ═══════════════════════════════════════════════════════════════════ verify_health() { step "Vérification des services" _chk() { local n=$1 u=$2 a=0; while [[ $a -lt 5 ]]; do curl -sf --max-time 10 "$u" &>/dev/null && success "$n" && return 0 a=$((a+1)); [[ $a -lt 5 ]] && sleep 8; done; warn "$n — ne répond pas"; return 1; } log "Services internes :"; echo "" _chk "teris-contract" "http://localhost:8004/health" _chk "IsoTeris" "http://localhost:8000/health" _chk "TerisEngine" "http://localhost:8000/health" _chk "TerisVista" "http://localhost:${VISTA_PORT}/health" [[ "$SELFHOSTED" == "true" ]] && _chk "TerisStudio" "http://localhost:8003/health" echo ""; log "Services publics (Caddy TLS) :"; echo "" local ed vd bd; ed=$(grep '^ENGINE_DOMAIN=' "$ENV_FILE"|cut -d= -f2) vd=$(grep '^VISTA_DOMAIN=' "$ENV_FILE"|cut -d= -f2) bd=$(grep '^BRAIN_DOMAIN=' "$ENV_FILE"|cut -d= -f2) _pub() { curl -sf --max-time 15 "$2" &>/dev/null && success "$1" || warn "$1 — DNS/TLS en cours"; } _pub "TerisEngine" "https://${ed}/api/ingest/health" _pub "TerisVista" "https://${vd}/health" _pub "IsoTeris" "https://${bd}/health" [[ "$SELFHOSTED" == "true" ]] && _pub "TerisStudio" "https://${TERISSTUDIO_DOMAIN}/health" if [[ -n "$BOOTSTRAP_DOMAIN" ]]; then echo ""; log "Bootstrap : $BOOTSTRAP_DOMAIN" curl -sf -X POST -H "x-api-key: ${VISTA_ADMIN_KEY}" -H "Content-Type: application/json" \ "http://localhost:${VISTA_PORT}/api/admin/studio/connect" 2>/dev/null | grep -q '"success":true' \ && success "Connexion TerisStudio établie" || warn "Bootstrap : utilisez /admin" fi # ── EPIC 47 — Callback onboarding ────────────────────────────── if [[ -n "${_ONBOARDING_CALLBACK_URL:-}" ]]; then log "Notification TerisStudio..." curl -sf -X POST "${_ONBOARDING_CALLBACK_URL}" \ -H "Content-Type: application/json" \ -d "{\"server_ip\":\"${_SERVER_IP:-}\",\"services_healthy\":[\"terisvista\",\"teris-engine\",\"isoteris\"]}" \ 2>/dev/null && success "TerisStudio notifié : déploiement confirmé" \ || warn "Callback TerisStudio échoué — non bloquant" fi } # ═══════════════════════════════════════════════════════════════════ # ÉTAPE 9 — Résumé # ═══════════════════════════════════════════════════════════════════ print_summary() { step "Installation terminée" local ed vd bd; ed=$(grep '^ENGINE_DOMAIN=' "$ENV_FILE"|cut -d= -f2) vd=$(grep '^VISTA_DOMAIN=' "$ENV_FILE"|cut -d= -f2) bd=$(grep '^BRAIN_DOMAIN=' "$ENV_FILE"|cut -d= -f2) echo -e "\n${BOLD}${GREEN} ╔═══════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${GREEN} ║ ✅ TERIS SUITE INSTALLÉE AVEC SUCCÈS ║${NC}" echo -e "${BOLD}${GREEN} ╚═══════════════════════════════════════════════════════╝${NC}\n" echo -e " ${BOLD}Tenant${NC} : ${TENANT_ID} ${BOLD}Domaine${NC} : ${TERIS_DOMAIN}" local mode="STANDARD (TerisStudio SaaS)"; [[ "$SELFHOSTED" == "true" ]] && mode="SELF-HOSTED (TerisStudio local)" echo -e " ${BOLD}Mode${NC} : ${mode}\n" echo -e " TerisVista Admin : https://${vd}/admin" echo -e " TerisEngine : https://${ed}" echo -e " IsoTeris : https://${bd}" [[ "$SELFHOSTED" == "true" ]] && echo -e " TerisStudio : https://${TERISSTUDIO_DOMAIN}" echo -e " Portainer : https://localhost:9443\n" echo -e " ${YELLOW}${BOLD}CREDENTIALS :${NC} ${CREDENTIALS_FILE}" echo -e " ${YELLOW}Imprimez et rangez en lieu sûr.${NC}\n" echo -e " ${BOLD}Commandes :${NC} make health | make logs | make update | make backup\n" echo -e " ${BOLD}Brevet Baroni / Leroy B1→B9 © 2026${NC}\n" } # ═══════════════════════════════════════════════════════════════════ main() { banner; parse_args "$@"; fetch_token_config; validate_args check_system; install_prerequisites; fetch_sources; generate_env check_dns; docker_registry_login; start_services; verify_health; print_summary; } main "$@"