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}
+
+ )}
+