Files
ja4-platform/services/dashboard/backend/templates/base.html
toto 6babc55e3e fix(dashboard): hover infobulles, full-width layout, UX polish
- 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>
2026-04-09 13:30:16 +02:00

371 lines
22 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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: { 50:'#eef2ff',100:'#e0e7ff',500:'#6366f1',600:'#4f46e5',700:'#4338ca',900:'#312e81' },
surface: { 800:'#1e293b', 900:'#0f172a', 950:'#020617' },
},
fontFamily: { sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'] },
}
}
}
</script>
<style type="text/tailwindcss">
body { font-family: 'Inter', system-ui, sans-serif; }
/* ── Threat badges ── */
.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 cards ── */
.kpi-card { @apply bg-gray-900/80 rounded-xl p-4 border border-gray-800 backdrop-blur; }
/* ── Data table ── */
.data-table { @apply w-full text-sm text-left; }
.data-table th { @apply px-3 py-2.5 bg-gray-900 text-gray-400 font-medium border-b border-gray-800 sticky top-0 text-xs uppercase tracking-wider; }
.data-table td { @apply px-3 py-2 border-b border-gray-800/50 text-gray-300; }
.data-table tbody tr:hover { @apply bg-gray-800/40; }
.data-table tbody tr { @apply cursor-pointer transition-colors; }
/* ── Badges ── */
.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; }
/* ── Filter buttons ── */
.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; }
/* ── Section card ── */
.section-card { @apply bg-gray-900/80 rounded-xl border border-gray-800 backdrop-blur; }
.section-header { @apply flex items-center justify-between px-5 py-3 border-b border-gray-800; }
.section-title { @apply text-sm font-semibold text-gray-200 flex items-center gap-2; }
.section-body { @apply p-5; }
/* ── Sidebar ── */
.nav-item { @apply flex items-center gap-3 px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800/60 transition-colors text-sm cursor-pointer; }
.nav-item.active { @apply bg-brand-600/20 text-brand-400 border-l-2 border-brand-500; }
.nav-group-title { @apply text-[10px] uppercase tracking-widest text-gray-600 px-3 pt-4 pb-1; }
</style>
<style>
/* ── Raw CSS (no @apply) for reliable rendering ── */
.sidebar { width: 220px; transition: width 0.2s ease; }
.sidebar.collapsed { width: 56px; }
.sidebar.collapsed .nav-text { display: none; }
.sidebar.collapsed .nav-group-title { display: none; }
.sidebar.collapsed .sidebar-logo-text { display: none; }
/* ── Infobulles (hover tooltips) ── */
.doc-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 16px; height: 16px; border-radius: 50%; font-size: 10px;
color: #4b5563; cursor: help; transition: all 0.15s;
vertical-align: middle; margin-left: 4px; flex-shrink: 0;
}
.doc-btn:hover { color: #d1d5db; background: #374151; }
.doc-panel {
display: none; position: absolute; z-index: 60;
top: calc(100% + 10px); right: -8px; width: 300px;
padding: 12px 14px; background: #111827; border: 1px solid #374151;
border-radius: 10px; box-shadow: 0 16px 48px rgba(0,0,0,0.6);
font-size: 11px; line-height: 1.6; color: #d1d5db;
pointer-events: auto;
}
.doc-panel::before {
content: ''; position: absolute; top: -5px; right: 14px;
width: 10px; height: 10px; background: #111827;
border-left: 1px solid #374151; border-top: 1px solid #374151;
transform: rotate(45deg);
}
.doc-panel h4 { color: white; font-weight: 600; font-size: 12px; margin: 0 0 6px; }
.doc-panel p { margin: 0 0 5px; }
.doc-panel .doc-source { color: #6b7280; font-style: italic; margin-top: 6px; padding-top: 6px; border-top: 1px solid #1f2937; font-size: 10px; }
/* Hover: show on parent hover */
.relative:has(> .doc-btn):hover > .doc-panel,
.doc-panel.show {
display: block; animation: ttIn 0.12s ease-out;
}
/* Mobile: keep tap toggle via JS */
@keyframes ttIn { from { opacity:0; transform:translateY(-3px); } to { opacity:1; transform:translateY(0); } }
/* ── Animations ── */
@keyframes fadeUp { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
.animate-in { animation: fadeUp 0.3s ease-out both; }
.kpi-card { animation: fadeUp 0.3s ease-out both; }
.kpi-card:nth-child(1) { animation-delay: 0ms; }
.kpi-card:nth-child(2) { animation-delay: 40ms; }
.kpi-card:nth-child(3) { animation-delay: 80ms; }
.kpi-card:nth-child(4) { animation-delay: 120ms; }
.kpi-card:nth-child(5) { animation-delay: 160ms; }
.kpi-card:nth-child(6) { animation-delay: 200ms; }
@keyframes pulse-dot { 0%,100%{opacity:1;} 50%{opacity:0.4;} }
.live-dot { width:6px; height:6px; background:#22c55e; border-radius:50%; display:inline-block; animation: pulse-dot 1.5s ease-in-out infinite; }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #4b5563; }
</style>
{% block head %}{% endblock %}
</head>
<body class="bg-gray-950 text-gray-200 min-h-screen flex">
<!-- ═══ Sidebar Navigation ═══ -->
<aside id="sidebar" class="sidebar fixed top-0 left-0 h-screen bg-gray-950 border-r border-gray-800 flex flex-col z-50 overflow-hidden">
<!-- Logo -->
<div class="flex items-center gap-2 px-3 h-14 border-b border-gray-800 shrink-0">
<button onclick="toggleSidebar()" class="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold text-sm hover:bg-brand-500 transition-colors shrink-0">J4</button>
<span class="sidebar-logo-text text-white font-semibold text-sm whitespace-nowrap">JA4 SOC</span>
</div>
<!-- Nav groups -->
<nav class="flex-1 overflow-y-auto py-2 px-2 space-y-0.5">
<div class="nav-group-title">Surveillance</div>
<a href="/" class="nav-item {% if active_page == 'overview' %}active{% endif %}" title="Vue d'ensemble">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
<span class="nav-text">Overview</span>
</a>
<a href="/detections" class="nav-item {% if active_page == 'detections' %}active{% endif %}" title="Détections d'anomalies">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
<span class="nav-text">Détections</span>
</a>
<a href="/scores" class="nav-item {% if active_page == 'scores' %}active{% endif %}" title="Scores ML">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
<span class="nav-text">Scores</span>
</a>
<a href="/campaigns" class="nav-item {% if active_page == 'campaigns' %}active{% endif %}" title="Campagnes de bots (clusters)">
<svg class="w-4 h-4 shrink-0" 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>
<span class="nav-text">Campagnes</span>
</a>
<div class="nav-group-title">Investigation</div>
<a href="/traffic" class="nav-item {% if active_page == 'traffic' %}active{% endif %}" title="Logs HTTP bruts">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2 1 3 3 3h10c2 0 3-1 3-3V7c0-2-1-3-3-3H7c-2 0-3 1-3 3z"/><path stroke-linecap="round" stroke-width="2" d="M9 12h6M9 8h6M9 16h3"/></svg>
<span class="nav-text">Trafic</span>
</a>
<a href="/network" class="nav-item {% if active_page == 'network' %}active{% endif %}" title="Analyse réseau">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9"/></svg>
<span class="nav-text">Réseau</span>
</a>
<a href="/features" class="nav-item {% if active_page == 'features' %}active{% endif %}" title="Features ML">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
<span class="nav-text">Features</span>
</a>
<div class="nav-group-title">Opérations</div>
<a href="/models" class="nav-item {% if active_page == 'models' %}active{% endif %}" title="Modèles ML">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
<span class="nav-text">Modèles</span>
</a>
<a href="/classify" class="nav-item {% if active_page == 'classify' %}active{% endif %}" title="Classification SOC">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
<span class="nav-text">Classifier</span>
</a>
</nav>
<!-- Footer -->
<div class="px-3 py-3 border-t border-gray-800 shrink-0">
<span class="flex items-center gap-2 text-[10px] text-gray-500">
<span class="live-dot"></span>
<span id="clock" class="nav-text"></span>
</span>
</div>
</aside>
<!-- ═══ Main Content ═══ -->
<div id="main-wrap" class="flex-1 min-h-screen" style="margin-left:220px; transition: margin-left 0.2s ease;">
<!-- Page header -->
<header class="sticky top-0 z-40 bg-gray-950/90 backdrop-blur-md border-b border-gray-800">
<div class="flex items-center h-12 px-4 lg:px-6">
<h1 class="text-sm lg:text-base font-semibold text-gray-100 truncate">{% block page_title %}{% endblock %}</h1>
<div class="flex-1"></div>
{% block header_actions %}{% endblock %}
</div>
</header>
<main class="px-3 py-4 lg:px-5 lg:py-5 xl:px-6">
{% block content %}{% endblock %}
</main>
</div>
<script>
// ── Sidebar toggle ──
function toggleSidebar() {
const sb = document.getElementById('sidebar');
const mw = document.getElementById('main-wrap');
sb.classList.toggle('collapsed');
mw.style.marginLeft = sb.classList.contains('collapsed') ? '56px' : '220px';
}
// Auto-collapse on narrow screens
if (window.innerWidth < 1024) toggleSidebar();
// ── Keyboard shortcuts ──
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
const routes = {'1':'/', '2':'/detections', '3':'/scores', '4':'/campaigns',
'5':'/traffic', '6':'/network', '7':'/features', '8':'/models', '9':'/classify'};
if (e.altKey && routes[e.key]) { e.preventDefault(); window.location = routes[e.key]; }
if (e.key === 'b' && e.altKey) { e.preventDefault(); toggleSidebar(); }
});
// ── Clock ──
function updateClock() {
const el = document.getElementById('clock');
if (el) el.textContent = new Date().toLocaleString('fr-FR');
}
updateClock(); setInterval(updateClock, 1000);
// ── Doc tooltip (mobile tap fallback) ──
function docToggle(btn) {
const panel = btn.nextElementSibling;
document.querySelectorAll('.doc-panel.show').forEach(p => { if (p !== panel) p.classList.remove('show'); });
panel.classList.toggle('show');
// Auto-dismiss after 8s on mobile
if (panel.classList.contains('show')) {
clearTimeout(panel._timer);
panel._timer = setTimeout(() => panel.classList.remove('show'), 8000);
}
}
document.addEventListener('click', e => {
if (!e.target.closest('.doc-btn') && !e.target.closest('.doc-panel'))
document.querySelectorAll('.doc-panel.show').forEach(p => p.classList.remove('show'));
});
// ── Number formatting ──
function fmtNum(n) {
if (n == null) return '—';
n = Number(n);
if (n >= 1000000) return (n/1000000).toFixed(1) + 'M';
if (n >= 1000) return (n/1000).toFixed(1) + 'K';
return n.toLocaleString('fr-FR');
}
function fmtPct(n) { return n == null ? '—' : (Number(n)*100).toFixed(1)+'%'; }
// ── Threat helpers ──
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','LEGITIMATE_BROWSER':'badge-low'
};
return `<span class="badge ${map[level]||'badge-normal'}">${escapeHtml(level||'')}</span>`;
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = String(s);
return d.innerHTML;
}
// ── Link formatters (all sanitized) ──
function fmtIP(ip) {
if (!ip) return '';
let s = String(ip).replace('::ffff:','');
return `<a href="/ip/${encodeURIComponent(s)}" class="text-brand-500 hover:underline font-mono text-xs">${escapeHtml(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} font-mono">${n.toFixed(4)}</span>`;
}
function fmtASN(org) {
if (!org) return '';
return `<a href="/network?asn_org=${encodeURIComponent(org)}" class="text-blue-400 hover:underline cursor-pointer">${escapeHtml(org)}</a>`;
}
function fmtCountry(cc) {
if (!cc) return '';
const flags = {'FR':'🇫🇷','DE':'🇩🇪','NL':'🇳🇱','GB':'🇬🇧','ES':'🇪🇸','US':'🇺🇸','RU':'🇷🇺','IT':'🇮🇹','JP':'🇯🇵','CN':'🇨🇳','KR':'🇰🇷','BR':'🇧🇷','AU':'🇦🇺','CA':'🇨🇦','IN':'🇮🇳','SG':'🇸🇬','SE':'🇸🇪','FI':'🇫🇮','IE':'🇮🇪','CH':'🇨🇭'};
return `<a href="/detections?country_code=${encodeURIComponent(cc)}" class="hover:underline cursor-pointer">${flags[cc]||'🏳️'} ${escapeHtml(cc)}</a>`;
}
function fmtJA4(ja4) {
if (!ja4) return '';
return `<a href="/detections?ja4=${encodeURIComponent(ja4)}" class="text-purple-400 hover:underline cursor-pointer font-mono text-[11px]" title="${escapeHtml(ja4)}">${escapeHtml(ja4.substring(0,22))}…</a>`;
}
function fmtJA4Full(ja4) {
if (!ja4) return '';
return `<a href="/detections?ja4=${encodeURIComponent(ja4)}" class="text-purple-400 hover:underline cursor-pointer font-mono text-[11px]">${escapeHtml(ja4)}</a>`;
}
function fmtBotName(name) {
if (!name) return '';
return `<a href="/detections?bot_name=${encodeURIComponent(name)}" class="text-cyan-400 hover:underline cursor-pointer">${escapeHtml(name)}</a>`;
}
function fmtThreatLink(level) {
if (!level) return '';
return `<a href="/detections?threat_level=${encodeURIComponent(level)}" class="cursor-pointer">${threatBadge(level)}</a>`;
}
function fmtLabel(label) {
if (!label) return '';
const colors = {human:'text-green-400 bg-green-500/10',datacenter:'text-red-400 bg-red-500/10',hosting:'text-orange-400 bg-orange-500/10',cdn:'text-cyan-400 bg-cyan-500/10'};
return `<span class="px-1.5 py-0.5 rounded text-xs ${colors[label]||'text-gray-400 bg-gray-500/10'}">${escapeHtml(label)}</span>`;
}
function fmtDuration(seconds) {
if (!seconds || seconds < 0) return '—';
if (seconds < 60) return Math.round(seconds) + 's';
if (seconds < 3600) return Math.round(seconds/60) + 'min';
return (seconds/3600).toFixed(1) + 'h';
}
function fmtAgo(dateStr) {
if (!dateStr) return '—';
const diff = (Date.now() - new Date(dateStr).getTime()) / 1000;
if (diff < 60) return 'à l\'instant';
if (diff < 3600) return Math.round(diff/60) + ' min';
if (diff < 86400) return Math.round(diff/3600) + 'h';
return Math.round(diff/86400) + 'j';
}
// ── ECharts helpers ──
const EC_COLORS = ['#6366f1','#22c55e','#f97316','#ef4444','#3b82f6','#eab308','#ec4899','#14b8a6','#8b5cf6','#f43f5e'];
const EC_TEXT = '#9ca3af';
const EC_GRID = '#374151';
function ecBase(overrides) {
return Object.assign({
backgroundColor: 'transparent',
textStyle: { color: EC_TEXT, fontFamily: 'Inter, system-ui, sans-serif', fontSize: 11 },
animation: true, animationDuration: 400,
}, overrides);
}
function ecTooltip(extra) {
return Object.assign({
backgroundColor: '#1f2937', borderColor: '#374151',
textStyle: { color: '#e5e7eb', fontSize: 11 },
confine: true,
}, extra);
}
function ecGrid(extra) {
return Object.assign({ left: 50, right: 20, top: 30, bottom: 30 }, extra);
}
// ── Chart resize on window resize ──
window.addEventListener('resize', () => {
document.querySelectorAll('[_echarts_instance_]').forEach(el => {
echarts.getInstanceByDom(el)?.resize();
});
});
// ── Generic table builder ──
function buildTable(tbody, rows, cols) {
tbody.innerHTML = rows.map(r =>
'<tr>' + cols.map(c => `<td>${c.fmt ? c.fmt(r[c.key], r) : escapeHtml(r[c.key]??'')}</td>`).join('') + '</tr>'
).join('');
}
// ── Doc helper: generates an ⓘ tooltip button + panel ──
function docHTML(title, body, source) {
return `<span class="relative inline-block ml-1"><button onclick="docToggle(this)" class="doc-btn" aria-label="Aide">ⓘ</button><div class="doc-panel"><h4>${escapeHtml(title)}</h4>${body}<p class="doc-source">Source : ${escapeHtml(source)}</p></div></span>`;
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>