feat(D): Phase D — Undo/Restore komplett implementiert
Check Cross-Plugin Imports / check (push) Has been cancelled

- D-GEN: RestoreRegistry mit RestoreConfig (model_class, restore_permission, excluded_fields, special_handler)
- D-HOOK: history_hooks.py mit register_history_hooks() für after_create/update/delete
- D-CORE: Company create+update record_history in companies.py
- D-PLUG: Task/Calendar/DMS record_history in services/routes
- D-SOFT: Alle registrierten Entitäten haben deleted_at + un-delete via Registry
- D-MAIL: Mail special_handler (IMAP Trash-Move, Folder-Verify) + record_history in delete/move
- D-TRASH: GET /entity-history/trash (filterbar, paginiert) + Frontend Trash.tsx
- D-TOAST: UndoToast.tsx (5s Auto-Dismiss, useUndoToast Hook)
- D-HIST-UI: HistoryPanel.tsx (Timeline, Diff-View, Restore-Button)
- D-BULK: POST /entity-history/bulk-restore mit partial_success Semantik
- D-RET: POST /entity-history/retention/archive (GDPR hard-delete >90 Tage)
- D-TEST: 26 Tests in test_restore_registry.py, alle grün
- D-DOC: test-strategy.md + security_kernel.md aktualisiert

Backend: 10 Dateien, Frontend: 7 Dateien, Tests: 1 Datei, Docs: 3 Dateien
26/26 Tests passed, TSC 0 errors, App import 492 routes
This commit is contained in:
Agent Zero
2026-08-13 23:08:29 +02:00
parent 29410f19d3
commit a4d0f0c35d
22 changed files with 2175 additions and 91 deletions
+69 -8
View File
@@ -1,5 +1,5 @@
/**
* Entity History hooks — undo/restore functionality.
* Entity History hooks — undo/restore/trash functionality.
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
@@ -10,9 +10,9 @@ export interface EntityHistoryEntry {
entity_type: string;
entity_id: string;
action: 'create' | 'update' | 'delete';
snapshot_before: Record<string, any> | null;
snapshot_after: Record<string, any> | null;
changes: Record<string, { old: any; new: any }> | null;
snapshot_before: Record<string, unknown> | null;
snapshot_after: Record<string, unknown> | null;
changes: Record<string, { old: unknown; new: unknown }> | null;
user_id: string | null;
created_at: string;
}
@@ -22,22 +22,82 @@ export interface EntityHistoryList {
total: number;
}
export function useEntityHistory(entityType?: string, entityId?: string) {
export interface TrashItem {
history_id: string;
entity_type: string;
entity_id: string;
snapshot_before: Record<string, unknown> | null;
deleted_at: string | null;
user_id: string | null;
}
export interface TrashListResponse {
items: TrashItem[];
total: number;
limit: number;
offset: number;
}
export interface BulkRestoreResultItem {
history_id: string;
success: boolean;
entity_type: string | null;
entity_id: string | null;
error: string | null;
}
export interface BulkRestoreResponse {
total: number;
succeeded: number;
failed: number;
results: BulkRestoreResultItem[];
partial_success: boolean;
}
export function useEntityHistory(entityType?: string, entityId?: string, limit = 50) {
return useQuery({
queryKey: ['entityHistory', entityType, entityId],
queryKey: ['entityHistory', entityType, entityId, limit],
queryFn: () =>
apiGet<EntityHistoryList>(`/entity-history/${entityType}/${entityId}`),
apiGet<EntityHistoryList>(`/entity-history/${entityType}/${entityId}?limit=${limit}`),
enabled: !!entityType && !!entityId,
});
}
export function useTrashList(entityType?: string, limit = 50, offset = 0) {
const params = new URLSearchParams();
params.set('limit', String(limit));
params.set('offset', String(offset));
if (entityType) {
params.set('entity_type', entityType);
}
return useQuery({
queryKey: ['trash', entityType ?? 'all', limit, offset],
queryFn: () => apiGet<TrashListResponse>(`/entity-history/trash?${params.toString()}`),
});
}
export function useRestoreFromHistory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (historyId: string) =>
apiClient.post('/entity-history/restore', { history_id: historyId }).then(r => r.data),
onSuccess: (_data, _variables, context) => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['entityHistory'] });
queryClient.invalidateQueries({ queryKey: ['trash'] });
queryClient.invalidateQueries({ queryKey: ['contacts'] });
queryClient.invalidateQueries({ queryKey: ['contact'] });
},
});
}
export function useBulkRestore() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (historyIds: string[]) =>
apiPost<BulkRestoreResponse>('/entity-history/bulk-restore', { history_ids: historyIds }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['entityHistory'] });
queryClient.invalidateQueries({ queryKey: ['trash'] });
queryClient.invalidateQueries({ queryKey: ['contacts'] });
queryClient.invalidateQueries({ queryKey: ['contact'] });
},
@@ -51,6 +111,7 @@ export function useUndoLastAction() {
apiPost(`/entity-history/undo/${entityType}/${entityId}`, {}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['entityHistory'] });
queryClient.invalidateQueries({ queryKey: ['trash'] });
queryClient.invalidateQueries({ queryKey: ['contacts'] });
queryClient.invalidateQueries({ queryKey: ['contact'] });
},