heatmap / frontend /dashboard.html
Ndg07's picture
Feat: 24-hour cleanup for local SQLite
c293f7c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard — Misinformation Heatmap</title>
<meta name="description" content="Real-time analytics dashboard for misinformation detection across India.">
<link rel="icon" type="image/x-icon" href="/public/Misinfo.ico">
<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@300;400;500;600;700;800&family=Outfit:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
--saffron: #E87722;
--saffron-light: #FF9933;
--saffron-dark: #c4601a;
--green: #138808;
--green-light: #1ea50e;
--navy: #000080;
--red: #dc2626;
--amber: #d97706;
--blue: #2563eb;
--bg: #F8F8F4;
--bg-card: #FFFFFF;
--border: #E8E8E0;
--border-accent: rgba(232,119,34,0.25);
--text-primary: #1A1A1A;
--text-secondary:#555555;
--text-muted: #888888;
--shadow-sm: 0 2px 8px rgba(0,0,0,0.07);
--shadow-md: 0 4px 20px rgba(0,0,0,0.10);
--radius: 16px;
--radius-sm: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
max-width: 100vw;
}
/* ─── BACKGROUND ─── */
.bg-blobs { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.blob { position: absolute; border-radius: 50%; filter: blur(90px); opacity: 0.12; }
.blob-1 { width: 600px; height: 600px; top: -150px; left: -100px; background: var(--saffron-light); }
.blob-2 { width: 500px; height: 500px; bottom: -100px; right: -100px; background: var(--green-light); }
/* ─── NAVBAR ─── */
.navbar {
position: sticky; top: 0; z-index: 100;
height: 60px; display: flex; align-items: center; padding: 0 2rem;
background: rgba(248,248,244,0.9); backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border); box-shadow: var(--shadow-sm);
}
.nav-inner { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.logo { display: flex; align-items: center; gap: 4px; font-family: 'Outfit', sans-serif; font-size: 1.05rem; font-weight: 800; text-decoration: none; color: var(--text-primary); }
.logo-badge { font-size: 0.6rem; font-weight: 700; background: var(--saffron); color: #fff; padding: 2px 5px; border-radius: 3px; }
.logo .logo-name { color: var(--saffron); }
.nav-links { display: flex; gap: 0.25rem; }
.nav-link { color: var(--text-secondary); text-decoration: none; font-size: 0.85rem; font-weight: 500; padding: 0.4rem 0.9rem; border-radius: var(--radius-sm); transition: all 0.2s; }
.nav-link:hover { color: var(--saffron); background: rgba(232,119,34,0.07); }
.nav-link.active { color: var(--saffron); background: rgba(232,119,34,0.1); border: 1px solid rgba(232,119,34,0.2); font-weight: 600; }
.nav-right { display: flex; align-items: center; gap: 0.75rem; }
.status-pill { display: flex; align-items: center; gap: 6px; background: rgba(19,136,8,0.08); border: 1px solid rgba(19,136,8,0.25); border-radius: 20px; padding: 4px 12px; font-size: 0.78rem; font-weight: 600; color: var(--green); }
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(19,136,8,0.4);}50%{opacity:0.8;box-shadow:0 0 0 5px rgba(19,136,8,0);} }
.refresh-btn { background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary); padding: 5px 14px; border-radius: var(--radius-sm); font-size: 0.8rem; cursor: pointer; transition: all 0.2s; font-family: 'Inter', sans-serif; }
.refresh-btn:hover { border-color: var(--saffron); color: var(--saffron); }
/* Hamburger Menu */
.hamburger-btn {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 8px;
z-index: 2000;
}
.hamburger-line {
width: 24px;
height: 2px;
background: var(--text-primary);
transition: all 0.3s ease;
border-radius: 2px;
}
.hamburger-btn.active .hamburger-line:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.hamburger-btn.active .hamburger-line:nth-child(2) {
opacity: 0;
}
.hamburger-btn.active .hamburger-line:nth-child(3) {
transform: rotate(-45deg) translate(5px, -5px);
}
/* Mobile Menu Overlay */
.mobile-menu-overlay {
position: fixed;
top: 0;
right: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255, 153, 51, 0.98), rgba(232, 119, 34, 0.95));
z-index: 1500;
transform: translateX(100%);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 80px 2rem 2rem;
}
.mobile-menu-overlay.active {
transform: translateX(0);
}
.mobile-menu-top {
display: flex;
flex-direction: column;
gap: 1rem;
}
.mobile-menu-link {
font-family: 'Outfit', sans-serif;
font-size: 1.5rem;
font-weight: 700;
color: #fff;
text-decoration: none;
padding: 1rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.mobile-menu-link:hover {
padding-left: 1rem;
background: rgba(255, 255, 255, 0.1);
}
.mobile-menu-bottom {
display: flex;
flex-direction: column;
gap: 1rem;
padding-top: 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.mobile-menu-brand {
font-family: 'Outfit', sans-serif;
font-size: 1.2rem;
font-weight: 800;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
}
.mobile-menu-brand .logo-badge {
background: #fff;
color: var(--saffron);
}
.mobile-menu-brand .logo-name {
color: #fff;
}
.mobile-menu-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.menu-tag {
font-size: 0.7rem;
font-weight: 700;
padding: 4px 10px;
border-radius: 12px;
text-transform: uppercase;
}
.menu-tag.alpha {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.menu-tag.live {
background: #fff;
color: var(--green);
animation: pulse 2s infinite;
}
/* ─── MAIN LAYOUT ─── */
.main { position: relative; z-index: 1; padding: 2rem; max-width: 1400px; margin: 0 auto; }
.page-header { margin-bottom: 2rem; }
.page-title { font-family: 'Outfit', sans-serif; font-size: 1.8rem; font-weight: 800; color: var(--text-primary); }
.page-sub { color: var(--text-muted); font-size: 0.88rem; margin-top: 4px; }
.last-updated { color: var(--text-muted); font-size: 0.78rem; margin-top: 2px; }
/* ─── STAT CARDS ─── */
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 1.25rem; margin-bottom: 2rem; }
.stat-card {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1.5rem; position: relative;
overflow: hidden; transition: all 0.25s; box-shadow: var(--shadow-sm);
}
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; }
.stat-card.total::before { background: var(--blue); }
.stat-card.fake::before { background: var(--red); }
.stat-card.real::before { background: var(--green); }
.stat-card.uncertain::before { background: var(--amber); }
.stat-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
.stat-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
.stat-label { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-muted); }
.stat-icon { font-size: 1.4rem; }
.stat-value { font-family: 'Outfit', sans-serif; font-size: 2.4rem; font-weight: 900; line-height: 1; }
.stat-card.total .stat-value { color: var(--blue); }
.stat-card.fake .stat-value { color: var(--red); }
.stat-card.real .stat-value { color: var(--green); }
.stat-card.uncertain .stat-value { color: var(--amber); }
.stat-desc { font-size: 0.78rem; color: var(--text-muted); margin-top: 6px; }
.stat-bar { height: 3px; background: var(--border); border-radius: 2px; margin-top: 1rem; overflow: hidden; }
.stat-bar-fill { height: 100%; border-radius: 2px; transition: width 1s ease; width: 0; }
.stat-card.total .stat-bar-fill { background: var(--blue); }
.stat-card.fake .stat-bar-fill { background: var(--red); }
.stat-card.real .stat-bar-fill { background: var(--green); }
.stat-card.uncertain .stat-bar-fill { background: var(--amber); }
/* ─── GRID ROWS ─── */
.row { display: grid; gap: 1.25rem; margin-bottom: 1.25rem; }
.row-2 { grid-template-columns: 1fr 1.6fr; }
.row-2b { grid-template-columns: 1.6fr 1fr; }
.row-3 { grid-template-columns: 1fr 1fr 1fr; }
.panel {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm);
display: flex; flex-direction: column;
}
.panel-header {
display: flex; align-items: center; justify-content: space-between;
padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border);
}
.panel-title { font-size: 0.95rem; font-weight: 700; color: var(--text-primary); }
.panel-sub { font-size: 0.8rem; color: var(--text-muted); margin-top: 2px; }
.panel-badge { font-size: 0.72rem; font-weight: 600; color: var(--text-muted); background: var(--bg); border: 1px solid var(--border); padding: 3px 10px; border-radius: 10px; }
.panel-body { padding: 1.5rem; flex: 1; }
/* ─── ACCURACY RING ─── */
.accuracy-center { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 1.5rem 0; }
.ring-wrap { position: relative; display: inline-flex; align-items: center; justify-content: center; }
.ring-label { position: absolute; text-align: center; }
.ring-pct { font-family: 'Outfit', sans-serif; font-size: 2.2rem; font-weight: 900; color: var(--green); line-height: 1; }
.ring-sub { font-size: 0.7rem; color: var(--text-muted); margin-top: 2px; }
.metric-bar-row { display: flex; align-items: center; justify-content: space-around; margin-top: 1.25rem; padding-top: 1.25rem; border-top: 1px solid var(--border); }
.metric-bar-item { text-align: center; }
.mbi-val { font-family: 'Outfit', sans-serif; font-size: 1.4rem; font-weight: 800; }
.mbi-lbl { font-size: 0.72rem; color: var(--text-muted); margin-top: 2px; }
/* ─── ACTIVITY FEED ─── */
.activity-feed { overflow-y: auto; max-height: 320px; display: flex; flex-direction: column; gap: 0.6rem; }
.activity-feed::-webkit-scrollbar { width: 4px; }
.activity-feed::-webkit-scrollbar-track { background: var(--bg); }
.activity-feed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.activity-item { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.75rem; border-radius: var(--radius-sm); background: var(--bg); border: 1px solid var(--border); transition: all 0.2s; }
.activity-item:hover { border-color: var(--border-accent); background: rgba(232,119,34,0.02); }
.activity-dot { font-size: 1.1rem; flex-shrink: 0; }
.activity-info { flex: 1; min-width: 0; }
.activity-title { font-size: 0.83rem; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.activity-meta { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 3px; font-size: 0.72rem; color: var(--text-muted); }
.activity-tag { font-weight: 700; padding: 1px 6px; border-radius: 4px; font-size: 0.65rem; }
.activity-tag.fake { background: rgba(220,38,38,0.1); color: var(--red); }
.activity-tag.real { background: rgba(19,136,8,0.1); color: var(--green); }
.activity-tag.uncertain { background: rgba(217,119,6,0.1); color: var(--amber); }
.feed-count { font-size: 0.78rem; color: var(--text-muted); }
/* ─── COVERAGE ─── */
.coverage-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 1rem; }
.cov-item { text-align: center; padding: 1rem; background: var(--bg); border-radius: var(--radius-sm); border: 1px solid var(--border); }
.cov-val { font-family: 'Outfit', sans-serif; font-size: 1.8rem; font-weight: 900; color: var(--saffron); }
.cov-label { font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; }
/* ─── HEALTH METRICS ─── */
.metric-row { display: flex; align-items: center; justify-content: space-between; padding: 0.7rem 0; border-bottom: 1px solid var(--border); }
.metric-row:last-child { border-bottom: none; }
.metric-name { font-size: 0.85rem; color: var(--text-secondary); }
.metric-badge { font-size: 0.72rem; font-weight: 600; padding: 3px 10px; border-radius: 10px; }
.metric-badge.green { background: rgba(19,136,8,0.1); color: var(--green); }
.metric-badge.amber { background: rgba(217,119,6,0.1); color: var(--amber); }
.metric-badge.red { background: rgba(220,38,38,0.1); color: var(--red); }
/* ─── MAP LINK ─── */
.map-link { display: inline-flex; align-items: center; gap: 6px; background: var(--saffron); color: #fff; font-size: 0.82rem; font-weight: 600; padding: 6px 14px; border-radius: var(--radius-sm); text-decoration: none; transition: all 0.2s; }
.map-link:hover { background: var(--saffron-dark); }
.empty-feed { text-align: center; padding: 2rem; color: var(--text-muted); font-size: 0.88rem; }
.empty-icon { font-size: 2rem; margin-bottom: 8px; }
/* Enhanced Mobile Responsiveness */
@media (max-width: 1200px) {
.navbar { padding: 0 1rem; }
.main { padding: 1.5rem; }
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 1rem; }
.row-2, .row-2b { grid-template-columns: 1fr; gap: 1rem; }
.coverage-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 1024px) {
.nav-links { gap: 0.5rem; }
.nav-link { font-size: 0.8rem; padding: 0.3rem 0.7rem; }
.row-2, .row-2b { grid-template-columns: 1fr; }
.coverage-grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.navbar { padding: 0 1rem; height: auto; }
.nav-inner { flex-direction: column; gap: 0.75rem; padding: 0.75rem 0; }
.nav-links { order: 2; }
.nav-right { order: 1; }
.main { padding: 1rem; }
.page-header { margin-bottom: 1.5rem; }
.page-title { font-size: 1.5rem; }
.stats-grid { grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.stat-card { padding: 1.25rem; }
.stat-value { font-size: 2rem; }
.row-3 { grid-template-columns: 1fr; }
.panel-header { padding: 1rem; }
.panel-body { padding: 1rem; }
.activity-feed { max-height: 250px; }
.coverage-grid { grid-template-columns: 1fr; gap: 0.75rem; }
.cov-item { padding: 0.75rem; }
.metric-row { padding: 0.5rem 0; font-size: 0.8rem; }
}
@media (max-width: 480px) {
.navbar { padding: 0 0.75rem; }
.logo { font-size: 0.9rem; }
.logo-badge { font-size: 0.55rem; }
.nav-inner { padding: 0.5rem 0; }
.nav-link { font-size: 0.75rem; padding: 0.25rem 0.5rem; }
.refresh-btn { font-size: 0.7rem; padding: 4px 10px; }
.status-pill { display: none; }
.hamburger-btn { display: flex; }
.main { padding: 0.75rem; }
.page-title { font-size: 1.3rem; }
.page-sub { font-size: 0.8rem; }
.stats-grid { grid-template-columns: 1fr; gap: 0.5rem; }
.stat-card { padding: 1rem; }
.stat-value { font-size: 1.8rem; }
.stat-label { font-size: 0.65rem; }
.stat-desc { font-size: 0.7rem; }
.panel-header { padding: 0.75rem; }
.panel-title { font-size: 0.85rem; }
.panel-sub { font-size: 0.75rem; }
.panel-body { padding: 0.75rem; }
.activity-feed { max-height: 200px; gap: 0.4rem; }
.activity-item { padding: 0.5rem; }
.activity-title { font-size: 0.75rem; }
.activity-meta { font-size: 0.65rem; }
.coverage-grid { grid-template-columns: 1fr; gap: 0.5rem; }
.cov-item { padding: 0.5rem; }
.cov-val { font-size: 1.5rem; }
.cov-label { font-size: 0.7rem; }
.metric-row { padding: 0.4rem 0; font-size: 0.75rem; }
.metric-name { font-size: 0.75rem; }
.metric-badge { font-size: 0.65rem; padding: 2px 6px; }
}
</style>
</head>
<body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
</div>
<!-- Navbar -->
<nav class="navbar">
<div class="nav-inner">
<a href="/" class="logo">
<span class="logo-badge">IN</span>
<span>Misinformation</span><span class="logo-name">&nbsp;Heatmap</span>
</a>
<div class="nav-links">
<a href="/" class="nav-link">Home</a>
<a href="/dashboard" class="nav-link active">Dashboard</a>
<a href="/map/enhanced-india-heatmap.html" class="nav-link">Heatmap</a>
</div>
<div class="nav-right">
<div class="status-pill">
<span class="status-dot"></span>
<span id="system-status">Connecting…</span>
</div>
<button class="refresh-btn" onclick="refreshAll()">↻ Refresh</button>
<button class="hamburger-btn" id="hamburger-btn" aria-label="Toggle menu">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
</div>
</div>
</nav>
<!-- Mobile Menu Overlay -->
<div class="mobile-menu-overlay" id="mobile-menu">
<div class="mobile-menu-top">
<a href="/" class="mobile-menu-link">Home</a>
<a href="/dashboard" class="mobile-menu-link">Dashboard</a>
<a href="/map/enhanced-india-heatmap.html" class="mobile-menu-link">Heatmap</a>
</div>
<div class="mobile-menu-bottom">
<div class="mobile-menu-brand">
<span class="logo-badge">IN</span>
<span>Misinformation</span><span class="logo-name">&nbsp;Heatmap</span>
</div>
<div class="mobile-menu-tags">
<span class="menu-tag alpha">Alpha v1.0</span>
<span class="menu-tag live">● Live</span>
</div>
</div>
</div>
<div class="main">
<!-- Header -->
<div class="page-header">
<div class="page-title">Real-Time Analytics</div>
<div class="page-sub">Comprehensive misinformation detection metrics across India</div>
<div class="last-updated" id="last-updated">Fetching data…</div>
</div>
<!-- Stat Cards -->
<div class="stats-grid">
<div class="stat-card total">
<div class="stat-header">
<div class="stat-label">Total Events</div>
<div class="stat-icon">📊</div>
</div>
<div class="stat-value" id="total-events"></div>
<div class="stat-desc">Processed in real-time</div>
<div class="stat-bar"><div class="stat-bar-fill" id="bar-total"></div></div>
</div>
<div class="stat-card fake">
<div class="stat-header">
<div class="stat-label">Misinformation</div>
<div class="stat-icon">🚨</div>
</div>
<div class="stat-value" id="fake-events"></div>
<div class="stat-desc">High confidence detections</div>
<div class="stat-bar"><div class="stat-bar-fill" id="bar-fake"></div></div>
</div>
<div class="stat-card real">
<div class="stat-header">
<div class="stat-label">Verified News</div>
<div class="stat-icon"></div>
</div>
<div class="stat-value" id="real-events"></div>
<div class="stat-desc">Authenticated sources</div>
<div class="stat-bar"><div class="stat-bar-fill" id="bar-real"></div></div>
</div>
<div class="stat-card uncertain">
<div class="stat-header">
<div class="stat-label">Under Review</div>
<div class="stat-icon">⚠️</div>
</div>
<div class="stat-value" id="uncertain-events"></div>
<div class="stat-desc">Requires human verification</div>
<div class="stat-bar"><div class="stat-bar-fill" id="bar-uncertain"></div></div>
</div>
</div>
<!-- Row: AI Performance + Live Activity -->
<div class="row row-2">
<!-- AI Performance -->
<div class="panel">
<div class="panel-header">
<div>
<div class="panel-title">AI Detection Performance</div>
<div class="panel-sub">Multi-layered analysis system accuracy</div>
</div>
</div>
<div class="panel-body">
<div class="accuracy-center">
<div class="ring-wrap">
<svg width="160" height="160" style="transform:rotate(-90deg)">
<circle cx="80" cy="80" r="56" fill="none" stroke="#E8E8E0" stroke-width="10"/>
<circle id="accuracy-ring-fill" cx="80" cy="80" r="56" fill="none"
stroke="#138808" stroke-width="10" stroke-linecap="round"
stroke-dasharray="351.86" stroke-dashoffset="351.86"
style="transition: stroke-dashoffset 1.5s ease"/>
</svg>
<div class="ring-label">
<div class="ring-pct" id="accuracy-pct">91%</div>
<div class="ring-sub">Overall Accuracy</div>
</div>
</div>
</div>
<div class="metric-bar-row">
<div class="metric-bar-item">
<div class="mbi-val" style="color:var(--blue);">94.2%</div>
<div class="mbi-lbl">Precision</div>
</div>
<div class="metric-bar-item">
<div class="mbi-val" style="color:var(--green);">91.7%</div>
<div class="mbi-lbl">Recall</div>
</div>
<div class="metric-bar-item">
<div class="mbi-val" style="color:var(--saffron);">92.9%</div>
<div class="mbi-lbl">F1-Score</div>
</div>
</div>
</div>
</div>
<!-- Live Activity -->
<div class="panel">
<div class="panel-header">
<div>
<div class="panel-title">Live Activity</div>
<div class="panel-sub">Recent classifications</div>
</div>
<span class="feed-count" id="events-count">Loading…</span>
</div>
<div class="panel-body" style="overflow:hidden; padding:1rem;">
<div class="activity-feed" id="activity-feed">
<div class="empty-feed"><div class="empty-icon"></div>Loading recent activity…</div>
</div>
</div>
</div>
</div>
<!-- Row: Coverage + Health -->
<div class="row row-2b">
<!-- Geographic Coverage -->
<div class="panel">
<div class="panel-header">
<div>
<div class="panel-title">Geographic Coverage</div>
<div class="panel-sub">Monitoring across all Indian states and union territories</div>
</div>
<a href="/map/enhanced-india-heatmap.html" class="map-link">🗺️ View Interactive Map</a>
</div>
<div class="panel-body">
<div class="coverage-grid">
<div class="cov-item">
<div class="cov-val" id="total-states">36</div>
<div class="cov-label">States &amp; UTs</div>
</div>
<div class="cov-item">
<div class="cov-val">34+</div>
<div class="cov-label">News Sources</div>
</div>
<div class="cov-item">
<div class="cov-val">100+</div>
<div class="cov-label">Articles/Second</div>
</div>
</div>
<div style="margin-top:1.25rem; padding:1rem; background:rgba(232,119,34,0.05); border:1px solid rgba(232,119,34,0.15); border-radius:12px; font-size:0.85rem; color:var(--text-secondary); line-height:1.6;">
🌐 Monitoring <strong style="color:var(--saffron)">all 28 states + 8 union territories</strong> with concurrent RSS ingestion, processed through our 5-model ML ensemble, IndicBERT NLP pipeline, and GDELT global news index.
</div>
</div>
</div>
<!-- System Health -->
<div class="panel">
<div class="panel-header">
<div class="panel-title">System Health</div>
<div class="panel-badge" id="health-badge">Checking…</div>
</div>
<div class="panel-body">
<div class="metric-row">
<span class="metric-name">API Server</span>
<span class="metric-badge green" id="health-api">● Online</span>
</div>
<div class="metric-row">
<span class="metric-name">Data Processing</span>
<span class="metric-badge amber" id="health-proc">● Checking</span>
</div>
<div class="metric-row">
<span class="metric-name">Database</span>
<span class="metric-badge green" id="health-db">● Connected</span>
</div>
<div class="metric-row">
<span class="metric-name">ML Models</span>
<span class="metric-badge green" id="health-ml">● Checking</span>
</div>
<div class="metric-row">
<span class="metric-name">RSS Feeds</span>
<span class="metric-badge green">● 34+ Active</span>
</div>
<div class="metric-row">
<span class="metric-name">GDELT Source</span>
<span class="metric-badge green">● Connected</span>
</div>
</div>
</div>
</div>
</div>
<script>
// ── API Configuration ──
const API_BASE = window.location.hostname.includes('netlify.app')
? 'https://ndg07-heatmap.hf.space'
: '';
// ── Hamburger menu toggle ──
const hamburgerBtn = document.getElementById('hamburger-btn');
const mobileMenu = document.getElementById('mobile-menu');
hamburgerBtn.addEventListener('click', () => {
hamburgerBtn.classList.toggle('active');
mobileMenu.classList.toggle('active');
document.body.style.overflow = mobileMenu.classList.contains('active') ? 'hidden' : '';
});
// Close menu when clicking on a link
document.querySelectorAll('.mobile-menu-link').forEach(link => {
link.addEventListener('click', () => {
hamburgerBtn.classList.remove('active');
mobileMenu.classList.remove('active');
document.body.style.overflow = '';
});
});
// Close menu when clicking outside
mobileMenu.addEventListener('click', (e) => {
if (e.target === mobileMenu) {
hamburgerBtn.classList.remove('active');
mobileMenu.classList.remove('active');
document.body.style.overflow = '';
}
});
const fmt = n => n >= 1000000 ? (n/1e6).toFixed(1)+'M' : n >= 1000 ? (n/1000).toFixed(1)+'K' : String(n);
function animateNum(el, to) {
if (!el) return;
const from = parseInt(el.textContent.replace(/[^0-9]/g,'')) || 0;
if (from === to) return;
const step = Math.max(1, Math.ceil(Math.abs(to - from) / 25));
let cur = from;
const timer = setInterval(() => {
cur += (to > from ? step : -step);
if ((to > from && cur >= to) || (to < from && cur <= to)) { cur = to; clearInterval(timer); }
el.textContent = fmt(cur);
}, 25);
}
async function updateStats(d) {
try {
if (!d) {
const res = await fetch(API_BASE + '/api/v1/stats');
if (!res.ok) throw new Error();
d = await res.json();
}
animateNum(document.getElementById('total-events'), d.total_events || 0);
animateNum(document.getElementById('fake-events'), d.fake_events || 0);
animateNum(document.getElementById('real-events'), d.real_events || 0);
animateNum(document.getElementById('uncertain-events'), d.uncertain_events || 0);
document.getElementById('total-states').textContent = d.total_states || 36;
const total = d.total_events || 1;
setTimeout(() => {
document.getElementById('bar-total').style.width = '100%';
document.getElementById('bar-fake').style.width = ((d.fake_events||0)/total*100) + '%';
document.getElementById('bar-real').style.width = ((d.real_events||0)/total*100) + '%';
document.getElementById('bar-uncertain').style.width = ((d.uncertain_events||0)/total*100) + '%';
}, 200);
const statusEl = document.getElementById('system-status');
const procEl = document.getElementById('health-proc');
const mlEl = document.getElementById('health-ml');
const badge = document.getElementById('health-badge');
statusEl.textContent = d.processing_active ? 'Live Processing' : (d.ml_ready ? 'System Ready' : 'Loading…');
procEl.textContent = d.processing_active ? '● Active' : '● Idle';
procEl.className = `metric-badge ${d.processing_active ? 'green' : 'amber'}`;
mlEl.textContent = d.ml_ready ? '● Loaded' : '● Loading…';
mlEl.className = `metric-badge ${d.ml_ready ? 'green' : 'amber'}`;
badge.textContent = d.processing_active ? 'All Systems Go' : 'Standby';
badge.style.color = d.processing_active ? 'var(--green)' : 'var(--text-muted)';
// Accuracy ring
const acc = d.classification_accuracy || 0.91;
document.getElementById('accuracy-pct').textContent = Math.round(acc * 100) + '%';
setTimeout(() => {
document.getElementById('accuracy-ring-fill').style.strokeDashoffset = 351.86 * (1 - acc);
}, 300);
document.getElementById('last-updated').textContent =
'Last updated: ' + new Date().toLocaleTimeString('en-IN', { hour12: true });
} catch(_) {
document.getElementById('system-status').textContent = 'Connecting…';
}
}
async function updateActivity(eventsPayload) {
try {
let events;
if (!eventsPayload) {
const res = await fetch(API_BASE + '/api/v1/events/live?limit=15');
if (!res.ok) throw new Error();
const d = await res.json();
events = d.events || [];
} else {
events = eventsPayload.events || [];
}
document.getElementById('events-count').textContent = events.length + ' events';
const feed = document.getElementById('activity-feed');
if (events.length === 0) {
feed.innerHTML = `<div class="empty-feed"><div class="empty-icon">📭</div><p>No recent events</p></div>`;
return;
}
feed.innerHTML = events.map(ev => {
const cls = ev.classification || 'uncertain';
const icon = cls === 'fake' ? '🚨' : cls === 'real' ? '✅' : '⚠️';
const time = ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString('en-IN', {hour:'2-digit', minute:'2-digit', hour12:true}) : '—';
const title = (ev.title || 'Processing event…').substring(0, 90);
const conf = ev.confidence ? Math.round(ev.confidence * 100) + '%' : '';
return `<div class="activity-item">
<div class="activity-dot">${icon}</div>
<div class="activity-info">
<div class="activity-title" title="${(ev.title||'').replace(/"/g,'&quot;')}">${title}</div>
<div class="activity-meta">
<span class="activity-tag ${cls}">${cls.toUpperCase()}</span>
<span>${ev.source || 'Unknown'}</span>
<span>📍 ${ev.state || 'India'}</span>
${conf ? `<span>${conf}</span>` : ''}
<span>🕐 ${time}</span>
</div>
</div>
</div>`;
}).join('');
} catch(_) {}
}
function refreshAll() { updateStats(); updateActivity(); }
// Initial load
updateStats();
updateActivity();
// SSE for real-time updates
const sse = new EventSource(API_BASE + '/api/v1/stream');
sse.addEventListener('stats', e => updateStats(JSON.parse(e.data)));
sse.addEventListener('live_events', e => updateActivity(JSON.parse(e.data)));
sse.onerror = () => { document.getElementById('system-status').textContent = 'Reconnecting…'; };
// Fallback polling every 15s in case SSE stalls
setInterval(refreshAll, 15000);
</script>
</body>
</html>