Spaces:
Running
Running
| 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; | |