feat(control-center): adapt AuditPage styling and token integration

This commit is contained in:
ochenstarik 2026-09-11 09:38:47 +07:00
parent b8739a5096
commit 3922e577f3

View file

@ -4,8 +4,12 @@ import {
Search, Search,
Download, Download,
ShieldCheck, ShieldCheck,
ChevronRight,
X,
Hash,
} from 'lucide-react'; } from 'lucide-react';
import { useKccStore, useOperatorRole } from '../store/kccStore'; import { useKccStore, useOperatorRole } from '../store/kccStore';
import { RU_AUDIT_ACTION, RU_ROLES } from '../locales/ru';
export const AuditPage: React.FC = () => { export const AuditPage: React.FC = () => {
const auditLog = useKccStore((state) => state.auditLog); const auditLog = useKccStore((state) => state.auditLog);
@ -15,11 +19,13 @@ export const AuditPage: React.FC = () => {
const [selectedEventId, setSelectedEventId] = useState<string | null>(null); const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
const filtered = auditLog.filter((item) => { const filtered = auditLog.filter((item) => {
const actionRu = RU_AUDIT_ACTION[item.action] || '';
const matchSearch = const matchSearch =
!search || !search ||
item.operatorName.toLowerCase().includes(search.toLowerCase()) || item.operatorName.toLowerCase().includes(search.toLowerCase()) ||
item.reason.toLowerCase().includes(search.toLowerCase()) || item.reason.toLowerCase().includes(search.toLowerCase()) ||
item.action.toLowerCase().includes(search.toLowerCase()) || item.action.toLowerCase().includes(search.toLowerCase()) ||
actionRu.toLowerCase().includes(search.toLowerCase()) ||
item.correlationId.toLowerCase().includes(search.toLowerCase()); item.correlationId.toLowerCase().includes(search.toLowerCase());
return matchSearch; return matchSearch;
}); });
@ -37,15 +43,15 @@ export const AuditPage: React.FC = () => {
}; };
return ( return (
<div className="space-y-6"> <div className="space-y-4 md:space-y-6">
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-zinc-800 pb-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-krona-border pb-4">
<div> <div>
<h1 className="text-xl font-bold text-zinc-100 flex items-center gap-2"> <h1 className="text-lg md:text-xl font-bold text-krona-primary flex items-center gap-2">
<FileCheck2 className="w-5 h-5 text-amber-400" /> <FileCheck2 className="w-5 h-5 text-amber-500 shrink-0" />
<span>Неизменяемый журнал аудита действий операторов</span> <span>Неизменяемый журнал аудита действий</span>
</h1> </h1>
<p className="text-xs text-zinc-400 mt-0.5"> <p className="text-xs text-krona-secondary mt-0.5">
Криптографическая цепочка событий (SHA-256 Hash Chain). Записи защищены от редактирования и удаления. Криптографическая цепочка событий (SHA-256 Hash Chain). Записи защищены от редактирования и удаления.
</p> </p>
</div> </div>
@ -55,59 +61,127 @@ export const AuditPage: React.FC = () => {
type="button" type="button"
disabled={!permissions.canExportAudit} disabled={!permissions.canExportAudit}
onClick={handleExportJson} 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="Экспорт неизменяемого журнала для регулятора/аудитора" title="Экспорт неизменяемого журнала для регулятора/аудитора"
> >
<Download className="w-3.5 h-3.5" /> <Download className="w-4 h-4" />
<span>Экспорт JSON</span> <span>Экспорт JSON</span>
</button> </button>
</div> </div>
</div> </div>
{/* Verification Badge */} {/* Verification Badge */}
<div className="rounded-xl border border-emerald-500/40 bg-emerald-950/20 p-3.5 flex items-center justify-between text-xs text-emerald-300"> <div className="rounded-xl border border-emerald-500/40 bg-emerald-500/10 p-3.5 flex flex-col sm:flex-row sm:items-center justify-between gap-2 text-xs text-emerald-600 dark:text-emerald-300">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-emerald-400" /> <ShieldCheck className="w-5 h-5 text-emerald-500 shrink-0" />
<span>Целостность журнала аудита подтверждена: <strong>{auditLog.length} событий</strong> в цепочке хешей.</span> <span>
Целостность журнала подтверждена: <strong>{auditLog.length} событий</strong> в цепочке хешей.
</span>
</div> </div>
<span className="font-mono text-[11px] text-emerald-400/80">WORM (Write Once, Read Many)</span> <span className="font-mono text-[11px] text-emerald-600/80 dark:text-emerald-400/80">
WORM (Write Once, Read Many)
</span>
</div> </div>
{/* Search */} {/* Search */}
<div className="relative max-w-md"> <div className="relative max-w-md">
<Search className="w-4 h-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" /> <Search className="w-4 h-4 text-krona-secondary absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
<input <input
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="Поиск по оператору, действию, correlation ID или обоснованию..." placeholder="Поиск по оператору, действию или обоснованию..."
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" 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]"
/> />
</div> </div>
{/* Audit Log Table */} {/* Mobile Card View (block lg:hidden) */}
<div className="rounded-xl border border-zinc-800 bg-[#091a13] overflow-hidden shadow-xl"> <div className="block lg:hidden space-y-3">
{filtered.map((item) => {
const actionLabel = RU_AUDIT_ACTION[item.action] || item.action;
const roleLabel = RU_ROLES[item.operatorRole]?.shortLabel || item.operatorRole;
return (
<div
key={item.eventId}
onClick={() => 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"
>
<div className="flex justify-between items-start gap-2 border-b border-krona-border pb-2.5">
<div>
<div className="font-bold text-sm text-krona-primary">{actionLabel}</div>
<div className="text-[11px] text-krona-secondary font-mono mt-0.5">
{item.operatorName} • <span className="text-amber-500 font-semibold">{roleLabel}</span>
</div>
</div>
<div className="text-right text-[11px] text-krona-secondary font-mono">
{new Date(item.occurredAt).toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
</div>
</div>
<div className="text-xs text-krona-secondary space-y-1">
<div className="flex items-center gap-1.5">
<span className="text-krona-secondary/70">Объект:</span>
<span className="font-medium text-krona-primary">
{item.target.displayName || item.target.id}
</span>
</div>
<div className="line-clamp-2 text-krona-primary/90 bg-krona-workspace p-2 rounded-lg border border-krona-border">
{item.reason}
</div>
</div>
<div className="pt-2 border-t border-krona-border flex items-center justify-between text-[11px] text-krona-secondary">
<span className="font-mono text-xs text-krona-secondary flex items-center gap-1">
<Hash className="w-3 h-3 text-amber-500" />
{item.hash.slice(0, 8)}...{item.hash.slice(-4)}
</span>
<span className="text-amber-500 font-medium inline-flex items-center gap-0.5">
Подробнее <ChevronRight className="w-3.5 h-3.5" />
</span>
</div>
</div>
);
})}
{filtered.length === 0 && (
<div className="p-8 rounded-xl border border-krona-border bg-krona-surface text-center text-xs text-krona-secondary">
Записи аудита не найдены
</div>
)}
</div>
{/* Desktop Audit Log Table (hidden lg:block) */}
<div className="hidden lg:block rounded-xl border border-krona-border bg-krona-surface overflow-hidden shadow-xs">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse"> <table className="w-full text-left text-xs border-collapse">
<thead> <thead>
<tr className="border-b border-zinc-800 bg-zinc-950/70 text-zinc-400 font-semibold uppercase tracking-wider text-[11px]"> <tr className="border-b border-krona-border bg-krona-workspace text-krona-secondary font-semibold uppercase tracking-wider text-[11px]">
<th className="py-3 px-4">Время (UTC)</th> <th className="py-3 px-4">Время</th>
<th className="py-3 px-3">Оператор / Роль</th> <th className="py-3 px-3">Оператор / Роль</th>
<th className="py-3 px-3 font-mono">Действие</th> <th className="py-3 px-3">Действие</th>
<th className="py-3 px-3">Целевой объект</th> <th className="py-3 px-3">Целевой объект</th>
<th className="py-3 px-3">Обоснование (Reason)</th> <th className="py-3 px-3">Обоснование</th>
<th className="py-3 px-3 font-mono">Correlation ID</th> <th className="py-3 px-3 font-mono">Correlation ID</th>
<th className="py-3 px-4 text-right font-mono">SHA-256 Hash</th> <th className="py-3 px-4 text-right font-mono">SHA-256 Hash</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-zinc-800/60"> <tbody className="divide-y divide-krona-border/60">
{filtered.map((item) => ( {filtered.map((item) => {
const actionLabel = RU_AUDIT_ACTION[item.action] || item.action;
const roleLabel = RU_ROLES[item.operatorRole]?.label || item.operatorRole;
return (
<tr <tr
key={item.eventId} key={item.eventId}
onClick={() => setSelectedEventId(item.eventId)} onClick={() => setSelectedEventId(item.eventId)}
className="hover:bg-zinc-800/40 cursor-pointer transition-colors" className="hover:bg-krona-workspace/50 cursor-pointer transition-colors"
> >
<td className="py-3 px-4 font-mono text-zinc-400 text-[11px] whitespace-nowrap"> <td className="py-3 px-4 font-mono text-krona-secondary text-[11px] whitespace-nowrap">
{new Date(item.occurredAt).toLocaleString('ru-RU', { {new Date(item.occurredAt).toLocaleString('ru-RU', {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
@ -117,27 +191,35 @@ export const AuditPage: React.FC = () => {
})} })}
</td> </td>
<td className="py-3 px-3"> <td className="py-3 px-3">
<div className="font-bold text-zinc-200">{item.operatorName}</div> <div className="font-bold text-krona-primary">{item.operatorName}</div>
<div className="text-[10px] text-amber-400/80 font-mono uppercase">{item.operatorRole}</div> <div className="text-[10px] text-amber-500 font-medium">{roleLabel}</div>
</td> </td>
<td className="py-3 px-3 font-mono text-amber-300 text-[11px]">{item.action}</td> <td className="py-3 px-3">
<td className="py-3 px-3 text-zinc-300"> <div className="font-semibold text-krona-primary text-xs">{actionLabel}</div>
<span className="font-semibold">{item.target.displayName || item.target.id}</span> <div className="font-mono text-[10px] text-krona-secondary">{item.action}</div>
<span className="text-[10px] text-zinc-500 block font-mono">type: {item.target.type}</span>
</td> </td>
<td className="py-3 px-3 text-zinc-200 max-w-xs truncate" title={item.reason}> <td className="py-3 px-3 text-krona-secondary">
<span className="font-semibold text-krona-primary">
{item.target.displayName || item.target.id}
</span>
<span className="text-[10px] text-krona-secondary block font-mono">
тип: {item.target.type}
</span>
</td>
<td className="py-3 px-3 text-krona-primary max-w-xs truncate" title={item.reason}>
{item.reason} {item.reason}
</td> </td>
<td className="py-3 px-3 font-mono text-zinc-400 text-[11px]">{item.correlationId}</td> <td className="py-3 px-3 font-mono text-krona-secondary text-[11px]">{item.correlationId}</td>
<td className="py-3 px-4 text-right font-mono text-zinc-500 text-[10px]" title={item.hash}> <td className="py-3 px-4 text-right font-mono text-krona-secondary text-[10px]" title={item.hash}>
{item.hash.slice(0, 10)}...{item.hash.slice(-6)} {item.hash.slice(0, 10)}...{item.hash.slice(-6)}
</td> </td>
</tr> </tr>
))} );
})}
{filtered.length === 0 && ( {filtered.length === 0 && (
<tr> <tr>
<td colSpan={7} className="py-8 text-center text-zinc-500"> <td colSpan={7} className="py-8 text-center text-krona-secondary">
Записи аудита не найдены Записи аудита не найдены
</td> </td>
</tr> </tr>
@ -150,38 +232,49 @@ export const AuditPage: React.FC = () => {
{/* Selected event modal / details */} {/* Selected event modal / details */}
{selectedEvent && ( {selectedEvent && (
<div <div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4" className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs p-4 animate-in fade-in"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
> >
<div className="w-full max-w-xl rounded-xl border border-zinc-700 bg-[#0d1e17] p-6 text-zinc-200 space-y-4 shadow-2xl"> <div className="w-full max-w-xl rounded-xl border border-krona-border bg-krona-surface p-5 md:p-6 text-krona-primary space-y-4 shadow-2xl">
<div className="flex justify-between items-start border-b border-zinc-800 pb-3"> <div className="flex justify-between items-start border-b border-krona-border pb-3">
<div> <div>
<div className="text-xs font-mono text-amber-400">{selectedEvent.eventId}</div> <div className="text-xs font-mono text-amber-500">{selectedEvent.eventId}</div>
<h3 className="text-base font-bold text-zinc-100">{selectedEvent.action}</h3> <h3 className="text-base font-bold text-krona-primary">
{RU_AUDIT_ACTION[selectedEvent.action] || selectedEvent.action}
</h3>
</div> </div>
<button <button
onClick={() => setSelectedEventId(null)} onClick={() => setSelectedEventId(null)}
className="text-zinc-400 hover:text-zinc-200 text-xs px-2 py-1 rounded-md bg-zinc-800" className="min-w-[44px] min-h-[44px] flex items-center justify-center text-krona-secondary hover:text-krona-primary rounded-lg hover:bg-krona-workspace transition-colors"
aria-label="Закрыть"
> >
Закрыть <X className="w-5 h-5" />
</button> </button>
</div> </div>
<div className="space-y-2 text-xs"> <div className="space-y-3 text-xs">
<div className="p-3 rounded-lg bg-zinc-950 border border-zinc-800 space-y-1.5"> <div className="p-3.5 rounded-lg bg-krona-workspace border border-krona-border space-y-2">
<div className="text-zinc-400">Обоснование действия:</div> <div className="flex justify-between text-krona-secondary">
<div className="font-medium text-zinc-100">{selectedEvent.reason}</div> <span>Оператор:</span>
<div className="text-zinc-400 text-[11px] pt-1 border-t border-zinc-850"> <span className="font-semibold text-krona-primary">
Влияние: <span className="text-amber-300">{selectedEvent.impactSummary}</span> {selectedEvent.operatorName} ({RU_ROLES[selectedEvent.operatorRole]?.label || selectedEvent.operatorRole})
</span>
</div>
<div className="text-krona-secondary">Обоснование действия:</div>
<div className="font-medium text-krona-primary p-2 bg-krona-surface rounded-md border border-krona-border">
{selectedEvent.reason}
</div>
<div className="text-krona-secondary text-[11px] pt-1 border-t border-krona-border">
Влияние: <span className="text-amber-500 font-medium">{selectedEvent.impactSummary}</span>
</div> </div>
</div> </div>
<div className="p-3 rounded-lg bg-zinc-950 border border-zinc-800 space-y-1 font-mono text-[11px]"> <div className="p-3.5 rounded-lg bg-krona-workspace border border-krona-border space-y-1.5 font-mono text-[11px]">
<div className="text-zinc-400">Previous Hash (N-1):</div> <div className="text-krona-secondary">Предыдущий хеш (Previous Hash N-1):</div>
<div className="text-zinc-500 break-all">{selectedEvent.previousHash}</div> <div className="text-krona-secondary/80 break-all text-[10px]">{selectedEvent.previousHash}</div>
<div className="text-zinc-400 pt-1">Current Hash (SHA-256):</div> <div className="text-krona-secondary pt-1">Текущий хеш (SHA-256):</div>
<div className="text-emerald-400 break-all font-bold">{selectedEvent.hash}</div> <div className="text-emerald-600 dark:text-emerald-400 break-all font-bold text-[10px]">{selectedEvent.hash}</div>
</div> </div>
</div> </div>
</div> </div>
@ -190,3 +283,4 @@ export const AuditPage: React.FC = () => {
</div> </div>
); );
}; };