| <!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; |
| } |
| |
| |
| .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 { |
| 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-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 { |
| 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 { 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; } |
| |
| |
| .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); } |
| |
| |
| .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-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 { 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-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; } |
| |
| |
| .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 { 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; } |
| |
| |
| @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> |
|
|
| |
| <nav class="navbar"> |
| <div class="nav-inner"> |
| <a href="/" class="logo"> |
| <span class="logo-badge">IN</span> |
| <span>Misinformation</span><span class="logo-name"> 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> |
|
|
| |
| <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"> 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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="row row-2"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="row row-2b"> |
| |
| <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 & 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> |
|
|
| |
| <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> |
| |
| const API_BASE = window.location.hostname.includes('netlify.app') |
| ? 'https://ndg07-heatmap.hf.space' |
| : ''; |
| |
| |
| 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' : ''; |
| }); |
| |
| |
| document.querySelectorAll('.mobile-menu-link').forEach(link => { |
| link.addEventListener('click', () => { |
| hamburgerBtn.classList.remove('active'); |
| mobileMenu.classList.remove('active'); |
| document.body.style.overflow = ''; |
| }); |
| }); |
| |
| |
| 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)'; |
| |
| |
| 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,'"')}">${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> |