Add MVP operational readiness checks
This commit is contained in:
@@ -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() {
|
export default function SettingsPage() {
|
||||||
return (
|
return <OperationsReadiness rows={getIntegrationReadiness(process.env)} />;
|
||||||
<DashboardPlaceholderPage
|
|
||||||
description="Provider-Status, Secrets-Hinweise und Workspace-Einstellungen folgen mit den Integrationen."
|
|
||||||
title="Einstellungen"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
61
components/settings/operations-readiness.tsx
Normal file
61
components/settings/operations-readiness.tsx
Normal file
@@ -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 (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<header className="border-b pb-3">
|
||||||
|
<p className="text-sm text-muted-foreground">MVP-Betrieb</p>
|
||||||
|
<h1 className="mt-2 text-3xl font-semibold tracking-normal">
|
||||||
|
Einstellungen
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Integrationsstatus</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Diese Übersicht zeigt nur fehlende Variablennamen und keine Secret-Werte.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-3 md:grid-cols-2">
|
||||||
|
{rows.map((row) => {
|
||||||
|
const isConfigured = row.status === "configured";
|
||||||
|
const Icon = isConfigured ? CheckCircle2 : AlertTriangle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="rounded-lg border p-4" key={row.id}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Icon
|
||||||
|
aria-hidden
|
||||||
|
className={isConfigured ? "mt-0.5 size-5 text-emerald-600" : "mt-0.5 size-5 text-amber-600"}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h2 className="text-base font-semibold">{row.label}</h2>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
{isConfigured ? "Konfiguration vorhanden" : "Konfiguration fehlt"}
|
||||||
|
</p>
|
||||||
|
{row.missingEnv.length > 0 ? (
|
||||||
|
<p className="mt-2 break-words text-xs text-muted-foreground">
|
||||||
|
Fehlend: {row.missingEnv.join(", ")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
{row.errorSurface}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
docs/coolify-deployment.md
Normal file
50
docs/coolify-deployment.md
Normal file
@@ -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.
|
||||||
46
docs/verification.md
Normal file
46
docs/verification.md
Normal file
@@ -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.
|
||||||
82
lib/operational-readiness.ts
Normal file
82
lib/operational-readiness.ts
Normal file
@@ -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<string, string | undefined>,
|
||||||
|
): 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",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
38
tests/operational-readiness.test.ts
Normal file
38
tests/operational-readiness.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
61
tests/ops-quality-source.test.ts
Normal file
61
tests/ops-quality-source.test.ts
Normal file
@@ -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/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user