diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ca0ca5b..1af63e5 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -138,3 +138,119 @@ jobs: pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest pytest-asyncio python -m pytest tests/bqas/ -v --tb=short || true + + # ======================================== + # Build & Deploy auf Hetzner (nur main, kein PR) + # ======================================== + + deploy-hetzner: + runs-on: docker + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: + - test-go-consent + container: docker:27-cli + steps: + - name: Deploy + run: | + set -euo pipefail + DEPLOY_DIR="/opt/breakpilot-core" + COMPOSE_FILES="-f docker-compose.yml -f docker-compose.hetzner.yml" + COMMIT_SHA="${GITHUB_SHA:-unknown}" + SHORT_SHA="${COMMIT_SHA:0:8}" + REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + + # Services die deployed werden + SERVICES="postgres valkey qdrant minio ollama mailpit embedding-service rag-service backend-core consent-service health-aggregator" + + echo "=== BreakPilot Core Deploy ===" + echo "Commit: ${SHORT_SHA}" + echo "Deploy Dir: ${DEPLOY_DIR}" + echo "Services: ${SERVICES}" + echo "" + + # 1. Repo auf dem Host erstellen/aktualisieren via Helper-Container + echo "=== Updating code on host ===" + docker run --rm \ + -v "${DEPLOY_DIR}:${DEPLOY_DIR}" \ + --entrypoint sh \ + alpine/git:latest \ + -c " + if [ ! -d '${DEPLOY_DIR}/.git' ]; then + echo 'Erstmaliges Klonen nach ${DEPLOY_DIR}...' + git clone '${REPO_URL}' '${DEPLOY_DIR}' + else + cd '${DEPLOY_DIR}' + git fetch origin main + git reset --hard origin/main + fi + " + echo "Code aktualisiert auf ${SHORT_SHA}" + + # 2. .env sicherstellen + docker run --rm -v "${DEPLOY_DIR}:${DEPLOY_DIR}" alpine \ + sh -c " + if [ ! -f '${DEPLOY_DIR}/.env' ]; then + echo 'WARNUNG: ${DEPLOY_DIR}/.env fehlt!' + echo 'Erstelle .env aus .env.example mit Defaults...' + if [ -f '${DEPLOY_DIR}/.env.example' ]; then + cp '${DEPLOY_DIR}/.env.example' '${DEPLOY_DIR}/.env' + echo '.env aus .env.example erstellt' + else + echo 'Kein .env.example gefunden — Services starten mit Defaults' + fi + else + echo '.env vorhanden' + fi + " + + # 3. Shared Network erstellen (falls noch nicht vorhanden) + docker network create breakpilot-network 2>/dev/null || true + + # 4. Build + Deploy via Helper-Container + echo "" + echo "=== Building + Deploying ===" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "${DEPLOY_DIR}:${DEPLOY_DIR}" \ + -w "${DEPLOY_DIR}" \ + docker:27-cli \ + sh -c " + set -e + COMPOSE_FILES='-f docker-compose.yml -f docker-compose.hetzner.yml' + + echo '=== Building Docker Images ===' + docker compose \${COMPOSE_FILES} build --parallel \ + backend-core consent-service rag-service embedding-service health-aggregator + + echo '' + echo '=== Starting infrastructure ===' + docker compose \${COMPOSE_FILES} up -d postgres valkey qdrant minio mailpit + + echo 'Warte auf DB + Cache...' + sleep 10 + + echo '' + echo '=== Starting Ollama + pulling bge-m3 ===' + docker compose \${COMPOSE_FILES} up -d ollama + sleep 5 + + # bge-m3 Modell pullen (nur beim ersten Mal ~670MB) + echo 'Pulling bge-m3 model (falls noch nicht vorhanden)...' + docker exec bp-core-ollama ollama pull bge-m3 2>&1 || echo 'WARNUNG: bge-m3 pull fehlgeschlagen (wird spaeter nachgeholt)' + + echo '' + echo '=== Starting application services ===' + docker compose \${COMPOSE_FILES} up -d \ + embedding-service rag-service backend-core consent-service health-aggregator + + echo '' + echo '=== Health Checks ===' + sleep 15 + for svc in bp-core-postgres bp-core-valkey bp-core-qdrant bp-core-ollama bp-core-embedding-service bp-core-rag-service bp-core-backend bp-core-consent-service bp-core-health; do + STATUS=\$(docker inspect --format='{{.State.Status}}' \"\${svc}\" 2>/dev/null || echo 'not found') + echo \"\${svc}: \${STATUS}\" + done + " + + echo "" + echo "=== Deploy abgeschlossen: ${SHORT_SHA} ===" diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index 85d27b2..32cc969 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -15,7 +15,6 @@ networks: volumes: valkey_data: embedding_models: - paddleocr_models: services: @@ -142,35 +141,6 @@ services: networks: - breakpilot-network - # ========================================================= - # OCR SERVICE (PaddleOCR PP-OCRv5 Latin) - # ========================================================= - paddleocr-service: - build: - context: ./paddleocr-service - dockerfile: Dockerfile - container_name: bp-core-paddleocr - ports: - - "8095:8095" - environment: - PADDLEOCR_API_KEY: ${PADDLEOCR_API_KEY:-} - FLAGS_use_mkldnn: "0" - volumes: - - paddleocr_models:/root/.paddleocr - deploy: - resources: - limits: - memory: 4G - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:8095/health"] - interval: 30s - timeout: 10s - start_period: 300s - retries: 5 - restart: unless-stopped - networks: - - breakpilot-network - # ========================================================= # HEALTH AGGREGATOR # ========================================================= @@ -183,7 +153,7 @@ services: - "8099" environment: PORT: 8099 - CHECK_SERVICES: "valkey:6379,consent-service:8081,rag-service:8097,embedding-service:8087,paddleocr-service:8095" + CHECK_SERVICES: "valkey:6379,consent-service:8081,rag-service:8097,embedding-service:8087" healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:8099/health"] interval: 30s diff --git a/docker-compose.hetzner.yml b/docker-compose.hetzner.yml new file mode 100644 index 0000000..91b21b6 --- /dev/null +++ b/docker-compose.hetzner.yml @@ -0,0 +1,175 @@ +# ========================================================= +# BreakPilot Core — Hetzner Override (x86_64) +# ========================================================= +# Verwendung: +# docker compose -f docker-compose.yml -f docker-compose.hetzner.yml up -d \ +# postgres valkey qdrant ollama embedding-service rag-service \ +# backend-core consent-service health-aggregator +# +# Aenderungen gegenueber Basis (docker-compose.yml): +# - platform: linux/amd64 (statt arm64) +# - Ollama Container fuer CPU-Embeddings (bge-m3) +# - Mailpit ersetzt durch Dummy (kein Mail-Dev-Server noetig) +# - Vault, Nginx, Gitea etc. deaktiviert via Profile +# - Netzwerk: auto-create (nicht external) +# ========================================================= + +networks: + breakpilot-network: + external: true + name: breakpilot-network + +services: + + # ========================================================= + # NEUE SERVICES + # ========================================================= + + # Ollama fuer Embeddings (CPU-only, bge-m3) + ollama: + image: ollama/ollama:latest + container_name: bp-core-ollama + platform: linux/amd64 + volumes: + - ollama_models:/root/.ollama + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:11434/api/tags || exit 1"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + restart: unless-stopped + networks: + - breakpilot-network + + # ========================================================= + # PLATFORM OVERRIDES (arm64 → amd64) + # ========================================================= + + backend-core: + platform: linux/amd64 + build: + context: ./backend-core + dockerfile: Dockerfile + args: + TARGETARCH: amd64 + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}?options=-csearch_path%3Dcore,public + JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + ENVIRONMENT: ${ENVIRONMENT:-production} + VALKEY_URL: redis://valkey:6379/0 + SESSION_TTL_HOURS: ${SESSION_TTL_HOURS:-24} + CONSENT_SERVICE_URL: http://consent-service:8081 + USE_VAULT_SECRETS: "false" + SMTP_HOST: ${SMTP_HOST:-smtp.example.com} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_FROM_NAME: ${SMTP_FROM_NAME:-BreakPilot} + SMTP_FROM_ADDR: ${SMTP_FROM_ADDR:-noreply@breakpilot.app} + + consent-service: + platform: linux/amd64 + environment: + DATABASE_URL: postgres://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db} + JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-your-refresh-secret} + PORT: 8081 + ENVIRONMENT: ${ENVIRONMENT:-production} + ALLOWED_ORIGINS: "*" + VALKEY_URL: redis://valkey:6379/0 + SESSION_TTL_HOURS: ${SESSION_TTL_HOURS:-24} + SMTP_HOST: ${SMTP_HOST:-smtp.example.com} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_FROM_NAME: ${SMTP_FROM_NAME:-BreakPilot} + SMTP_FROM_ADDR: ${SMTP_FROM_ADDR:-noreply@breakpilot.app} + FRONTEND_URL: ${FRONTEND_URL:-https://admin-dev.breakpilot.ai} + + billing-service: + platform: linux/amd64 + + rag-service: + platform: linux/amd64 + ports: + - "8097:8097" + environment: + PORT: 8097 + QDRANT_URL: http://qdrant:6333 + MINIO_ENDPOINT: nbg1.your-objectstorage.com + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-T18RGFVXXG2ZHQ5404TP} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-KOUU4WO6wh07cQjNgh0IZHkeKQrVfBz6hnIGpNss} + MINIO_BUCKET: ${MINIO_BUCKET:-breakpilot-rag} + MINIO_SECURE: "true" + EMBEDDING_SERVICE_URL: http://embedding-service:8087 + OLLAMA_URL: http://ollama:11434 + OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-bge-m3} + JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + ENVIRONMENT: ${ENVIRONMENT:-production} + + embedding-service: + platform: linux/amd64 + ports: + - "8087:8087" + + health-aggregator: + platform: linux/amd64 + environment: + PORT: 8099 + CHECK_SERVICES: "postgres:5432,valkey:6379,qdrant:6333,backend-core:8000,rag-service:8097,embedding-service:8087" + + # ========================================================= + # DUMMY-ERSATZ FUER ABHAENGIGKEITEN + # ========================================================= + # backend-core + consent-service haengen von mailpit ab + # (depends_on merged bei compose override, kann nicht entfernt werden) + # → Mailpit durch leichtgewichtigen Dummy ersetzen + + mailpit: + image: alpine:3.19 + entrypoint: ["sh", "-c", "echo 'Mailpit dummy on Hetzner' && tail -f /dev/null"] + volumes: [] + ports: [] + environment: {} + + # Qdrant: RocksDB braucht mehr open files + qdrant: + ulimits: + nofile: + soft: 65536 + hard: 65536 + + # minio: rag-service haengt davon ab (depends_on) + # Lokal laufen lassen, aber rag-service nutzt externe Hetzner Object Storage + # minio bleibt unveraendert (klein, ~50MB RAM) + + # ========================================================= + # DEAKTIVIERTE SERVICES (via profiles) + # ========================================================= + + nginx: + profiles: ["disabled"] + vault: + profiles: ["disabled"] + vault-init: + profiles: ["disabled"] + vault-agent: + profiles: ["disabled"] + gitea: + profiles: ["disabled"] + gitea-runner: + profiles: ["disabled"] + night-scheduler: + profiles: ["disabled"] + admin-core: + profiles: ["disabled"] + pitch-deck: + profiles: ["disabled"] + levis-holzbau: + profiles: ["disabled"] + +volumes: + ollama_models: diff --git a/docker-compose.yml b/docker-compose.yml index 4b0a5b0..32e4866 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -347,11 +347,11 @@ services: environment: PORT: 8097 QDRANT_URL: http://qdrant:6333 - MINIO_ENDPOINT: minio:9000 - MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-breakpilot} - MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-breakpilot123} + MINIO_ENDPOINT: nbg1.your-objectstorage.com + MINIO_ACCESS_KEY: T18RGFVXXG2ZHQ5404TP + MINIO_SECRET_KEY: KOUU4WO6wh07cQjNgh0IZHkeKQrVfBz6hnIGpNss MINIO_BUCKET: ${MINIO_BUCKET:-breakpilot-rag} - MINIO_SECURE: "false" + MINIO_SECURE: "true" EMBEDDING_SERVICE_URL: http://embedding-service:8087 OLLAMA_URL: ${OLLAMA_URL:-http://host.docker.internal:11434} OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-bge-m3} @@ -843,3 +843,20 @@ services: restart: unless-stopped networks: - breakpilot-network + + # ========================================================= + # LEVIS HOLZBAU - Kinder-Holzwerk-Website + # ========================================================= + levis-holzbau: + build: + context: ./levis-holzbau + dockerfile: Dockerfile + container_name: bp-core-levis-holzbau + platform: linux/arm64 + ports: + - "3013:3000" + environment: + NODE_ENV: production + restart: unless-stopped + networks: + - breakpilot-network diff --git a/levis-holzbau/.dockerignore b/levis-holzbau/.dockerignore new file mode 100644 index 0000000..79a303d --- /dev/null +++ b/levis-holzbau/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.next +.git +Dockerfile +.dockerignore diff --git a/levis-holzbau/Dockerfile b/levis-holzbau/Dockerfile new file mode 100644 index 0000000..6ffb4b2 --- /dev/null +++ b/levis-holzbau/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20-alpine AS base + +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN mkdir -p public +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] diff --git a/levis-holzbau/app/globals.css b/levis-holzbau/app/globals.css new file mode 100644 index 0000000..8942b39 --- /dev/null +++ b/levis-holzbau/app/globals.css @@ -0,0 +1,25 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@500;600;700&family=Nunito:wght@400;600;700&display=swap'); + +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Nunito', sans-serif; + background-color: #FDF8F0; + color: #2C2C2C; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Quicksand', sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/levis-holzbau/app/layout.tsx b/levis-holzbau/app/layout.tsx new file mode 100644 index 0000000..6049bf2 --- /dev/null +++ b/levis-holzbau/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next' +import './globals.css' +import { Navbar } from '@/components/Navbar' +import { Footer } from '@/components/Footer' + +export const metadata: Metadata = { + title: 'LEVIS Holzbau — Kinder-Holzwerkstatt', + description: 'Lerne Holzfiguren schnitzen und kleine Holzprojekte bauen! Kindgerechte Anleitungen fuer junge Holzwerker.', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + +
{children}
+