diff --git a/.env.example b/.env.example index a76c64d..d0babef 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ -POSTGRES_PASSWORD=scope_pass_2024 -ANTHROPIC_API_KEY=your-api-key-here +POSTGRES_PASSWORD=your_strong_password_here +ANTHROPIC_API_KEY=your-anthropic-api-key +AZURE_TENANT_ID=your-azure-tenant-id +AZURE_CLIENT_ID=your-azure-client-id diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..9387708 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# deploy.sh — idempotent deploy script for GMAL Scope Builder +# Run from the repo root: sudo ./deploy.sh +# First-time setup: clone repo to /opt/gmal-scope-builder, create .env, then run this script. + +set -euo pipefail + +# ── Config ──────────────────────────────────────────────────────────────────── +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WEB_DIR="/var/www/html/gmal-scope-builder" +APACHE_CONF="/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf" +APACHE_MARKER="gmal-scope-builder" # Used to detect if block already added +APP_URL_PATH="/gsb" +BACKEND_PORT="8002" +HEALTH_URL="http://127.0.0.1:${BACKEND_PORT}/api/health" + +# ── Colours ─────────────────────────────────────────────────────────────────── +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +log() { echo -e "${GREEN}[deploy]${NC} $*"; } +warn() { echo -e "${YELLOW}[ warn ]${NC} $*"; } +die() { echo -e "${RED}[error ]${NC} $*" >&2; exit 1; } + +# ── 1. Pre-flight checks ────────────────────────────────────────────────────── +log "Running pre-flight checks..." + +[[ -f "$REPO_DIR/.env" ]] || die ".env not found at $REPO_DIR/.env — copy .env.example and fill in values" + +# Check required env vars +for var in POSTGRES_PASSWORD ANTHROPIC_API_KEY AZURE_TENANT_ID AZURE_CLIENT_ID; do + grep -q "^${var}=" "$REPO_DIR/.env" || die "Missing required variable '$var' in .env" +done + +command -v docker &>/dev/null || die "Docker is not installed" +sudo docker compose version &>/dev/null || die "Docker Compose plugin not available" + +# Warn if no GMAL Excel file (non-fatal — data may already be in the DB) +if ! ls "$REPO_DIR/data/"*.xlsx &>/dev/null 2>&1; then + warn "No .xlsx file found in $REPO_DIR/data/ — GMAL ingest will need to be triggered manually after deploy" +fi + +log "Pre-flight OK" + +# ── 2. Pull latest code ─────────────────────────────────────────────────────── +log "Pulling latest code from origin/main..." +git -C "$REPO_DIR" pull origin main + +# ── 3. Build and start backend services ────────────────────────────────────── +log "Building Docker images and starting services (using build cache)..." +sudo docker compose -f "$REPO_DIR/docker-compose.yml" --env-file "$REPO_DIR/.env" \ + up -d --build --remove-orphans + +# ── 4. Wait for backend health ──────────────────────────────────────────────── +log "Waiting for backend to become healthy..." +TIMEOUT=90 +ELAPSED=0 +until curl -sf "$HEALTH_URL" > /dev/null 2>&1; do + sleep 3 + ELAPSED=$((ELAPSED + 3)) + [[ $ELAPSED -ge $TIMEOUT ]] && { + warn "Backend logs:" + sudo docker compose -f "$REPO_DIR/docker-compose.yml" logs --tail=30 backend + die "Backend did not become healthy within ${TIMEOUT}s" + } + log " Still waiting... (${ELAPSED}s)" +done +log "Backend healthy at $HEALTH_URL" + +# ── 5. Database migrations ──────────────────────────────────────────────────── +# create_all() runs automatically inside start.sh on each container start (idempotent). +# Uncomment the line below once Alembic migrations are set up: +# sudo docker compose -f "$REPO_DIR/docker-compose.yml" exec -T backend alembic upgrade head +log "Database schema is managed by start.sh (create_all) — no separate migration step needed" + +# ── 6. Build frontend ───────────────────────────────────────────────────────── +log "Building frontend via Node Docker container..." +sudo docker run --rm \ + -v "$REPO_DIR/frontend:/app" \ + -w /app \ + node:20-alpine \ + sh -c "npm ci --prefer-offline && npm run build" + +# ── 7. Deploy frontend static files ────────────────────────────────────────── +log "Deploying frontend to $WEB_DIR..." +sudo mkdir -p "$WEB_DIR" +sudo rm -rf "${WEB_DIR:?}"/* +sudo cp -r "$REPO_DIR/frontend/dist/." "$WEB_DIR/" +sudo chown -R www-data:www-data "$WEB_DIR" +log "Frontend deployed ($(find "$REPO_DIR/frontend/dist" -type f | wc -l) files)" + +# ── 8. Apache config (idempotent) ───────────────────────────────────────────── +if sudo grep -q "$APACHE_MARKER" "$APACHE_CONF" 2>/dev/null; then + log "Apache block for $APP_URL_PATH already present — skipping" +else + log "Adding Apache config block for $APP_URL_PATH ..." + sudo python3 - << PYEOF +apache_conf = "$APACHE_CONF" +block = """ + # ---------------------------------------------------------------- + # GMAL Scope Builder — FastAPI backend at :$BACKEND_PORT + # ---------------------------------------------------------------- + ProxyPass $APP_URL_PATH/api/ http://127.0.0.1:$BACKEND_PORT/api/ + ProxyPassReverse $APP_URL_PATH/api/ http://127.0.0.1:$BACKEND_PORT/api/ + + # GMAL Scope Builder SPA + Alias $APP_URL_PATH $WEB_DIR + + + Options -Indexes +FollowSymLinks + AllowOverride None + Require all granted + RewriteEngine On + RewriteBase $APP_URL_PATH/ + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^ index.html [L] + +""" +with open(apache_conf) as f: + content = f.read() +if "$APACHE_MARKER" not in content: + content = content.replace("", block + "\n") + with open(apache_conf, "w") as f: + f.write(content) + print("Apache config updated") +else: + print("Apache config already has $APACHE_MARKER block (written by another process)") +PYEOF +fi + +# ── 9. Validate and reload Apache ───────────────────────────────────────────── +log "Validating Apache config..." +sudo apache2ctl configtest +log "Reloading Apache..." +sudo systemctl reload apache2 + +# ── Done ────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Deployment complete! ║${NC}" +echo -e "${GREEN}║ https://optical-dev.oliver.solutions${APP_URL_PATH}/ ║${NC}" +echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}" +echo "" +log "If this is the first deploy, trigger GMAL ingest:" +log " curl -X POST -H 'Authorization: Bearer ' https://optical-dev.oliver.solutions${APP_URL_PATH}/api/gmal/ingest" diff --git a/frontend/src/auth/msalConfig.ts b/frontend/src/auth/msalConfig.ts index 576945e..fe93485 100644 --- a/frontend/src/auth/msalConfig.ts +++ b/frontend/src/auth/msalConfig.ts @@ -9,7 +9,6 @@ export const msalConfig: Configuration = { }, cache: { cacheLocation: 'localStorage', - storeAuthStateInCookie: false, }, }; diff --git a/frontend/src/pages/GmalEditor.tsx b/frontend/src/pages/GmalEditor.tsx index d3e9563..d1b6755 100644 --- a/frontend/src/pages/GmalEditor.tsx +++ b/frontend/src/pages/GmalEditor.tsx @@ -77,7 +77,7 @@ export default function GmalEditor() { complexity_description: asset.complexity_description || '', caveats: asset.caveats || '', master_adapt: asset.master_adapt || '', - ai_efficiency_pct: asset.ai_efficiency_pct, + ai_efficiency_pct: asset.ai_efficiency_pct ?? null, ai_enhanced_description: asset.ai_enhanced_description || '', }); // Build hour cells from existing data