feat(dashboard): rebuild SOC dashboard + fix ClickHouse SQL
Complete rewrite of the SOC dashboard using FastAPI + Jinja2 + htmx + Chart.js + Tailwind CSS. Replaces the old React/Vite frontend with server-rendered templates. Dashboard pages: - Overview: KPIs, timeline chart, threat distribution, top IPs - Detections: paginated/filterable anomaly table - Scores: ml_all_scores with AE error & XGB prob columns - Traffic: HTTP logs with method/host filters - IP Investigation: full deep-dive (scores, features, HTTP logs, classify) - Classification: SOC feedback form + history - Features: AI + thesis feature stats - Models: scoring stats + model metadata API: 9 JSON endpoints with parameterized queries, sort whitelists SQL fixes: - 05_aggregation_tables: add deduplicate_merge_projection_mode - 11_views: fix nested aggregate (argMax inside sum) - 12_thesis_features: remove invalid 'let' bindings, fix groupArrayIf type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
100
services/dashboard/backend/templates/base.html
Normal file
100
services/dashboard/backend/templates/base.html
Normal file
@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}JA4 SOC Dashboard{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: { 50:'#eef2ff',100:'#e0e7ff',500:'#6366f1',600:'#4f46e5',700:'#4338ca',900:'#312e81' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
|
||||
.threat-critical { color: #ef4444; font-weight: 700; }
|
||||
.threat-high { color: #f97316; font-weight: 600; }
|
||||
.threat-medium { color: #eab308; }
|
||||
.threat-low { color: #22c55e; }
|
||||
.threat-normal { color: #6b7280; }
|
||||
.kpi-card { @apply bg-gray-800 rounded-xl p-5 border border-gray-700; }
|
||||
.data-table { @apply w-full text-sm text-left; }
|
||||
.data-table th { @apply px-4 py-3 bg-gray-800 text-gray-300 font-medium border-b border-gray-700 sticky top-0; }
|
||||
.data-table td { @apply px-4 py-2.5 border-b border-gray-800 text-gray-300; }
|
||||
.data-table tbody tr:hover { @apply bg-gray-800/50; }
|
||||
.nav-link { @apply px-4 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800 transition-colors text-sm font-medium; }
|
||||
.nav-link.active { @apply bg-brand-600 text-white; }
|
||||
.badge { @apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium; }
|
||||
.badge-critical { @apply bg-red-500/20 text-red-400; }
|
||||
.badge-high { @apply bg-orange-500/20 text-orange-400; }
|
||||
.badge-medium { @apply bg-yellow-500/20 text-yellow-400; }
|
||||
.badge-low { @apply bg-green-500/20 text-green-400; }
|
||||
.badge-normal { @apply bg-gray-500/20 text-gray-400; }
|
||||
.badge-known { @apply bg-blue-500/20 text-blue-400; }
|
||||
.htmx-request .htmx-indicator { display: inline-block; }
|
||||
.htmx-indicator { display: none; }
|
||||
.filter-btn { @apply px-3 py-1.5 text-xs rounded-lg border border-gray-700 text-gray-400 hover:border-brand-500 hover:text-brand-500 transition-colors cursor-pointer; }
|
||||
.filter-btn.active { @apply border-brand-500 bg-brand-500/20 text-brand-500; }
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-gray-950 text-gray-200 min-h-screen">
|
||||
<!-- Top Nav -->
|
||||
<nav class="bg-gray-900 border-b border-gray-800 sticky top-0 z-50">
|
||||
<div class="max-w-[1600px] mx-auto px-4 flex items-center h-14 gap-2">
|
||||
<a href="/" class="flex items-center gap-2 mr-6">
|
||||
<div class="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold text-sm">J4</div>
|
||||
<span class="text-white font-semibold hidden sm:inline">JA4 SOC</span>
|
||||
</a>
|
||||
<a href="/" class="nav-link {% if active_page == 'overview' %}active{% endif %}">Overview</a>
|
||||
<a href="/detections" class="nav-link {% if active_page == 'detections' %}active{% endif %}">Détections</a>
|
||||
<a href="/scores" class="nav-link {% if active_page == 'scores' %}active{% endif %}">Scores</a>
|
||||
<a href="/traffic" class="nav-link {% if active_page == 'traffic' %}active{% endif %}">Trafic</a>
|
||||
<a href="/features" class="nav-link {% if active_page == 'features' %}active{% endif %}">Features</a>
|
||||
<a href="/models" class="nav-link {% if active_page == 'models' %}active{% endif %}">Modèles</a>
|
||||
<a href="/classify" class="nav-link {% if active_page == 'classify' %}active{% endif %}">Classifier</a>
|
||||
<div class="flex-1"></div>
|
||||
<span class="text-xs text-gray-500" id="clock"></span>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Content -->
|
||||
<main class="max-w-[1600px] mx-auto px-4 py-6">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<script>
|
||||
function updateClock() {
|
||||
document.getElementById('clock').textContent = new Date().toLocaleString('fr-FR');
|
||||
}
|
||||
updateClock(); setInterval(updateClock, 1000);
|
||||
|
||||
function threatBadge(level) {
|
||||
const map = {
|
||||
'CRITICAL':'badge-critical','HIGH':'badge-high','MEDIUM':'badge-medium',
|
||||
'LOW':'badge-low','NORMAL':'badge-normal','KNOWN_BOT':'badge-known','ANUBIS_DENY':'badge-critical'
|
||||
};
|
||||
return `<span class="badge ${map[level]||'badge-normal'}">${level}</span>`;
|
||||
}
|
||||
function fmtIP(ip) {
|
||||
if (!ip) return '';
|
||||
let s = String(ip).replace('::ffff:','');
|
||||
return `<a href="/ip/${encodeURIComponent(s)}" class="text-brand-500 hover:underline">${s}</a>`;
|
||||
}
|
||||
function fmtScore(v) {
|
||||
let n = parseFloat(v);
|
||||
if (isNaN(n)) return '-';
|
||||
let color = n > 0.7 ? 'text-red-400' : n > 0.4 ? 'text-orange-400' : n > 0.1 ? 'text-yellow-400' : 'text-green-400';
|
||||
return `<span class="${color}">${n.toFixed(4)}</span>`;
|
||||
}
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user