grantforge-api / frontend-react /src /pages /AdminDashboard.tsx
GrantForge Bot
Deploy to Hugging Face
3b7f713
import React, { useEffect, useState, useRef, useMemo } from 'react';
import { useAuth } from '@clerk/clerk-react';
import { apiClient, getAdminStats } from '../api/client';
import { toast } from 'react-hot-toast';
import {
Activity, Users, Folder, FileText, TerminalSquare, Wrench,
Pause, Play, Search, Trash2, ShieldAlert, Zap, Server,
CheckCircle2, Database, Trash, Cpu, ShieldCheck, DatabaseZap, AlertTriangle
} from 'lucide-react';
import { SnapshotDashboard } from '../components/project/SnapshotDashboard';
import { motion, AnimatePresence } from 'framer-motion';
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
BarChart, Bar, Cell
} from 'recharts';
import '../styles/admin.css';
interface AdminStats {
status: string;
database: {
total_projects: number;
total_users: number;
total_generated_sections: number;
};
generator: {
active_tasks_count: number;
active_tasks: string[];
subscribers: Record<string, number>;
};
recent_projects?: {
id: string;
title: string;
created_at: string;
has_final_document: boolean;
has_audit: boolean;
overall_score?: number;
}[];
throughput?: any;
}
type TabType = 'overview' | 'telemetry' | 'tools' | 'regulation';
type LogLevel = 'INFO' | 'WARNING' | 'ERROR' | 'DEBUG';
const systemThroughputData = [
{ time: '00:00', load: 12 }, { time: '02:00', load: 18 },
{ time: '04:00', load: 15 }, { time: '06:00', load: 25 },
{ time: '08:00', load: 45 }, { time: '10:00', load: 60 },
{ time: '12:00', load: 85 }, { time: '14:00', load: 75 },
{ time: '16:00', load: 90 }, { time: '18:00', load: 65 },
{ time: '20:00', load: 40 }, { time: '22:00', load: 20 },
];
const AdminDashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('overview');
const [lawHistory, setLawHistory] = useState<any[]>([]);
const { getToken } = useAuth();
const [stats, setStats] = useState<AdminStats | null>(null);
const [loading, setLoading] = useState(true);
const [logs, setLogs] = useState<any[]>([]);
const [isPaused, setIsPaused] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [activeLevels, setActiveLevels] = useState<LogLevel[]>(['INFO', 'WARNING', 'ERROR']);
const logsEndRef = useRef<HTMLDivElement>(null);
const isPausedRef = useRef(isPaused);
const [isRunningCritic, setIsRunningCritic] = useState(false);
const [targetProjectId, setTargetProjectId] = useState('');
const [isSyncingRag, setIsSyncingRag] = useState(false);
const [ragCategory, setRagCategory] = useState('');
const [isClearingCache, setIsClearingCache] = useState(false);
const [serviceStatus, setServiceStatus] = useState<any>(null);
const [isCheckingStatus, setIsCheckingStatus] = useState(false);
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
// Cycle 16: Load law change history for in-app notifications
useEffect(() => {
fetch('/api/admin/law-monitoring/history')
.then(r => r.json())
.then(data => {
if (data.history) setLawHistory(data.history);
})
.catch(() => {});
}, []);
const fetchStats = async () => {
try {
const data = await getAdminStats();
setStats(data);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 5000);
let sse: EventSource;
const setupSSE = async () => {
const token = await getToken();
const baseURL = import.meta.env.VITE_API_URL
? import.meta.env.VITE_API_URL.replace('/api', '')
: 'http://localhost:8000';
sse = new EventSource(`${baseURL}/api/admin/diagnostics/stream?token=${token}`);
sse.addEventListener('telemetry_log', (e) => {
if (isPausedRef.current) return;
try {
const log = JSON.parse(e.data);
setLogs(prev => {
const newLogs = [...prev, log];
return newLogs.slice(-300);
});
} catch (err) {
console.error("Failed to parse log", err);
}
});
};
setupSSE();
return () => {
clearInterval(interval);
if (sse) sse.close();
};
}, [getToken]);
useEffect(() => {
if (!isPaused) {
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [logs, isPaused]);
const toggleLogLevel = (level: LogLevel) => {
setActiveLevels(prev =>
prev.includes(level) ? prev.filter(l => l !== level) : [...prev, level]
);
};
const filteredLogs = useMemo(() => {
return logs.filter(log => {
if (!activeLevels.includes(log.level as LogLevel)) return false;
if (searchQuery && !log.message?.toLowerCase().includes(searchQuery.toLowerCase()) &&
!log.agent?.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
return true;
});
}, [logs, activeLevels, searchQuery]);
const handleRunGlobalCritic = async () => {
if (!targetProjectId.trim()) {
toast.error('Proszę wprowadzić ID projektu');
return;
}
setIsRunningCritic(true);
const loadingToast = toast.loading('Uruchamianie analizy Global Critic...', {
style: { background: '#1e1b4b', color: '#c7d2fe', border: '1px solid #3730a3' }
});
try {
const { data } = await apiClient.post(`/api/projects/${targetProjectId.trim()}/holistic-review`);
toast.dismiss(loadingToast);
if (data.status === 'pending') {
toast.success(`Rozpoczęto analizę w tle. Status: w toku.`, { duration: 5000, style: { background: '#064e3b', color: '#6ee7b7' } });
} else if (data.is_approved !== undefined) {
if (data.is_approved) {
toast.success(`Zatwierdzono: ${data.feedback || ''}`, { duration: 5000, style: { background: '#064e3b', color: '#6ee7b7' } });
} else {
toast.error(`Odrzucono [${data.severity || 'high'}]: ${data.feedback || ''}`, { duration: 8000, style: { background: '#4c1d95', color: '#c4b5fd' } });
}
} else {
toast.success(`Zlecono pomyślnie.`, { duration: 5000, style: { background: '#064e3b', color: '#6ee7b7' } });
}
} catch (err: any) {
console.error(err);
toast.error(err.response?.data?.detail || 'Wystąpił błąd podczas analizy', { id: loadingToast });
} finally {
setIsRunningCritic(false);
}
};
const handleSyncRag = async () => {
setIsSyncingRag(true);
const loadingToast = toast.loading('Synchronizacja Bazy Wiedzy...');
try {
await apiClient.post('/api/rag/sync', { category: ragCategory || 'SMART' });
toast.success('Synchronizacja RAG zakończona', { id: loadingToast });
} catch (err: any) {
console.error(err);
toast.error(err.response?.data?.detail || 'Synchronizacja RAG nie powiodła się', { id: loadingToast });
} finally {
setIsSyncingRag(false);
}
};
const handleClearCache = async () => {
setIsClearingCache(true);
const loadingToast = toast.loading('Czyszczenie pamięci podręcznej systemu...');
try {
const { data } = await apiClient.post('/api/admin/clear_cache');
toast.success(data.message || 'Pamięć podręczna wyczyszczona pomyślnie', { id: loadingToast });
} catch (err: any) {
console.error(err);
toast.error('Błąd podczas czyszczenia pamięci podręcznej', { id: loadingToast });
} finally {
setIsClearingCache(false);
}
};
const handleCheckServices = async () => {
setIsCheckingStatus(true);
try {
const { data } = await apiClient.get('/api/admin/diagnostics/services_status');
setServiceStatus(data);
toast.success('Zaktualizowano status usług');
} catch (err: any) {
console.error(err);
toast.error('Nie udało się pobrać statusu usług');
} finally {
setIsCheckingStatus(false);
}
};
if (loading) return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '60vh', width: '100%', backgroundColor: '#050505', color: 'white' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1.5rem' }}>
<div className="admin-spinner" style={{ width: '4rem', height: '4rem', borderWidth: '3px' }} />
<div style={{ color: '#818cf8', fontSize: '0.875rem', fontWeight: 600, letterSpacing: '0.2em', textTransform: 'uppercase' }}>
Inicjalizacja Systemu Głównego
</div>
</div>
</div>
);
return (
<div className="admin-wrapper">
<div className="admin-bg-effects">
<div className="admin-bg-blob-1" />
<div className="admin-bg-blob-2" />
<div className="admin-bg-blob-3" />
<div className="admin-bg-noise" />
</div>
<div className="admin-container">
{/* HEADER */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="admin-header"
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div className="admin-system-badge">
<span className="ping-dot">
<span className="ping-dot-animate"></span>
<span className="ping-dot-static"></span>
</span>
<span>SYSTEM ONLINE</span>
</div>
<h1 className="admin-title">
Nexus <span className="admin-title-highlight">Control</span>
</h1>
<p className="admin-subtitle">
Zaawansowana telemetria, zarządzanie infrastrukturą i podgląd operacyjny na żywo dla GrantForge AI.
</p>
</div>
<div className="admin-tabs-container">
<TabButton active={activeTab === 'overview'} onClick={() => setActiveTab('overview')} icon={<Activity size={18} />} label="Przegląd" />
<TabButton active={activeTab === 'telemetry'} onClick={() => setActiveTab('telemetry')} icon={<TerminalSquare size={18} />} label="Telemetria" />
<TabButton active={activeTab === 'tools'} onClick={() => setActiveTab('tools')} icon={<Wrench size={18} />} label="Narzędzia" />
<TabButton active={activeTab === 'regulation'} onClick={() => setActiveTab('regulation')} icon={<DatabaseZap size={18} />} label="Regulation & Trust" />
</div>
</motion.div>
<AnimatePresence mode="wait">
{/* TAB 1: OVERVIEW */}
{activeTab === 'overview' && (
<motion.div
key="overview"
initial={{ opacity: 0, scale: 0.98, filter: 'blur(4px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.98, filter: 'blur(4px)' }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}
>
{/* KPI Metrics */}
<div className="admin-grid-3">
<GlassCard>
<StatCardContent title="Zarejestrowani" value={stats?.database.total_users || 0} icon={<Users size={24} color="#60a5fa" />} trend="+12% w tym tygodniu" />
</GlassCard>
<GlassCard>
<StatCardContent title="Aktywne Projekty" value={stats?.database.total_projects || 0} icon={<Folder size={24} color="#818cf8" />} trend="Stabilny wzrost" />
</GlassCard>
<GlassCard>
<StatCardContent title="Wygenerowane Sekcje" value={stats?.database.total_generated_sections || 0} icon={<FileText size={24} color="#34d399" />} trend="Wysoka przepustowość" />
</GlassCard>
</div>
{/* CHARTS ROW */}
<div className="admin-grid-2">
{/* Area Chart */}
<GlassCard className="admin-glass-card-no-pad" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column' }}>
<div className="admin-card-header">
<Activity size={18} color="#818cf8" />
<h3 className="admin-card-title">Przepustowość Systemu (24h)</h3>
</div>
<div className="admin-chart-container">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={stats?.throughput || systemThroughputData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="colorLoad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#6366f1" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<XAxis dataKey="time" stroke="rgba(255,255,255,0.2)" fontSize={11} tickMargin={10} />
<YAxis stroke="rgba(255,255,255,0.2)" fontSize={11} tickMargin={10} />
<RechartsTooltip
contentStyle={{ backgroundColor: 'rgba(0,0,0,0.8)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px' }}
itemStyle={{ color: '#818cf8' }}
/>
<Area type="monotone" dataKey="load" stroke="#818cf8" strokeWidth={3} fillOpacity={1} fill="url(#colorLoad)" />
</AreaChart>
</ResponsiveContainer>
</div>
</GlassCard>
{/* Bar Chart for Scores */}
<GlassCard className="admin-glass-card-no-pad" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column' }}>
<div className="admin-card-header">
<ShieldCheck size={18} color="#34d399" />
<h3 className="admin-card-title">Ostatnie Wyniki Audytu</h3>
</div>
<div className="admin-chart-container">
{stats?.recent_projects?.filter(p => p.overall_score).length ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={stats.recent_projects.filter(p => p.overall_score).slice(0, 7)} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<XAxis dataKey="title" stroke="rgba(255,255,255,0.2)" fontSize={10} tickFormatter={(val) => val.length > 10 ? val.substring(0,10)+'...' : val} />
<YAxis stroke="rgba(255,255,255,0.2)" fontSize={11} />
<RechartsTooltip
contentStyle={{ backgroundColor: 'rgba(0,0,0,0.8)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px' }}
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
/>
<Bar dataKey="overall_score" radius={[4, 4, 0, 0]}>
{stats.recent_projects.filter(p => p.overall_score).slice(0, 7).map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.overall_score && entry.overall_score >= 80 ? '#34d399' : entry.overall_score && entry.overall_score >= 50 ? '#fbbf24' : '#f43f5e'} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<div className="admin-empty-state">
Brak ostatnich wyników audytu
</div>
)}
</div>
</GlassCard>
</div>
<div className="admin-grid-1-2">
{/* Active Tasks Panel */}
<GlassCard className="admin-col-span-1 admin-glass-card-no-pad" style={{ display: 'flex', flexDirection: 'column' }}>
<div className="admin-panel-header">
<h2 className="admin-card-title"><Zap size={16} color="#fbbf24" /> Kolejka Zadań</h2>
<div className="admin-badge admin-badge-amber">
<span className="ping-dot">
{stats?.generator.active_tasks_count ? <span className="ping-dot-animate" style={{backgroundColor: '#fbbf24'}} /> : null}
<span className="ping-dot-static" style={{backgroundColor: '#f59e0b'}} />
</span>
<span style={{ fontSize: '0.75rem', fontWeight: 'bold' }}>{stats?.generator.active_tasks_count || 0} Aktywnych</span>
</div>
</div>
<div className="admin-pipeline-list custom-scrollbar">
{stats?.generator.active_tasks?.length ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{stats.generator.active_tasks.map((taskId, i) => (
<motion.div
initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: i * 0.1 }}
key={taskId}
className="admin-task-item"
>
<div className="admin-task-header">
<div className="admin-task-id">
TASK::{taskId.split('-')[0]}
</div>
<span className="admin-task-users">
{stats.generator.subscribers[taskId] || 0} USERS
</span>
</div>
<div className="admin-task-progress">
<div className="admin-task-progress-bar" />
</div>
</motion.div>
))}
</div>
) : (
<div className="admin-empty-state">
<div style={{ padding: '1rem', backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: '50%', marginBottom: '1rem' }}>
<Cpu size={28} />
</div>
<span style={{ fontSize: '0.875rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>System w spoczynku</span>
<span style={{ fontSize: '0.75rem', marginTop: '0.5rem' }}>Oczekuje na zadania generacyjne</span>
</div>
)}
</div>
</GlassCard>
{/* Recent Projects Table */}
<GlassCard className="admin-col-span-2 admin-glass-card-no-pad" style={{ display: 'flex', flexDirection: 'column' }}>
<div className="admin-panel-header">
<h2 className="admin-card-title"><Activity size={16} color="#2dd4bf" /> Dziennik Operacji</h2>
</div>
<div className="admin-table-container custom-scrollbar">
<table className="admin-table">
<thead>
<tr>
<th>Projekt</th>
<th>Stan Generacji</th>
<th>Status Audytu</th>
<th style={{ textAlign: 'right' }}>Trust</th>
<th style={{ textAlign: 'right' }}>Wynik</th>
</tr>
</thead>
<tbody>
{stats?.recent_projects?.length ? stats.recent_projects.map((proj) => (
<tr key={proj.id}>
<td>
<div style={{ fontWeight: 500, color: '#e2e8f0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '250px' }}>{proj.title}</div>
<div style={{ fontSize: '0.625rem', fontFamily: 'monospace', color: '#64748b', marginTop: '0.25rem' }}>{proj.id}</div>
</td>
<td>
{proj.has_final_document ? (
<span className="admin-status-badge admin-status-complete">
<CheckCircle2 size={12} /> GOTOWE
</span>
) : (
<span className="admin-status-badge admin-status-processing">
<div className="ping-dot-static" style={{ backgroundColor: '#fbbf24', width: '6px', height: '6px' }} /> PRZETWARZANIE
</span>
)}
</td>
<td>
{proj.has_audit ? (
<span className="admin-status-badge admin-status-verified">
<ShieldCheck size={12} /> ZWERYFIKOWANO
</span>
) : (
<span style={{ color: '#475569' }}>-</span>
)}
</td>
<td style={{ textAlign: 'right' }}>
{proj.trust_score !== undefined ? (
<div className={`admin-score-box ${
proj.trust_score >= 80 ? 'admin-score-high' :
proj.trust_score >= 65 ? 'admin-score-med' :
'admin-score-low'
}`}>
{proj.trust_score}
</div>
) : (
<span style={{ color: '#475569' }}>-</span>
)}
</td>
<td style={{ textAlign: 'right' }}>
{proj.overall_score !== undefined && proj.overall_score !== null ? (
<div className={`admin-score-box ${
proj.overall_score >= 80 ? 'admin-score-high' :
proj.overall_score >= 50 ? 'admin-score-med' :
'admin-score-low'
}`}>
{proj.overall_score}
</div>
) : (
<span style={{ color: '#475569' }}>-</span>
)}
</td>
</tr>
)) : (
<tr><td colSpan={4} style={{ textAlign: 'center', padding: '3rem', opacity: 0.5 }}>No recent operational data found.</td></tr>
)}
</tbody>
</table>
</div>
</GlassCard>
</div>
</motion.div>
)}
{/* TAB 2: TELEMETRY */}
{activeTab === 'telemetry' && (
<motion.div
key="telemetry"
initial={{ opacity: 0, scale: 0.98, filter: 'blur(4px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.98, filter: 'blur(4px)' }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
className="admin-terminal-window"
>
<div className="admin-terminal-gradient" />
{/* Terminal Header */}
<div className="admin-terminal-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: '#f43f5e' }}></div>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: '#f59e0b' }}></div>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: '#10b981' }}></div>
</div>
<div style={{ fontSize: '0.75rem', fontFamily: 'monospace', fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: '0.5rem', color: '#cbd5e1' }}>
<TerminalSquare size={16} color="#818cf8" /> core_telemetry.log
</div>
</div>
<div className="admin-terminal-controls">
<div className="admin-input-group">
<Search size={14} className="admin-input-icon" />
<input
type="text"
placeholder="Grep stream..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="admin-input"
/>
</div>
<div className="admin-filter-group">
{(['INFO', 'WARNING', 'ERROR', 'DEBUG'] as LogLevel[]).map(level => {
const isActive = activeLevels.includes(level);
return (
<button
key={level}
onClick={() => toggleLogLevel(level)}
className={`admin-filter-btn ${isActive ? 'active ' + level.toLowerCase() : ''}`}
>
{level}
</button>
);
})}
</div>
<button
onClick={() => setLogs([])}
className="admin-icon-btn"
title="Clear Stream"
>
<Trash2 size={16} />
</button>
<button
onClick={() => setIsPaused(!isPaused)}
className={`admin-action-btn ${isPaused ? 'admin-btn-halted' : 'admin-btn-streaming'}`}
title={isPaused ? "Wznów odbieranie nowych logów z serwera na żywo" : "Wstrzymaj strumień logów (pozwala na spokojne czytanie)"}
>
{isPaused ? <Pause size={14} /> : <Play size={14} />}
{isPaused ? 'HALTED' : 'STREAMING'}
</button>
</div>
</div>
<div style={{ fontSize: '0.75rem', color: '#94a3b8', padding: '0 1.5rem 0.5rem 1.5rem', textAlign: 'right' }}>
*Przycisk <strong>STREAMING/HALTED</strong> wstrzymuje automatyczne przewijanie i pojawianie się nowych logów, co ułatwia ich analizę.
</div>
{/* Terminal Body */}
<div className="admin-terminal-body custom-scrollbar">
{filteredLogs.length === 0 ? (
<div className="admin-empty-state" style={{ border: '1px dashed rgba(255,255,255,0.1)', borderRadius: '0.75rem', height: '100%' }}>
<span style={{ animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite' }}>Awaiting matrix input...</span>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem', paddingBottom: '1.5rem' }}>
{filteredLogs.map((log, i) => {
let colorClass = '#cbd5e1';
let labelColor = '#38bdf8';
if (log.level === 'ERROR') { colorClass = '#fda4af'; labelColor = '#f43f5e'; }
if (log.level === 'WARNING') { colorClass = '#fde68a'; labelColor = '#fbbf24'; }
if (log.level === 'DEBUG') { colorClass = '#64748b'; labelColor = '#475569'; }
return (
<div key={i} className="admin-log-row">
<div className="admin-log-meta">
<span className="admin-log-time">{new Date(log.timestamp).toLocaleTimeString(undefined, {hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit', fractionalSecondDigits: 3})}</span>
<span className="admin-log-level" style={{ color: labelColor }}>{log.level.padEnd(5)}</span>
</div>
<div className="admin-log-content">
<span className="admin-log-source">[{log.agent}]</span>
<span style={{ color: colorClass }}>{log.message}</span>
{log.metadata && Object.keys(log.metadata).length > 0 && (
<div className="admin-log-details custom-scrollbar">
<pre>{JSON.stringify(log.metadata, null, 2)}</pre>
</div>
)}
</div>
</div>
);
})}
<div ref={logsEndRef} style={{ height: '1rem' }} />
</div>
)}
</div>
{/* Terminal Footer */}
<div className="admin-terminal-footer">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#10b981', boxShadow: '0 0 5px rgba(16,185,129,0.8)', animation: 'pulse 2s infinite' }} />
TCP/IP Socket ESTABLISHED
</div>
<div>Rendered {filteredLogs.length} of {logs.length} vectors</div>
</div>
</motion.div>
)}
{/* TAB 3: TOOLS */}
{activeTab === 'tools' && (
<motion.div
key="tools"
initial={{ opacity: 0, scale: 0.98, filter: 'blur(4px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.98, filter: 'blur(4px)' }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
className="admin-grid-2"
>
{/* Holistic Critic */}
<GlassCard className="admin-glass-card-no-pad" style={{ display: 'flex', flexDirection: 'column', position: 'relative', overflow: 'hidden', padding: '1.75rem' }}>
<div className="admin-tool-bg-glow admin-tool-bg-fuchsia" />
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem', position: 'relative', zIndex: 10 }}>
<div className="admin-tool-icon-box admin-tool-icon-fuchsia">
<ShieldAlert size={24} />
</div>
<div>
<h3 style={{ fontSize: '1.125rem', fontWeight: 700, margin: 0 }}>Całościowa Ocena Krytyczna</h3>
<p style={{ fontSize: '0.875rem', color: '#94a3b8', margin: '0.25rem 0 0 0' }}>Wymuś weryfikację poprawności i testy LLM na wybranym projekcie. UUID projektu znajdziesz w adresie URL po otwarciu edytora projektu.</p>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem', marginTop: 'auto', position: 'relative', zIndex: 10 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<input
type="text"
value={targetProjectId}
onChange={(e) => setTargetProjectId(e.target.value)}
placeholder="UUID docelowego projektu"
className="admin-tool-input fuchsia"
/>
<button
onClick={handleRunGlobalCritic}
disabled={isRunningCritic || !targetProjectId.trim()}
className="admin-execute-btn admin-execute-primary"
>
{isRunningCritic ? <><div className="admin-spinner" style={{ width: '1.25rem', height: '1.25rem' }} /> TRWA ANALIZA...</> : 'WYKONAJ DYREKTYWĘ'}
</button>
</div>
{isRunningCritic && logs.length > 0 && (
<motion.div
initial={{ opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }}
style={{ fontSize: '0.75rem', color: '#c4b5fd', display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'rgba(139, 92, 246, 0.1)', padding: '0.5rem', borderRadius: '4px', border: '1px solid rgba(139, 92, 246, 0.2)' }}
>
<Activity size={14} className="admin-pulse" />
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{logs[logs.length - 1].message}
</span>
</motion.div>
)}
</div>
</GlassCard>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{/* RAG Sync */}
<GlassCard className="admin-glass-card-no-pad" style={{ display: 'flex', flexDirection: 'column', position: 'relative', overflow: 'hidden', padding: '1.75rem' }}>
<div className="admin-tool-bg-glow admin-tool-bg-teal" />
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem', position: 'relative', zIndex: 10 }}>
<div className="admin-tool-icon-box admin-tool-icon-teal">
<Database size={24} />
</div>
<div>
<h3 style={{ fontSize: '1.125rem', fontWeight: 700, margin: 0 }}>Synchronizacja Bazy Wektorowej</h3>
<p style={{ fontSize: '0.875rem', color: '#94a3b8', margin: '0.25rem 0 0 0' }}>Wymusza odświeżenie wektorów embeddings w Pinecone dla podanej kategorii. Używane do RAG.</p>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginTop: 'auto', position: 'relative', zIndex: 10 }}>
<div style={{ display: 'flex', gap: '1rem' }}>
<input
type="text"
value={ragCategory}
onChange={(e) => setRagCategory(e.target.value)}
placeholder="Kategoria (domyślnie: SMART)"
className="admin-tool-input teal"
/>
<button
onClick={handleSyncRag}
disabled={isSyncingRag}
className="admin-execute-btn admin-execute-secondary"
style={{ width: 'auto', padding: '0.75rem 1.5rem' }}
>
{isSyncingRag ? 'SYNCHRONIZACJA...' : 'SYNCHRONIZUJ'}
</button>
</div>
{isSyncingRag && logs.length > 0 && (
<motion.div
initial={{ opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }}
style={{ fontSize: '0.75rem', color: '#5eead4', display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'rgba(20, 184, 166, 0.1)', padding: '0.5rem', borderRadius: '4px', border: '1px solid rgba(20, 184, 166, 0.2)' }}
>
<Activity size={14} className="admin-pulse" />
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{logs[logs.length - 1].message}
</span>
</motion.div>
)}
</div>
</GlassCard>
{/* Cache & Services */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1.5rem' }}>
<button
onClick={handleClearCache}
disabled={isClearingCache}
className="admin-large-btn rose"
title="Czyści pamięć podręczną zapytań (Redis/Memory) w celu wymuszenia pobrania najświeższych danych."
>
<div className="admin-large-btn-hover" />
<div className="admin-large-btn-icon">
<Trash size={24} />
</div>
<span>WYCZYŚĆ CACHE</span>
</button>
<button
onClick={handleCheckServices}
disabled={isCheckingStatus}
className="admin-large-btn sky"
title="Odpytuje podłączone systemy (Neo4j, LLM, bazy danych) i zwraca aktualne opóźnienia oraz stan na żywo."
>
<div className="admin-large-btn-hover" />
<div className="admin-large-btn-icon">
<Cpu size={24} />
</div>
<span>SPRAWDŹ SIEĆ</span>
</button>
</div>
{/* Service Status Results */}
{serviceStatus && (
<GlassCard className="admin-glass-card-no-pad" style={{ marginTop: '1rem', padding: '1.25rem', overflow: 'hidden', position: 'relative' }}>
<div className="admin-tool-bg-glow admin-tool-bg-sky" style={{ right: '-20%', top: '-20%' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1rem', position: 'relative', zIndex: 10 }}>
<Server size={18} color="#38bdf8" />
<h3 className="admin-card-title" style={{ margin: 0 }}>Diagnostyka Sieci</h3>
</div>
<div className="admin-status-grid">
{Object.entries(serviceStatus).map(([service, info]: [string, any]) => (
<div key={service} className="admin-status-item">
<div className="admin-status-label">{service}</div>
<div className="admin-status-row">
<span className={`admin-status-mini-badge ${info.status === 'ok' ? 'ok' : info.status === 'warning' ? 'warning' : 'error'}`}>
{info.status === 'ok' ? 'ONLINE' : info.status === 'warning' ? 'WARNING' : 'ERROR'}
</span>
<span className={`admin-status-latency ${info.latency_ms > 1000 ? 'slow' : 'fast'}`}>
{info.latency_ms !== undefined ? `${info.latency_ms}ms` : 'N/A'}
</span>
</div>
{(info.status === 'error' || info.status === 'warning') && (
<div className="admin-status-error" title={info.message} style={{ color: info.status === 'warning' ? '#fbbf24' : undefined }}>{info.message}</div>
)}
</div>
))}
</div>
</GlassCard>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* TAB 4: REGULATION & TRUST (Cycle 6) */}
{activeTab === 'regulation' && (
<motion.div
key="regulation"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
style={{ marginTop: '1rem' }}
>
{/* Cycle 17: In-app Law Change Notifications */}
{lawHistory.length > 0 && (
<GlassCard style={{ marginBottom: '1.5rem', borderLeft: '4px solid #f59e0b' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.75rem' }}>
<AlertTriangle size={18} color="#f59e0b" />
<h3 className="admin-card-title" style={{ margin: 0, color: '#f59e0b' }}>Recent Law Changes</h3>
<span style={{ fontSize: '0.75rem', color: '#64748b', marginLeft: 'auto' }}>{lawHistory.length} changes detected</span>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
{lawHistory.slice(0, 4).map((change, idx) => (
<div key={idx} style={{ padding: '0.25rem 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<strong>{change.program}</strong> — new snapshot created <span style={{ color: '#64748b' }}>({new Date(change.timestamp).toLocaleDateString()})</span>
</div>
))}
</div>
<div style={{ marginTop: '0.5rem', fontSize: '0.7rem', color: '#64748b' }}>
These changes automatically triggered new Regulation Snapshots with higher Trust Score.
</div>
</GlassCard>
)}
<SnapshotDashboard />
</motion.div>
)}
</div>
</div>
);
};
// Subcomponents
const TabButton = ({ active, onClick, icon, label }: { active: boolean, onClick: () => void, icon: React.ReactNode, label: string }) => (
<button
onClick={onClick}
className={`admin-tab-btn ${active ? 'active' : ''}`}
>
{active && (
<motion.div
layoutId="activeTab"
className="admin-tab-active-bg"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span>
{icon}
{label}
</span>
</button>
);
const GlassCard = ({ children, className = '', style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => (
<div className={`admin-glass-card ${className}`} style={style}>
<div className="admin-card-hover-bg" style={{ backgroundImage: 'linear-gradient(to top right, rgba(255,255,255,0.05), transparent)' }} />
<div className="admin-glass-card-content" style={{ height: '100%' }}>
{children}
</div>
</div>
);
const StatCardContent = ({ title, value, icon, trend }: { title: string, value: string | number, icon: React.ReactNode, trend: string }) => {
return (
<>
<div className="admin-stat-header">
<div className="admin-stat-icon">
{icon}
</div>
<div className="admin-stat-trend">
{trend}
</div>
</div>
<div>
<div className="admin-stat-value">{value}</div>
<div className="admin-stat-title">{title}</div>
</div>
</>
);
};
export default AdminDashboard;