From d3928d61c4cb7eab08b13ad2c514b53bd84974a3 Mon Sep 17 00:00:00 2001 From: Matthias Meister Date: Fri, 5 Jun 2026 21:46:16 +0200 Subject: [PATCH] Add MVP operational readiness checks --- app/dashboard/settings/page.tsx | 10 +-- components/settings/operations-readiness.tsx | 61 +++++++++++++++ docs/coolify-deployment.md | 50 ++++++++++++ docs/verification.md | 46 +++++++++++ lib/operational-readiness.ts | 82 ++++++++++++++++++++ tests/operational-readiness.test.ts | 38 +++++++++ tests/ops-quality-source.test.ts | 61 +++++++++++++++ 7 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 components/settings/operations-readiness.tsx create mode 100644 docs/coolify-deployment.md create mode 100644 docs/verification.md create mode 100644 lib/operational-readiness.ts create mode 100644 tests/operational-readiness.test.ts create mode 100644 tests/ops-quality-source.test.ts diff --git a/app/dashboard/settings/page.tsx b/app/dashboard/settings/page.tsx index 8d0f5bb..3dd9ecb 100644 --- a/app/dashboard/settings/page.tsx +++ b/app/dashboard/settings/page.tsx @@ -1,10 +1,6 @@ -import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page"; +import { OperationsReadiness } from "@/components/settings/operations-readiness"; +import { getIntegrationReadiness } from "@/lib/operational-readiness"; export default function SettingsPage() { - return ( - - ); + return ; } diff --git a/components/settings/operations-readiness.tsx b/components/settings/operations-readiness.tsx new file mode 100644 index 0000000..3c419b4 --- /dev/null +++ b/components/settings/operations-readiness.tsx @@ -0,0 +1,61 @@ +import { AlertTriangle, CheckCircle2 } from "lucide-react"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import type { IntegrationReadinessRow } from "@/lib/operational-readiness"; + +type OperationsReadinessProps = { + rows: IntegrationReadinessRow[]; +}; + +export function OperationsReadiness({ rows }: OperationsReadinessProps) { + return ( +
+
+

MVP-Betrieb

+

+ Einstellungen +

