Files
ja4-platform/services/dashboard/backend/templates/base.html
toto b735bab5a5 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>
2026-04-08 03:21:05 +02:00

101 lines
5.3 KiB
HTML

<!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>