feat(marketing): savings-scan form -> compliance backend (real submit + polling)
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 39s
CI / test-python-voice (push) Successful in 31s
CI / test-bqas (push) Successful in 32s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 39s
CI / test-python-voice (push) Successful in 31s
CI / test-bqas (push) Successful in 32s
- POST /api/scan/start: server-proxy zu api-dev.breakpilot.ai/saving-scan/start (kein CORS-Bypass, env-konfigurierbar via COMPLIANCE_BACKEND_URL) - GET /api/scan/status/<checkId>: server-proxy fuer Status-Polling - savings-scan/page.tsx: echte Submission + 5s-Polling + Progress-Bar + Consent- Checkbox + Error-Branch (skipped_tdm, failed) - Datenschutzhinweis im Disclaimer ergaenzt (§ 44b UrhG TDM-Respekt) Backend-Endpoint in breakpilot-compliance@6c223c7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* GET /api/scan/status/<checkId>
|
||||
* Proxy to compliance backend /api/compliance/agent/compliance-check/<id>.
|
||||
*
|
||||
* 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<string | null>(null)
|
||||
const [progress, setProgress] = useState<string>('')
|
||||
const [progressPct, setProgressPct] = useState<number>(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const pollingRef = useRef<boolean>(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 (
|
||||
<>
|
||||
<Navbar />
|
||||
@@ -87,9 +129,24 @@ export default function SavingsScanPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="flex items-start gap-2 text-xs text-white/60 cursor-pointer">
|
||||
<input type="checkbox" checked={consent} onChange={e => setConsent(e.target.checked)}
|
||||
className="mt-0.5 accent-emerald-500" />
|
||||
<span>
|
||||
Ich stimme zu, dass meine E-Mail fuer den Saving-Report + ein
|
||||
einmaliges Sales-Follow-Up genutzt wird. Widerruf jederzeit per E-Mail.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-amber-300/80 bg-amber-500/10 border border-amber-400/30 rounded px-3 py-2">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
disabled={submitting || !consent}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-full
|
||||
bg-emerald-500 hover:bg-emerald-400 transition-colors
|
||||
text-enterprise-dark font-semibold disabled:opacity-50"
|
||||
@@ -99,24 +156,47 @@ export default function SavingsScanPage() {
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-white/40 pt-2">
|
||||
Wir analysieren ausschliesslich oeffentlich abrufbare Daten Ihrer Website.
|
||||
Keine Anmeldung, keine Daten werden gespeichert. Ergebnis innerhalb von
|
||||
~5 Minuten per E-Mail.
|
||||
Wir analysieren ausschliesslich oeffentlich abrufbare Daten Ihrer Website
|
||||
unter Beachtung maschinenlesbarer Nutzungsvorbehalte (§ 44b UrhG).
|
||||
Pro Domain max. 1 Scan / 24h. Ergebnis innerhalb von ~3-5 Minuten per E-Mail.
|
||||
</p>
|
||||
</form>
|
||||
</GlassCard>
|
||||
) : (
|
||||
<GlassCard>
|
||||
<div className="text-center py-6">
|
||||
<CheckCircle2 className="w-12 h-12 text-emerald-400 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold mb-2">Scan gestartet</h3>
|
||||
<p className="text-white/60 mb-4">
|
||||
Wir analysieren Ihre Website jetzt. Der Bericht kommt in
|
||||
den naechsten 5 Minuten per E-Mail an <strong className="text-white/90">{email}</strong>.
|
||||
</p>
|
||||
<p className="text-xs text-white/40">
|
||||
Pruefen Sie auch Ihren Spam-Ordner.
|
||||
</p>
|
||||
{error ? (
|
||||
<AlertTriangle className="w-12 h-12 text-amber-400 mx-auto mb-4" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-12 h-12 text-emerald-400 mx-auto mb-4" />
|
||||
)}
|
||||
<h3 className="text-xl font-bold mb-2">
|
||||
{error ? 'Scan-Hinweis' : (progressPct >= 100 ? 'Scan abgeschlossen' : 'Scan laeuft')}
|
||||
</h3>
|
||||
{error ? (
|
||||
<p className="text-amber-300/80 mb-4 text-sm">{error}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-white/60 mb-4">
|
||||
{progressPct >= 100
|
||||
? <>Der Bericht ist unterwegs an <strong className="text-white/90">{email}</strong>.</>
|
||||
: <>Wir analysieren <strong className="text-white/90">{url}</strong>. Bericht kommt in ~3-5 Min an <strong className="text-white/90">{email}</strong>.</>
|
||||
}
|
||||
</p>
|
||||
{progress && progressPct < 100 && (
|
||||
<div className="max-w-md mx-auto mt-4">
|
||||
<div className="text-xs text-white/50 mb-2">{progress}</div>
|
||||
<div className="w-full bg-white/10 rounded-full h-2 overflow-hidden">
|
||||
<div className="bg-emerald-400 h-full transition-all" style={{ width: `${progressPct}%` }} />
|
||||
</div>
|
||||
<div className="text-xs text-white/40 mt-1">{progressPct}%</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-white/40 mt-4">
|
||||
Pruefen Sie auch Ihren Spam-Ordner.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user