feat(dsms): version chain history + diff endpoint + Audit Timeline UI
DSMS Stufe 3 — making the parent_cid chain useful end-to-end.
Gateway (dsms-gateway):
- /api/v1/documents/{cid}/history alias added next to the legacy
/documents/{cid}/history (history endpoint itself was already there,
just under an inconsistent prefix).
- NEW /api/v1/documents/{cid_a}/diff/{cid_b}: fetches both packages from
IPFS, computes a metadata diff (per-field old/new), and renders a
unified text diff for utf-8 payloads. Binary payloads return only
metadata diff with a "binary — compare via rendered export" note.
- 4 new pytest cases (mocking ipfs_cat): text diff, binary fallback,
fetch error, history chain depth — all green.
Frontend (admin-compliance):
- CIDHistoryModal: lazy-loads /dsms/documents/:cid/history, renders the
version chain as a vertical timeline, marks the AKTUELL entry, and
per-step exposes a "Diff zu V<n>" button that loads + renders the diff
inline (metadata table + unified text diff in a monospace panel).
- AuditTimelinePage: existing CID badge now sits next to a "Verlauf
anzeigen" link that opens the modal. Handles both Python's plain-CID
audit values and the Go techfile flow's JSON envelope {cid, filename,
size} via extractCID() helper.
This makes "show me how this CE-Akte changed between V2 and V3"
self-service in the UI instead of a curl-against-IPFS workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useAuditTimeline, type AuditEntry } from './_hooks/useAuditTimeline'
|
||||
import CIDHistoryModal from './_components/CIDHistoryModal'
|
||||
|
||||
const ENTITY_LABELS: Record<string, string> = {
|
||||
evidence: 'Nachweis', control: 'Control', document: 'Dokument',
|
||||
@@ -16,8 +18,24 @@ const ACTION_COLORS: Record<string, string> = {
|
||||
|
||||
const FILTER_OPTIONS = ['all', 'evidence', 'dsms_archive', 'control', 'document', 'dsfa', 'vvt', 'tom']
|
||||
|
||||
// new_value may be a plain CID (from Python evidence flow) or a JSON envelope
|
||||
// {"cid":"X","filename":"...","size":"..."} (from the Go IACE tech-file flow).
|
||||
function extractCID(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed.startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
if (typeof parsed.cid === 'string') return parsed.cid
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
export default function AuditTimelinePage() {
|
||||
const { entries, loading, filter, setFilter } = useAuditTimeline()
|
||||
const [historyCID, setHistoryCID] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
@@ -58,16 +76,18 @@ export default function AuditTimelinePage() {
|
||||
|
||||
<div className="space-y-4">
|
||||
{entries.map((entry) => (
|
||||
<TimelineEntry key={entry.id} entry={entry} />
|
||||
<TimelineEntry key={entry.id} entry={entry} onShowHistory={setHistoryCID} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{historyCID && <CIDHistoryModal cid={historyCID} onClose={() => setHistoryCID(null)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineEntry({ entry }: { entry: AuditEntry }) {
|
||||
function TimelineEntry({ entry, onShowHistory }: { entry: AuditEntry; onShowHistory: (cid: string) => void }) {
|
||||
const dotColor = ACTION_COLORS[entry.action] || 'bg-gray-400'
|
||||
const isCID = entry.field_changed === 'dsms_cid' || entry.action === 'archive'
|
||||
const date = new Date(entry.performed_at)
|
||||
@@ -94,7 +114,7 @@ function TimelineEntry({ entry }: { entry: AuditEntry }) {
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">{entry.change_summary}</p>
|
||||
)}
|
||||
{isCID && entry.new_value && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="mt-2 flex items-center gap-2 flex-wrap">
|
||||
<svg className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
@@ -102,6 +122,16 @@ function TimelineEntry({ entry }: { entry: AuditEntry }) {
|
||||
{entry.new_value.length > 20 ? entry.new_value.slice(0, 8) + '...' + entry.new_value.slice(-6) : entry.new_value}
|
||||
</code>
|
||||
<span className="text-[10px] text-emerald-500">DSMS/IPFS</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (entry.new_value) onShowHistory(extractCID(entry.new_value))
|
||||
}}
|
||||
className="text-[10px] text-purple-600 hover:text-purple-800 dark:text-purple-400 underline-offset-2 hover:underline"
|
||||
title="DSMS-Versionsverlauf und Diff zur Vorversion anzeigen"
|
||||
>
|
||||
Verlauf anzeigen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user