feat(dse): kuratierter DSEAgent + Snapshot-Tab (Art. 13/14, kein Firehose)

DSEAgent wrappt die existierende ART13_CHECKLIST (33 kuratierte Pflichtangaben
L1 + Detailchecks L2) → strukturierter AgentOutput, NICHT der 90k-Library-
Firehose (eCall/Gesundheit/Telekom-Lärm). GET /snapshots/{id}/dse-check spiegelt
impressum-check; doc_input_from_snapshot generalisiert. Frontend: generischer
AgentModuleTab (lazy → AgentResultTab) für Impressum + DSE; DSE-Tab in der
Snapshot-Seite. Plus HRB-Pattern \d→\d+ (volle Registernummer als Beleg).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-11 12:46:46 +02:00
parent be93859645
commit 76be96556d
10 changed files with 352 additions and 40 deletions
@@ -3,8 +3,9 @@
/**
* Snapshot-Detail — öffnet einen gespeicherten Check aus der Historie und
* 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).
* Cookies & Tracking + Impressum + Datenschutzerklärung (AGB folgen).
* Doc-Agenten (Impressum/DSE) laufen beim Öffnen des Tabs auf dem gespeicherten
* Text — generisch via AgentModuleTab.
*/
import React, { use as useUnwrap, useEffect, useMemo, useState } from 'react'
@@ -12,7 +13,7 @@ import Link from 'next/link'
import { CookieLibraryPanel } from '../../_components/CookieLibraryPanel'
import { CookieResultView } from '../../_components/CookieResultView'
import { AgentResultTab } from '../../_components/AgentResultTab'
import { AgentModuleTab } from '../../_components/AgentModuleTab'
export default function SnapshotDetail(
{ params }: { params: Promise<{ snapshotId: string }> },
@@ -20,8 +21,6 @@ export default function SnapshotDetail(
const { snapshotId } = useUnwrap(params)
const [snap, setSnap] = 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>('')
@@ -51,29 +50,20 @@ export default function SnapshotDetail(
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 hasDoc = (dt: string) => docs.some(
(e: any) => e.doc_type === dt && (e.text || e.content || '').length > 100)
const modules = useMemo(() => [
...(hasCookies ? [{ key: 'cookie', label: 'Cookies & Tracking' }] : []),
...(hasImpressum ? [{ key: 'impressum', label: 'Impressum' }] : []),
], [hasCookies, hasImpressum])
...(hasDoc('impressum') ? [{ key: 'impressum', label: 'Impressum' }] : []),
...(hasDoc('dse') ? [{ key: 'dse', label: 'Datenschutzerklärung' }] : []),
// eslint-disable-next-line react-hooks/exhaustive-deps
], [snap])
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'}`}>
@@ -108,17 +98,11 @@ export default function SnapshotDetail(
)}
{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
<AgentModuleTab snapshotId={snapshotId} docType="impressum" label="Impressum" />
)}
{tab === 'dse' && (
<AgentModuleTab snapshotId={snapshotId} docType="dse" label="Datenschutzerklärung" />
)}
</>
)}