diff --git a/.env.example b/.env.example index a15b785..f1c7790 100644 --- a/.env.example +++ b/.env.example @@ -12,8 +12,17 @@ AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef AZURE_REDIRECT_URI=https://ai-sandbox.oliver.solutions/ac-helper/ # ── Admin bootstrap ─────────────────────────────────────────────────────────── -# First login with this email automatically receives the admin role +# First login with these emails automatically receives the admin role (comma-separated) ADMIN_EMAIL= +ADMIN_EMAILS= + +# ── Emergency access (bypass SSO) ──────────────────────────────────────────── +# Set EMERGENCY_TOKEN to a long random string to allow token-based login when +# Azure AD / 2FA is unavailable. Leave blank to disable entirely. +# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" +EMERGENCY_TOKEN= +EMERGENCY_USER_EMAIL= +EMERGENCY_USER_NAME=Emergency Access # ── OpenAI ──────────────────────────────────────────────────────────────────── OPENAI_API_KEY= @@ -51,6 +60,11 @@ ENABLE_COST_ESTIMATION=true MAX_PROCESSING_COST_USD=10.00 MAX_CONCURRENT_JOBS=5 +# ── PostgreSQL ──────────────────────────────────────────────────────────────── +# Password for the ac-tool DB user. Change before deploying. +# Generate with: python3 -c "import secrets; print(secrets.token_hex(24))" +POSTGRES_PASSWORD= + # ── Security ────────────────────────────────────────────────────────────────── # Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" SESSION_SECRET= diff --git a/deploy.sh b/deploy.sh index 9756544..ba73fa3 100755 --- a/deploy.sh +++ b/deploy.sh @@ -10,6 +10,7 @@ WEB_USER="www-data" ENV_FILE="$APP_DIR/.env" ENV_EXAMPLE="$APP_DIR/.env.example" CONTAINER_NAME="ac-tool" +DB_CONTAINER_NAME="ac-tool-db" FE_BUILD_TAG="ac-tool-fe-extract" # ── Colours ─────────────────────────────────────────────────────────────────── @@ -47,28 +48,28 @@ if [[ ! -f "$ENV_FILE" ]]; then exit 0 fi -# Load .env into the current shell so we can read APP_PORT +# Load .env into the current shell so we can read APP_PORT etc. set -o allexport # shellcheck disable=SC1090 source "$ENV_FILE" set +o allexport APP_PORT="${APP_PORT:-8100}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" # Warn about blank required keys MISSING="" -for KEY in GEMINI_API_KEY ADMIN_EMAIL SESSION_SECRET; do +for KEY in GEMINI_API_KEY ADMIN_EMAIL SESSION_SECRET POSTGRES_PASSWORD; do VAL="${!KEY:-}" [[ -z "$VAL" ]] && MISSING="$MISSING $KEY" done -[[ -n "$MISSING" ]] && warn "These required keys are empty in .env:$MISSING" +[[ -n "$MISSING" ]] && warn "These keys are empty in .env:$MISSING" ok ".env loaded (APP_PORT=$APP_PORT)" # ── 4. Port check ───────────────────────────────────────────────────────────── log "Checking port $APP_PORT..." if ss -tlnp | grep -q ":${APP_PORT} "; then - # Port is in use — check if it's our own container if docker ps --format '{{.Names}} {{.Ports}}' | grep -q "${CONTAINER_NAME}.*${APP_PORT}"; then warn "Port $APP_PORT already used by our container (will be restarted)" else @@ -93,7 +94,7 @@ else ok "Updated to $(git rev-parse --short HEAD)" fi -# ── 6. Docker build (with cache, refresh base images) ───────────────────────── +# ── 6. Docker build ─────────────────────────────────────────────────────────── log "Building Docker image..." docker compose build --pull ok "Docker image built" @@ -103,10 +104,7 @@ log "Building and extracting frontend..." TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"; docker rmi "$FE_BUILD_TAG" >/dev/null 2>&1 || true' EXIT -# Build only the Node.js frontend-builder stage (no Python/LibreOffice — fast) docker build --target frontend-builder --tag "$FE_BUILD_TAG" "$APP_DIR" - -# Copy dist/ out of the image via a throwaway container EXTRACT_CONTAINER=$(docker create "$FE_BUILD_TAG") docker cp "$EXTRACT_CONTAINER:/app/frontend/dist/." "$TMPDIR/" docker rm "$EXTRACT_CONTAINER" >/dev/null @@ -117,20 +115,32 @@ ok "Frontend built: $FILE_COUNT files" # ── 8. Deploy frontend static files ─────────────────────────────────────────── log "Deploying frontend to $WEB_DIR..." mkdir -p "$WEB_DIR" -# Wipe old files (preserve directory itself so Apache doesn't break mid-deploy) find "${WEB_DIR}" -mindepth 1 -delete cp -r "$TMPDIR/." "$WEB_DIR/" chown -R "$WEB_USER:$WEB_USER" "$WEB_DIR" chmod -R 755 "$WEB_DIR" ok "Frontend deployed to $WEB_DIR" -# ── 9. Restart container ────────────────────────────────────────────────────── -log "Restarting application container..." +# ── 9. Restart containers ───────────────────────────────────────────────────── +log "Restarting containers (app + postgres)..." docker compose down --remove-orphans 2>/dev/null || true docker compose up -d -ok "Container started" +ok "Containers started" -# ── 10. Health check ────────────────────────────────────────────────────────── +# ── 10. Wait for PostgreSQL ──────────────────────────────────────────────────── +log "Waiting for PostgreSQL to be ready..." +for i in $(seq 1 30); do + if docker exec "$DB_CONTAINER_NAME" pg_isready -U achelper -d achelper >/dev/null 2>&1; then + ok "PostgreSQL is ready" + break + fi + if [[ $i -eq 30 ]]; then + die "PostgreSQL did not become ready after 60s. Check logs: docker logs $DB_CONTAINER_NAME" + fi + sleep 2 +done + +# ── 11. Health check ────────────────────────────────────────────────────────── log "Waiting for application to be healthy..." HEALTH_URL="http://localhost:${APP_PORT}/health" for i in $(seq 1 30); do @@ -144,18 +154,43 @@ for i in $(seq 1 30); do sleep 2 done -# ── 11. First-run initialisation ────────────────────────────────────────────── +# ── 12. Data directory ──────────────────────────────────────────────────────── DATA_DIR="$APP_DIR/data" -if [[ ! -f "$DATA_DIR/users.json" ]]; then - log "First run — initialising data directory..." - mkdir -p "$DATA_DIR/uploads" "$DATA_DIR/outputs" "$DATA_DIR/sheets" - # Trigger the app's startup seeding (dropdowns.json + users.json are created - # automatically on the first API request if they don't exist) - curl -sf "http://localhost:${APP_PORT}/api/dropdowns/categories" >/dev/null 2>&1 || true - ok "Data directory initialised at $DATA_DIR" +mkdir -p "$DATA_DIR/uploads" "$DATA_DIR/outputs" +ok "Data directories ready at $DATA_DIR" + +# ── 13. JSON → PostgreSQL migration (first deploy after adding Postgres) ─────── +# +# If old JSON data files exist, offer to run the one-time migration script +# that imports users, clients, dropdowns, sheets, and export templates into DB. +# +JSON_MIGRATION_MARKER="$DATA_DIR/.pg_migrated" +if [[ ! -f "$JSON_MIGRATION_MARKER" ]]; then + HAS_JSON=false + for f in "$DATA_DIR/users.json" "$DATA_DIR/sheets_metadata.json" "$DATA_DIR/clients.json"; do + [[ -f "$f" ]] && HAS_JSON=true && break + done + + if [[ "$HAS_JSON" == "true" ]]; then + echo "" + warn "Old JSON data files detected. Run the one-time migration to import them into PostgreSQL?" + warn " yes — migrate now (recommended)" + warn " no — skip (data already in DB or you'll migrate manually)" + read -r -p " Migrate now? [yes/no]: " MIGRATE_ANSWER /dev/null; then echo "" hr @@ -191,9 +226,10 @@ fi # ── Summary ─────────────────────────────────────────────────────────────────── echo "" ok "Deployment complete!" -echo " Container: docker logs -f $CONTAINER_NAME" -echo " Health: $HEALTH_URL" -echo " Frontend: $WEB_DIR ($FILE_COUNT files)" -echo " Data: $DATA_DIR" -echo " Commit: $(git -C "$APP_DIR" rev-parse --short HEAD)" +echo " App container: docker logs -f $CONTAINER_NAME" +echo " DB container: docker logs -f $DB_CONTAINER_NAME" +echo " Health: $HEALTH_URL" +echo " Frontend: $WEB_DIR ($FILE_COUNT files)" +echo " Data: $DATA_DIR" +echo " Commit: $(git -C "$APP_DIR" rev-parse --short HEAD)" echo ""