- Fix doc tooltips: split CSS into <style type='text/tailwindcss'> for @apply directives + raw CSS for reliable doc panel rendering - Convert doc panels from click-toggle to hover-based infobulles with arrow pointer, fade-in animation, and auto-dismiss on mobile - Replace '?' icons with 'ⓘ' across all 11 templates (51 tooltips) - Full-width layout: reduce padding on mobile (px-3), scale up on desktop (lg:px-5, xl:px-6) for maximum screen utilization - Auto-collapse sidebar on narrow screens (<1024px) - Keyboard shortcuts: Alt+1–9 for page navigation, Alt+B toggle sidebar - Add LEGITIMATE_BROWSER filter button to detections page - Sticky header with stronger blur (backdrop-blur-md) - All 46 routes pass tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
364 lines
22 KiB
HTML
364 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}JA4 SOC — Overview{% endblock %}
|
|
{% block page_title %}
|
|
Centre de commande
|
|
<span class="relative inline-block ml-1"><button onclick="docToggle(this)" class="doc-btn" aria-label="Aide">ⓘ</button><div class="doc-panel">
|
|
<h4>Centre de commande SOC</h4>
|
|
<p>Vue d'ensemble temps réel de la posture de sécurité. Les KPI montrent les dernières 24h. La timeline et les alertes se rafraîchissent toutes les 60s.</p>
|
|
<p><strong>Workflow :</strong> Identifiez les pics → cliquez sur une alerte → investiguez l'IP → classifiez.</p>
|
|
<p class="doc-source">Sources : ml_detected_anomalies, ml_all_scores, http_logs</p>
|
|
</div></span>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="space-y-4">
|
|
<!-- ═══ KPI Row ═══ -->
|
|
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3" id="kpi-grid">
|
|
<div class="kpi-card">
|
|
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-red-500"></span><span class="text-[11px] text-gray-500">Détections 24h</span></div>
|
|
<div class="text-2xl font-bold text-red-400" id="kpi-detections">—</div>
|
|
<div class="text-[10px] text-gray-600 mt-1" id="kpi-det-trend"></div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-orange-500"></span><span class="text-[11px] text-gray-500">Critical + High</span></div>
|
|
<div class="text-2xl font-bold text-orange-400" id="kpi-critical">—</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-indigo-400"></span><span class="text-[11px] text-gray-500">Sessions ML</span></div>
|
|
<div class="text-2xl font-bold text-brand-500" id="kpi-scored">—</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-yellow-400"></span><span class="text-[11px] text-gray-500">IPs uniques</span></div>
|
|
<div class="text-2xl font-bold text-yellow-400" id="kpi-ips">—</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-green-400"></span><span class="text-[11px] text-gray-500">Navigateurs légit.</span></div>
|
|
<div class="text-2xl font-bold text-green-400" id="kpi-browsers">—</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="flex items-center gap-2 mb-1"><span class="w-2 h-2 rounded-full bg-blue-400"></span><span class="text-[11px] text-gray-500">Bots connus</span></div>
|
|
<div class="text-2xl font-bold text-blue-400" id="kpi-knownbots">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ Row 2: Timeline + Live alerts ═══ -->
|
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
|
<!-- Timeline (2/3) -->
|
|
<div class="xl:col-span-2 section-card">
|
|
<div class="section-header">
|
|
<span class="section-title">
|
|
<svg class="w-4 h-4 text-brand-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
|
|
Timeline détections (24h)
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>Timeline des détections</h4>
|
|
<p>Courbes empilées par niveau de menace. Un pic soudain indique une attaque en cours ou un nouveau pattern détecté.</p>
|
|
<p><strong>Action :</strong> Cliquez sur un pic pour filtrer les détections de cette heure.</p>
|
|
<p class="doc-source">Source : ml_detected_anomalies GROUP BY hour, threat_level</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="section-body"><div id="chart-timeline" style="height:280px"></div></div>
|
|
</div>
|
|
<!-- Live alerts (1/3) -->
|
|
<div class="section-card flex flex-col">
|
|
<div class="section-header">
|
|
<span class="section-title">
|
|
<svg class="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
|
|
Alertes récentes
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>Flux d'alertes temps réel</h4>
|
|
<p>Dernières détections HIGH/CRITICAL/KNOWN_BOT. Cliquez sur une IP pour démarrer l'investigation.</p>
|
|
<p class="doc-source">Source : ml_detected_anomalies ORDER BY detected_at DESC</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="flex-1 overflow-y-auto" style="max-height:280px" id="alerts-feed">
|
|
<div class="px-4 py-8 text-center text-gray-500 text-xs">Chargement…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ Row 3: Threat pie + Browsers + Top IPs ═══ -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<span class="section-title">Threat Levels
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>Distribution des menaces</h4>
|
|
<p>Répartition des sessions par niveau : CRITICAL (score >0.70), HIGH (>0.40), MEDIUM (>0.10), NORMAL, LEGITIMATE_BROWSER, KNOWN_BOT.</p>
|
|
<p><strong>Action :</strong> Cliquez sur un segment pour filtrer les détections.</p>
|
|
<p class="doc-source">Source : ml_all_scores.threat_level</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="section-body"><div id="chart-threats" style="height:220px"></div></div>
|
|
</div>
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<span class="section-title">Navigateurs
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>Familles de navigateurs</h4>
|
|
<p>Identification via dictionnaire JA4 → browser_family. Les navigateurs légitimes sont exemptés du scoring ML.</p>
|
|
<p class="doc-source">Source : ml_all_scores.browser_family</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="section-body"><div id="chart-browsers" style="height:220px"></div></div>
|
|
</div>
|
|
<div class="lg:col-span-2 section-card">
|
|
<div class="section-header">
|
|
<span class="section-title">Top 10 IPs suspectes
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>IPs les plus détectées</h4>
|
|
<p>IPs avec le plus grand nombre de détections sur 24h. Le score indique le pire score observé.</p>
|
|
<p><strong>Action :</strong> Cliquez sur une IP pour l'investiguer en profondeur.</p>
|
|
<p class="doc-source">Source : ml_detected_anomalies GROUP BY src_ip</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="section-body p-0">
|
|
<div class="overflow-x-auto">
|
|
<table class="data-table">
|
|
<thead><tr><th>IP</th><th>Dét.</th><th>Score</th><th>Threat</th><th>ASN</th><th>Pays</th></tr></thead>
|
|
<tbody id="top-ips-body"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ Row 4: ASN + Geo + Campaigns ═══ -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<span class="section-title">Top ASN
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>Systèmes autonomes</h4>
|
|
<p>ASN les plus représentés. Couleur : <span class="text-green-400">ISP</span> (résidentiel), <span class="text-red-400">Datacenter</span>, <span class="text-orange-400">Hosting</span>.</p>
|
|
<p><strong>Action :</strong> Cliquez pour voir les détails réseau.</p>
|
|
<p class="doc-source">Source : view_ai_features_1h.asn_org</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="section-body"><div id="chart-asn" style="height:260px"></div></div>
|
|
</div>
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<span class="section-title">Géographie
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>Répartition géographique</h4>
|
|
<p>Treemap par pays et type d'ASN. La taille indique le nombre de sessions.</p>
|
|
<p class="doc-source">Source : view_ai_features_1h.country_code</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="section-body"><div id="chart-geo" style="height:260px"></div></div>
|
|
</div>
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<span class="section-title">
|
|
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
Campagnes bots
|
|
<span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
|
|
<h4>Campagnes détectées (HDBSCAN)</h4>
|
|
<p>Groupes d'IPs présentant des patterns comportementaux similaires, identifiés par clustering HDBSCAN sur les features ML.</p>
|
|
<p><strong>Action :</strong> Une campagne multi-IP indique une attaque coordonnée (botnet, scraping distribué).</p>
|
|
<p class="doc-source">Source : ml_detected_anomalies.campaign_id</p>
|
|
</div></span>
|
|
</span>
|
|
</div>
|
|
<div class="section-body p-0">
|
|
<div class="overflow-y-auto" style="max-height:260px" id="campaigns-list">
|
|
<div class="px-4 py-8 text-center text-gray-500 text-xs">Chargement…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
const THREAT_COLORS = {CRITICAL:'#ef4444',HIGH:'#f97316',MEDIUM:'#eab308',LOW:'#22c55e',NORMAL:'#6b7280',KNOWN_BOT:'#3b82f6',ANUBIS_DENY:'#dc2626',LEGITIMATE_BROWSER:'#22c55e'};
|
|
const LABEL_COLORS = {isp:'#22c55e',datacenter:'#ef4444',hosting:'#f97316',cdn:'#06b6d4',unknown:'#6b7280'};
|
|
|
|
let charts = {};
|
|
function initChart(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return null;
|
|
if (charts[id]) charts[id].dispose();
|
|
charts[id] = echarts.init(el);
|
|
return charts[id];
|
|
}
|
|
|
|
async function loadOverview() {
|
|
try {
|
|
const [ov, geo, fp, alerts, tl, campaigns] = await Promise.all([
|
|
fetch('/api/overview').then(r=>r.json()),
|
|
fetch('/api/geo').then(r=>r.json()),
|
|
fetch('/api/fingerprints').then(r=>r.json()),
|
|
fetch('/api/alerts?limit=20').then(r=>r.json()),
|
|
fetch('/api/timeline-detail').then(r=>r.json()),
|
|
fetch('/api/campaigns').then(r=>r.json()),
|
|
]);
|
|
|
|
// ── KPIs ──
|
|
const threatMap = {};
|
|
(ov.threat_distribution||[]).forEach(t => { threatMap[t.threat_level] = t.cnt; });
|
|
|
|
document.getElementById('kpi-detections').textContent = fmtNum(ov.detections_24h);
|
|
document.getElementById('kpi-critical').textContent = fmtNum((threatMap.CRITICAL||0)+(threatMap.HIGH||0));
|
|
document.getElementById('kpi-scored').textContent = fmtNum(ov.scored_24h);
|
|
document.getElementById('kpi-ips').textContent = fmtNum(ov.unique_ips);
|
|
document.getElementById('kpi-browsers').textContent = fmtNum(ov.legitimate_browsers);
|
|
document.getElementById('kpi-knownbots').textContent = fmtNum(threatMap.KNOWN_BOT||0);
|
|
|
|
// ── Stacked timeline ──
|
|
const timeline = tl.timeline || [];
|
|
if (timeline.length) {
|
|
const ch = initChart('chart-timeline');
|
|
const levels = ['CRITICAL','HIGH','MEDIUM','KNOWN_BOT'];
|
|
const hours = timeline.map(t => t.hour?.substring(11,16)||'');
|
|
ch.setOption(ecBase({
|
|
tooltip: ecTooltip({trigger:'axis'}),
|
|
legend: {data:levels, textStyle:{color:EC_TEXT,fontSize:10}, top:0, right:0, itemWidth:12, itemHeight:8},
|
|
grid: ecGrid({top:30}),
|
|
xAxis: {type:'category', data:hours, axisLine:{lineStyle:{color:EC_GRID}}, axisLabel:{color:EC_TEXT,fontSize:10}},
|
|
yAxis: {type:'value', splitLine:{lineStyle:{color:EC_GRID,type:'dashed'}}, axisLabel:{color:EC_TEXT}},
|
|
series: levels.map(lvl => ({
|
|
name:lvl, type:'bar', stack:'total',
|
|
data: timeline.map(t => t[lvl]||0),
|
|
itemStyle:{color:THREAT_COLORS[lvl]},
|
|
emphasis:{focus:'series'},
|
|
}))
|
|
}));
|
|
}
|
|
|
|
// ── Live alerts feed ──
|
|
const feed = document.getElementById('alerts-feed');
|
|
const alertRows = alerts.alerts || [];
|
|
if (alertRows.length) {
|
|
feed.innerHTML = alertRows.map(a => {
|
|
const ip = String(a.src_ip||'').replace('::ffff:','');
|
|
const color = a.threat_level === 'CRITICAL' ? 'border-l-red-500' :
|
|
a.threat_level === 'HIGH' ? 'border-l-orange-500' :
|
|
a.threat_level === 'KNOWN_BOT' ? 'border-l-blue-500' : 'border-l-gray-700';
|
|
return `<a href="/ip/${encodeURIComponent(ip)}" class="block px-4 py-2.5 border-b border-l-2 ${color} border-gray-800/50 hover:bg-gray-800/40 transition-colors">
|
|
<div class="flex items-center gap-2">
|
|
${threatBadge(a.threat_level)}
|
|
<span class="font-mono text-xs text-brand-400">${escapeHtml(ip)}</span>
|
|
<span class="text-[10px] text-gray-500 ml-auto">${fmtAgo(a.detected_at)}</span>
|
|
</div>
|
|
<div class="text-[11px] text-gray-400 mt-0.5 truncate">
|
|
${a.bot_name ? '<span class="text-cyan-400">'+escapeHtml(a.bot_name)+'</span> · ' : ''}
|
|
${escapeHtml(a.host||'')} · ${a.hits||0} hits · score ${parseFloat(a.anomaly_score||0).toFixed(3)}
|
|
</div>
|
|
</a>`;
|
|
}).join('');
|
|
} else {
|
|
feed.innerHTML = '<div class="px-4 py-8 text-center text-gray-500 text-xs">Aucune alerte récente</div>';
|
|
}
|
|
|
|
// ── Threats donut ──
|
|
if (ov.threat_distribution?.length) {
|
|
const ch = initChart('chart-threats');
|
|
ch.setOption(ecBase({
|
|
tooltip: ecTooltip({trigger:'item', formatter:'{b}: {c} ({d}%)'}),
|
|
series: [{
|
|
type:'pie', radius:['50%','80%'], center:['50%','55%'],
|
|
label:{show:false},
|
|
data: ov.threat_distribution.map(t=>({
|
|
name:t.threat_level, value:t.cnt,
|
|
itemStyle:{color:THREAT_COLORS[t.threat_level]||'#6b7280'}
|
|
})),
|
|
emphasis:{label:{show:true,color:'#fff',fontSize:12,fontWeight:'bold'}},
|
|
}]
|
|
}));
|
|
ch.on('click', p => { if(p.name) window.location.href='/detections?threat_level='+encodeURIComponent(p.name); });
|
|
}
|
|
|
|
// ── Browser donut ──
|
|
if (ov.browser_stats?.length) {
|
|
const ch = initChart('chart-browsers');
|
|
ch.setOption(ecBase({
|
|
tooltip: ecTooltip({trigger:'item', formatter:'{b}: {c} ({d}%)'}),
|
|
series: [{
|
|
type:'pie', radius:['50%','80%'], center:['50%','55%'],
|
|
label:{show:false},
|
|
data: ov.browser_stats.map((b,i)=>({
|
|
name:b.browser_family, value:b.cnt,
|
|
itemStyle:{color:EC_COLORS[i%EC_COLORS.length]}
|
|
})),
|
|
emphasis:{label:{show:true,color:'#fff',fontSize:11}},
|
|
}]
|
|
}));
|
|
}
|
|
|
|
// ── ASN horizontal bar ──
|
|
if (geo.asns?.length) {
|
|
const top = geo.asns.slice(0,10);
|
|
const ch = initChart('chart-asn');
|
|
ch.setOption(ecBase({
|
|
tooltip: ecTooltip({trigger:'axis',axisPointer:{type:'shadow'}}),
|
|
grid: {left:130,right:30,top:5,bottom:5},
|
|
yAxis: {type:'category', data:top.map(a=>a.asn_org).reverse(), axisLine:{show:false}, axisLabel:{color:EC_TEXT,fontSize:10,width:120,overflow:'truncate'}},
|
|
xAxis: {type:'value', show:false},
|
|
series: [{
|
|
type:'bar', data:top.map(a=>({value:a.sessions,itemStyle:{color:LABEL_COLORS[a.asn_label]||'#6b7280'}})).reverse(),
|
|
barWidth:'60%', label:{show:true,position:'right',color:EC_TEXT,fontSize:10},
|
|
}]
|
|
}));
|
|
ch.on('click', p => { if(p.name) window.location.href='/network?asn_org='+encodeURIComponent(p.name); });
|
|
}
|
|
|
|
// ── Country treemap ──
|
|
if (geo.countries?.length) {
|
|
const byCountry = {};
|
|
geo.countries.forEach(c => {
|
|
if (!byCountry[c.country_code]) byCountry[c.country_code] = {name:c.country_code, value:0, children:[]};
|
|
byCountry[c.country_code].value += c.sessions;
|
|
byCountry[c.country_code].children.push({name:c.asn_label, value:c.sessions});
|
|
});
|
|
const ch = initChart('chart-geo');
|
|
ch.setOption(ecBase({
|
|
tooltip: ecTooltip({formatter:i=>`${i.name}: ${(i.value||0).toLocaleString()} sessions`}),
|
|
series:[{type:'treemap', data:Object.values(byCountry).sort((a,b)=>b.value-a.value).slice(0,20),
|
|
width:'100%',height:'100%',
|
|
label:{show:true,fontSize:12,color:'#fff',fontWeight:'bold'},
|
|
itemStyle:{borderColor:'#111827',borderWidth:2,gapWidth:1},
|
|
levels:[{itemStyle:{borderColor:'#1f2937',borderWidth:3}},{colorSaturation:[0.3,0.7]}],
|
|
color:EC_COLORS,
|
|
}]
|
|
}));
|
|
ch.on('click', p => { if(p.data?.name?.length<=3) window.location.href='/detections?country_code='+encodeURIComponent(p.data.name); });
|
|
}
|
|
|
|
// ── Top IPs table ──
|
|
document.getElementById('top-ips-body').innerHTML = (ov.top_ips||[]).map(ip => `<tr onclick="window.location='/ip/${encodeURIComponent(String(ip.src_ip).replace('::ffff:',''))}'">
|
|
<td>${fmtIP(ip.src_ip)}</td><td class="font-mono">${ip.cnt}</td><td>${fmtScore(ip.worst_score)}</td>
|
|
<td>${threatBadge(ip.threat_level)}</td><td class="text-xs">${fmtASN(ip.asn_org)}</td><td>${fmtCountry(ip.country_code)}</td>
|
|
</tr>`).join('') || '<tr><td colspan="6" class="text-center py-4 text-gray-500">Aucune donnée</td></tr>';
|
|
|
|
// ── Campaigns list ──
|
|
const campEl = document.getElementById('campaigns-list');
|
|
const camps = campaigns.campaigns || [];
|
|
if (camps.length) {
|
|
campEl.innerHTML = camps.map(c => `<div class="px-4 py-2.5 border-b border-gray-800/50 hover:bg-gray-800/40 transition-colors cursor-pointer" onclick="window.location='/campaigns'">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="text-xs font-mono text-purple-400">Camp. #${escapeHtml(String(c.campaign_id).substring(0,8))}</span>
|
|
<span class="badge badge-high">${c.members} membres</span>
|
|
<span class="text-[10px] text-gray-500 ml-auto">${c.unique_ips} IPs</span>
|
|
</div>
|
|
<div class="text-[11px] text-gray-400">
|
|
Score max: ${fmtScore(c.max_score)} · ASN: ${(c.asn_list||[]).map(a=>escapeHtml(a)).join(', ')||'—'}
|
|
</div>
|
|
</div>`).join('');
|
|
} else {
|
|
campEl.innerHTML = '<div class="px-4 py-8 text-center text-gray-500 text-xs">Aucune campagne active</div>';
|
|
}
|
|
|
|
} catch(e) { console.error('Overview load error:', e); }
|
|
}
|
|
loadOverview();
|
|
setInterval(loadOverview, 60000);
|
|
</script>
|
|
{% endblock %}
|