# Staging / PR-preview compose file.
#
# Used for both the persistent staging stack and ephemeral PR previews.
# The cd-staging.yml workflow picks the project name and fills in the host
# ports + DOMAIN + STAGING_DATABASE from the PR number (or "staging" for the
# persistent stack):
#
#   # Persistent staging
#   PROJECT=trails-staging
#   JOURNAL_HOST_PORT=3100 PLANNER_HOST_PORT=3101 \
#   DOMAIN=staging.trails.cool STAGING_DATABASE=trails_staging \
#   docker compose -f docker-compose.staging.yml -p $PROJECT up -d
#
#   # PR 123 preview (port = 3200 + 2N for journal, 3201 + 2N for planner)
#   PROJECT=trails-pr-123
#   JOURNAL_HOST_PORT=3446 PLANNER_HOST_PORT=3447 \
#   DOMAIN=pr-123.staging.trails.cool STAGING_DATABASE=trails_pr_123 \
#   JOURNAL_IMAGE_TAG=pr-123 PLANNER_IMAGE_TAG=pr-123 \
#   docker compose -f docker-compose.staging.yml -p $PROJECT up -d
#
# `trails-shared` is created by the production compose file (see
# docker-compose.yml) so staging/preview containers can reach the production
# `postgres` host. BRouter lives on a separate host and is reached over
# vSwitch using the production-shared BROUTER_URL / BROUTER_AUTH_TOKEN.
#
# Container names are not pinned — Docker Compose generates them from the
# project name (e.g. `trails-pr-123-journal-1`), which keeps each preview
# isolated without manual naming.

services:
  journal:
    image: ghcr.io/trails-cool/journal:${JOURNAL_IMAGE_TAG:-latest}
    restart: unless-stopped
    # Published on 0.0.0.0 (not 127.0.0.1) so the production Caddy container
    # can reach it via host.docker.internal — Docker's host-gateway resolves
    # to the bridge IP, not loopback. The Hetzner Cloud firewall blocks all
    # inbound on ports 3000+ from the public internet, so this is effectively
    # internal-only despite the bind address.
    ports:
      - "${JOURNAL_HOST_PORT:?JOURNAL_HOST_PORT must be set}:3000"
    networks:
      - trails-shared
    environment:
      DOMAIN: ${DOMAIN:?DOMAIN must be set}
      ORIGIN: https://${DOMAIN}
      # PR previews override this to point at the persistent staging
      # planner (planner.staging.trails.cool). Persistent staging defaults
      # to its own planner subdomain.
      PLANNER_URL: ${PLANNER_URL:-https://planner.${DOMAIN}}
      IS_FLAGSHIP: ""
      DATABASE_URL: postgres://trails:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in SOPS secrets.app.env}@postgres:5432/${STAGING_DATABASE:?STAGING_DATABASE must be set}
      JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}
      SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set}
      NODE_ENV: production
      PORT: 3000
      SENTRY_DSN: ${SENTRY_DSN_JOURNAL:-}
      SENTRY_RELEASE: ${SENTRY_RELEASE:-}
      SMTP_URL: ""
      SMTP_FROM: trails.cool staging <noreply@staging.trails.cool>
      WAHOO_CLIENT_ID: ""
      WAHOO_CLIENT_SECRET: ""
      WAHOO_WEBHOOK_TOKEN: ""
      GARMIN_CLIENT_ID: ${GARMIN_CLIENT_ID:-}
      GARMIN_CLIENT_SECRET: ${GARMIN_CLIENT_SECRET:-}
      DEMO_BOT_ENABLED: ""
      # Federation (social-federation rollout). Enabled by cd-staging.yml
      # for persistent staging (12.2) and for PR previews (12.4 — every
      # preview is a second live instance staging can follow across).
      FEDERATION_ENABLED: ${FEDERATION_ENABLED:-}
      FEDERATION_KEY_ENCRYPTION_KEY: ${FEDERATION_KEY_ENCRYPTION_KEY:-}
      FEDERATION_LOG_LEVEL: ${FEDERATION_LOG_LEVEL:-}
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/health || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 256M

  planner:
    image: ghcr.io/trails-cool/planner:${PLANNER_IMAGE_TAG:-latest}
    # Only the persistent staging stack runs a planner. PR previews are
    # journal-only and point their PLANNER_URL at the persistent
    # planner.staging.trails.cool. Saves ~256MB per active preview.
    profiles: ["persistent"]
    restart: unless-stopped
    # Same rationale as the journal port — see the comment there.
    ports:
      - "${PLANNER_HOST_PORT:-3101}:3001"
    networks:
      - trails-shared
    environment:
      BROUTER_URL: ${BROUTER_URL:?BROUTER_URL must be set}
      BROUTER_AUTH_TOKEN: ${BROUTER_AUTH_TOKEN:?BROUTER_AUTH_TOKEN must be set}
      DATABASE_URL: postgres://trails:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in SOPS secrets.app.env}@postgres:5432/${STAGING_DATABASE}
      NODE_ENV: production
      PORT: 3001
      SENTRY_DSN: ${SENTRY_DSN_PLANNER:-}
      SENTRY_RELEASE: ${SENTRY_RELEASE:-}
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:3001/health || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 256M

networks:
  # Staging/preview services attach only to trails-shared — created (and
  # IPv6-enabled) by the production compose project. There is no per-project
  # default network here, so v6 egress comes from trails-shared itself.
  trails-shared:
    external: true
    name: trails-shared
