version: '3.9'

# Docker Swarm Stack Configuration - DEVELOPMENT
# Deploy with: docker stack deploy -c infra/stacks/vello/stack.dev.yml vello-dev
# Features: Hot reload, single replicas, development volumes

secrets:
  # Database
  database_url:
    external: true
  supabase_url:
    external: true
  supabase_key:
    external: true

  # Security
  jwt_secret:
    external: true
  encryption_key:
    external: true
  service_token:
    external: true

  # Redis
  redis_password:
    external: true

  # OpenAI
  openai_api_key:
    external: true

  # Email provider secrets
  sendgrid_api_key:
    external: true
  mailgun_api_key:
    external: true
  mailgun_domain:
    external: true
  resend_api_key:
    external: true
  email_from_address:
    external: true
  email_from_name:
    external: true
  # v2 secrets removidos — per-stack overrides are deprecated; use global secrets only (see docs/security/SECRETS_BOOTSTRAP.md)

  # Sentry (dev)
  sentry_api_token_2025_10_08_22_42:
    external: true
  sentry_dsn:
    external: true

services:
  # Redis Service - Development with data persistence
  redis:
    image: redis:7-alpine
    networks:
      - vello-overlay
    ports:
      - target: ${REDIS_INTERNAL_PORT}
        published: ${REDIS_PUBLIC_PORT}
        protocol: tcp
    secrets:
      - redis_password
    volumes:
      - redis_data:/data
    environment:
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - REDIS_PORT=${REDIS_INTERNAL_PORT}
    command: sh -c 'redis-server --port "$${REDIS_PORT}" --requirepass "$$(cat $$REDIS_PASSWORD_FILE)"'
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      test: ["CMD", "sh", "-c", "redis-cli -p $${REDIS_PORT} -a $$(cat /run/secrets/redis_password) ping"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  # API Service - Development with hot reload
  api:
    image: vello-api:latest
    networks:
      - vello-overlay
    ports:
      - target: ${API_INTERNAL_PORT}
        published: ${API_PUBLIC_PORT}
        protocol: tcp
    secrets:
      - database_url
      - supabase_url
      - supabase_key
      - jwt_secret
      - encryption_key
      - service_token
      - sendgrid_api_key
      - mailgun_api_key
      - mailgun_domain
      - resend_api_key
      - email_from_address
      - email_from_name
      - redis_password
      - sentry_api_token_2025_10_08_22_42
      - sentry_dsn
    environment:
      # Redis base URL for internal use (rate limiting, ed25519, etc.)
      - REDIS_INTERNAL_BASE_URL=${REDIS_INTERNAL_BASE_URL}
      - REDIS_URL=${REDIS_URL}
      # Prometheus base URL (admin_metrics / monitoring)
      - PROMETHEUS_INTERNAL_BASE_URL=${PROMETHEUS_INTERNAL_BASE_URL}
      - CORS_FILE=/etc/uw/cors.list
      - CORS_ALLOW_CREDENTIALS=true
      # Use secret files
      - DATABASE_URL_FILE=/run/secrets/database_url
      - DATABASE_URL=${DATABASE_URL}
      - SUPABASE_URL_FILE=/run/secrets/supabase_url
      - SUPABASE_URL=${SUPABASE_URL}
      - SUPABASE_KEY_FILE=/run/secrets/supabase_key
      - SUPABASE_KEY=${SUPABASE_KEY}
      - SECRET_KEY_FILE=/run/secrets/jwt_secret
      - SECRET_KEY=${SECRET_KEY}
      - ENCRYPTION_KEY_FILE=/run/secrets/encryption_key
      - SERVICE_TOKEN_FILE=/run/secrets/service_token
      - SUPABASE_DB_PASSWORD=${SUPABASE_DB_PASSWORD}

      # Email provider configuration via Swarm secret files
      - EMAIL_FROM_ADDRESS_FILE=/run/secrets/email_from_address
      - EMAIL_FROM_NAME_FILE=/run/secrets/email_from_name
      - RESEND_API_KEY_FILE=/run/secrets/resend_api_key
      - SENDGRID_API_KEY_FILE=/run/secrets/sendgrid_api_key
      - MAILGUN_API_KEY_FILE=/run/secrets/mailgun_api_key
      - MAILGUN_DOMAIN_FILE=/run/secrets/mailgun_domain
      # Dev-only delivery simulation to populate TME in audit
      - EMAIL_SIMULATE_DELIVERY_ON_ACCEPT=true

      # Development settings
      - ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
      - API_HOST=${API_HOST}
      - API_PORT=${API_INTERNAL_PORT}
      # Web User public base URL (used in verification links)
      - WEB_USER_PUBLIC_BASE_URL=${WEB_USER_PUBLIC_BASE_URL}
      - API_RELOAD=true
      - PYTHONPYCACHEPREFIX=/app/temp/pycache
      - ACCESS_TOKEN_EXPIRE_MINUTES=15
      - REFRESH_TOKEN_EXPIRE_DAYS=7
      - WS_HEARTBEAT_INTERVAL=30
      - ENVIRONMENT=development

      # Redis with secret
      - REDIS_HOST=${REDIS_INTERNAL_HOST}
      - REDIS_PORT=${REDIS_INTERNAL_PORT}
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - GIT_SHA=dev-099d991
      - INTERNAL_ADMIN_TOKEN=dev-internal-admin
      # Sentry API access (for issues endpoint) and DSN
      - SENTRY_API_BASE_URL=https://sentry.io/api/0
      - SENTRY_ORG_SLUG=agenticca
      - SENTRY_PROJECT_API=vello
      - SENTRY_API_TOKEN_FILE=/run/secrets/sentry_api_token_2025_10_08_22_42
      - SENTRY_DSN_API_FILE=/run/secrets/sentry_dsn
      # Ed25519 nonce backend: use DB to avoid Redis false-positives in dev
      - ED25519_NONCE_BACKEND=db
      - ED25519_DISABLE_REDIS=true
      - ERROR_ISOLATION_EXCLUDE=jobs
      - JOBS_ADD_COMPAT_ENABLED=false
    volumes:
      - ${API_SOURCE_DIR}:/app:rw
      # Dev: mount CORS list directly from the API source tree (avoid Swarm config immutability).
      - ${API_SOURCE_DIR}/CORS:/etc/uw/cors.list:ro
      - api_logs:/app/logs
      - api_temp:/app/temp
      - ${HOST_LOGS_DIR:-/tmp/vello/infra-docker/logs}:/host-logs:ro
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '1'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 5s
        # Dev: avoid getting permanently stuck at 0/1 after transient DNS/ISP/Supabase pooler hiccups.
        max_attempts: 0
        window: 120s
    healthcheck:
      test: ["CMD", "curl", "-f", "${API_HEALTHCHECK_URL}"]
      # Dev: tighter, faster feedback
      interval: 5s
      timeout: 5s
      retries: 12
      start_period: 10s
    depends_on:
      - redis

  # Models Gateway Service - Development with config hot reload
  models-gateway:
    image: vello-modelsgateway:latest
    networks:
      - vello-overlay
    ports:
      - target: ${MODELSGATEWAY_INTERNAL_PORT}
        published: ${MODELSGATEWAY_PUBLIC_PORT}
        protocol: tcp
    secrets:
      - jwt_secret
      - redis_password
      - openai_api_key
    configs:
      - source: modelsgateway_nginx_conf_debug_v6
        target: /usr/local/openresty/nginx/conf/nginx.conf
        mode: 0444
      - source: modelsgateway_conf_overlay_v22
        target: /etc/nginx/conf.d/vello.conf
        mode: 0444
    environment:
      # Use secret files
      - JWT_SECRET_FILE=/run/secrets/jwt_secret
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - OPENAI_API_KEY_FILE=/run/secrets/openai_api_key

      - PROMPT_FALLBACK_DIR=/data/prompts
      - PROMPT_FALLBACK_ALWAYS=true
      - DEBUG_LOG=1
      - MODEL_SHUB_OUTBOX_DIR=/tmp/modelsgateway/outbox
      # Development settings
      - ENVIRONMENT=development
      - FORCE_GPT41_TEST=false
      - CONFIG_CACHE_TTL=60
      - INTERNAL_ADMIN_TOKEN=dev-internal-admin
      - ALLOW_DEV_AUTH_BYPASS=true
      - E2E_TEST_ENABLED=true
      # Network hygiene under WSL2 mirrored: prefer IPv4 and bypass proxies for local cluster
      - DNS_SERVERS=127.0.0.11,1.1.1.1
      - NO_PROXY=127.0.0.1,localhost,api,models-gateway,job-collector
      - REDIS_HOST=${REDIS_INTERNAL_HOST}
      - REDIS_PORT=${REDIS_INTERNAL_PORT}
      # Use internal Redis only in dev stacks
      - API_HOST=${API_INTERNAL_HOST}
      - API_PORT=${API_INTERNAL_PORT}
      - API_INTERNAL_BASE_URL=${API_INTERNAL_BASE_URL}
      - BACKEND_URL=${API_INTERNAL_BASE_URL}
      - MODEL_SHUB_LOG_BASE_URL=${API_INTERNAL_BASE_URL}
      - MODEL_SHUB_LOG_ENDPOINT=${API_INTERNAL_BASE_URL}/api/internal/modelsgateway/requests/log
      # Performance/diagnostics toggles
      - CAPTURE_SSE_RAW=false
      - LOG_PROMPT_DEBUG=false
      # Fallback: when GPT‑5 SSE returns no content, run non‑stream retry in background (dev only)
      - GPT5_SSE_EMPTY_FALLBACK=false
      # Force non-stream for GPT‑5 family in dev to ensure JSON content for outbox/collector
      # Keep non-stream disabled by default (SSE pass-through)
      - GPT5_FORCE_NON_STREAMING=false
    volumes:
      - nginx_cache:/var/cache/nginx
      - models_config:/data/models
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      test: ["CMD", "curl", "-f", "${MODELSGATEWAY_HEALTHCHECK_URL}"]
      interval: 5s
      timeout: 5s
      retries: 12
      start_period: 10s
    depends_on:
      - redis
      - api

  # Job Collector Service - Development with hot reload
  job-collector:
    image: vello-job-collector:latest
    networks:
      - vello-overlay
    ports:
      - target: ${JOBCOLLECTOR_INTERNAL_PORT}
        published: ${JOBCOLLECTOR_PUBLIC_PORT}
        protocol: tcp
    secrets:
      - database_url
      - redis_password
      - service_token
      - encryption_key
    environment:
      # Use secret files
      - DATABASE_URL_FILE=/run/secrets/database_url
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - SERVICE_TOKEN_FILE=/run/secrets/service_token
      - ENCRYPTION_KEY_FILE=/run/secrets/encryption_key

      # Development settings
      - ENVIRONMENT=development
      - REDIS_HOST=${REDIS_INTERNAL_HOST}
      - REDIS_PORT=${REDIS_INTERNAL_PORT}
      # DEV redis bridge disabled in dev stack; rely on internal Redis only
      - API_HOST=${API_INTERNAL_HOST}
      - API_PORT=${API_INTERNAL_PORT}
      - BACKEND_URL=${API_INTERNAL_BASE_URL}
      - JOBCOLLECTOR_PUBLIC_PORT=${JOBCOLLECTOR_PUBLIC_PORT}
      - JOBCOLLECTOR_PUBLIC_BASE_URL=${JOBCOLLECTOR_PUBLIC_BASE_URL}
      - JOBCOLLECTOR_INTERNAL_PORT=${JOBCOLLECTOR_INTERNAL_PORT}
      - JOBCOLLECTOR_INTERNAL_BASE_URL=${JOBCOLLECTOR_INTERNAL_BASE_URL}
      # - PREFERRED_STREAMING_MODEL=gpt-4.1
      - PREFERRED_STREAMING_TEMPERATURE=0.7
      - DEV_AUTO_RELOAD=true
      - JOB_COLLECTOR_SRC_DIR=/app/service
      - CONFIG_SYNC_INTERVAL_SECONDS=300
      - PROMPT_FALLBACK_DIR=/data/prompts
      - MODELSGATEWAY_URL=${MODELSGATEWAY_INTERNAL_BASE_URL}
      - INTERNAL_ADMIN_TOKEN=dev-internal-admin
      - CANONICAL_EVENTS_ENABLED=true
      - CANONICAL_EVENTS_STREAM=agenticca:events:modelsgateway:v1
      - BACKEND_DISPATCH_ENABLED=false
      - GIT_SHA=dev-099d991
      # Dev resilience: fill processing_text from prompt when upstream yields empty
      - PROCESSING_TEXT_BYPASS=copy_prompt
    volumes:
      # image-only mode when no worktree present for job-collector (no bind mounts)
      - job_collector_logs:/app/logs
      - models_config:/data/models
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      test: ["CMD", "curl", "-f", "${JOBCOLLECTOR_HEALTHCHECK_URL}"]
      interval: 5s
      timeout: 5s
      retries: 12
      start_period: 10s
    depends_on:
      - redis
      - api

  # Vello Ingester Service - Development
  vello-ingester:
    image: vello-ingester:latest
    networks:
      - vello-overlay
    secrets:
      - database_url
      - redis_password
    environment:
      - DATABASE_URL_FILE=/run/secrets/database_url
      - REDIS_PASSWORD_FILE=/run/secrets/redis_password
      - REDIS_INTERNAL_BASE_URL=${REDIS_INTERNAL_BASE_URL}
      - REDIS_URL=${REDIS_URL}
      - CANONICAL_EVENTS_STREAM=agenticca:events:modelsgateway:v1
      - INGESTER_WRITE_ENABLED=true
      - INGESTER_DRY_RUN=false
      - INGESTER_CONSUMER_GROUP=vello-ingester
      - INGESTER_CONSUMER_NAME=vello-ingester-dev
      - INGESTER_BLOCK_MS=250
      - ENVIRONMENT=development
      - LOG_LEVEL=INFO
    volumes:
      - vello_ingester_logs:/app/logs
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    depends_on:
      - redis
      - api

  # Web Admin Service - Development with hot reload
  web-admin:
    image: vello-web-admin:latest
    user: "1000:1000"  # Run as non-root to prevent ownership issues with bind mounts
    networks:
      - vello-overlay
    ports:
      # In dev we expose the Vite dev server on WEB_ADMIN_INTERNAL_PORT (default 5000)
      - target: ${WEB_ADMIN_INTERNAL_PORT}
        published: ${WEB_ADMIN_PUBLIC_PORT}
        protocol: tcp
    secrets:
      - database_url
      - jwt_secret
    environment:
      - OBSERVABILITY_ENABLED=${OBSERVABILITY_ENABLED:-true}
      # Use secret files
      - DATABASE_URL_FILE=/run/secrets/database_url
      - SECRET_KEY_FILE=/run/secrets/jwt_secret

      # Development settings
      - NODE_ENV=development
      # Express dev server (tsx) port; Vite runs on WEB_ADMIN_INTERNAL_PORT
      # Run Express dev server on a different port to avoid colliding with Vite
      - WEB_ADMIN_SERVER_PORT=5001
      # Vite dev server listens on WEB_ADMIN_INTERNAL_PORT (default 5000)
      - PORT=${WEB_ADMIN_INTERNAL_PORT}
      - API_URL=${API_INTERNAL_BASE_URL}
      - VITE_API_PUBLIC_BASE_URL=${API_PUBLIC_BASE_URL}
      - VITE_API_INTERNAL_BASE_URL=${API_INTERNAL_BASE_URL}
      - VITE_CACHE_DIR=/app/data/vite
      # Dev server listens on WEB_ADMIN_INTERNAL_PORT; HMR client must use published port
      - WEB_ADMIN_DEV_SERVER_HOST=0.0.0.0
      - WEB_ADMIN_DEV_SERVER_PORT=${WEB_ADMIN_INTERNAL_PORT}
      - WEB_ADMIN_PUBLIC_PORT=${WEB_ADMIN_INTERNAL_PORT}
      - WEB_ADMIN_HMR_HOST=${WEB_ADMIN_PUBLIC_HOST}
      # HMR client uses the published port in dev
      - WEB_ADMIN_HMR_PORT=${WEB_ADMIN_PUBLIC_PORT}
      - VITE_API_PUBLIC_WS_BASE_URL=${API_PUBLIC_WS_BASE_URL}
      # Jobs auto-refresh (build-time via Vite, effective at dev server start)
      - VITE_ENABLE_JOBS_AUTOREFRESH=true
      - VITE_JOBS_REFRESH_INTERVAL_MS=3000
      # Improve stability of build/dev tools in constrained containers
      - NODE_OPTIONS=--max-old-space-size=1024
    working_dir: /app
    # Hot reload: only install deps when node_modules is missing; then run dev server
    command: ["sh", "-lc", "if [ ! -d node_modules ] || [ -z \"$$(ls -A node_modules 2>/dev/null)\" ]; then echo '[web-admin] installing deps (npm ci)...'; npm ci --no-audit --loglevel=warn; else echo '[web-admin] deps present, skipping install'; fi; npm run dev"]
    volumes:
      - ${WEB_ADMIN_SOURCE_DIR}:/app:rw
      # Must be writable to allow nested volume mounts (e.g., /packages/web-panel-core/node_modules).
      - ${WEB_ADMIN_SOURCE_DIR}/../../packages/web-panel-core:/packages/web-panel-core:rw
      - ${WEB_ADMIN_SOURCE_DIR}/../../packages/web-panel-presets:/packages/web-panel-presets:ro
      - web_admin_node_modules:/packages/web-panel-core/node_modules
      - web_panel_data:/app/data
      - web_admin_node_modules:/app/node_modules
    deploy:
      replicas: 1
      update_config:
        order: stop-first
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '0.5'
          memory: 1024M
        reservations:
          cpus: '0.25'
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      # In dev, Vite dev server answers on WEB_ADMIN_INTERNAL_PORT
      test: ["CMD", "sh", "-lc", "curl -fsS http://127.0.0.1:${WEB_ADMIN_INTERNAL_PORT}/ >/dev/null 2>&1"]
      interval: 5s
      timeout: 5s
      retries: 60
      start_period: 20s
    depends_on:
      - redis
      - api

  # Web User Service - Development with hot reload
  web-user:
    image: node:20-alpine
    user: "1000:1000"  # Run as non-root to prevent ownership issues with bind mounts
    networks:
      - vello-overlay
    ports:
      - target: ${WEB_USER_INTERNAL_PORT}
        published: ${WEB_USER_PUBLIC_PORT}
        protocol: tcp
    environment:
      - OBSERVABILITY_ENABLED=${OBSERVABILITY_ENABLED:-true}
      - NODE_ENV=development
      - VITE_CACHE_DIR=/app/data/vite
      - NPM_LOCK_STATE_FILE=/app/data/.npm-lock.sha
      - PORT=${WEB_USER_INTERNAL_PORT}
      - WEB_USER_DEV_SERVER_HOST=0.0.0.0
      - WEB_USER_DEV_SERVER_PORT=${WEB_USER_INTERNAL_PORT}
      - WEB_USER_HMR_HOST=127.0.0.1
      - WEB_USER_HMR_PORT=${WEB_USER_INTERNAL_PORT}
      - VITE_API_PUBLIC_BASE_URL=${API_PUBLIC_BASE_URL}
      - VITE_API_PUBLIC_WS_BASE_URL=${API_PUBLIC_WS_BASE_URL}
      # Allow Vite /api proxy to target the internal API service in the same stack
      - API_INTERNAL_BASE_URL=${API_INTERNAL_BASE_URL}
    working_dir: /app
    command: ["sh", "-lc", "chmod +x /app/dev-entrypoint.sh; /app/dev-entrypoint.sh"]
    volumes:
      - ${WEB_USER_SOURCE_DIR}:/app:rw
      # Must be writable to allow nested volume mounts (e.g., /packages/web-panel-core/node_modules).
      - ${WEB_USER_SOURCE_DIR}/../../packages/web-panel-core:/packages/web-panel-core:rw
      - ${WEB_USER_SOURCE_DIR}/../../packages/web-panel-presets:/packages/web-panel-presets:ro
      - web_user_node_modules:/packages/web-panel-core/node_modules
      - web_user_data:/app/data
      - web_user_node_modules:/app/node_modules
    deploy:
      replicas: 1
      update_config:
        order: stop-first
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      # In dev, Vite dev server answers on WEB_USER_INTERNAL_PORT
      test: ["CMD", "sh", "-lc", "wget -q -O - http://127.0.0.1:${WEB_USER_INTERNAL_PORT}/ >/dev/null 2>&1"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 60s
    depends_on:
      - api

volumes:
  redis_data:
    driver: local
  api_logs:
    driver: local
  api_temp:
    driver: local
  modelsgateway_logs:
    driver: local
  job_collector_logs:
    driver: local
  vello_ingester_logs:
    driver: local
  models_config:
    driver: local
  nginx_cache:
    driver: local
  web_panel_data:
    driver: local
  web_admin_node_modules:
    driver: local
  web_user_data:
    driver: local
  web_user_node_modules:
    driver: local

networks:
  vello-overlay:
    driver: overlay
    attachable: true


configs:
  modelsgateway_nginx_conf_debug_v6:
    file: ../../../../platform/services/models-gateway/nginx/nginx.conf
  modelsgateway_conf_overlay_v22:
    file: ../../../../platform/services/models-gateway/nginx/conf.d/vello.conf
