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