diff --git a/marketing-website/app/api/scan/start/route.ts b/marketing-website/app/api/scan/start/route.ts new file mode 100644 index 0000000..90c88d7 --- /dev/null +++ b/marketing-website/app/api/scan/start/route.ts @@ -0,0 +1,43 @@ +/** + * POST /api/scan/start + * Proxy to compliance backend /api/compliance/agent/saving-scan/start. + * + * Body: { url: string; email: string; consent?: boolean } + * + * Server-side proxy avoids cross-origin POST from breakpilot.ai to + * api-dev.breakpilot.ai — same-origin from the browser, secure egress + * from the Next.js server. Backend handles rate-limit + TDM + lead-DB. + */ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = + process.env.COMPLIANCE_BACKEND_URL || 'https://api-dev.breakpilot.ai' + +export async function POST(request: NextRequest) { + let body: unknown + try { + body = await request.json() + } catch { + return NextResponse.json( + { error: 'Body muss JSON sein' }, { status: 400 }, + ) + } + + try { + const res = await fetch( + `${BACKEND_URL}/api/compliance/agent/saving-scan/start`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(20000), + }, + ) + const data = await res.json().catch(() => ({})) + return NextResponse.json(data, { status: res.status }) + } catch { + return NextResponse.json( + { error: 'Backend nicht erreichbar' }, { status: 503 }, + ) + } +} diff --git a/marketing-website/app/api/scan/status/[checkId]/route.ts b/marketing-website/app/api/scan/status/[checkId]/route.ts new file mode 100644 index 0000000..14e2f35 --- /dev/null +++ b/marketing-website/app/api/scan/status/[checkId]/route.ts @@ -0,0 +1,29 @@ +/** + * GET /api/scan/status/ + * Proxy to compliance backend /api/compliance/agent/compliance-check/. + * + * Polled every ~5s by the savings-scan page until status==completed/failed. + */ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = + process.env.COMPLIANCE_BACKEND_URL || 'https://api-dev.breakpilot.ai' + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ checkId: string }> }, +) { + const { checkId } = await params + try { + const res = await fetch( + `${BACKEND_URL}/api/compliance/agent/compliance-check/${checkId}`, + { signal: AbortSignal.timeout(15000) }, + ) + const data = await res.json().catch(() => ({})) + return NextResponse.json(data, { status: res.status }) + } catch { + return NextResponse.json( + { error: 'Backend nicht erreichbar' }, { status: 503 }, + ) + } +} diff --git a/marketing-website/app/savings-scan/page.tsx b/marketing-website/app/savings-scan/page.tsx index 976909f..8adccf9 100644 --- a/marketing-website/app/savings-scan/page.tsx +++ b/marketing-website/app/savings-scan/page.tsx @@ -1,34 +1,76 @@ 'use client' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import Navbar from '@/components/layout/Navbar' import Footer from '@/components/layout/Footer' import ChatFAB from '@/components/layout/ChatFAB' import PageHeader from '@/components/ui/PageHeader' import GlassCard from '@/components/ui/GlassCard' import FadeInView from '@/components/ui/FadeInView' -import { Cookie, ShieldCheck, Mail, ArrowRight, CheckCircle2 } from 'lucide-react' +import { Cookie, ShieldCheck, Mail, ArrowRight, CheckCircle2, AlertTriangle } from 'lucide-react' export default function SavingsScanPage() { const [url, setUrl] = useState('') const [email, setEmail] = useState('') + const [consent, setConsent] = useState(true) const [submitting, setSubmitting] = useState(false) const [done, setDone] = useState(false) + const [checkId, setCheckId] = useState(null) + const [progress, setProgress] = useState('') + const [progressPct, setProgressPct] = useState(0) + const [error, setError] = useState(null) + const pollingRef = useRef(false) async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!url || !email) return + setError(null) setSubmitting(true) try { - // TODO: wire to compliance-check backend - // For now: send to contact form / placeholder - await new Promise(r => setTimeout(r, 800)) + const res = await fetch('/api/scan/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, email, consent }), + }) + const data = await res.json() + if (!res.ok) { + setError(data.detail || data.error || 'Scan konnte nicht gestartet werden') + return + } + setCheckId(data.check_id) setDone(true) + } catch { + setError('Netzwerkfehler — bitte erneut versuchen.') } finally { setSubmitting(false) } } + useEffect(() => { + if (!checkId || pollingRef.current) return + pollingRef.current = true + let cancelled = false + const poll = async () => { + for (let i = 0; i < 60 && !cancelled; i++) { + await new Promise(r => setTimeout(r, 5000)) + try { + const res = await fetch(`/api/scan/status/${checkId}`) + const data = await res.json() + if (data.progress) setProgress(data.progress) + if (typeof data.progress_pct === 'number') setProgressPct(data.progress_pct) + if (['completed', 'failed', 'skipped_tdm'].includes(data.status)) { + if (data.status !== 'completed') { + setError(data.error || 'Scan abgebrochen') + } + return + } + } catch { /* retry */ } + } + } + poll() + return () => { cancelled = true; pollingRef.current = false } + }, [checkId]) + return ( <> @@ -87,9 +129,24 @@ export default function SavingsScanPage() {

+ + + {error && ( +

+ {error} +

+ )} +