grantforge-api / frontend-react /src /components /project /SnapshotDashboard.tsx
GrantForge Bot
Deploy sha-3d4ebe3261a5907e50617c6c64d42c59bd3d4877 — source build (no GHCR)
61d1e19
Raw
History Blame Contribute Delete
7.48 kB
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;