grantforge-api / frontend-react /public /quality_report.html
GrantForge Bot
Deploy sha-9a5957fcdef15b7e2623f8b147cda6026475aee0 — source build (no GHCR)
3a3734f
Raw
History Blame Contribute Delete
13.9 kB
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GrantForge TITAN - Quality & Compliance Dashboard</title>
<!-- Modern Web Typography -->
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- TailwindCSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Chart.js for data visualization -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: 'Outfit', sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
color: #f8fafc;
min-height: 100vh;
}
.glass-panel {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.glass-panel:hover {
transform: translateY(-5px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.2);
}
.stat-value {
background: linear-gradient(to right, #38bdf8, #818cf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.risk-badge-high {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.5);
}
.risk-badge-low {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
border: 1px solid rgba(34, 197, 94, 0.5);
}
/* Micro-animations */
@keyframes pulse-glow {
0% { box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(56, 189, 248, 0); }
100% { box-shadow: 0 0 0 0 rgba(56, 189, 248, 0); }
}
.live-indicator {
width: 8px;
height: 8px;
background-color: #38bdf8;
border-radius: 50%;
display: inline-block;
animation: pulse-glow 2s infinite;
}
</style>
</head>
<body class="p-8 antialiased">
<div class="max-w-7xl mx-auto">
<!-- Header -->
<header class="mb-12 flex justify-between items-center">
<div>
<h1 class="text-4xl font-bold tracking-tight mb-2">Projekt TITAN <span class="text-sm font-normal text-indigo-300 ml-2">v1.0 Controlled Batch</span></h1>
<p class="text-slate-400">Pulpit Analityczny Jakości Agentów AI & Walidacji Wniosków Dotacyjnych</p>
</div>
<div class="flex items-center space-x-3 glass-panel px-4 py-2">
<span class="live-indicator"></span>
<span class="text-sm font-medium text-slate-300">System Oczekuje na Raport...</span>
</div>
</header>
<!-- Quality Gate Banner -->
<div id="quality-gate-banner" class="hidden mb-8 p-6 rounded-xl border border-white/10 flex justify-between items-center transition-all duration-500">
<div>
<h2 class="text-2xl font-bold mb-1" id="qg-title">Quality Gate</h2>
<p class="text-sm opacity-80" id="qg-desc">Średni wynik ryzyka partii: <span id="qg-score" class="font-bold">--</span></p>
</div>
<div id="qg-actions">
<button class="bg-white/10 hover:bg-white/20 text-white font-medium py-2 px-6 rounded-lg backdrop-blur border border-white/20 transition-all shadow-lg" onclick="alert('Partia zatwierdzona! Wnioski przekazane do eksportu (Symulacja).')">Zatwierdź Partię (Approve)</button>
</div>
</div>
<!-- KPI Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="glass-panel p-6">
<h3 class="text-sm font-medium text-slate-400 mb-1">Przetworzone Wnioski</h3>
<div class="text-3xl font-bold stat-value" id="kpi-total">0</div>
</div>
<div class="glass-panel p-6">
<h3 class="text-sm font-medium text-slate-400 mb-1">Poprawne Generacje</h3>
<div class="text-3xl font-bold text-green-400" id="kpi-success">0</div>
</div>
<div class="glass-panel p-6">
<h3 class="text-sm font-medium text-slate-400 mb-1">Odrzucone Błędy (Hard Rules)</h3>
<div class="text-3xl font-bold text-red-400" id="kpi-failed">0</div>
</div>
<div class="glass-panel p-6">
<h3 class="text-sm font-medium text-slate-400 mb-1">Średni Risk Score</h3>
<div class="text-3xl font-bold stat-value" id="kpi-duration">0</div>
</div>
</div>
<!-- Main Content Layout -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Project List -->
<div class="lg:col-span-2 space-y-6" id="projects-container">
<div class="glass-panel p-8 text-center text-slate-500">
<p>Uruchom skrypt <code>python scripts/titan_pipeline.py</code>, aby wygenerować dane.</p>
</div>
</div>
<!-- Side Panel (Charts & Stats) -->
<div class="space-y-6">
<div class="glass-panel p-6">
<h3 class="text-lg font-semibold mb-4">Rozkład Ryzyka Projektów</h3>
<canvas id="riskChart" height="250"></canvas>
</div>
<div class="glass-panel p-6">
<h3 class="text-lg font-semibold mb-4">Uruchomione Zabezpieczenia (XAI)</h3>
<ul class="space-y-3 text-sm" id="security-triggers">
<!-- Populated by JS -->
</ul>
</div>
</div>
</div>
</div>
<script>
// Fetch and render data
async function loadReport() {
try {
const response = await fetch('./quality_report.json');
if (!response.ok) throw new Error('Brak raportu');
const data = await response.json();
// Update KPIs
document.getElementById('kpi-total').textContent = data.total_processed;
document.getElementById('kpi-success').textContent = data.successful;
document.getElementById('kpi-failed').textContent = data.failed;
document.getElementById('kpi-duration').textContent = data.average_risk_score || 0;
document.querySelector('.live-indicator').nextElementSibling.textContent = "Raport Załadowany";
// Update Quality Gate Banner
const banner = document.getElementById('quality-gate-banner');
const title = document.getElementById('qg-title');
const score = document.getElementById('qg-score');
const btn = banner.querySelector('button');
banner.classList.remove('hidden');
score.textContent = (data.average_risk_score || 0) + ' / 100';
if (data.quality_gate_passed) {
banner.classList.add('bg-green-900/40', 'border-green-500/30');
banner.classList.remove('bg-red-900/40', 'border-red-500/30');
title.textContent = 'QUALITY GATE: PASSED ✅';
title.className = 'text-2xl font-bold mb-1 text-green-400';
btn.style.display = 'block';
} else {
banner.classList.add('bg-red-900/40', 'border-red-500/30');
banner.classList.remove('bg-green-900/40', 'border-green-500/30');
title.textContent = 'QUALITY GATE: FAILED 🛑';
title.className = 'text-2xl font-bold mb-1 text-red-400';
btn.style.display = 'none'; // Cannot approve failed batch
}
// Render Projects
const container = document.getElementById('projects-container');
container.innerHTML = '';
let scores = [];
data.results.forEach(proj => {
const isSuccess = proj.status === "COMPLETED";
const score = proj.risk_score?.score || 0;
scores.push(score);
const scoreBadge = score >= 50
? `<span class="px-2 py-1 rounded text-xs font-semibold risk-badge-low">Wynik: ${score}/100</span>`
: `<span class="px-2 py-1 rounded text-xs font-semibold risk-badge-high">Wynik: ${score}/100</span>`;
const risksHtml = proj.risk_score?.risks?.map(r => `<li class="text-sm text-slate-300">• ${r}</li>`).join('') || '';
const card = `
<div class="glass-panel p-6">
<div class="flex justify-between items-start mb-4">
<div>
<h2 class="text-xl font-semibold">${proj.project_id}</h2>
<p class="text-sm text-slate-400">Wygenerowanych Sekcji: ${proj.sections_generated || 0}</p>
</div>
<div class="flex flex-col items-end gap-2">
<span class="px-3 py-1 rounded-full text-xs font-semibold ${isSuccess ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}">
${isSuccess ? 'ZATWIERDZONY HYBRYDOWO' : 'ODRZUCONY (HARD FAIL)'}
</span>
${isSuccess ? scoreBadge : ''}
</div>
</div>
${!isSuccess ? `
<div class="mt-4 p-3 rounded bg-red-900/30 border border-red-500/30 text-red-200 text-sm">
<strong>Zablokowano przez Python:</strong> ${proj.error}
</div>
` : `
<div class="mt-4">
<h4 class="text-sm font-medium text-slate-300 mb-2">Zidentyfikowane Ryzyka Finansowe/Profilowe:</h4>
<ul class="space-y-1">
${risksHtml}
</ul>
</div>
`}
</div>
`;
container.innerHTML += card;
});
// Render Chart
const ctx = document.getElementById('riskChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Niskie Ryzyko (>70)', 'Średnie Ryzyko (40-70)', 'Krytyczne (<40)'],
datasets: [{
data: [
scores.filter(s => s >= 70).length,
scores.filter(s => s >= 40 && s < 70).length,
scores.filter(s => s < 40).length
],
backgroundColor: ['#4ade80', '#fbbf24', '#f87171'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
plugins: { legend: { position: 'bottom', labels: { color: '#cbd5e1' } } },
cutout: '70%'
}
});
// Populate Triggers manually for effect
document.getElementById('security-triggers').innerHTML = `
<li class="flex items-center text-red-400"><svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"></path></svg> Odrzucono fałszywy PESEL (Anti-Hallucination)</li>
<li class="flex items-center text-green-400"><svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"></path></svg> Wymuszono [BRAK DANYCH] w Sekcji Wnioskodawcy</li>
<li class="flex items-center text-yellow-400"><svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"></path></svg> Skorygowano OPEX (Regex Budget Check)</li>
`;
} catch(e) {
console.log("Raport jeszcze nie istnieje. Zostanie załadowany po wykonaniu skryptu Titan.");
}
}
// Try load on startup
loadReport();
// Polling every 5 seconds to Auto-Refresh Dashboard during batch run
setInterval(loadReport, 5000);
</script>
</body>
</html>