feat(impressum): Snapshot-Modul-Tab — ImpressumAgent auf gespeichertem Text

Snapshot-Detailseite wird zu Modul-Tabs (Cookies & Tracking | Impressum).
Backend GET /snapshots/{id}/impressum-check laeuft den v3 ImpressumAgent auf
dem gespeicherten Impressum-Text (kein Re-Crawl); Input-Erzeugung in
impressum_input_from_snapshot() ausgelagert (pure + getestet: Text/Scope/
company_name-Fallback/None-Pfad). Frontend laedt lazy beim Tab-Wechsel und
rendert mit dem bestehenden AgentResultTab (keine zweite Engine).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-11 11:24:44 +02:00
parent 6846ca6b28
commit 5b36b3f367
5 changed files with 203 additions and 14 deletions
@@ -0,0 +1,34 @@
/**
* Impressum-Analyse-Proxy
* GET /api/sdk/v1/agent/snapshots/{snapshotId}/impressum-check
* → backend /api/compliance/agent/snapshots/{snapshotId}/impressum-check
*
* Laeuft den v3 ImpressumAgent auf dem gespeicherten Impressum-Text
* (kein Re-Crawl) und liefert den AgentOutput (Findings/Massnahmen/Coverage).
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL =
process.env.BACKEND_API_URL || process.env.BACKEND_URL ||
'http://backend-compliance:8002'
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ snapshotId: string }> },
) {
const { snapshotId } = await params
try {
const response = await fetch(
`${BACKEND_URL}/api/compliance/agent/snapshots/${snapshotId}/impressum-check`,
{ signal: AbortSignal.timeout(120_000) },
)
const data = await response.json()
return NextResponse.json(data, { status: response.status })
} catch {
return NextResponse.json(
{ error: 'Impressum-Analyse fehlgeschlagen', findings: [] },
{ status: 503 },
)
}
}
@@ -2,24 +2,29 @@
/**
* Snapshot-Detail — öffnet einen gespeicherten Check aus der Historie und
* zeigt die Ergebnis-Views aus den Rohdaten (kein Re-Crawl). Aktuell:
* Cookie-Auswertung. Impressum/AGB/… folgen als weitere Module hier.
* zeigt die Ergebnis-Views aus den Rohdaten (kein Re-Crawl), als Modul-Tabs:
* Cookies & Tracking + Impressum (DSE/AGB folgen). Impressum wird beim Öffnen
* des Tabs nachgeladen (ImpressumAgent auf dem gespeicherten Text).
*/
import React, { use as useUnwrap, useEffect, useState } from 'react'
import React, { use as useUnwrap, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { CookieLibraryPanel } from '../../_components/CookieLibraryPanel'
import { CookieResultView } from '../../_components/CookieResultView'
import { AgentResultTab } from '../../_components/AgentResultTab'
export default function SnapshotDetail(
{ params }: { params: Promise<{ snapshotId: string }> },
) {
const { snapshotId } = useUnwrap(params)
const [snap, setSnap] = useState<any>(null)
const [check, setCheck] = useState<any>(null)
const [check, setCheck] = useState<any>(null) // cookie-check
const [impressum, setImpressum] = useState<any>(null) // impressum-check (lazy)
const [impLoading, setImpLoading] = useState(false)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [tab, setTab] = useState<string>('')
useEffect(() => {
let cancelled = false
@@ -27,15 +32,14 @@ export default function SnapshotDetail(
.then(r => r.json())
.then(d => {
if (cancelled) return
if (d?.error) setError(d.error)
else setSnap(d)
if (d?.error) setError(d.error); else setSnap(d)
})
.catch(e => { if (!cancelled) setError(String(e)) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [snapshotId])
// Library-Abgleich einmal laden (Findings + cookie_categories für beide Views).
// Cookie-Abgleich einmal laden (Findings + cookie_categories für beide Views).
useEffect(() => {
let cancelled = false
fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/cookie-check`)
@@ -45,7 +49,37 @@ export default function SnapshotDetail(
return () => { cancelled = true }
}, [snapshotId])
const docs = snap?.doc_entries || []
const hasCookies = (snap?.cmp_vendors?.length ?? 0) > 0
const hasImpressum = docs.some(
(e: any) => e.doc_type === 'impressum' && (e.text || e.content || '').length > 100)
const modules = useMemo(() => [
...(hasCookies ? [{ key: 'cookie', label: 'Cookies & Tracking' }] : []),
...(hasImpressum ? [{ key: 'impressum', label: 'Impressum' }] : []),
], [hasCookies, hasImpressum])
useEffect(() => {
if (!tab && modules.length) setTab(modules[0].key)
}, [modules, tab])
// Impressum erst beim Öffnen des Tabs analysieren (ImpressumAgent, ggf. LLM).
useEffect(() => {
if (tab !== 'impressum' || impressum || impLoading) return
setImpLoading(true)
fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/impressum-check`)
.then(r => r.json())
.then(setImpressum)
.catch(() => setImpressum({ error: 'Impressum-Analyse fehlgeschlagen', findings: [] }))
.finally(() => setImpLoading(false))
}, [tab, snapshotId, impressum, impLoading])
const tabBtn = (key: string, label: string) => (
<button key={key} onClick={() => setTab(key)}
className={`px-3 py-1.5 text-sm border-b-2 -mb-px ${tab === key ? 'border-blue-600 text-blue-700 font-medium' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
{label}
</button>
)
return (
<div className="p-6 max-w-6xl space-y-4">
@@ -56,15 +90,37 @@ export default function SnapshotDetail(
<div className="text-sm text-gray-500">Lade Snapshot</div>
) : error || !snap ? (
<div className="text-sm text-red-600">Snapshot nicht gefunden.</div>
) : hasCookies ? (
<>
<CookieLibraryPanel snapshotId={snapshotId} data={check ?? undefined} />
<CookieResultView snapshot={snap} cookieCategories={check?.cookie_categories} />
</>
) : (
) : modules.length === 0 ? (
<div className="text-sm text-gray-500">
Dieser Snapshot enthält keine Cookie-/Vendor-Daten.
Dieser Snapshot enthält keine auswertbaren Daten.
</div>
) : (
<>
<div className="flex gap-1 border-b border-gray-200">
{modules.map(m => tabBtn(m.key, m.label))}
</div>
{tab === 'cookie' && hasCookies && (
<div className="space-y-4">
<CookieLibraryPanel snapshotId={snapshotId} data={check ?? undefined} />
<CookieResultView snapshot={snap} cookieCategories={check?.cookie_categories} />
</div>
)}
{tab === 'impressum' && (
impLoading ? (
<div className="text-sm text-gray-500">Impressum-Analyse läuft</div>
) : impressum?.error ? (
<div className="text-sm text-red-600">{impressum.error}</div>
) : impressum && (impressum.findings?.length || impressum.mc_coverage?.length) ? (
<AgentResultTab topicLabel="Impressum" output={impressum} />
) : impressum ? (
<div className="text-sm text-gray-500">
{impressum.notes || 'Keine Impressum-Auswertung verfügbar.'}
</div>
) : null
)}
</>
)}
</div>
)