+
+ + + + Integrationsstatus + + Diese Übersicht zeigt nur fehlende Variablennamen und keine Secret-Werte. + + + + {rows.map((row) => { + const isConfigured = row.status === "configured"; + const Icon = isConfigured ? CheckCircle2 : AlertTriangle; + + return ( +
+
+ +
+

{row.label}

+

+ {isConfigured ? "Konfiguration vorhanden" : "Konfiguration fehlt"} +

+ {row.missingEnv.length > 0 ? ( +

+ Fehlend: {row.missingEnv.join(", ")} +

+ ) : null} +

+ {row.errorSurface} +

+
+
+
+ ); + })} +
+
+
+ ); +} diff --git a/docs/coolify-deployment.md b/docs/coolify-deployment.md new file mode 100644 index 0000000..d153966 --- /dev/null +++ b/docs/coolify-deployment.md @@ -0,0 +1,50 @@ +# Coolify Deployment + +## Environment Variables + +Set production values in Coolify and Convex secrets, not in source code. + +- `APP_ENV` +- `NEXT_PUBLIC_APP_URL` +- `NEXT_PUBLIC_CONVEX_URL` +- `NEXT_PUBLIC_CONVEX_SITE_URL` +- `CONVEX_DEPLOYMENT` +- `BETTER_AUTH_SECRET` +- `GOOGLE_GEOCODING_API_KEY` +- `GOOGLE_PLACES_API_KEY` +- `PAGESPEED_API_KEY` +- `PAGESPEED_TIMEOUT_MS` +- `OPENROUTER_API_KEY` +- `OPENROUTER_MODEL_CLASSIFICATION` +- `OPENROUTER_MODEL_MULTIMODAL_AUDIT` +- `OPENROUTER_MODEL_GERMAN_COPY` +- `OPENROUTER_MODEL_QUALITY_REVIEW` +- `OPENROUTER_APP_NAME` +- `OPENROUTER_APP_URL` +- `SMTP_HOST` +- `SMTP_PORT` +- `SMTP_USER` +- `SMTP_PASSWORD` +- `SMTP_FROM` +- `RYBBIT_API_URL` +- `RYBBIT_API_KEY` +- `NEXT_PUBLIC_RYBBIT_SITE_ID` +- `TASK8_BROWSER_ASSET_URL` + +## Build And Runtime + +Coolify commands: + +- Install: `pnpm install --frozen-lockfile` +- Build: `pnpm build` +- Start: `pnpm start` + +Expose Port 3000 from the Next.js container. + +## Playwright + +Website enrichment uses `playwright-core` with a hosted Chromium bundle. Configure `TASK8_BROWSER_ASSET_URL` to a reachable browser asset. If the platform image also installs system browser dependencies, keep them aligned with the Chromium bundle used by `@sparticuz/chromium-min`. + +## Domains + +Set `NEXT_PUBLIC_APP_URL` to the public dashboard Domain. Configure Convex deployment URLs in `NEXT_PUBLIC_CONVEX_URL` and `NEXT_PUBLIC_CONVEX_SITE_URL`. Public audit links assume the same app domain unless a reverse proxy maps `/audit/*` separately. diff --git a/docs/verification.md b/docs/verification.md new file mode 100644 index 0000000..774b6e9 --- /dev/null +++ b/docs/verification.md @@ -0,0 +1,46 @@ +# MVP Verification Notes + +Diese Checkliste ist die wiederholbare manuelle Prüfung für die kritischen MVP-Flows. + +## Login + +1. `/login` öffnen. +2. Mit Admin-Zugang anmelden. +3. Prüfen, dass `/dashboard` erreichbar ist und geschützte Routen ohne Session zurück zu `/login` gehen. + +## Kampagnenlauf + +1. Kampagne mit deutscher PLZ und aktivem Status anlegen. +2. `Jetzt ausführen` starten. +3. In Kampagnen-Run-Logs prüfen, dass der Lauf `pending/running/succeeded` oder ein sichtbarer Fehlerstatus wird. + +## Audit-Generierung + +1. Lead mit Website durch Enrichment/PageSpeed laufen lassen. +2. Prüfen, dass PageSpeed-Erfolg oder -Fehler Audit-Generierung queued. +3. Im Outreach Review Workspace prüfen, dass Audit-Text, Quellen und Skills sichtbar sind. + +## Freigabe + +1. Public-Audit-Text editieren. +2. Änderungen speichern. +3. Audit veröffentlichen und öffentliche Audit-URL öffnen. + +## Versand + +1. E-Mail-Betreff und Text prüfen. +2. E-Mail freigeben. +3. Finale SMTP-Bestätigung kontrollieren und senden. +4. Bei SMTP-Fehler prüfen, dass der Datensatz retrybar bleibt und keine Credentials angezeigt werden. + +## Follow-up + +1. Nach Erstversand prüfen, dass ein Follow-up-Draft mit Due-Date entsteht. +2. Follow-up erst nach manueller Review/Freigabe senden. +3. `Antwort erhalten` oder `Kein Interesse` setzen und prüfen, dass Follow-up-Prompts verschwinden. + +## Analytics + +1. Öffentliche Audit-Seite öffnen und CTA klicken. +2. `/dashboard/analytics` prüfen. +3. Convex-Metriken und Rybbit-Fehlerzustand bzw. Rybbit-Signale kontrollieren. diff --git a/lib/operational-readiness.ts b/lib/operational-readiness.ts new file mode 100644 index 0000000..5d0caf1 --- /dev/null +++ b/lib/operational-readiness.ts @@ -0,0 +1,82 @@ +export type IntegrationReadinessStatus = "configured" | "missing"; + +export type IntegrationReadinessDefinition = { + id: + | "google" + | "pagespeed" + | "openrouter" + | "playwright" + | "smtp" + | "convex_jobs" + | "rybbit"; + label: string; + requiredEnv: string[]; + errorSurface: string; +}; + +export type IntegrationReadinessRow = IntegrationReadinessDefinition & { + status: IntegrationReadinessStatus; + missingEnv: string[]; +}; + +export const integrationReadinessDefinitions: IntegrationReadinessDefinition[] = [ + { + id: "google", + label: "Google", + requiredEnv: ["GOOGLE_GEOCODING_API_KEY", "GOOGLE_PLACES_API_KEY"], + errorSurface: "Run-Events der Lead-Recherche zeigen Google-Fehler.", + }, + { + id: "pagespeed", + label: "PageSpeed", + requiredEnv: ["PAGESPEED_API_KEY", "PAGESPEED_TIMEOUT_MS"], + errorSurface: "PageSpeed-Run-Events und Audit-Quellen zeigen Fehlerdetails.", + }, + { + id: "openrouter", + label: "OpenRouter", + requiredEnv: ["OPENROUTER_API_KEY"], + errorSurface: "Audit-Generierungsruns zeigen Modell- und Guard-Fehler.", + }, + { + id: "playwright", + label: "Playwright", + requiredEnv: ["TASK8_BROWSER_ASSET_URL"], + errorSurface: "Website-Enrichment-Runs zeigen Browser- und Crawl-Fehler.", + }, + { + id: "smtp", + label: "SMTP", + requiredEnv: ["SMTP_HOST", "SMTP_USER", "SMTP_PASSWORD", "SMTP_FROM"], + errorSurface: "Outreach-Sendeversuche zeigen SMTP-Fehler ohne Credentials.", + }, + { + id: "convex_jobs", + label: "Convex Jobs", + requiredEnv: ["NEXT_PUBLIC_CONVEX_URL", "CONVEX_DEPLOYMENT"], + errorSurface: "Kampagnen-Run-Logs und Lifecycle-Runs zeigen Job-Status.", + }, + { + id: "rybbit", + label: "Rybbit", + requiredEnv: ["RYBBIT_API_URL", "RYBBIT_API_KEY", "NEXT_PUBLIC_RYBBIT_SITE_ID"], + errorSurface: "Analytics zeigt API-Fehler als nicht blockierende Meldung.", + }, +]; + +export function getIntegrationReadiness( + env: Record, +): IntegrationReadinessRow[] { + return integrationReadinessDefinitions.map((definition) => { + const missingEnv = definition.requiredEnv.filter((key) => { + const value = env[key]; + return !value || value.trim().length === 0; + }); + + return { + ...definition, + missingEnv, + status: missingEnv.length === 0 ? "configured" : "missing", + }; + }); +} diff --git a/tests/operational-readiness.test.ts b/tests/operational-readiness.test.ts new file mode 100644 index 0000000..a32aeef --- /dev/null +++ b/tests/operational-readiness.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + getIntegrationReadiness, + integrationReadinessDefinitions, +} from "../lib/operational-readiness"; + +test("integration readiness covers all MVP providers", () => { + assert.deepEqual( + integrationReadinessDefinitions.map((definition) => definition.id), + [ + "google", + "pagespeed", + "openrouter", + "playwright", + "smtp", + "convex_jobs", + "rybbit", + ], + ); +}); + +test("integration readiness reports missing configuration without leaking values", () => { + const rows = getIntegrationReadiness({ + GOOGLE_GEOCODING_API_KEY: "secret-google", + GOOGLE_PLACES_API_KEY: "secret-places", + PAGESPEED_API_KEY: "", + }); + + const google = rows.find((row) => row.id === "google"); + const pageSpeed = rows.find((row) => row.id === "pagespeed"); + + assert.equal(google?.status, "configured"); + assert.equal(pageSpeed?.status, "missing"); + assert.equal(JSON.stringify(rows).includes("secret-google"), false); + assert.equal(JSON.stringify(rows).includes("secret-places"), false); +}); diff --git a/tests/ops-quality-source.test.ts b/tests/ops-quality-source.test.ts new file mode 100644 index 0000000..3002682 --- /dev/null +++ b/tests/ops-quality-source.test.ts @@ -0,0 +1,61 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import test from "node:test"; + +function source(path: string) { + return readFileSync(join(process.cwd(), ...path.split("/")), "utf8"); +} + +test("settings page surfaces integration status instead of a placeholder", () => { + const pageSource = source("app/dashboard/settings/page.tsx"); + const componentSource = source("components/settings/operations-readiness.tsx"); + const helperSource = source("lib/operational-readiness.ts"); + + assert.doesNotMatch(pageSource, /DashboardPlaceholderPage/); + assert.match(pageSource, /OperationsReadiness/); + + for (const label of [ + "Google", + "PageSpeed", + "OpenRouter", + "Playwright", + "SMTP", + "Convex Jobs", + "Rybbit", + "Konfiguration fehlt", + ]) { + assert.match(`${componentSource}\n${helperSource}`, new RegExp(label)); + } +}); + +test("verification notes cover critical MVP flows", () => { + const docPath = join(process.cwd(), "docs", "verification.md"); + assert.equal(existsSync(docPath), true); + const doc = readFileSync(docPath, "utf8"); + + for (const label of [ + "Login", + "Kampagnenlauf", + "Audit-Generierung", + "Freigabe", + "Versand", + "Follow-up", + "Analytics", + ]) { + assert.match(doc, new RegExp(label)); + } +}); + +test("Coolify deployment notes cover env vars, Playwright dependencies, port, and domains", () => { + const docPath = join(process.cwd(), "docs", "coolify-deployment.md"); + assert.equal(existsSync(docPath), true); + const doc = readFileSync(docPath, "utf8"); + + assert.match(doc, /Environment Variables/); + assert.match(doc, /TASK8_BROWSER_ASSET_URL/); + assert.match(doc, /Playwright/); + assert.match(doc, /Port 3000/); + assert.match(doc, /NEXT_PUBLIC_APP_URL/); + assert.match(doc, /Domain/); +});