Compare commits
2 Commits
e50f3dfbee
...
7c17321089
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c17321089 | |||
| 5be1c171cb |
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Banner Check API Proxy — calls consent-tester /scan endpoint
|
||||||
|
*
|
||||||
|
* POST /api/sdk/v1/agent/banner-check → runs 3-phase cookie banner test
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { url } = body
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return NextResponse.json({ error: 'URL erforderlich' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call backend which proxies to consent-tester
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/compliance/agent/banner-check`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url }),
|
||||||
|
signal: AbortSignal.timeout(120000), // 2 min for Playwright
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Backend: ${response.status}`, detail: errorText },
|
||||||
|
{ status: response.status },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return NextResponse.json(data)
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
return NextResponse.json({ error: msg }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
interface BannerResult {
|
||||||
|
banner_detected: boolean
|
||||||
|
banner_provider: string
|
||||||
|
banner_text: string
|
||||||
|
banner_checks?: {
|
||||||
|
violations: { code: string; text: string; severity: string }[]
|
||||||
|
passes: { code: string; text: string }[]
|
||||||
|
}
|
||||||
|
phases?: {
|
||||||
|
before_consent: { cookies: number; scripts: number; violations: string[] }
|
||||||
|
after_reject: { cookies: number; scripts: number; violations: string[] }
|
||||||
|
after_accept: { cookies: number; scripts: number; violations: string[] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BannerCheckTab() {
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [progress, setProgress] = useState('')
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [result, setResult] = useState<BannerResult | null>(null)
|
||||||
|
|
||||||
|
const handleScan = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!url.trim()) return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
setResult(null)
|
||||||
|
setProgress('Cookie-Banner wird analysiert...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/sdk/v1/agent/banner-check', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url: url.trim() }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`Fehler: ${res.status}`)
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (data.scan_id) {
|
||||||
|
// Async polling
|
||||||
|
let attempts = 0
|
||||||
|
while (attempts < 60) {
|
||||||
|
await new Promise(r => setTimeout(r, 3000))
|
||||||
|
const poll = await fetch(`/api/sdk/v1/agent/banner-check?scan_id=${data.scan_id}`)
|
||||||
|
if (!poll.ok) { attempts++; continue }
|
||||||
|
const pollData = await poll.json()
|
||||||
|
if (pollData.progress) setProgress(pollData.progress)
|
||||||
|
if (pollData.status === 'completed' && pollData.result) {
|
||||||
|
setResult(pollData.result)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (pollData.status === 'failed') throw new Error(pollData.error || 'Scan fehlgeschlagen')
|
||||||
|
attempts++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setResult(data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
setProgress('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const violations = result?.banner_checks?.violations || []
|
||||||
|
const passes = result?.banner_checks?.passes || []
|
||||||
|
const total = violations.length + passes.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-blue-900">Cookie-Banner Compliance Check</h3>
|
||||||
|
<p className="text-xs text-blue-700 mt-1">
|
||||||
|
Playwright-basierter 3-Phasen-Test: Vor Interaktion, nach Ablehnen, nach Akzeptieren.
|
||||||
|
Prueft Dark Patterns, Pre-Consent-Cookies, Farbkontrast, Klick-Paritaet und 20+ weitere Kriterien.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleScan} className="flex gap-3">
|
||||||
|
<input
|
||||||
|
type="url" value={url} onChange={e => setUrl(e.target.value)}
|
||||||
|
placeholder="https://www.example.com/"
|
||||||
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm"
|
||||||
|
disabled={loading} required
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={loading || !url.trim()}
|
||||||
|
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors flex items-center gap-2 text-sm font-medium">
|
||||||
|
{loading ? (
|
||||||
|
<><svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>Pruefe...</>
|
||||||
|
) : 'Banner pruefen'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{progress && (
|
||||||
|
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 text-sm text-purple-700 flex items-center gap-3">
|
||||||
|
<svg className="animate-spin w-5 h-5 text-purple-500 shrink-0" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
{progress}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className={`text-2xl`}>
|
||||||
|
{result.banner_detected ? '🛡️' : '⚠️'}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900">
|
||||||
|
{result.banner_detected
|
||||||
|
? `Banner erkannt: ${result.banner_provider || 'Unbekannter Anbieter'}`
|
||||||
|
: 'Kein Cookie-Banner erkannt'}
|
||||||
|
</h3>
|
||||||
|
{total > 0 && (
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
|
{passes.length}/{total} Pruefungen bestanden
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{total > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-24 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full ${violations.length === 0 ? 'bg-green-500' : violations.length <= 3 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||||
|
style={{ width: `${Math.round(passes.length / total * 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs font-medium ${violations.length === 0 ? 'text-green-700' : 'text-red-700'}`}>
|
||||||
|
{Math.round(passes.length / total * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3-Phase Summary */}
|
||||||
|
{result.phases && (
|
||||||
|
<div className="px-6 py-3 border-b border-gray-100 grid grid-cols-3 gap-4">
|
||||||
|
{[
|
||||||
|
{ label: 'Vor Consent', data: result.phases.before_consent, icon: '🔒' },
|
||||||
|
{ label: 'Nach Ablehnen', data: result.phases.after_reject, icon: '🚫' },
|
||||||
|
{ label: 'Nach Akzeptieren', data: result.phases.after_accept, icon: '✅' },
|
||||||
|
].map(phase => (
|
||||||
|
<div key={phase.label} className="text-center">
|
||||||
|
<div className="text-lg">{phase.icon}</div>
|
||||||
|
<div className="text-xs font-medium text-gray-700">{phase.label}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
{phase.data.cookies} Cookies, {phase.data.scripts} Scripts
|
||||||
|
</div>
|
||||||
|
{phase.data.violations.length > 0 && (
|
||||||
|
<div className="text-xs text-red-600 font-medium">
|
||||||
|
{phase.data.violations.length} Verstoesse
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Violations */}
|
||||||
|
{violations.length > 0 && (
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<h4 className="text-xs font-semibold text-red-700 uppercase tracking-wide mb-2">
|
||||||
|
Verstoesse ({violations.length})
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{violations.map((v, i) => (
|
||||||
|
<div key={i} className="flex items-start gap-2">
|
||||||
|
<svg className="w-4 h-4 text-red-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-red-700">{v.text}</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-0.5">{v.code} | {v.severity}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Passes */}
|
||||||
|
{passes.length > 0 && (
|
||||||
|
<div className="px-6 py-4 border-t border-gray-100">
|
||||||
|
<h4 className="text-xs font-semibold text-green-700 uppercase tracking-wide mb-2">
|
||||||
|
Bestanden ({passes.length})
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{passes.map((p, i) => (
|
||||||
|
<div key={i} className="flex items-start gap-2">
|
||||||
|
<svg className="w-4 h-4 text-green-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
<div className="text-sm text-gray-600">{p.text}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!result.banner_detected && violations.length === 0 && passes.length === 0 && (
|
||||||
|
<div className="px-6 py-4 text-sm text-gray-500">
|
||||||
|
Kein Cookie-Banner auf dieser Seite gefunden. Falls Cookies gesetzt werden, ist ein Banner nach §25 TDDDG Pflicht.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,9 +7,10 @@ import { AnalysisHistory } from './_components/AnalysisHistory'
|
|||||||
import { FollowUpQuestions } from './_components/FollowUpQuestions'
|
import { FollowUpQuestions } from './_components/FollowUpQuestions'
|
||||||
import { ScanResult } from './_components/ScanResult'
|
import { ScanResult } from './_components/ScanResult'
|
||||||
import { DocCheckTab } from './_components/DocCheckTab'
|
import { DocCheckTab } from './_components/DocCheckTab'
|
||||||
|
import { BannerCheckTab } from './_components/BannerCheckTab'
|
||||||
|
|
||||||
type AnalysisMode = 'pre_launch' | 'post_launch'
|
type AnalysisMode = 'pre_launch' | 'post_launch'
|
||||||
type AnalysisTab = 'quick' | 'scan' | 'doc-check'
|
type AnalysisTab = 'quick' | 'scan' | 'doc-check' | 'banner-check'
|
||||||
|
|
||||||
const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = [
|
const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = [
|
||||||
{ id: 'pre_launch', label: 'Internes Dokument', desc: 'Vor Veroeffentlichung pruefen', icon: '📋' },
|
{ id: 'pre_launch', label: 'Internes Dokument', desc: 'Vor Veroeffentlichung pruefen', icon: '📋' },
|
||||||
@@ -20,6 +21,7 @@ const TABS: { id: AnalysisTab; label: string; desc: string }[] = [
|
|||||||
{ id: 'quick', label: 'Schnellanalyse', desc: 'Einzelne Seite klassifizieren + bewerten' },
|
{ id: 'quick', label: 'Schnellanalyse', desc: 'Einzelne Seite klassifizieren + bewerten' },
|
||||||
{ id: 'scan', label: 'Website-Scan', desc: 'Mehrere Seiten scannen + Dienstleister abgleichen' },
|
{ id: 'scan', label: 'Website-Scan', desc: 'Mehrere Seiten scannen + Dienstleister abgleichen' },
|
||||||
{ id: 'doc-check', label: 'Dokumenten-Pruefung', desc: 'Einzelne Dokumente gezielt pruefen' },
|
{ id: 'doc-check', label: 'Dokumenten-Pruefung', desc: 'Einzelne Dokumente gezielt pruefen' },
|
||||||
|
{ id: 'banner-check', label: 'Banner-Check', desc: 'Cookie-Banner auf DSGVO-Konformitaet testen' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function AgentPage() {
|
export default function AgentPage() {
|
||||||
@@ -224,8 +226,11 @@ export default function AgentPage() {
|
|||||||
{/* Doc Check Tab — own component */}
|
{/* Doc Check Tab — own component */}
|
||||||
{tab === 'doc-check' && <DocCheckTab />}
|
{tab === 'doc-check' && <DocCheckTab />}
|
||||||
|
|
||||||
|
{/* Banner Check Tab — own component */}
|
||||||
|
{tab === 'banner-check' && <BannerCheckTab />}
|
||||||
|
|
||||||
{/* URL Input (quick + scan only) */}
|
{/* URL Input (quick + scan only) */}
|
||||||
{tab !== 'doc-check' && <form onSubmit={handleSubmit} className="flex gap-3">
|
{(tab === 'quick' || tab === 'scan') && <form onSubmit={handleSubmit} className="flex gap-3">
|
||||||
<input type="url" value={url} onChange={e => setUrl(e.target.value)}
|
<input type="url" value={url} onChange={e => setUrl(e.target.value)}
|
||||||
placeholder={tab === 'scan' ? 'https://www.example.com/' : 'https://example.com/datenschutz'}
|
placeholder={tab === 'scan' ? 'https://www.example.com/' : 'https://example.com/datenschutz'}
|
||||||
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm"
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm"
|
||||||
|
|||||||
+3
-3
@@ -198,9 +198,9 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
|
|||||||
return (
|
return (
|
||||||
<tr key={h.id} className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors">
|
<tr key={h.id} className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors">
|
||||||
{/* Hazard info */}
|
{/* Hazard info */}
|
||||||
<td className="px-3 py-2 max-w-[200px]">
|
<td className="px-3 py-2 min-w-[250px]">
|
||||||
<div className="font-medium text-gray-900 dark:text-white truncate">{h.name}</div>
|
<div className="font-medium text-gray-900 dark:text-white">{h.name}</div>
|
||||||
{h.component_name && <div className="text-[10px] text-gray-400 truncate">{h.component_name}</div>}
|
{h.component_name && <div className="text-[10px] text-gray-400">{h.component_name}</div>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 border-r border-gray-200 dark:border-gray-600">
|
<td className="px-3 py-2 border-r border-gray-200 dark:border-gray-600">
|
||||||
<span className="inline-block px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-[10px] font-medium">
|
<span className="inline-block px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-[10px] font-medium">
|
||||||
|
|||||||
@@ -122,19 +122,19 @@ export default function ProjectOverviewPage() {
|
|||||||
|
|
||||||
async function fetchProject() {
|
async function fetchProject() {
|
||||||
try {
|
try {
|
||||||
// Fetch project detail + live risk summary + mitigations count in parallel
|
// Only fetch project detail + lightweight risk summary (NO heavy lists)
|
||||||
const [projRes, riskRes, mitRes, hazRes] = await Promise.all([
|
const [projRes, riskRes] = await Promise.all([
|
||||||
fetch(`/api/sdk/v1/iace/projects/${projectId}`),
|
fetch(`/api/sdk/v1/iace/projects/${projectId}`),
|
||||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/risk-summary`),
|
fetch(`/api/sdk/v1/iace/projects/${projectId}/risk-summary`),
|
||||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`),
|
|
||||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards`),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!projRes.ok) return
|
if (!projRes.ok) return
|
||||||
const json = await projRes.json()
|
const json = await projRes.json()
|
||||||
|
|
||||||
// Live risk summary from dedicated endpoint
|
// Live risk summary from dedicated endpoint (lightweight — just counts)
|
||||||
let rs = json.risk_summary || {}
|
let rs = json.risk_summary || {}
|
||||||
|
let hazCount = 0
|
||||||
|
let mitCount = 0
|
||||||
if (riskRes.ok) {
|
if (riskRes.ok) {
|
||||||
const riskJson = await riskRes.json()
|
const riskJson = await riskRes.json()
|
||||||
const live = riskJson.risk_summary || riskJson || {}
|
const live = riskJson.risk_summary || riskJson || {}
|
||||||
@@ -146,18 +146,8 @@ export default function ProjectOverviewPage() {
|
|||||||
negligible: live.negligible || 0,
|
negligible: live.negligible || 0,
|
||||||
total: live.total_hazards || live.total || 0,
|
total: live.total_hazards || live.total || 0,
|
||||||
}
|
}
|
||||||
}
|
hazCount = live.total_hazards || live.total || 0
|
||||||
|
mitCount = live.total_mitigations || 0
|
||||||
// Live counts
|
|
||||||
let mitCount = 0
|
|
||||||
if (mitRes.ok) {
|
|
||||||
const mitJson = await mitRes.json()
|
|
||||||
mitCount = mitJson.total || (mitJson.mitigations || []).length || 0
|
|
||||||
}
|
|
||||||
let hazCount = 0
|
|
||||||
if (hazRes.ok) {
|
|
||||||
const hazJson = await hazRes.json()
|
|
||||||
hazCount = hazJson.total || (hazJson.hazards || []).length || 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate dynamic completeness percentage
|
// Calculate dynamic completeness percentage
|
||||||
|
|||||||
@@ -92,6 +92,26 @@ class DocCheckStatusResponse(BaseModel):
|
|||||||
error: str = ""
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class BannerCheckRequest(BaseModel):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/banner-check")
|
||||||
|
async def run_banner_check(req: BannerCheckRequest):
|
||||||
|
"""Run cookie banner compliance check via consent-tester."""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{CONSENT_TESTER_URL}/scan",
|
||||||
|
json={"url": req.url, "timeout_per_phase": 10},
|
||||||
|
)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return resp.json()
|
||||||
|
return {"error": f"Consent-Tester: HTTP {resp.status_code}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)[:200]}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/doc-check")
|
@router.post("/doc-check")
|
||||||
async def start_doc_check(req: DocCheckRequest):
|
async def start_doc_check(req: DocCheckRequest):
|
||||||
"""Start async multi-URL document check."""
|
"""Start async multi-URL document check."""
|
||||||
|
|||||||
Reference in New Issue
Block a user