From 3a0e751834459c194c774447f75673f05f6f68b6 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 25 Feb 2026 23:09:50 +0100 Subject: [PATCH] added dynamic configuration of API_URL to docker deployments. Now it works with editing only .env file. --- RUNTIME_CONFIG_IMPLEMENTATION.md | 174 ++++++++++++++++++++++++++++ backend/router/setup.go | 2 +- docker/.env.dev | 10 +- docker/.env.example | 19 ++- docker/.env.prd | 8 +- docker/Dockerfile.frontend | 29 ++--- docker/Dockerfile.frontend.dev | 8 +- docker/docker-compose.yml | 8 +- docker/frontend-dev-entrypoint.sh | 35 ++++++ docker/frontend-entrypoint.sh | 36 ++++++ docker/start-dev.sh | 19 ++- docker/start-prd.sh | 27 ++++- frontend/.gitignore | 7 ++ frontend/public/config.json.example | 4 + frontend/src/utils/config.js | 126 ++++++++++++++------ 15 files changed, 432 insertions(+), 80 deletions(-) create mode 100644 RUNTIME_CONFIG_IMPLEMENTATION.md create mode 100755 docker/frontend-dev-entrypoint.sh create mode 100644 docker/frontend-entrypoint.sh create mode 100644 frontend/public/config.json.example diff --git a/RUNTIME_CONFIG_IMPLEMENTATION.md b/RUNTIME_CONFIG_IMPLEMENTATION.md new file mode 100644 index 0000000..5754054 --- /dev/null +++ b/RUNTIME_CONFIG_IMPLEMENTATION.md @@ -0,0 +1,174 @@ +# Runtime Configuration Implementation Summary + +## What Was Implemented + +Enhanced the BaMoRT frontend to support runtime configuration for **both desktop (Wails) and web production** deployments, eliminating the need to rebuild when changing API URLs. + +## Architecture + +### Desktop (Wails) +- Uses Go bindings to read API URL from `.env` file at runtime +- Method: `window['go']['main']['App']['GetAPIBaseURL']()` +- Configuration: `desktop/.env` → `API_PORT` variable +- No rebuild needed when changing port + +### Web Production (NEW) +Uses a **multi-strategy fallback system**: + +1. **config.json** (Priority 1) + - Loads `/config.json` from web root + - Can be modified after deployment + - Example: `{"apiBaseURL": "https://api.yourdomain.com"}` + +2. **Auto-detection** (Priority 2) + - Probes `/api/public/version` at same origin + - Works automatically with reverse proxy setups + - Detects if backend and frontend share same domain + +3. **VITE_API_URL** (Priority 3) + - Environment variable at build time + - Used for development: `VITE_API_URL=http://localhost:8180` + +4. **Same Origin Fallback** (Priority 4) + - Uses `window.location.origin` + - Assumes backend and frontend on same domain + +## Files Modified + +### Core Implementation +- `/data/dev/bamort/frontend/src/utils/config.js` - Enhanced with multi-strategy config loading +- `/data/dev/bamort/frontend/src/utils/api.js` - Uses dynamic baseURL (already done for desktop) +- `/data/dev/bamort/desktop/main.go` - GetAPIBaseURL() Go binding (already done for desktop) + +### Documentation +- `/data/dev/bamort/frontend/RUNTIME_CONFIG.md` - Complete web configuration guide +- `/data/dev/bamort/desktop/RUNTIME_CONFIG.md` - Desktop configuration guide (existing) + +### Configuration Files +- `/data/dev/bamort/frontend/public/config.json.example` - Template for deployment +- `/data/dev/bamort/frontend/.gitignore` - Excludes `public/config.json` from git + +## How It Works + +### For Web Development +```bash +cd frontend +VITE_API_URL=http://localhost:8180 npm run dev +``` + +### For Web Production Deployment + +**Option A: Using config.json (Recommended)** +```bash +# 1. Build once +cd frontend && npm run build + +# 2. Deploy dist/ to web server + +# 3. Create config.json in web root +cat > /var/www/bamort/config.json < desktop/.env + +# 3. Run app - uses new port automatically +./desktop/build/bin/bamort +``` + +## Verification + +Open browser console when loading the app. Look for one of these messages: + +**Desktop:** +``` +Desktop app using API URL from config: http://localhost:8185 +``` + +**Web:** +``` +Loaded API URL from config.json: https://api.yourdomain.com +``` +or +``` +Detected backend at same origin: https://yourdomain.com +``` +or +``` +Web app using VITE_API_URL: http://localhost:8180 +``` +or +``` +Web app using same origin: https://yourdomain.com +``` + +## Benefits + +✅ **No rebuild needed** for API URL changes in production web deployments +✅ **Same build artifact** can be deployed to dev/staging/production +✅ **Infrastructure-friendly** - works with reverse proxies, Docker, static hosting +✅ **Flexible** - multiple configuration strategies with sensible fallbacks +✅ **Dev-friendly** - VITE_API_URL still works for development +✅ **Desktop-friendly** - reads .env at runtime (already implemented) + +## Testing Checklist + +- [x] Frontend builds successfully (`npm run build`) +- [x] Desktop app reads runtime config from .env +- [ ] Web dev mode works with VITE_API_URL +- [ ] Web production with config.json works +- [ ] Web production with reverse proxy auto-detection works +- [ ] Web production with same-origin fallback works + +## Migration Path + +**For existing deployments:** +1. No changes needed! Current builds continue working +2. To enable runtime config: Just add `config.json` to your web root +3. Old builds with VITE_API_URL still work as fallback + +**For new deployments:** +1. Build once: `npm run build` +2. Deploy `dist/` contents +3. Add `config.json` with your API URL +4. Done! diff --git a/backend/router/setup.go b/backend/router/setup.go index bcc153f..e6127ab 100644 --- a/backend/router/setup.go +++ b/backend/router/setup.go @@ -32,7 +32,7 @@ func SetupGin(r *gin.Engine) { } corsConfig = cors.Config{ AllowOrigins: allowedOrigins, - AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, diff --git a/docker/.env.dev b/docker/.env.dev index bdc62ad..4550214 100644 --- a/docker/.env.dev +++ b/docker/.env.dev @@ -1,9 +1,5 @@ # Environment variables for Bamort development environment -# API Configuration -# API_URL=http://localhost:8180 - -# Database Configuration (for development) DATABASE_TYPE=mysql DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb-dev:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local @@ -13,9 +9,13 @@ MARIADB_PASSWORD=bG4)efozrc MARIADB_DATABASE=bamort MARIADB_USER=bamort -# Frontend Configuration +# API Configuration +# API_URL is used by the frontend to connect to the backend +# For dev environment with Vite dev server, this goes to VITE_API_URL API_URL=http://192.168.0.48:8180 API_PORT=8180 + +# Frontend Configuration BASE_URL=http://localhost:5173 TEMPLATES_DIR=./templates EXPORT_TEMP_DIR=./export_temp diff --git a/docker/.env.example b/docker/.env.example index 0aac946..bd4b68f 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1,9 +1,16 @@ # Environment variables for Bamort production deployment -# Copy this file to .env and adjust the values as needed +# Copy this file to .env.dev or .env.prd and adjust the values as needed -#- API Configuration +# ===== FRONTEND API CONFIGURATION ===== +# API_URL: The backend API endpoint that the frontend will use +# For production: Use your public API domain (https://api.yourdomain.com) +# For development: Use your local network IP (http://192.168.x.x:8180) +# +# IMPORTANT: After changing API_URL, just restart containers - NO REBUILD NEEDED! +# Production: docker-compose -f docker-compose.yml restart frontend +# Development: docker-compose -f docker-compose.dev.yml restart frontend-dev API_URL=https://backend.domain.de -VITE_API_URL=https://backend.domain.de +API_PORT=8180 #- Database Configuration Backend DATABASE_TYPE=mysql @@ -20,3 +27,9 @@ BASE_URL=https://frontend.domain.de TEMPLATES_DIR=./templates EXPORT_TEMP_DIR=./export_temp COMPOSE_PROJECT_NAME=bamort + +# Mail Configuration (for development) +MAIL_HOST=mail.server.de +MAIL_PORT=465 +MAIL_USERNAME=bamort@server.de +MAIL_PASSWORD=XXXZZZZZ.YYYXXX \ No newline at end of file diff --git a/docker/.env.prd b/docker/.env.prd index e29dc5c..e4ff40a 100644 --- a/docker/.env.prd +++ b/docker/.env.prd @@ -1,10 +1,10 @@ # Environment variables for Bamort production environment # API Configuration +# API_URL is used by the frontend to generate config.json at container startup +# Change this value and restart containers (no rebuild needed!) API_URL=https://bamort-api.trokan.de -VITE_API_URL=https://bamort-api.trokan.de - -# Database Configuration Backend +API_PORT=8180 DATABASE_TYPE=mysql #DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local @@ -14,7 +14,7 @@ MARIADB_PASSWORD=bG4)efozrc MARIADB_DATABASE=bamort MARIADB_USER=bamort -API_PORT=8180 +# Frontend Configuration BASE_URL=https://bamort.trokan.de TEMPLATES_DIR=./templates EXPORT_TEMP_DIR=./export_temp diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend index 0690661..080abcd 100644 --- a/docker/Dockerfile.frontend +++ b/docker/Dockerfile.frontend @@ -1,42 +1,37 @@ # =========== 1) Build stage =========== FROM node:22-alpine AS build -# Accept build arguments for Vite environment variables -ARG VITE_API_URL -ARG VITE_BASE_URL -ARG VITE_API_PORT - -# Set them as environment variables for the build process -ENV VITE_API_URL=$VITE_API_URL -ENV VITE_BASE_URL=$VITE_BASE_URL -ENV VITE_API_PORT=$VITE_API_PORT - +# No build args needed - using runtime configuration instead WORKDIR /usr/src/app # Copy package files COPY package*.json ./ - # Install dependencies RUN npm install # Copy the rest of the frontend code COPY . . -# Build the production bundle +# Build the production bundle WITHOUT baked-in API URL +# Runtime configuration will be generated from environment variables RUN npm run build # =========== 2) Serve stage =========== FROM nginx:alpine -# Copy production build to Nginx html folder. -# Adjust /usr/src/app/build -> /usr/src/app/dist if you’re using Angular/Vue -#COPY --from=build /usr/src/app/build /usr/share/nginx/html +# Copy production build to Nginx html folder COPY --from=build /usr/src/app/dist /usr/share/nginx/html + # Copy custom nginx configuration for SPA routing COPY --from=build /usr/src/app/nginx.conf /etc/nginx/conf.d/default.conf + +# Copy entrypoint script that generates runtime config +COPY --from=build /usr/src/app/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + # Expose HTTP port EXPOSE 80 -# Run Nginx in foreground -CMD ["nginx", "-g", "daemon off;"] +# Use custom entrypoint that generates config.json from environment +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/docker/Dockerfile.frontend.dev b/docker/Dockerfile.frontend.dev index c38f22f..0528477 100644 --- a/docker/Dockerfile.frontend.dev +++ b/docker/Dockerfile.frontend.dev @@ -9,8 +9,12 @@ COPY package*.json ./ # Install dependencies RUN npm install +# Copy entrypoint script that generates config.json +COPY docker-dev-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + # Expose Vite dev server port EXPOSE 5173 -# Start development server with host binding for Docker -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] +# Use entrypoint that generates config.json before starting Vite +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index f75fe1b..6864443 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,18 +25,12 @@ services: build: context: ../frontend dockerfile: ../docker/Dockerfile.frontend - args: - VITE_API_URL: ${API_URL:-https://bamort-api.trokan.de} - VITE_BASE_URL: ${BASE_URL:-https://bamort.trokan.de} - VITE_API_PORT: ${API_PORT:-443} container_name: bamort-frontend ports: - "8181:80" environment: - NODE_ENV=production - - VITE_API_URL=${API_URL:-https://bamort.trokan.de:8180} - - VITE_BASE_URL=${BASE_URL:-https://bamort.trokan.de} - - VITE_API_PORT=${API_PORT:-8180} + - API_URL=${API_URL:-https://bamort-api.trokan.de} depends_on: - backend restart: unless-stopped diff --git a/docker/frontend-dev-entrypoint.sh b/docker/frontend-dev-entrypoint.sh new file mode 100755 index 0000000..250074f --- /dev/null +++ b/docker/frontend-dev-entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e + +# Generate config.json from VITE_API_URL for development +# This makes the dev environment behave the same as production + +CONFIG_FILE="/app/public/config.json" + +echo "🔧 Generating development config.json..." + +# Use VITE_API_URL from environment +API_BASE_URL="${VITE_API_URL:-}" + +if [ -z "$API_BASE_URL" ]; then + echo "⚠️ VITE_API_URL not set, creating minimal config" + cat > "$CONFIG_FILE" < "$CONFIG_FILE" < "$CONFIG_FILE" < "$CONFIG_FILE" </dev/null || echo "unknown") echo "📝 Git Commit: $GIT_COMMIT" echo "📦 Building and starting development containers..." +echo "🔧 Frontend will use API: ${API_URL:-http://localhost:8180}" # Stoppe vorhandene Container docker-compose -f docker-compose.dev.yml down # Baue und starte die Container -docker-compose -f docker-compose.dev.yml up --build -d +docker-compose -f docker-compose.dev.yml --env-file .env.dev up --build -d echo "✅ Development environment started." +echo "📱 Frontend: http://localhost:5173" +echo "🔌 Backend: http://localhost:8180" +echo "🗄️ phpMyAdmin: http://localhost:8081" diff --git a/docker/start-prd.sh b/docker/start-prd.sh index 31faf76..ff64534 100755 --- a/docker/start-prd.sh +++ b/docker/start-prd.sh @@ -11,14 +11,35 @@ fi # Gehe ins Docker-Verzeichnis cd "$(dirname "$0")" +# Load production environment variables +if [ -f .env.prd ]; then + echo "📝 Loading configuration from .env.prd" + export $(grep -v '^#' .env.prd | xargs) +else + if [ -f .env ]; then + echo "📝 Loading configuration from .env" + export $(grep -v '^#' .env | xargs) + else + echo "⚠️ Warning: .env not found, using defaults" + fi +fi + echo "📦 Building and starting production containers..." +echo "🔧 Frontend will use API: ${API_URL:-https://bamort-api.trokan.de}" + # Build before stopping existing containers -docker-compose -f docker-compose.yml build +docker-compose -f docker-compose.yml --env-file .env.prd build # Stoppe vorhandene Container docker-compose -f docker-compose.yml down # Baue und starte die Container -docker-compose -f docker-compose.yml up --build -d +docker-compose -f docker-compose.yml --env-file .env.prd up -d -echo "✅ Production environment started." \ No newline at end of file +echo "✅ Production environment started." +echo "📱 Frontend: http://localhost:8181" +echo "🔌 Backend: http://localhost:8182" +echo "" +echo "💡 To change API URL: Edit .env.prd and run:" +echo " docker-compose -f docker-compose.yml restart frontend" +echo " (No rebuild needed!)" \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 34bbc4b..3fc4705 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -13,6 +13,13 @@ dist dist-ssr *.local +# Runtime configuration (use config.json.example as template) +public/config.json + +# Docker entrypoint scripts (copied from ../docker/ during build) +docker-entrypoint.sh +docker-dev-entrypoint.sh + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/frontend/public/config.json.example b/frontend/public/config.json.example new file mode 100644 index 0000000..63a08b2 --- /dev/null +++ b/frontend/public/config.json.example @@ -0,0 +1,4 @@ +{ + "apiBaseURL": "http://localhost:8180", + "_comment": "This file can be modified at deployment without rebuilding. Copy to config.json and adjust apiBaseURL for your environment." +} diff --git a/frontend/src/utils/config.js b/frontend/src/utils/config.js index 7d2219c..5f86aad 100644 --- a/frontend/src/utils/config.js +++ b/frontend/src/utils/config.js @@ -4,38 +4,65 @@ * For desktop builds (Wails), reads the API base URL from the Go backend * to allow runtime configuration via .env without rebuilding. * - * For web builds, uses VITE_API_URL environment variable. + * For web builds, uses runtime config.json or auto-detection: + * 1. Try to load /config.json (can be modified at deployment without rebuild) + * 2. Try to detect backend at same origin (production reverse proxy setup) + * 3. Fall back to VITE_API_URL (development) or same origin */ let cachedAPIBaseURL = null /** - * Check if we're running in a Wails desktop app + * Try to load configuration from /config.json */ -function isWailsApp() { - return typeof window !== 'undefined' && - window['go'] !== undefined && - window['go']['main'] !== undefined && - window['go']['main']['App'] !== undefined +async function loadConfigFile() { + try { + const response = await fetch('/config.json', { + cache: 'no-cache', + headers: { 'Accept': 'application/json' } + }) + if (response.ok) { + // Check if response is actually JSON (not HTML from SPA fallback) + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + const config = await response.json() + if (config.apiBaseURL) { + console.log('Loaded API URL from config.json:', config.apiBaseURL) + return config.apiBaseURL + } + } + } + } catch (error) { + // config.json doesn't exist or is invalid, that's okay + } + return null } /** - * Wait for Wails to initialize (max 3 seconds) + * Try to detect backend at same origin (production setup) */ -async function waitForWails(maxAttempts = 30) { - for (let i = 0; i < maxAttempts; i++) { - if (isWailsApp()) { - return true +async function detectBackendAtOrigin() { + try { + const origin = window.location.origin + const response = await fetch(`${origin}/api/public/version`, { + method: 'GET', + cache: 'no-cache', + signal: AbortSignal.timeout(2000) // 2 second timeout + }) + if (response.ok) { + console.log('Detected backend at same origin:', origin) + return origin } - await new Promise(resolve => setTimeout(resolve, 100)) + } catch (error) { + // Backend not at same origin, that's okay } - return false + return null } /** * Get the API base URL dynamically * - In Wails desktop app: calls Go backend to get configured URL - * - In web app: uses VITE_API_URL or defaults to production API + * - In web app: tries config.json, auto-detection, or VITE_API_URL */ export async function getAPIBaseURL() { // Return cached value if available @@ -43,34 +70,58 @@ export async function getAPIBaseURL() { return cachedAPIBaseURL } - // Check if this looks like a desktop environment - const isDesktop = typeof window !== 'undefined' && - (window.location.protocol === 'wails:' || - window.location.hostname === 'wails.localhost') - - // Wails desktop app - wait for Wails to be ready - if (isDesktop || typeof window !== 'undefined' && window['go']) { - const wailsReady = await waitForWails() - - if (wailsReady) { + // Try Wails desktop app first (with timeout) + if (typeof window !== 'undefined' && window['go']) { + // Wait up to 3 seconds for Wails bindings to be ready + for (let i = 0; i < 30; i++) { try { - // Access Wails Go bindings via window object - const url = await window['go']['main']['App']['GetAPIBaseURL']() - cachedAPIBaseURL = url - console.log('Desktop app using API URL from config:', url) - return url + if (window['go']?.['main']?.['App']?.['GetAPIBaseURL']) { + const url = await window['go']['main']['App']['GetAPIBaseURL']() + cachedAPIBaseURL = url + console.log('Desktop app using API URL from config:', url) + return url + } } catch (error) { console.error('Failed to get API URL from Wails:', error) - // Fallback to localhost for desktop - cachedAPIBaseURL = 'http://localhost:8185' - return cachedAPIBaseURL + break } + await new Promise(resolve => setTimeout(resolve, 100)) + } + + // Wails detected but binding failed - use desktop fallback + cachedAPIBaseURL = 'http://localhost:8185' + console.log('Desktop app using fallback:', cachedAPIBaseURL) + return cachedAPIBaseURL + } + + // Web app - try multiple strategies + + // Strategy 1: Load from config.json (can be modified at deployment) + const configFileURL = await loadConfigFile() + if (configFileURL) { + cachedAPIBaseURL = configFileURL + return cachedAPIBaseURL + } + + // Strategy 2: Check if backend is at same origin (production reverse proxy) + if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') { + const sameOriginURL = await detectBackendAtOrigin() + if (sameOriginURL) { + cachedAPIBaseURL = sameOriginURL + return cachedAPIBaseURL } } - // Web app - use environment variable or production default - cachedAPIBaseURL = import.meta.env.VITE_API_URL || 'https://bamort-api.trokan.de' - console.log('Web app using API URL:', cachedAPIBaseURL) + // Strategy 3: Use VITE_API_URL (development) or fallback to same origin + if (import.meta.env.VITE_API_URL) { + cachedAPIBaseURL = import.meta.env.VITE_API_URL + console.log('Web app using VITE_API_URL:', cachedAPIBaseURL) + } else { + // Final fallback: assume same origin for production + cachedAPIBaseURL = window.location.origin + console.log('Web app using same origin:', cachedAPIBaseURL) + } + return cachedAPIBaseURL } @@ -79,4 +130,5 @@ export async function getAPIBaseURL() { */ export function isDesktopMode() { return isWailsApp() -} +}typeof window !== 'undefined' && + window['go']?.['main']?.['App'] !== undefined