Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import { Database, RefreshCw, ShieldCheck, AlertTriangle, Eye } from 'lucide-react'; | |
| interface Snapshot { | |
| id: string; | |
| program: string; | |
| call_name: string; | |
| source_url: string; | |
| fetched_at: string; | |
| version_hash: string; | |
| regulation_link_quality?: string; | |
| key_rules_count: number; | |
| exclusions_count: number; | |
| trust_score?: number; | |
| } | |
| export const SnapshotDashboard: React.FC<{ onTriggerSnapshot?: () => void }> = ({ onTriggerSnapshot }) => { | |
| const [snapshots, setSnapshots] = useState<Snapshot[]>([]); | |
| const [loading, setLoading] = useState(false); | |
| const [health, setHealth] = useState<any>(null); | |
| const [searchTerm, setSearchTerm] = useState(""); | |
| const [lawHistory, setLawHistory] = useState<any[]>([]); | |
| const loadData = async () => { | |
| setLoading(true); | |
| try { | |
| const [snapsRes, healthRes] = await Promise.all([ | |
| fetch('/api/admin/regulation-snapshots'), | |
| fetch('/api/admin/credibility-health') | |
| ]); | |
| const snapsData = await snapsRes.json(); | |
| const healthData = await healthRes.json(); | |
| setSnapshots(snapsData.snapshots || []); | |
| setHealth(healthData); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| setLoading(false); | |
| }; | |
| useEffect(() => { | |
| loadData(); | |
| // Load law change history for notifications | |
| fetch('/api/admin/law-monitoring/history') | |
| .then(r => r.json()) | |
| .then(data => setLawHistory(data.history || [])) | |
| .catch(() => {}); | |
| }, []); | |
| const filteredSnapshots = snapshots.filter(s => | |
| s.program.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| (s.call_name || "").toLowerCase().includes(searchTerm.toLowerCase()) | |
| ); | |
| const triggerNewSnapshot = async () => { | |
| if (onTriggerSnapshot) onTriggerSnapshot(); | |
| const url = prompt("Podaj URL regulaminu do snapshotu (np. https://.../regulamin.pdf):"); | |
| const program = prompt("Podaj kod programu (np. FENG, PARP_SMART):", "FENG"); | |
| if (!url || !program) return; | |
| try { | |
| const res = await fetch('/api/admin/regulation-engine/trigger-snapshot', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ url, program }) | |
| }); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| alert(`Snapshot utworzony! ID: ${data.snapshot_id}`); | |
| } else { | |
| alert("Błąd: " + (data.error || JSON.stringify(data))); | |
| } | |
| } catch (e) { | |
| alert("Błąd wywołania API: " + e); | |
| } | |
| setTimeout(loadData, 1500); | |
| }; | |
| return ( | |
| <div style={{ padding: 16, background: '#f8fafc', borderRadius: 10, border: '1px solid #e2e8f0' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> | |
| <Database size={18} /> | |
| <h3 style={{ margin: 0, fontSize: 16 }}>Regulation Snapshots Dashboard</h3> | |
| <span style={{ fontSize: 11, background: '#e0f2fe', padding: '1px 6px', borderRadius: 4 }}> | |
| {snapshots.length} snapshots | |
| </span> | |
| </div> | |
| <div style={{ display: 'flex', gap: 6 }}> | |
| <button onClick={loadData} disabled={loading} style={{ padding: '4px 10px', fontSize: 12, borderRadius: 6 }}> | |
| <RefreshCw size={14} /> Odśwież | |
| </button> | |
| <button onClick={triggerNewSnapshot} style={{ padding: '4px 10px', fontSize: 12, background: '#1e40af', color: 'white', border: 'none', borderRadius: 6 }}> | |
| + Trigger Snapshot | |
| </button> | |
| </div> | |
| </div> | |
| {health && health.components?.acquisition_regulation_link_quality && ( | |
| <div style={{ marginBottom: 10, fontSize: 12, color: '#334155' }}> | |
| Link Quality: <strong>{health.components.acquisition_regulation_link_quality.high_quality_links_pct}%</strong> high precision | |
| </div> | |
| )} | |
| <input | |
| type="text" | |
| placeholder="Filtruj po programie..." | |
| value={searchTerm} | |
| onChange={(e) => setSearchTerm(e.target.value)} | |
| style={{ width: '100%', marginBottom: 8, padding: 6, fontSize: 12 }} | |
| /> | |
| {/* Law Change Alerts (Cycle 18) */} | |
| {lawHistory.length > 0 && ( | |
| <div style={{ marginBottom: 12, background: 'rgba(245, 158, 11, 0.1)', border: '1px solid #f59e0b', borderRadius: 8, padding: '0.75rem 1rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}> | |
| <AlertTriangle size={16} color="#f59e0b" /> | |
| <strong style={{ fontSize: 13, color: '#92400e' }}>Recent Law Changes</strong> | |
| </div> | |
| <div style={{ fontSize: 11, color: '#78350f' }}> | |
| {lawHistory.slice(0, 4).map((change, i) => ( | |
| <div key={i} style={{ padding: '2px 0' }}> | |
| {new Date(change.timestamp).toLocaleDateString()} — <strong>{change.program}</strong> → new snapshot created | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ marginTop: '0.4rem', fontSize: 10, color: '#92400e', fontStyle: 'italic' }}> | |
| These changes automatically improved Trust Scores of affected programs. | |
| </div> | |
| </div> | |
| )} | |
| <div style={{ maxHeight: 320, overflow: 'auto' }}> | |
| {filteredSnapshots.length === 0 && <div style={{ color: '#64748b', fontSize: 13 }}>Brak snapshotów pasujących do filtra.</div>} | |
| {filteredSnapshots.slice(0, 15).map((s, idx) => ( | |
| <div key={idx} style={{ | |
| background: 'white', | |
| border: '1px solid #e2e8f0', | |
| borderRadius: 6, | |
| padding: 8, | |
| marginBottom: 6, | |
| fontSize: 12 | |
| }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between' }}> | |
| <div> | |
| <strong>{s.program}</strong> — {s.call_name?.slice(0, 50)} | |
| </div> | |
| <div style={{ fontSize: 10, color: s.regulation_link_quality === 'high' ? '#10b981' : '#f59e0b' }}> | |
| {s.regulation_link_quality || 'medium'} | |
| </div> | |
| </div> | |
| <div style={{ fontSize: 10.5, color: '#64748b', marginTop: 2 }}> | |
| Hash: <code>{s.version_hash}</code> | Fetched: {new Date(s.fetched_at).toLocaleDateString()} | |
| | Rules: {s.key_rules_count} | Exclusions: {s.exclusions_count} | |
| {s.trust_score !== undefined && ( | |
| <span style={{ marginLeft: 8, background: s.trust_score > 75 ? '#dcfce7' : s.trust_score > 60 ? '#fef3c7' : '#fee2e2', padding: '1px 5px', borderRadius: 3 }}> | |
| Trust: {s.trust_score} | |
| </span> | |
| )} | |
| </div> | |
| {s.source_url && ( | |
| <a href={s.source_url} target="_blank" style={{ fontSize: 10, color: '#1e40af' }}> | |
| Otwórz źródło | |
| </a> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ marginTop: 8, fontSize: 10, color: '#64748b' }}> | |
| Snapshots są automatycznie wzbogacane o live EUR-Lex + ISAP przy ingestowaniu. Wysoka jakość linków = wyższa wiarygodność certificate'ów. | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default SnapshotDashboard; |