feat(iace): DSMS-CID-Badge im Tech-File-Export + aggregierter Bulk-Diff
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
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>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface BulkDiffStep {
|
||||
from: string
|
||||
from_version: string | null
|
||||
to: string
|
||||
to_version: string | null
|
||||
created_at: string | null
|
||||
kind: 'text' | 'binary'
|
||||
added_lines: number
|
||||
removed_lines: number
|
||||
metadata_diff_fields: string[]
|
||||
}
|
||||
|
||||
interface BulkDiffResponse {
|
||||
cid_latest: string
|
||||
cid_baseline: string
|
||||
versions: number
|
||||
steps: BulkDiffStep[]
|
||||
totals: {
|
||||
added_lines: number
|
||||
removed_lines: number
|
||||
metadata_fields_changed: number
|
||||
binary_steps: 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 BulkDiffPanel({ cid, onClose }: Props) {
|
||||
const [data, setData] = useState<BulkDiffResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancel = false
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
fetch(`/api/sdk/v1/dsms/documents/${encodeURIComponent(cid)}/bulk-diff`)
|
||||
.then(async (r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
const json = (await r.json()) as BulkDiffResponse
|
||||
if (!cancel) setData(json)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!cancel) setError(e?.message || 'Fehler beim Laden')
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancel) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}, [cid])
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Aggregierter Diff: V1 → V_latest
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-[11px] text-gray-500 hover:text-gray-700"
|
||||
aria-label="Bulk-Diff schliessen"
|
||||
>
|
||||
Schliessen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && <div className="text-xs text-gray-500">Bulk-Diff wird berechnet…</div>}
|
||||
{error && <div className="text-xs text-red-600 dark:text-red-400">{error}</div>}
|
||||
|
||||
{!loading && !error && data && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-center">
|
||||
<Stat label="Versionen" value={data.versions} tone="neutral" />
|
||||
<Stat label="Zeilen +" value={data.totals.added_lines} tone="positive" />
|
||||
<Stat label="Zeilen −" value={data.totals.removed_lines} tone="negative" />
|
||||
<Stat label="Metadaten-Felder" value={data.totals.metadata_fields_changed} tone="neutral" />
|
||||
</div>
|
||||
|
||||
{data.totals.binary_steps > 0 && (
|
||||
<div className="text-[11px] text-amber-700 dark:text-amber-400 italic">
|
||||
{data.totals.binary_steps} von {data.steps.length} Schritten binaer — Text-Diff nicht moeglich.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.steps.length === 0 ? (
|
||||
<div className="text-xs text-gray-500 italic">{data.note || 'Keine Vorgaengerversion vorhanden.'}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-[11px]">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-500 border-b border-gray-200 dark:border-gray-700">
|
||||
<th className="py-1 pr-2 font-medium">Schritt</th>
|
||||
<th className="py-1 pr-2 font-medium">Datum</th>
|
||||
<th className="py-1 pr-2 font-medium">Typ</th>
|
||||
<th className="py-1 pr-2 font-medium text-right">+</th>
|
||||
<th className="py-1 pr-2 font-medium text-right">−</th>
|
||||
<th className="py-1 font-medium">Metadaten-Felder</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.steps.map((step, i) => (
|
||||
<tr key={`${step.from}-${step.to}`} className="border-b border-gray-100 dark:border-gray-800">
|
||||
<td className="py-1 pr-2 text-gray-700 dark:text-gray-300">
|
||||
V{step.from_version || '?'} → V{step.to_version || '?'}
|
||||
<div className="text-[9px] font-mono text-gray-400">
|
||||
{shorten(step.from)} → {shorten(step.to)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-1 pr-2 text-gray-500">
|
||||
{step.created_at ? new Date(step.created_at).toLocaleDateString('de-DE') : '—'}
|
||||
</td>
|
||||
<td className="py-1 pr-2">
|
||||
<span
|
||||
className={
|
||||
step.kind === 'binary'
|
||||
? 'text-amber-700 dark:text-amber-400'
|
||||
: 'text-gray-700 dark:text-gray-300'
|
||||
}
|
||||
>
|
||||
{step.kind === 'binary' ? 'binaer' : 'text'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-1 pr-2 text-right text-emerald-700 dark:text-emerald-400">
|
||||
{step.kind === 'binary' ? '—' : step.added_lines}
|
||||
</td>
|
||||
<td className="py-1 pr-2 text-right text-red-700 dark:text-red-400">
|
||||
{step.kind === 'binary' ? '—' : step.removed_lines}
|
||||
</td>
|
||||
<td className="py-1 text-gray-600 dark:text-gray-400">
|
||||
{step.metadata_diff_fields.length === 0
|
||||
? '—'
|
||||
: step.metadata_diff_fields.slice(0, 3).join(', ') +
|
||||
(step.metadata_diff_fields.length > 3 ? ` (+${step.metadata_diff_fields.length - 3})` : '')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Stat({ label, value, tone }: { label: string; value: number; tone: 'positive' | 'negative' | 'neutral' }) {
|
||||
const color =
|
||||
tone === 'positive'
|
||||
? 'text-emerald-700 dark:text-emerald-400'
|
||||
: tone === 'negative'
|
||||
? 'text-red-700 dark:text-red-400'
|
||||
: 'text-gray-800 dark:text-gray-200'
|
||||
return (
|
||||
<div className="bg-gray-50 dark:bg-gray-900/40 rounded p-2 border border-gray-200 dark:border-gray-700">
|
||||
<div className={`text-base font-semibold ${color}`}>{value.toLocaleString('de-DE')}</div>
|
||||
<div className="text-[10px] uppercase tracking-wide text-gray-500">{label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user