diff --git a/.gitignore b/.gitignore index a550dc2..adc9573 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,9 @@ Thumbs.db .env .env.local *.local + +# Claude Code agent state (lokale Definitionen, nicht versioniert) +.claude/ + +# xgit binary (generated) +xgit diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d67f374 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted diff --git a/backend/nitro.config.ts b/backend/nitro.config.ts index b49b574..351f572 100644 --- a/backend/nitro.config.ts +++ b/backend/nitro.config.ts @@ -16,27 +16,65 @@ export default defineNitroConfig({ }, runtimeConfig: { + // ─── Database / Core ───────────────────────────────────────────────── databaseUrl: process.env.DATABASE_URL ?? "", + encryptionKey: process.env.ENCRYPTION_KEY ?? "", + + // ─── Admin / Cron ──────────────────────────────────────────────────── adminSecret: process.env.ADMIN_SECRET ?? "", + cronSecret: process.env.CRON_SECRET ?? "", + + // ─── LLM-Provider ──────────────────────────────────────────────────── openrouterApiKey: process.env.OPENROUTER_API_KEY ?? "", - deepgramApiKey: process.env.DEEPGRAM_API_KEY ?? "", - googleApiKey: process.env.GOOGLE_API_KEY ?? "", + openaiApiKey: process.env.OPENAI_API_KEY ?? "", + groqApiKey: process.env.GROQ_API_KEY ?? "", googleAiApiKey: process.env.GOOGLE_AI_API_KEY ?? "", + + // ─── TTS-Provider ──────────────────────────────────────────────────── + googleApiKey: process.env.GOOGLE_API_KEY ?? "", + deepgramApiKey: process.env.DEEPGRAM_API_KEY ?? "", azureTtsKey: process.env.AZURE_TTS_KEY ?? "", azureTtsRegion: process.env.AZURE_TTS_REGION ?? "", - openaiApiKey: process.env.OPENAI_API_KEY ?? "", + // NEU im backend/-Layout (existieren in nuxt.config.ts NICHT, aber backend code liest sie) + cartesiaApiKey: process.env.CARTESIA_API_KEY ?? "", + cartesiaVoiceId: process.env.CARTESIA_VOICE_ID ?? "", + elevenlabsApiKey: process.env.ELEVENLABS_API_KEY ?? "", + elevenlabsVoiceId: process.env.ELEVENLABS_VOICE_ID ?? "", + + // ─── Supabase (Server-only) ────────────────────────────────────────── + // Im alten Nuxt-Layout via @nuxtjs/supabase auto-injected. In standalone Nitro + // explizit deklarieren, damit auth/middleware nicht 500't. + // server/utils/auth.ts:32 liest `config.public.supabase ?? config.supabase`, + // also beide Pfade müssen existieren. + supabaseUrl: process.env.SUPABASE_URL ?? "https://db-staging.rebreak.org", + supabaseAnonKey: process.env.SUPABASE_ANON_KEY ?? "", + supabaseServiceKey: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "", + + // ─── Stripe ────────────────────────────────────────────────────────── stripeSecretKey: process.env.STRIPE_SECRET_KEY ?? "", stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? "", + + // ─── Email / External APIs ─────────────────────────────────────────── resendApiKey: process.env.RESEND_API_KEY ?? "", - encryptionKey: process.env.ENCRYPTION_KEY ?? "", + + // ─── Bot-User-IDs (DB-User-References für Lyra/Rebreak-Bot-Posts) ──── lyraBotUserId: process.env.LYRA_BOT_USER_ID ?? "", rebreakBotUserId: process.env.REBREAK_BOT_USER_ID ?? "", - groqApiKey: process.env.GROQ_API_KEY ?? "", - cronSecret: process.env.CRON_SECRET ?? "", + + // ─── Public (client-readable) ──────────────────────────────────────── public: { stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY ?? "", appUrl: process.env.APP_URL ?? "https://staging.rebreak.org", apiBase: process.env.API_BASE ?? "https://staging.rebreak.org", + // server/utils/auth.ts liest config.public.supabase.{url,key} + // — wenn das fehlt, 500-cascade auf allen authentifizierten Routes + // (Incident 2026-05-06). + supabase: { + url: + process.env.SUPABASE_URL ?? + "https://db-staging.rebreak.org", + key: process.env.SUPABASE_ANON_KEY ?? "", + }, }, }, }); diff --git a/backend/start-staging.sh b/backend/start-staging.sh old mode 100644 new mode 100755 index 84e51d5..a8f62bc --- a/backend/start-staging.sh +++ b/backend/start-staging.sh @@ -1,10 +1,27 @@ #!/bin/bash # rebreak-backend Staging — startet Nitro mit Infisical-Secrets. -# Pattern analog trucko-backend/start-prod.sh, aber env=staging. +# +# Pattern: infisical login (universal-auth) → infisical run (--env=staging) +# spritzt secrets als process.env.X in den node-Prozess. +# Nitro's runtimeConfig (siehe nitro.config.ts) liest sie direkt — kein +# NUXT_*-Prefix-Mapping mehr nötig (jeder Key in nitro.config.ts hat +# `process.env.X ?? ""` als Default). +# +# Pfad-Konvention (Backyard-Layout, post-cutover): +# - Repo-Root: /srv/rebreak +# - Backend-Dir: /srv/rebreak/backend +# - Build-Output (deployt von scripts/deploy.sh): backend/.output-staging/server/index.mjs +# +# IMAP-Services (rebreak-imap-staging, rebreak-idle-staging) sind NICHT mehr +# Teil dieses Scripts — sie werden separat über ecosystem.config.js verwaltet +# (Mo's Scope, fährt unter /srv/rebreak/apps/rebreak/imap-{proxy,idle}/). + +set -euo pipefail + source /etc/environment -if [[ -z "$INFISICAL_CLIENT_ID" || -z "$INFISICAL_CLIENT_SECRET" ]]; then - echo "❌ INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET nicht gesetzt in /etc/environment" >&2 +if [[ -z "${INFISICAL_CLIENT_ID:-}" || -z "${INFISICAL_CLIENT_SECRET:-}" ]]; then + echo "[start-staging] FEHLER: INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET nicht in /etc/environment" >&2 exit 1 fi @@ -15,7 +32,7 @@ INFISICAL_TOKEN=$(infisical login \ --silent --plain 2>/dev/null) if [[ -z "$INFISICAL_TOKEN" ]]; then - echo "❌ Infisical login fehlgeschlagen" >&2 + echo "[start-staging] FEHLER: Infisical login fehlgeschlagen" >&2 exit 1 fi @@ -24,8 +41,16 @@ export NITRO_PORT=3016 export NITRO_HOST=127.0.0.1 export PORT=3016 +NODE_BIN="/root/.nvm/versions/node/v24.11.1/bin/node" +INDEX_MJS="/srv/rebreak/backend/.output-staging/server/index.mjs" + +if [[ ! -f "$INDEX_MJS" ]]; then + echo "[start-staging] FEHLER: $INDEX_MJS nicht gefunden — wurde deploy.sh ausgeführt?" >&2 + exit 1 +fi + exec infisical run \ - --projectId="${INFISICAL_PROJECT_ID}" \ + --projectId="${INFISICAL_PROJECT_ID:-14b11b35-ef59-4b8a-a16b-398f0cc3ad93}" \ --env=staging \ --token="$INFISICAL_TOKEN" \ - -- /root/.nvm/versions/node/v24.11.1/bin/node /srv/rebreak-monorepo/backend/.output/server/index.mjs + -- "$NODE_BIN" "$INDEX_MJS" diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..88dfd47 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,91 @@ +/** + * ecosystem.config.js – PM2 Prozess-Konfiguration für Rebreak + * (backend/-Layout, post-cutover) + * + * Repo-Root: /srv/rebreak + * Backend: /srv/rebreak/backend (standalone Nitro) + * Node: /root/.nvm/versions/node/v24.11.1/bin/node + * + * Aktivierung auf Server: + * pm2 startOrReload /srv/rebreak/ecosystem.config.js + */ + +const NODE_BIN = "/root/.nvm/versions/node/v24.11.1/bin/node"; +const REPO_ROOT = "/srv/rebreak"; +const APP_DIR = `${REPO_ROOT}/backend`; + +module.exports = { + apps: [ + // ─── Rebreak Staging (Nitro standalone) ──────────────────────────────── + { + name: "rebreak-staging", + script: `${APP_DIR}/start-staging.sh`, + interpreter: "bash", + cwd: APP_DIR, + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: "700M", + env: { + NODE_ENV: "production", + PORT: "3016", + NITRO_PORT: "3016", + }, + }, + + // ─── Rebreak Prod (Nitro standalone) ─────────────────────────────────── + // Wird erst aktiviert wenn Phase 3 (DNS-Cutover) abgeschlossen ist. + // start-prod.sh wird analog start-staging.sh aufgesetzt + // (existiert noch nicht im backend/ — nicht-blockierend für Cutover). + // Start: pm2 start ecosystem.config.js --only rebreak + // { + // name: "rebreak", + // script: `${APP_DIR}/start-prod.sh`, + // interpreter: "bash", + // cwd: APP_DIR, + // instances: 1, + // autorestart: true, + // watch: false, + // max_memory_restart: "700M", + // env: { + // NODE_ENV: "production", + // PORT: "3015", + // NITRO_PORT: "3015", + // }, + // }, + + // ─── Webhook-Listener ────────────────────────────────────────────────── + { + name: "rebreak-webhook", + script: `${REPO_ROOT}/scripts/deploy-webhook/server.mjs`, + interpreter: NODE_BIN, + cwd: REPO_ROOT, + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: "128M", + }, + + // ─── DNS-Blocker (auskommentiert bis DNS-Daemons aufgesetzt sind) ────── + // { + // name: "dns-rebreak", + // script: `${APP_DIR}/server/dns/start-prod.sh`, + // interpreter: "bash", + // cwd: `${APP_DIR}/server/dns`, + // instances: 1, + // autorestart: true, + // watch: false, + // max_memory_restart: "512M", + // }, + // { + // name: "dns-rebreak-staging", + // script: `${APP_DIR}/server/dns/start-staging.sh`, + // interpreter: "bash", + // cwd: `${APP_DIR}/server/dns`, + // instances: 1, + // autorestart: true, + // watch: false, + // max_memory_restart: "512M", + // }, + ], +}; diff --git a/ops/CUTOVER_PLAN.md b/ops/CUTOVER_PLAN.md new file mode 100644 index 0000000..c8a7a38 --- /dev/null +++ b/ops/CUTOVER_PLAN.md @@ -0,0 +1,407 @@ +# Rebreak Cutover-Plan: `apps/rebreak/` (Nuxt) → `backend/` (Standalone Nitro) + +**Verantwortlich:** Backyard-Scope (Hetzner-Pipeline + Cutover-Architektur) +**Erstellt:** 2026-05-06 +**Status:** PLAN — nicht ausgeführt. Alle destruktiven Schritte sind ⚠️-markiert und bedürfen explizitem User-Approval. +**Server:** `ssh rebreak-server` (49.13.55.22, rebreak-prod-01, CX23, 4 GB RAM) +**Repo:** `git@github.com:RaynisDev/rebreak.git` + +--- + +## 1. Status quo (Stand 2026-05-06, post-rollback) + +### 1.1 Server `/srv/rebreak/` (live, Backyard's Layout) + +``` +/srv/rebreak/ +├── apps/ +│ ├── rebreak/ ← Nuxt-App (LIVE) +│ │ ├── nuxt.config.ts ← runtimeConfig (vollständig) +│ │ ├── start-staging.sh ← Infisical login + jq-eval + NUXT_*-Mapping +│ │ ├── server/api/... ← API-Routes +│ │ ├── .output-staging/ ← gebauter Output (pm2 fährt das) +│ │ ├── imap-proxy/ ← Mo's Scope +│ │ └── imap-idle/ ← Mo's Scope +│ └── rebreak-native/ ← React-Native-App +├── scripts/ +│ ├── deploy.sh ← cd apps/rebreak + pnpm --filter @trucko/rebreak build +│ └── deploy-webhook/server.mjs ← GitHub HMAC validator → spawnt deploy.sh +├── ecosystem.config.js ← pm2 zeigt auf apps/rebreak/start-staging.sh +└── ops/nginx/ ← Source-tracking only (nicht live) +``` + +- **Git-HEAD:** `6eca0b5` (rolled back, Backyard's working state) +- **Branch:** `main` +- **Remote-main:** `6eca0b5` (force-pushed back to safe state) +- **pm2:** `rebreak-staging` online, `rebreak-imap-staging` online, `rebreak-idle-staging` online, `rebreak-webhook` online +- **Smoke-Tests:** `https://staging.rebreak.org/` → 200, `/api/auth/me` → 401 (correct). + +### 1.2 Mac `~/mono/rebreak-monorepo/` (Cutover-Target) + +``` +~/mono/rebreak-monorepo/ +├── backend/ ← Standalone Nitro (NEU) +│ ├── nitro.config.ts ← runtimeConfig UNVOLLSTÄNDIG (siehe §3) +│ ├── start-staging.sh ← infisical run wrapper, zeigt auf /srv/rebreak-monorepo/backend/.output/server/index.mjs +│ ├── package.json ← name: rebreak-backend +│ ├── prisma/ ← Prisma 7 +│ ├── server/api/... +│ └── tsconfig.json +├── apps/rebreak-native/ +├── ops/nginx/ ← gleiche Source-Tracking-Configs wie auf Server +├── pnpm-workspace.yaml ← packages: apps/* + backend +├── package.json ← name: rebreak-monorepo +└── xgit +``` + +- **Git-HEAD:** `5dfbe88` (mit `cutover-llm-toggle-prep`-Arbeit, llmProvider-Toggle-Code) +- **Branch:** `main` (Mac), preserved as `cutover-llm-toggle-prep` auf Remote +- **Kein `.npmrc`** — siehe §3 Risiko Prisma 7 transitive deps + +### 1.3 Was kaputt war (Incident 2026-05-06) + +User force-pushte aus dem neuen Mac-Repo (`backend/`-Layout) zu `RaynisDev/rebreak.git` → +Server-Webhook triggerte `deploy.sh` → `cd /srv/rebreak/apps/rebreak` schlug fehl +(Pfad existiert nicht im neuen Layout) bzw. anderer build-pfad lief → schließlich +crashte Auth-Middleware mit HTTP 500: `Cannot read properties of undefined (reading 'url')`, +weil `backend/nitro.config.ts.runtimeConfig` keine `supabase`-Section hat und die +`@nuxtjs/supabase`-Module-Config (die das im alten Setup auto-injected) im Standalone-Nitro +nicht mehr existiert. Cascade: alle authentifizierten Endpoints kaputt. + +**Rollback durchgeführt vor dieser Session:** `RaynisDev/rebreak.git` main = `6eca0b5`, +Server-HEAD = `6eca0b5`, pm2 läuft wieder, HTTP 200 + 401. + +--- + +## 2. Pipeline-Diagramm + +### 2.1 Pipeline AKTUELL (Backyard-Layout, läuft) + +``` + Mac (trucko-monorepo) GitHub Hetzner-Server (49.13.55.22) + ──────────────────── ─────── ───────────────────────────── + ./xgit "msg" ─push──▶ RaynisDev/rebreak.git ─hook─▶ rebreak-webhook (pm2, :9000) + │ HMAC-validate + ▼ + scripts/deploy.sh + │ cd /srv/rebreak + │ git fetch && reset --hard origin/main + │ pnpm install --frozen-lockfile + │ cd apps/rebreak + │ pnpm --filter @trucko/rebreak build + │ cp -r .output .output-staging-new + │ rm -rf .output-staging + │ mv .output-staging-new .output-staging + ▼ + pm2 restart rebreak-staging + │ → bash apps/rebreak/start-staging.sh + │ → infisical login (universal-auth) + │ → infisical secrets --env=staging --output=json + │ → eval/export VAR=val + NUXT_*-Mapping + │ → pm2 start imap-staging + idle-staging + │ → exec node apps/rebreak/.output-staging/server/index.mjs + ▼ + nginx: staging.rebreak.org → 127.0.0.1:3016 +``` + +### 2.2 Pipeline ZIEL (nach Cutover, neues Layout) + +``` + Mac (rebreak-monorepo) GitHub Hetzner-Server + ──────────────────── ─────── ────────────── + ./xgit "msg" ─push──▶ RaynisDev/rebreak.git ─hook─▶ rebreak-webhook + ▼ + scripts/deploy.sh ← MUSS GEÄNDERT WERDEN + │ cd /srv/rebreak + │ git fetch && reset --hard origin/main + │ pnpm install --frozen-lockfile + │ cd backend ← NEU + │ pnpm --filter rebreak-backend build ← NEU + │ cp -r .output .output-staging-new ← NEU (relativ zu backend/) + │ rm -rf .output-staging + │ mv .output-staging-new .output-staging + ▼ + pm2 restart rebreak-staging + │ → bash backend/start-staging.sh ← NEU + │ → infisical login + run --env=staging + │ → exec node backend/.output-staging/server/index.mjs + ▼ + nginx: staging.rebreak.org → 127.0.0.1:3016 +``` + +**Wichtigste Pfad-Änderungen:** + +| Bereich | Vorher (live) | Nachher (Cutover) | +|---|---|---| +| App-Dir | `/srv/rebreak/apps/rebreak/` | `/srv/rebreak/backend/` | +| Build-Filter | `pnpm --filter @trucko/rebreak build` | `pnpm --filter rebreak-backend build` | +| Build-Output | `apps/rebreak/.output-staging/server/index.mjs` | `backend/.output-staging/server/index.mjs` | +| Start-Script | `apps/rebreak/start-staging.sh` | `backend/start-staging.sh` | +| ecosystem.config.js | zeigt auf apps/rebreak | zeigt auf backend | +| start-staging.sh-Pattern | jq+eval+NUXT_*-Mapping | `infisical run -- node …/.output/server/index.mjs` (kein Mapping nötig — Nitro liest `process.env.X` direkt via runtimeConfig-Defaults) | + +**Achtung:** `backend/start-staging.sh` aktuell zeigt auf `/srv/rebreak-monorepo/backend/...` — falscher Pfad, +muss `/srv/rebreak/backend/...` sein (siehe §4 Schritt 6). + +--- + +## 3. Fehlende runtimeConfig-Keys + +Quelle 1: `/srv/rebreak/apps/rebreak/nuxt.config.ts` (live, vollständig) +Quelle 2: grep `config.X` in `~/mono/rebreak-monorepo/backend/server/` +Ziel: `backend/nitro.config.ts.runtimeConfig` + +### 3.1 Keys die im AKTUELLEN `backend/nitro.config.ts` schon existieren (16 total) + +✅ databaseUrl, adminSecret, openrouterApiKey, deepgramApiKey, googleApiKey, googleAiApiKey, +azureTtsKey, azureTtsRegion, openaiApiKey, stripeSecretKey, stripeWebhookSecret, resendApiKey, +encryptionKey, lyraBotUserId, rebreakBotUserId, groqApiKey, cronSecret + +public.{stripePublishableKey, appUrl, apiBase} + +### 3.2 Keys die FEHLEN (Cutover-Blocker) + +| Key | Quelle (Mac backend code) | Wofür | Default | +|---|---|---|---| +| `cartesiaApiKey` | `server/api/coach/speak-cartesia.post.ts:22` | Cartesia TTS API | `process.env.CARTESIA_API_KEY ?? ""` | +| `cartesiaVoiceId` | `server/api/coach/speak-cartesia.post.ts:24` | Cartesia voice override | `process.env.CARTESIA_VOICE_ID ?? ""` | +| `elevenlabsApiKey` | `server/api/coach/speak-elevenlabs.post.ts:26,34` | ElevenLabs TTS API | `process.env.ELEVENLABS_API_KEY ?? ""` | +| `elevenlabsVoiceId` | `server/api/coach/speak-elevenlabs.post.ts:28` | ElevenLabs voice ID | `process.env.ELEVENLABS_VOICE_ID ?? ""` | + +> Diese 4 Keys sind in der live `nuxt.config.ts` **ebenfalls nicht deklariert** — +> die TTS-Routes für Cartesia/ElevenLabs sind **NEU** in der `backend/`-Welt +> (existieren als untracked files: `backend/server/api/coach/speak-cartesia.post.ts` +> + `speak-elevenlabs.post.ts`). Cutover-Risiko: gering (alte Routes existierten gar nicht), +> aber für Vollständigkeit muss `nitro.config.ts` diese Keys deklarieren, sonst +> `config.cartesiaApiKey === undefined` zur Laufzeit. + +### 3.3 Supabase-Keys (Hauptbruchstelle des Incidents) + +⚠️ **Im aktuellen `backend/nitro.config.ts` fehlen `supabase.url` / `supabase.key` / `supabase.serviceKey` komplett.** + +Im alten Nuxt-Layout setzt `@nuxtjs/supabase` automatisch +`config.public.supabase = { url, key }` aus dem `supabase: { ... }`-Block. +Im standalone-Nitro gibt es kein Modul → muss explizit deklariert werden. + +Backend-Code-Aufrufer (siehe `server/utils/auth.ts:30` etc.) lesen Supabase +typischerweise via `@supabase/supabase-js`-Client, der direkt `process.env.SUPABASE_URL` / +`SUPABASE_SERVICE_ROLE_KEY` liest. Trotzdem: für Konsistenz mit Frontend/native +und falls irgendein File `config.public.supabase.url` liest, **müssen wir +`public.supabase.{url, key}` und top-level `supabaseServiceKey` ergänzen**. + +→ siehe `backend/nitro.config.ts.cutover-draft` für vollständige Liste. + +### 3.4 Verifikation des Cutover-Drafts + +Nach Cutover: +``` +ssh rebreak-server "tr '\\0' '\\n' < /proc/$(ss -tlnp | grep :3016 | grep -oE 'pid=[0-9]+' | cut -d= -f2)/environ | grep -E 'API_KEY|SUPABASE|JWT|SECRET' | sort" +``` +Liste muss alle erwarteten Werte enthalten. Wenn `process.env.X` leer ist, ist +Infisical-Wrapper kaputt — siehe `feedback_infisical_secrets.md` Verify-Trick. + +--- + +## 4. Cutover-Sequence (sequentiell abarbeiten, jeden Step bestätigen lassen) + +> Annahme: Mac-Repo ist auf einem Pre-Cutover-Branch (z.B. `cutover-llm-toggle-prep`), +> `main` lokal = `5dfbe88`. Server `main` = `6eca0b5`. Webhook ist online. + +### Phase A — Vorbereitung (nicht-destruktiv, lokal auf Mac) + +**Step 1.** Aktiviere `backend/nitro.config.ts.cutover-draft` → +nach Review der Datei: `mv backend/nitro.config.ts.cutover-draft backend/nitro.config.ts`. + +**Step 2.** Aktiviere `.npmrc.cutover-draft` → +`mv .npmrc.cutover-draft .npmrc` (Repo-root-level, sorgt für `node-linker=hoisted`). + +**Step 3.** Korrigiere Pfad in `backend/start-staging.sh` Zeile 31: +- VORHER: `/srv/rebreak-monorepo/backend/.output/server/index.mjs` +- NACHHER: `/srv/rebreak/backend/.output-staging/server/index.mjs` (analog zur prod-pattern: gebauten output nach `.output-staging` movt deploy.sh). + +**Step 4.** Lokaler Build-Test: `cd backend && pnpm install && pnpm build`. Muss `.output/server/index.mjs` produzieren. + +**Step 5.** Smoke-Test lokal: `node backend/.output/server/index.mjs` mit allen ENV-Vars manuell gesetzt → Health-Check (Port 3016). + +### Phase B — Server-Vorbereitung (read-only, kein Eingriff in laufendes System) + +**Step 6.** SSH-Vergleich: `ssh rebreak-server "ls /srv/rebreak/"`. Verifiziere dass `backend/`-Pfad NICHT existiert (sonst stale Files vom letzten Force-Push-Versuch). + +**Step 7.** ⚠️ **Falls stale `backend/` existiert:** `ssh rebreak-server "ls -la /srv/rebreak/backend/ 2>&1"` Output an User eskalieren. **NICHT selbst löschen.** + +**Step 8.** Infisical-Secrets Audit: User soll bestätigen dass alle in §3.2 + §3.3 genannten +Keys (CARTESIA_API_KEY, ELEVENLABS_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY etc.) +im Infisical-staging-Env existieren. **NICHT selbst Infisical-CLI ausführen** (Token-Scope-Risiko). + +### Phase C — Pipeline-Anpassung (auf Mac, dann xgit) + +**Step 9.** Edit `scripts/deploy.sh` (Mac-Repo erstellen, Server hat noch alte Version): +- `APP_DIR="${REPO_ROOT}/apps/rebreak"` → `APP_DIR="${REPO_ROOT}/backend"` +- `pnpm --filter @trucko/rebreak build` → `pnpm --filter rebreak-backend build` + +**Step 10.** Edit `ecosystem.config.js` (falls im neuen Repo gemirrort wird — derzeit ist es nur auf Server): +- `script: \`${APP_DIR}/start-staging.sh\`` (APP_DIR jetzt = backend) +- `cwd: APP_DIR` analog +- Webhook-Eintrag `script: \`${REPO_ROOT}/scripts/deploy-webhook/server.mjs\`` bleibt (existiert weiter unter scripts/). + +> **Klärung an User:** Liegt `scripts/deploy.sh` und `ecosystem.config.js` im neuen Mac-Repo? Falls nein, müssen sie aus `/srv/rebreak/` rüberkopiert + angepasst werden. Wenn der Webhook auf dem Server diese Files NICHT aus dem Repo zieht (sondern lokal vorhält), dann werden Step 9+10 zu manuellen Server-Edits — siehe Step 14 ⚠️. + +### Phase D — Deploy (DESTRUKTIV) + +**Step 11. ⚠️** Sichere Backyard-State auf Server: `ssh rebreak-server "cd /srv/rebreak && git tag -a backyard-pre-cutover -m 'Pre-cutover safe-state 6eca0b5' 6eca0b5"` — User-OK einholen (read-only-tag, aber zur Sicherheit). + +**Step 12. ⚠️** Force-push neuen Layout-State: aus `~/mono/rebreak-monorepo` mit `./xgit "cutover: backend/-Layout aktiviert"`. +- Webhook-Triggert deploy.sh AUTOMATISCH. +- **Risiko:** alte deploy.sh ist auf `apps/rebreak/`-Pfad fixiert, wird also `cd: apps/rebreak: No such file` werfen → build crash → kein restart, alter `.output-staging` läuft weiter. +- **Mitigation:** Der CRASH ist gewollt — er verhindert dass kaputter Code gestartet wird. Server bleibt auf laufenden `rebreak-staging` (alter index.mjs) live. + +**Step 13. ⚠️** Manueller Server-Edit für `scripts/deploy.sh` UND `ecosystem.config.js`: +- **NICHT von dieser Agent-Session ausgeführt — User macht das selbst.** +- Alternativen je nach User-Wahl: (a) ssh + sed-Edit der Server-Files (b) deploy.sh wird Teil des Repos und der webhook-deploy-flow zieht es aus dem Repo (cleaner, aber selbst-referentielles Bootstrap-Problem: alter deploy.sh muss erst die neue deploy.sh ans richtige `scripts/`-Pfad kopieren). +- **Empfehlung:** (a) für ersten Cutover, danach (b) als follow-up commit. + +**Step 14. ⚠️** Trigger Re-Deploy nach Server-deploy.sh-Edit: `ssh rebreak-server "bash /srv/rebreak/scripts/deploy.sh"` ODER neuer xgit-trivial-commit (z.B. README touch). User-Wahl. + +**Step 15. ⚠️** Manueller pm2-restart falls deploy.sh letzten restart noch nicht getriggert hat: +`ssh rebreak-server "pm2 restart rebreak-staging --update-env"`. Das `--update-env` ist wichtig damit +neue Infisical-secrets gezogen werden. + +### Phase E — Verifikation (read-only) + +**Step 16.** `ssh rebreak-server "pm2 logs rebreak-staging --nostream --lines 50"` — keine 500er, kein crash-loop. + +**Step 17.** Endpoint-Tests (siehe §6 Test-Checklist). + +**Step 18.** Wenn 30 Minuten stable: cleanup `ssh rebreak-server "rm -rf /srv/rebreak/apps/rebreak/.output-staging /srv/rebreak/apps/rebreak/.output"`. **An User eskalieren — `rm -rf` ist destruktiv.** + +--- + +## 5. Rollback-Plan (Cutover scheitert, <5 min) + +### 5.1 Schneller Rollback (Webhook re-triggert alter HEAD) + +``` +# Aus Mac-Repo: +git checkout main +git reset --hard 6eca0b5 # Backyard's safe state +git push --force origin main # ⚠️ destructiv, User-OK + +# Dann auf Server: +ssh rebreak-server "cd /srv/rebreak && git reset --hard 6eca0b5" # ⚠️ +ssh rebreak-server "bash /srv/rebreak/scripts/deploy.sh" # alter deploy.sh, alter Pfad +ssh rebreak-server "pm2 restart rebreak-staging --update-env" +``` + +### 5.2 Falls Server `scripts/deploy.sh` schon im Cutover modifiziert wurde + +User muss manuell die alte Version aus `git show 6eca0b5:scripts/deploy.sh > /srv/rebreak/scripts/deploy.sh` schreiben (read-only-Mac, dann scp), bevor Re-Deploy klappt. + +### 5.3 Falls `.output-staging` bereits gelöscht wurde + +``` +ssh rebreak-server "cd /srv/rebreak/apps/rebreak && pnpm --filter @trucko/rebreak build && cp -r .output .output-staging" +``` +Vorher User-OK einholen (Build dauert 90-180s, zwischenzeitlich keine Auth möglich). + +### 5.4 Smoke-Test nach Rollback + +``` +curl -s -o /dev/null -w '%{http_code}\n' https://staging.rebreak.org/ +curl -s -o /dev/null -w '%{http_code}\n' https://staging.rebreak.org/api/auth/me +# Erwartung: 200 + 401 +``` + +--- + +## 6. Test-Checklist nach Cutover + +**Pflicht (alle müssen pass):** + +- [ ] `GET https://staging.rebreak.org/` → 200 +- [ ] `GET https://staging.rebreak.org/api/auth/me` → 401 (unauthenticated, NICHT 500) +- [ ] `GET https://staging.rebreak.org/api/auth/me` mit gültigem JWT → 200 + user-data +- [ ] `POST https://staging.rebreak.org/api/coach/sos-session` mit JWT → 200 + sessionId +- [ ] `GET https://staging.rebreak.org/api/coach/sos-stream?session=` → SSE stream openend +- [ ] `POST /api/coach/speak-google` mit JWT → audio/mpeg blob +- [ ] `POST /api/coach/speak-deepgram` mit JWT → audio blob +- [ ] `POST /api/coach/speak-gemini` mit JWT → audio blob +- [ ] `POST /api/coach/speak-cartesia` mit JWT → audio blob (NEU) +- [ ] `POST /api/coach/speak-elevenlabs` mit JWT → audio blob (NEU) +- [ ] `POST /api/coach/transcribe` mit audio + JWT → text +- [ ] `GET /api/community/posts` mit JWT → posts-array +- [ ] `GET /api/dns/profile` mit JWT → profile-config (achtet auf `config.public.appUrl.includes("staging")`) +- [ ] `GET /api/lyra/welcome-back` mit JWT → message +- [ ] `POST /api/stripe/checkout` mit JWT → session-url + +**Best-effort (admin/cron):** + +- [ ] `GET /api/admin/stats?secret=` → 200 +- [ ] `POST /api/cron/lyra-post?secret=` → 200/204 + +**Process-Health:** + +- [ ] `pm2 logs rebreak-staging --nostream --lines 100` — keine 500er, keine crash-restarts +- [ ] `pm2 jlist | jq '.[] | select(.name=="rebreak-staging") | .pm2_env.restart_time'` → 0 increments über 5 min +- [ ] `tr '\0' '\n' < /proc//environ | grep -E 'API_KEY|SUPABASE'` → alle keys present + +**iOS/Android-App-Test (User-Side):** + +- [ ] App-Login → User-Profile lädt +- [ ] SOS-Session öffnen → Lyra streamt + spricht (TtsProvider beide testen) +- [ ] Community-Tab → posts laden +- [ ] DNS-Profile-Sync läuft + +--- + +## 7. Risiken + Open Questions + +### Risiken + +- **R1 (high):** `scripts/deploy.sh` und `ecosystem.config.js` sind auf dem Server, NICHT im Repo. Cutover-Bootstrap ist selbstreferentiell — entweder manueller Server-Edit (Step 13) ODER Repo-Migration der Files. Diese Sequenz hat ein narrow window in dem ein altes deploy.sh gegen neue Repo-Struktur baut → fail. +- **R2 (medium):** `node-linker=hoisted` in `.npmrc` muss BEFORE first `pnpm install` aktiv sein, sonst landet `@prisma/client-runtime-utils` in einem nested `node_modules/.pnpm/...` und ESM-Loader findet's nicht. Step 2 muss vor Step 4 passieren. Strikte Order. +- **R3 (medium):** `start-staging.sh` im neuen Backend nutzt `infisical run -- node ...` — beim alten gab es `eval` von secrets-JSON. Falls irgendeine ENV-Var-Mapping-Magie (NUXT_*-Prefix) gebraucht wird, muss sie nachgezogen werden. Audit nötig: existiert ein Code-Pfad der `process.env.NUXT_X` direkt liest und nicht via `useRuntimeConfig`? +- **R4 (low):** PWA-Manifest + i18n-locales aus `nuxt.config.ts` haben kein Äquivalent in standalone Nitro. Falls App-Frontend (rebreak-native) auf SSR-i18n-Endpoints angewiesen ist → kaputt. Native-App-Test im Detail. +- **R5 (low):** `nitro.config.externals.inline: [/^(?!@supabase\/supabase-js)/]` ist eine Anti-pattern-Regex (matched fast alles). Möglicherweise produziert sie unerwartet riesige `.output`-bundles. Code-Review im Cutover-PR sinnvoll. + +### Open Questions an User + +1. **Sollen `scripts/deploy.sh` + `ecosystem.config.js` Teil des Cutover-Commits werden** (im Repo unter `scripts/` und `ecosystem.config.js` Root)? Das vermeidet manuelle Server-Edits und ist Backyard-konform. Bootstrap-Frage: einmaliger manueller `git pull && cp scripts/deploy.sh /srv/rebreak/scripts/` first-time, danach automatisch. +2. **`backend/start-staging.sh` Pfad:** soll auf `/srv/rebreak/backend/.output-staging/server/index.mjs` zeigen (analog Backyard's apps/rebreak-Pattern, mit deploy.sh-`.output → .output-staging` move) ODER direkt `/srv/rebreak/backend/.output/server/index.mjs` (ohne staging-suffix-Move)? Erste Variante = Konsistenz mit Backyard. Zweite = simpler Cutover. +3. **Webhook-Build-Filter:** soll der Filter `pnpm --filter rebreak-backend build` den Frontend-Native-App-Build IGNORIEREN (bisher: nur Backend baut)? Die rebreak-native-App buildet via Capacitor/Metro, nicht via pnpm — also ist `--filter rebreak-backend` korrekt restriktiv. Bestätigen. +4. **Gibt es eine Test-Staging-Umgebung** wo wir den Cutover proben können bevor wir die echte staging.rebreak.org nehmen? Andernfalls ist DiGA-User-Test-Window (TestFlight) während Cutover potentiell down. +5. **Cartesia/ElevenLabs-Secrets in Infisical:** existieren `CARTESIA_API_KEY` und `ELEVENLABS_API_KEY` schon im staging-Env? Falls nein, sind die neuen TTS-Routes nach Cutover broken (404-degradation, nicht 500 — `if (!key) return error`). + +--- + +## 8. Operations-Cheatsheet (post-Cutover) + +``` +# Logs live +ssh rebreak-server "pm2 logs rebreak-staging --lines 100" + +# Manueller Re-Deploy (nur bei broken webhook) +ssh rebreak-server "bash /srv/rebreak/scripts/deploy.sh" + +# pm2-status +ssh rebreak-server "pm2 list" + +# Verify .output-staging is fresh +ssh rebreak-server "stat -c '%y' /srv/rebreak/backend/.output-staging/server/index.mjs" + +# Verify config-key present in built bundle +ssh rebreak-server "grep -l 'cartesiaApiKey' /srv/rebreak/backend/.output-staging/server/chunks/*.mjs" + +# Check ENV-Vars in running node-process +ssh rebreak-server "tr '\\0' '\\n' < /proc/\$(ss -tlnp | grep :3016 | grep -oE 'pid=[0-9]+' | cut -d= -f2)/environ | grep -E 'CARTESIA|ELEVENLABS|SUPABASE' | sort" + +# nginx reload (NUR wenn nginx-conf changed; wir machen das nicht in diesem Cutover) +ssh rebreak-server "nginx -t && systemctl reload nginx" +``` + +**KEIN-GO-Liste (gilt während Cutover und danach):** + +- ❌ `pm2 delete rebreak-staging` (nur restart) +- ❌ `git reset --hard` direkt auf Server (deploy.sh macht das offiziell) +- ❌ `rm -rf` auf `/srv/rebreak/...` ohne explizites User-OK +- ❌ DNS-Änderungen +- ❌ Infisical-Token-Rotation +- ❌ `pnpm install` parallel zum Webhook-Deploy (OOM auf 4 GB) diff --git a/scripts/deploy-webhook/server.mjs b/scripts/deploy-webhook/server.mjs new file mode 100644 index 0000000..9096def --- /dev/null +++ b/scripts/deploy-webhook/server.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env node +/** + * Rebreak GitHub Webhook Listener + * + * Empfängt GitHub push-Events, validiert HMAC-SHA256-Signatur, + * und triggert das deploy.sh Script im Hintergrund. + * + * Port: 9000 (intern, wird von nginx reverse-proxied) + * Secret: GITHUB_WEBHOOK_SECRET in /etc/environment (via Infisical) + */ + +import http from "http"; +import crypto from "crypto"; +import { spawn } from "child_process"; +import { readFileSync } from "fs"; + +const REPO_ROOT = "/srv/rebreak"; +const DEPLOY_SCRIPT = `${REPO_ROOT}/scripts/deploy.sh`; +const PORT = 9000; + +// Lade GITHUB_WEBHOOK_SECRET aus /etc/environment +function loadEnvFile() { + try { + const content = readFileSync("/etc/environment", "utf-8"); + for (const line of content.split("\n")) { + const match = line.match(/^([^=]+)="?([^"]*)"?$/); + if (match) process.env[match[1]] = match[2]; + } + } catch { + // /etc/environment nicht lesbar – Env-Vars müssen anderweitig gesetzt sein + } +} +loadEnvFile(); + +const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET; +if (!WEBHOOK_SECRET) { + console.error( + "[Webhook] FATAL: GITHUB_WEBHOOK_SECRET nicht in /etc/environment gesetzt", + ); + process.exit(1); +} + +function verifySignature(secret, signature, payload) { + const hmac = crypto.createHmac("sha256", secret); + hmac.update(payload, "utf-8"); + const digest = `sha256=${hmac.digest("hex")}`; + try { + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(digest), + ); + } catch { + return false; + } +} + +// Deploy-Queue: verhindert parallele Builds (OOM-Schutz auf 4 GB RAM) +let deployRunning = false; +let pendingDeploy = false; + +function runDeploy() { + if (deployRunning) { + pendingDeploy = true; + console.log("[Webhook] Deploy already running – queued next deploy"); + return; + } + + deployRunning = true; + console.log("[Webhook] Starting deploy.sh..."); + + const proc = spawn("bash", [DEPLOY_SCRIPT], { + cwd: REPO_ROOT, + stdio: ["ignore", "pipe", "pipe"], + detached: false, + }); + + proc.stdout.on("data", (d) => process.stdout.write(`[deploy] ${d}`)); + proc.stderr.on("data", (d) => process.stderr.write(`[deploy:err] ${d}`)); + + proc.on("close", (code) => { + deployRunning = false; + console.log(`[Webhook] deploy.sh exited with code ${code}`); + if (pendingDeploy) { + pendingDeploy = false; + console.log("[Webhook] Running queued deploy..."); + runDeploy(); + } + }); + + proc.on("error", (err) => { + deployRunning = false; + console.error("[Webhook] deploy.sh spawn error:", err.message); + if (pendingDeploy) { + pendingDeploy = false; + runDeploy(); + } + }); +} + +const server = http.createServer((req, res) => { + if (req.method !== "POST" || req.url !== "/webhook") { + res.writeHead(404); + res.end("Not found"); + return; + } + + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + const sig = req.headers["x-hub-signature-256"]; + if (!sig) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Missing signature" })); + return; + } + + if (!verifySignature(WEBHOOK_SECRET, sig, body)) { + res.writeHead(401); + res.end(JSON.stringify({ error: "Invalid signature" })); + return; + } + + let payload; + try { + payload = JSON.parse(body); + } catch { + res.writeHead(400); + res.end(JSON.stringify({ error: "Invalid JSON" })); + return; + } + + const branch = payload.ref?.split("/").pop(); + if (branch !== "main") { + console.log(`[Webhook] Ignoring non-main branch: ${branch}`); + res.writeHead(200); + res.end(JSON.stringify({ ok: false, reason: "Not main branch", branch })); + return; + } + + const changedFiles = (payload.commits || []).flatMap((c) => [ + ...(c.added || []), + ...(c.modified || []), + ...(c.removed || []), + ]); + + console.log( + `[Webhook] Push to main by ${payload.pusher?.name} – ${changedFiles.length} files changed`, + ); + console.log("[Webhook] Changed files:", changedFiles.slice(0, 10)); + + res.writeHead(202); + res.end( + JSON.stringify({ + ok: true, + message: deployRunning + ? "Deploy queued (previous still running)" + : "Deploy started", + files: changedFiles.length, + }), + ); + + // Asynchron deployen – Response ist bereits gesendet + runDeploy(); + }); +}); + +server.listen(PORT, "127.0.0.1", () => { + console.log(`[Webhook] Listening on http://127.0.0.1:${PORT}/webhook`); + console.log(`[Webhook] Secret loaded: ${WEBHOOK_SECRET ? "yes" : "NO – FATAL"}`); +}); + +process.on("SIGTERM", () => { + console.log("[Webhook] SIGTERM received, shutting down"); + server.close(() => process.exit(0)); +}); diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..73956a8 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# deploy.sh – Rebreak Deploy Script (backend/-Layout, post-cutover) +# +# Wird vom Webhook-Listener (scripts/deploy-webhook/server.mjs) aufgerufen. +# Repo-Root: /srv/rebreak +# Clone: git@github.com:RaynisDev/rebreak.git +# Backend: /srv/rebreak/backend (standalone Nitro, package: rebreak-backend) +# +# Ablauf: +# 1. Git pull (via Deploy-Key) +# 2. pnpm install --frozen-lockfile (mit hoisted node-linker via .npmrc) +# 3. cd backend && pnpm --filter rebreak-backend build (Prisma generate + Nitro build) +# 4. .output → .output-staging (atomisch via tmp) +# 5. pm2 restart rebreak-staging --update-env +# 6. pm2 restart rebreak-imap-staging / rebreak-idle-staging (best-effort, falls vorhanden) +# 7. pm2 restart dns-rebreak-staging / dns-rebreak (best-effort, falls vorhanden) +# +# Secrets: via Infisical (INFISICAL_CLIENT_ID/SECRET in /etc/environment) — NICHT hier, +# sondern in start-staging.sh / ecosystem.config.js zur Laufzeit. + +set -euo pipefail + +REPO_ROOT="/srv/rebreak" +APP_DIR="${REPO_ROOT}/backend" +NODE_BIN="/root/.nvm/versions/node/v24.11.1/bin/node" +PNPM_BIN="/root/.nvm/versions/node/v24.11.1/bin/pnpm" +PM2_BIN="/root/.nvm/versions/node/v24.11.1/bin/pm2" + +log() { echo "[deploy] $(date '+%H:%M:%S') $*"; } +log_err() { echo "[deploy:err] $(date '+%H:%M:%S') $*" >&2; } + +log "=== Rebreak Deploy gestartet (backend/-Layout) ===" + +# 0. Sicherstellen dass PATH stimmt +export PATH="/root/.nvm/versions/node/v24.11.1/bin:$PATH" + +# 1. Git pull via Deploy-Key (SSH ist konfiguriert in /root/.ssh/config) +log "Step 1: git pull..." +cd "${REPO_ROOT}" +git fetch origin main 2>&1 +git reset --hard origin/main 2>&1 +git clean -fd 2>&1 +log "Git updated to $(git rev-parse --short HEAD)" + +# 2. pnpm install (workspace-root, .npmrc mit node-linker=hoisted ist im Repo) +log "Step 2: pnpm install --frozen-lockfile..." +cd "${REPO_ROOT}" +CI=true "${PNPM_BIN}" install --frozen-lockfile 2>&1 || { + log_err "frozen-lockfile fehlgeschlagen, fallback ohne frozen..." + CI=true "${PNPM_BIN}" install --no-frozen-lockfile 2>&1 +} +log "pnpm install done" + +# 3. Build backend (Nitro standalone) — Prisma generate ist Teil des build-scripts +log "Step 3: pnpm --filter rebreak-backend build..." +cd "${APP_DIR}" +# NODE_OPTIONS: max 1.5 GB für den Build-Prozess (4 GB RAM Server) +NODE_OPTIONS="--max-old-space-size=1536" CI=true "${PNPM_BIN}" --filter rebreak-backend build 2>&1 +log "Build done" + +# 4. Atomisches Deploy: .output → .output-staging (relativ zu backend/) +log "Step 4: Atomisches Deploy .output → .output-staging..." +cd "${APP_DIR}" +if [ -d ".output" ]; then + rm -rf .output-staging-new + cp -r .output .output-staging-new + rm -rf .output-staging + mv .output-staging-new .output-staging + log ".output-staging aktualisiert" +else + log_err "FEHLER: .output Verzeichnis nicht gefunden nach Build!" + exit 1 +fi + +# 5. pm2 restart rebreak-staging (--update-env zieht neue Infisical-secrets) +log "Step 5: pm2 restart rebreak-staging..." +"${PM2_BIN}" restart rebreak-staging --update-env 2>/dev/null || \ + "${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only rebreak-staging +log "rebreak-staging restarted" + +# 6. IMAP + DNS Services (optional – kein Fehler wenn nicht vorhanden, Mo's Scope) +log "Step 6: Optional services restart..." +"${PM2_BIN}" restart rebreak-imap-staging 2>/dev/null || true +"${PM2_BIN}" restart rebreak-idle-staging 2>/dev/null || true +"${PM2_BIN}" restart dns-rebreak-staging 2>/dev/null || \ + "${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only dns-rebreak-staging 2>/dev/null || true +"${PM2_BIN}" restart dns-rebreak 2>/dev/null || \ + "${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only dns-rebreak 2>/dev/null || true + +# 7. pm2 save +"${PM2_BIN}" save 2>/dev/null || true + +log "=== Deploy erfolgreich: $(git -C ${REPO_ROOT} rev-parse --short HEAD) ==="