diff --git a/src/features/control-center/pages/AuditPage.tsx b/src/features/control-center/pages/AuditPage.tsx index 7b901fa..b5c4646 100644 --- a/src/features/control-center/pages/AuditPage.tsx +++ b/src/features/control-center/pages/AuditPage.tsx @@ -4,8 +4,12 @@ import { Search, Download, ShieldCheck, + ChevronRight, + X, + Hash, } from 'lucide-react'; import { useKccStore, useOperatorRole } from '../store/kccStore'; +import { RU_AUDIT_ACTION, RU_ROLES } from '../locales/ru'; export const AuditPage: React.FC = () => { const auditLog = useKccStore((state) => state.auditLog); @@ -15,11 +19,13 @@ export const AuditPage: React.FC = () => { const [selectedEventId, setSelectedEventId] = useState(null); const filtered = auditLog.filter((item) => { + const actionRu = RU_AUDIT_ACTION[item.action] || ''; const matchSearch = !search || item.operatorName.toLowerCase().includes(search.toLowerCase()) || item.reason.toLowerCase().includes(search.toLowerCase()) || item.action.toLowerCase().includes(search.toLowerCase()) || + actionRu.toLowerCase().includes(search.toLowerCase()) || item.correlationId.toLowerCase().includes(search.toLowerCase()); return matchSearch; }); @@ -37,15 +43,15 @@ export const AuditPage: React.FC = () => { }; return ( -
+
{/* Header */} -
+
-

- - Неизменяемый журнал аудита действий операторов +

+ + Неизменяемый журнал аудита действий

-

+

Криптографическая цепочка событий (SHA-256 Hash Chain). Записи защищены от редактирования и удаления.

@@ -55,89 +61,165 @@ export const AuditPage: React.FC = () => { type="button" disabled={!permissions.canExportAudit} onClick={handleExportJson} - className="px-3 py-1.5 rounded-lg bg-zinc-900 border border-zinc-700 hover:bg-zinc-800 text-zinc-200 text-xs font-semibold flex items-center gap-1.5 transition-colors disabled:opacity-40" + className="min-h-[44px] px-4 py-2.5 rounded-lg bg-krona-surface border border-krona-border hover:bg-krona-workspace text-krona-primary text-xs font-semibold flex items-center justify-center gap-2 transition-colors disabled:opacity-40 shadow-xs" title="Экспорт неизменяемого журнала для регулятора/аудитора" > - + Экспорт JSON
{/* Verification Badge */} -
+
- - Целостность журнала аудита подтверждена: {auditLog.length} событий в цепочке хешей. + + + Целостность журнала подтверждена: {auditLog.length} событий в цепочке хешей. +
- WORM (Write Once, Read Many) + + WORM (Write Once, Read Many) +
{/* Search */}
- + setSearch(e.target.value)} - placeholder="Поиск по оператору, действию, correlation ID или обоснованию..." - className="w-full bg-[#0b1d16] border border-zinc-800 rounded-lg pl-9 pr-3 py-2 text-xs text-zinc-100 placeholder-zinc-500 focus:outline-hidden focus:border-amber-500 transition-colors" + placeholder="Поиск по оператору, действию или обоснованию..." + className="w-full bg-krona-workspace border border-krona-border rounded-lg pl-9 pr-3 py-2.5 text-xs text-krona-primary placeholder:text-krona-secondary/60 focus:outline-hidden focus:border-amber-500 transition-colors min-h-[44px]" />
- {/* Audit Log Table */} -
+ {/* Mobile Card View (block lg:hidden) */} +
+ {filtered.map((item) => { + const actionLabel = RU_AUDIT_ACTION[item.action] || item.action; + const roleLabel = RU_ROLES[item.operatorRole]?.shortLabel || item.operatorRole; + + return ( +
setSelectedEventId(item.eventId)} + className="p-4 rounded-xl border border-krona-border bg-krona-surface space-y-3 cursor-pointer shadow-xs hover:border-amber-500/50 transition-colors" + > +
+
+
{actionLabel}
+
+ {item.operatorName} • {roleLabel} +
+
+
+ {new Date(item.occurredAt).toLocaleTimeString('ru-RU', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} +
+
+ +
+
+ Объект: + + {item.target.displayName || item.target.id} + +
+
+ {item.reason} +
+
+ +
+ + + {item.hash.slice(0, 8)}...{item.hash.slice(-4)} + + + Подробнее + +
+
+ ); + })} + + {filtered.length === 0 && ( +
+ Записи аудита не найдены +
+ )} +
+ + {/* Desktop Audit Log Table (hidden lg:block) */} +
- - + + - + - + - - {filtered.map((item) => ( - setSelectedEventId(item.eventId)} - className="hover:bg-zinc-800/40 cursor-pointer transition-colors" - > - - - - - - - - - ))} + + {filtered.map((item) => { + const actionLabel = RU_AUDIT_ACTION[item.action] || item.action; + const roleLabel = RU_ROLES[item.operatorRole]?.label || item.operatorRole; + + return ( + setSelectedEventId(item.eventId)} + className="hover:bg-krona-workspace/50 cursor-pointer transition-colors" + > + + + + + + + + + ); + })} {filtered.length === 0 && ( - @@ -150,38 +232,49 @@ export const AuditPage: React.FC = () => { {/* Selected event modal / details */} {selectedEvent && (
-
-
+
+
-
{selectedEvent.eventId}
-

{selectedEvent.action}

+
{selectedEvent.eventId}
+

+ {RU_AUDIT_ACTION[selectedEvent.action] || selectedEvent.action} +

-
-
-
Обоснование действия:
-
{selectedEvent.reason}
-
- Влияние: {selectedEvent.impactSummary} +
+
+
+ Оператор: + + {selectedEvent.operatorName} ({RU_ROLES[selectedEvent.operatorRole]?.label || selectedEvent.operatorRole}) + +
+
Обоснование действия:
+
+ {selectedEvent.reason} +
+
+ Влияние: {selectedEvent.impactSummary}
-
-
Previous Hash (N-1):
-
{selectedEvent.previousHash}
-
Current Hash (SHA-256):
-
{selectedEvent.hash}
+
+
Предыдущий хеш (Previous Hash N-1):
+
{selectedEvent.previousHash}
+
Текущий хеш (SHA-256):
+
{selectedEvent.hash}
@@ -190,3 +283,4 @@ export const AuditPage: React.FC = () => {
); }; +
Время (UTC)
Время Оператор / РольДействиеДействие Целевой объектОбоснование (Reason)Обоснование Correlation ID SHA-256 Hash
- {new Date(item.occurredAt).toLocaleString('ru-RU', { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - -
{item.operatorName}
-
{item.operatorRole}
-
{item.action} - {item.target.displayName || item.target.id} - type: {item.target.type} - - {item.reason} - {item.correlationId} - {item.hash.slice(0, 10)}...{item.hash.slice(-6)} -
+ {new Date(item.occurredAt).toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + +
{item.operatorName}
+
{roleLabel}
+
+
{actionLabel}
+
{item.action}
+
+ + {item.target.displayName || item.target.id} + + + тип: {item.target.type} + + + {item.reason} + {item.correlationId} + {item.hash.slice(0, 10)}...{item.hash.slice(-6)} +
+ Записи аудита не найдены