216c7b8eca
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 10s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m21s
CI / test-go (push) Failing after 37s
CI / iace-gt-coverage (push) Successful in 23s
CI / test-python-backend (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Successful in 17s
Punkt 1 — UI-CID-Badge nach erfolgreichem Tech-File-Export:
- archiveTechFile setzt X-DSMS-CID / X-DSMS-Filename / X-DSMS-Size response
headers + Access-Control-Expose-Headers, sobald DSMS-Archive durchlief
- Split iace_handler_techfile.go (war ueber 500 LOC) → archiveTechFile lebt
jetzt in iace_handler_techfile_archive.go, setDSMSResponseHeaders als
pure Helper mit 3 unit tests
- Next.js IACE-Proxy forwarded die X-DSMS-* Header und erkennt jetzt auch
XLSX/DOCX/MD als Binary-Response (vorher nur PDF/ZIP/octet-stream)
- ExportCIDBadge.tsx zeigt CID, Filename, Groesse + Kopieren-Button +
"Verlauf anzeigen" (oeffnet CIDHistoryModal)
Punkt 2 — Bulk-Diff Report V1 → V_latest:
- Neuer Endpoint GET /api/v1/documents/{cid}/bulk-diff im dsms-gateway:
laeuft parent_cid-Kette ab, berechnet chronologische Step-Diffs,
aggregiert Totals (added/removed lines, metadata_fields_changed,
binary_steps). Edge-Cases: einzelne Version, binaere Steps, abgebrochene
Kette
- BulkDiffPanel.tsx zeigt 4-Stat-Header + Step-Tabelle
- CIDHistoryModal bekommt Toggle-Button "Bulk-Diff V1 → V_latest anzeigen"
neben dem Versions-Counter; damit auch vom IACE-Export-Badge erreichbar
Tests: 3 neue Go-Tests, 4 neue pytest-Tests, alle gruen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
223 lines
9.6 KiB
TypeScript
223 lines
9.6 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import BulkDiffPanel from './BulkDiffPanel'
|
|
|
|
interface HistoryEntry {
|
|
cid: string
|
|
version: string | null
|
|
document_type: string | null
|
|
document_id: string | null
|
|
parent_cid: string | null
|
|
created_at: string | null
|
|
checksum: string | null
|
|
}
|
|
|
|
interface DiffResponse {
|
|
kind: 'text' | 'binary'
|
|
cid_a: string
|
|
cid_b: string
|
|
metadata_diff: Record<string, { old: unknown; new: unknown }>
|
|
diff?: string
|
|
added_lines?: number
|
|
removed_lines?: number
|
|
note?: string
|
|
}
|
|
|
|
interface Props {
|
|
cid: string
|
|
onClose: () => void
|
|
}
|
|
|
|
function shorten(cid: string): string {
|
|
if (cid.length <= 14) return cid
|
|
return cid.slice(0, 8) + '…' + cid.slice(-6)
|
|
}
|
|
|
|
export default function CIDHistoryModal({ cid, onClose }: Props) {
|
|
const [history, setHistory] = useState<HistoryEntry[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [diffPair, setDiffPair] = useState<{ a: string; b: string } | null>(null)
|
|
const [diff, setDiff] = useState<DiffResponse | null>(null)
|
|
const [diffLoading, setDiffLoading] = useState(false)
|
|
const [showBulkDiff, setShowBulkDiff] = useState(false)
|
|
|
|
useEffect(() => {
|
|
let cancel = false
|
|
setLoading(true)
|
|
setError(null)
|
|
fetch(`/api/sdk/v1/dsms/documents/${encodeURIComponent(cid)}/history`)
|
|
.then(async (r) => {
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
const json = await r.json()
|
|
if (!cancel) setHistory(json.history || [])
|
|
})
|
|
.catch((e) => {
|
|
if (!cancel) setError(e?.message || 'Fehler beim Laden')
|
|
})
|
|
.finally(() => {
|
|
if (!cancel) setLoading(false)
|
|
})
|
|
return () => {
|
|
cancel = true
|
|
}
|
|
}, [cid])
|
|
|
|
async function loadDiff(a: string, b: string) {
|
|
setDiffPair({ a, b })
|
|
setDiff(null)
|
|
setDiffLoading(true)
|
|
try {
|
|
const res = await fetch(
|
|
`/api/sdk/v1/dsms/documents/${encodeURIComponent(a)}/diff/${encodeURIComponent(b)}`
|
|
)
|
|
if (res.ok) {
|
|
const json = (await res.json()) as DiffResponse
|
|
setDiff(json)
|
|
} else {
|
|
setDiff({ kind: 'binary', cid_a: a, cid_b: b, metadata_diff: {}, note: `HTTP ${res.status}` })
|
|
}
|
|
} finally {
|
|
setDiffLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
|
|
<div
|
|
className="w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col bg-white dark:bg-gray-800 rounded-xl shadow-xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
<div>
|
|
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">DSMS-Versionsverlauf</h2>
|
|
<code className="text-[10px] font-mono text-gray-500 dark:text-gray-400">{shorten(cid)}</code>
|
|
</div>
|
|
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 text-sm">
|
|
Schliessen
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-5 space-y-4">
|
|
{loading && <div className="text-sm text-gray-500">Verlauf wird geladen…</div>}
|
|
{error && <div className="text-sm text-red-600 dark:text-red-400">{error}</div>}
|
|
|
|
{!loading && !error && history.length === 0 && (
|
|
<div className="text-sm text-gray-500 italic">
|
|
Kein Versionsverlauf gefunden. Diese CID hat keine parent_cid-Kette.
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && history.length > 0 && (
|
|
<>
|
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
{history.length} Version{history.length > 1 ? 'en' : ''} in der Kette (neueste oben).
|
|
</div>
|
|
{history.length > 1 && (
|
|
<button
|
|
onClick={() => setShowBulkDiff((v) => !v)}
|
|
className="text-[11px] px-2 py-1 rounded border border-purple-300 text-purple-700 hover:bg-purple-50 dark:border-purple-700 dark:text-purple-300 dark:hover:bg-purple-900/30"
|
|
title="Aggregierter Diff ueber alle Versionen"
|
|
>
|
|
{showBulkDiff ? 'Bulk-Diff ausblenden' : `Bulk-Diff V1 → V${history[0].version || '?'} anzeigen`}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{showBulkDiff && <BulkDiffPanel cid={cid} onClose={() => setShowBulkDiff(false)} />}
|
|
<ol className="relative border-l-2 border-emerald-500/40 pl-4 space-y-3">
|
|
{history.map((entry, idx) => {
|
|
const next = history[idx + 1]
|
|
return (
|
|
<li key={entry.cid} className="relative">
|
|
<div className="absolute -left-[1.4rem] top-1.5 w-3 h-3 rounded-full bg-emerald-500 ring-2 ring-white dark:ring-gray-800" />
|
|
<div className="bg-gray-50 dark:bg-gray-900/40 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
|
Version {entry.version || '?'} {idx === 0 && <span className="ml-2 text-[10px] text-emerald-600 font-semibold">AKTUELL</span>}
|
|
</div>
|
|
<code className="text-[10px] font-mono text-gray-500 dark:text-gray-400 break-all">{entry.cid}</code>
|
|
</div>
|
|
{next && (
|
|
<button
|
|
onClick={() => loadDiff(next.cid, entry.cid)}
|
|
className="shrink-0 text-[11px] text-purple-600 hover:text-purple-800 dark:text-purple-400 hover:underline"
|
|
title="Aenderungen zur Vorversion anzeigen"
|
|
>
|
|
Diff zu V{next.version || '?'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400 flex flex-wrap gap-x-3 gap-y-0.5">
|
|
{entry.document_type && <span>Typ: {entry.document_type}</span>}
|
|
{entry.document_id && <span>Dok-ID: {entry.document_id}</span>}
|
|
{entry.created_at && <span>{new Date(entry.created_at).toLocaleString('de-DE')}</span>}
|
|
</div>
|
|
{entry.checksum && (
|
|
<div className="mt-1 text-[10px] text-gray-400 font-mono">SHA-256: {entry.checksum.slice(0, 16)}…</div>
|
|
)}
|
|
</div>
|
|
</li>
|
|
)
|
|
})}
|
|
</ol>
|
|
</>
|
|
)}
|
|
|
|
{diffPair && (
|
|
<div className="mt-4 border-t border-gray-200 dark:border-gray-700 pt-4 space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-semibold text-gray-900 dark:text-white">
|
|
Diff: {shorten(diffPair.a)} → {shorten(diffPair.b)}
|
|
</h3>
|
|
<button onClick={() => { setDiff(null); setDiffPair(null) }} className="text-[11px] text-gray-500 hover:text-gray-700">
|
|
Schliessen
|
|
</button>
|
|
</div>
|
|
{diffLoading && <div className="text-xs text-gray-500">Diff wird geladen…</div>}
|
|
{!diffLoading && diff && (
|
|
<>
|
|
{Object.keys(diff.metadata_diff || {}).length > 0 && (
|
|
<div className="text-xs">
|
|
<div className="font-medium text-gray-700 dark:text-gray-300 mb-1">Metadaten-Aenderungen</div>
|
|
<table className="w-full">
|
|
<tbody>
|
|
{Object.entries(diff.metadata_diff).map(([field, { old, new: nv }]) => (
|
|
<tr key={field} className="border-b border-gray-100 dark:border-gray-800">
|
|
<td className="py-0.5 pr-2 font-mono text-[10px] text-gray-500">{field}</td>
|
|
<td className="py-0.5 pr-2 text-red-600 dark:text-red-400 line-through">{JSON.stringify(old)}</td>
|
|
<td className="py-0.5 text-green-700 dark:text-green-400">{JSON.stringify(nv)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
{diff.kind === 'text' && diff.diff && (
|
|
<>
|
|
<div className="text-[11px] text-gray-500">
|
|
{diff.added_lines ?? 0} Zeilen hinzu, {diff.removed_lines ?? 0} entfernt
|
|
</div>
|
|
<pre className="text-[10px] font-mono whitespace-pre-wrap bg-gray-900 text-gray-100 p-3 rounded max-h-64 overflow-y-auto">
|
|
{diff.diff}
|
|
</pre>
|
|
</>
|
|
)}
|
|
{diff.kind === 'binary' && (
|
|
<div className="text-xs text-amber-700 dark:text-amber-400 italic">
|
|
{diff.note || 'Binaere Datei — kein Text-Diff verfuegbar.'}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|