diff --git a/admin-compliance/app/api/sdk/v1/agent/audit/[checkId]/route.ts b/admin-compliance/app/api/sdk/v1/agent/audit/[checkId]/route.ts index 5e599ea0..4e6efb18 100644 --- a/admin-compliance/app/api/sdk/v1/agent/audit/[checkId]/route.ts +++ b/admin-compliance/app/api/sdk/v1/agent/audit/[checkId]/route.ts @@ -10,9 +10,9 @@ const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:80 export async function GET( request: NextRequest, - { params }: { params: { checkId: string } }, + { params }: { params: Promise<{ checkId: string }> }, ) { - const checkId = params.checkId + const { checkId } = await params const qs = request.nextUrl.searchParams.toString() const url = `${BACKEND_URL}/api/compliance/agent/audit/${checkId}${qs ? `?${qs}` : ''}` try { diff --git a/admin-compliance/app/api/sdk/v1/agent/banner/[checkId]/route.ts b/admin-compliance/app/api/sdk/v1/agent/banner/[checkId]/route.ts new file mode 100644 index 00000000..496ac98d --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/banner/[checkId]/route.ts @@ -0,0 +1,28 @@ +/** + * Proxy: GET /api/sdk/v1/agent/banner/ + * -> backend GET /api/compliance/agent/banner/ + * + * Liefert das volle banner_result (phases, structured_checks, category_tests). + */ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002' + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ checkId: string }> }, +) { + const { checkId } = await params + try { + const resp = await fetch( + `${BACKEND_URL}/api/compliance/agent/banner/${checkId}`, + { signal: AbortSignal.timeout(15000) }, + ) + const data = await resp.json().catch(() => ({})) + return NextResponse.json(data, { status: resp.status }) + } catch { + return NextResponse.json( + { error: 'Banner-Abfrage fehlgeschlagen' }, { status: 503 }, + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/agent/findings/[checkId]/route.ts b/admin-compliance/app/api/sdk/v1/agent/findings/[checkId]/route.ts index a7d59519..98044f2c 100644 --- a/admin-compliance/app/api/sdk/v1/agent/findings/[checkId]/route.ts +++ b/admin-compliance/app/api/sdk/v1/agent/findings/[checkId]/route.ts @@ -10,9 +10,9 @@ const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:80 export async function GET( request: NextRequest, - { params }: { params: { checkId: string } }, + { params }: { params: Promise<{ checkId: string }> }, ) { - const checkId = params.checkId + const { checkId } = await params const qs = request.nextUrl.searchParams.toString() const url = `${BACKEND_URL}/api/compliance/agent/findings/${checkId}${qs ? `?${qs}` : ''}` try { diff --git a/admin-compliance/app/sdk/agent/audit/[checkId]/BannerTab.tsx b/admin-compliance/app/sdk/agent/audit/[checkId]/BannerTab.tsx new file mode 100644 index 00000000..4255c137 --- /dev/null +++ b/admin-compliance/app/sdk/agent/audit/[checkId]/BannerTab.tsx @@ -0,0 +1,302 @@ +'use client' + +import React, { useEffect, useState } from 'react' + +type Phase = { + cookies?: string[] + scripts?: string[] + tracking_services?: (string | { name?: string })[] + new_tracking?: unknown[] + violations?: Array<{ severity?: string; text?: string }> + undocumented?: unknown[] +} + +type CategoryTest = { + category: string + category_label: string + tracking_services?: (string | { name?: string })[] + cookies_set?: string[] + provider_details_visible?: boolean + violations?: Array<{ severity?: string; text?: string; legal_ref?: string }> +} + +type BannerViolation = { + severity?: string + text?: string + legal_ref?: string +} + +type StructuredCheck = { + id: string + label: string + passed: boolean + skipped?: boolean + severity: string + level?: number + hint?: string +} + +type BannerResp = { + found: boolean + check_id: string + banner?: { + banner_provider?: string + banner_detected?: boolean + completeness_pct?: number + correctness_pct?: number + phases?: Record + banner_checks?: { violations?: BannerViolation[] } + category_tests?: CategoryTest[] + structured_checks?: StructuredCheck[] + summary?: Record + } +} + +const PHASE_LABEL: Record = { + before_consent: 'Vor Consent', + after_reject: 'Nach Ablehnung', + after_accept: 'Nach Annahme', +} + +const SEV_BADGE: Record = { + CRITICAL: 'bg-red-600 text-white', + HIGH: 'bg-red-100 text-red-800', + MEDIUM: 'bg-amber-100 text-amber-800', + LOW: 'bg-blue-100 text-blue-800', + INFO: 'bg-gray-100 text-gray-600', +} + +function pctColor(pct?: number): string { + if (pct === undefined || pct === null) return 'text-gray-400' + return pct >= 80 ? 'text-green-700' : pct >= 50 ? 'text-amber-700' : 'text-red-700' +} + +export default function BannerTab({ checkId }: { checkId: string }) { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [checkFilter, setCheckFilter] = useState<'all' | 'fail' | 'critical'>('fail') + + useEffect(() => { + let cancelled = false + setLoading(true) + fetch(`/api/sdk/v1/agent/banner/${checkId}`) + .then(r => r.json()) + .then(d => { if (!cancelled) setData(d) }) + .catch(e => { if (!cancelled) setError(String(e)) }) + .finally(() => { if (!cancelled) setLoading(false) }) + return () => { cancelled = true } + }, [checkId]) + + if (loading) return
Lade Banner-Daten…
+ if (error) return
Fehler: {error}
+ if (!data?.found || !data.banner) { + return
Keine Banner-Daten zu diesem Check.
+ } + + const b = data.banner + const phases = b.phases || {} + const cats = b.category_tests || [] + const violations = b.banner_checks?.violations || [] + const checks = b.structured_checks || [] + const summary = b.summary || {} + + const filteredChecks = checks.filter(c => { + if (checkFilter === 'all') return true + if (checkFilter === 'fail') return !c.passed && !c.skipped + return !c.passed && !c.skipped && ['CRITICAL', 'HIGH'].includes(c.severity) + }) + + return ( +
+ {/* Quality Cards */} +
+
+
Vollstaendigkeit
+
+ {b.completeness_pct ?? '–'}{b.completeness_pct !== undefined && '%'} +
+
+
+
Korrektheit
+
+ {b.correctness_pct ?? '–'}{b.correctness_pct !== undefined && '%'} +
+
+
+
Verstoesse
+
+ {summary.total_violations ?? violations.length} +
+
+ crit:{summary.critical ?? 0} · high:{summary.high ?? 0} +
+
+
+
CMP
+
+ {b.banner_provider || 'unbekannt'} +
+
+ {b.banner_detected ? 'Banner erkannt' : 'kein Banner'} +
+
+
+ + {/* Phases */} +
+
+ Cookie-Setzungen pro Phase (echter Browser-Test) +
+ + + + + + + + + + + {(['before_consent', 'after_reject', 'after_accept'] as const).map(key => { + const p = phases[key] || {} + const nc = (p.cookies || []).length + const nt = (p.tracking_services || []).length + const issues: string[] = [] + if (p.violations?.length) issues.push(`${p.violations.length} Verstoss`) + if (p.new_tracking?.length) issues.push(`${p.new_tracking.length} neue Tracker`) + if (p.undocumented?.length) issues.push(`${p.undocumented.length} undokumentiert`) + const color = key === 'before_consent' + ? (nc === 0 ? 'text-green-600' : 'text-red-600') + : key === 'after_reject' + ? (nc <= 1 ? 'text-green-600' : 'text-amber-600') + : 'text-gray-700' + return ( + + + + + + + ) + })} + +
PhaseCookiesTrackerAuffaelligkeiten
{PHASE_LABEL[key]}{nc}{nt}{issues.join(', ') || '—'}
+
+ + {/* Per-Category */} + {cats.length > 0 && ( +
+
+ Provider-Listing pro Kategorie (P19 Click-Through-Test) +
+ + + + + + + + + + + {cats.map(c => { + const pdv = c.provider_details_visible + const pdv_label = pdv === true ? 'Ja' : pdv === false ? 'Nein' : '–' + const pdv_color = pdv === false ? 'text-red-700' : pdv === true ? 'text-green-700' : 'text-gray-400' + return ( + + + + + + + ) + })} + +
KategorieAnbieter sichtbarTracker erkanntViolations
{c.category_label}{pdv_label}{(c.tracking_services || []).length} + {(c.violations || []).map(v => v.text?.slice(0, 80)).join('; ') || '—'} +
+
+ )} + + {/* Banner-Checks Violations */} + {violations.length > 0 && ( +
+
+ Banner-Verstoesse ({violations.length}) +
+
    + {violations.map((v, i) => { + const sev = (v.severity || 'MEDIUM').toUpperCase() + return ( +
  • +
    + {sev} +
    +
    {v.text}
    + {v.legal_ref &&
    Quelle: {v.legal_ref}
    } +
    +
    +
  • + ) + })} +
+
+ )} + + {/* 46 structured_checks Drilldown */} +
+
+ Banner-Checks ({checks.length}) +
+ {(['all', 'fail', 'critical'] as const).map(f => ( + + ))} +
+
+ + + + + + + + + + {filteredChecks.map(c => ( + + + + + + ))} + {filteredChecks.length === 0 && ( + + )} + +
StatusSevCheck
+ {c.passed ? + : c.skipped ? + : } + + + {c.severity} + + +
{c.label}
+ {c.hint && !c.passed && ( +
{c.hint.slice(0, 200)}
+ )} +
Keine Checks fuer den Filter.
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/agent/audit/[checkId]/page.tsx b/admin-compliance/app/sdk/agent/audit/[checkId]/page.tsx index 1891357a..242deeed 100644 --- a/admin-compliance/app/sdk/agent/audit/[checkId]/page.tsx +++ b/admin-compliance/app/sdk/agent/audit/[checkId]/page.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState, useMemo } from 'react' import { use as useUnwrap } from 'react' import FindingsTab from './FindingsTab' +import BannerTab from './BannerTab' type MCRow = { id: number @@ -92,7 +93,7 @@ export default function AuditPage( const [filterReg, setFilterReg] = useState('') const [filterDoc, setFilterDoc] = useState('') const [expanded, setExpanded] = useState(null) - const [tab, setTab] = useState<'mc' | 'all'>('all') + const [tab, setTab] = useState<'mc' | 'all' | 'banner'>('all') useEffect(() => { let cancelled = false @@ -155,6 +156,7 @@ export default function AuditPage(
{([ { key: 'all', label: 'Voll-Audit (alle Findings)' }, + { key: 'banner', label: 'Cookie-Banner-Analyse' }, { key: 'mc', label: 'Nur MC-Scorecard' }, ] as const).map(t => (