Spaces:
Runtime error
Runtime error
| /** | |
| * ═══════════════════════════════════════════════════════════════════════════════ | |
| * SENTINEL INTELLIGENCE DASHBOARD - ENGINE v2.5.0 | |
| * ═══════════════════════════════════════════════════════════════════════════════ | |
| */ | |
| const SentinelDash = (function () { | |
| // --- STATE --- | |
| let state = { | |
| apiBase: "/api/v1", | |
| activeView: "command", | |
| refreshIntervals: [], | |
| graph: null, | |
| stats: { | |
| interceptions: 0, | |
| savings: 0, | |
| risk: "LOW", | |
| active: 0 | |
| }, | |
| agents: { | |
| detector: false, | |
| brain: false, | |
| siem: false | |
| } | |
| }; | |
| /** | |
| * Initialization | |
| */ | |
| function init() { | |
| console.log("Sentinel Dashboard Engine Initializing..."); | |
| // Loader Simulation | |
| setTimeout(() => { | |
| const loader = document.getElementById('loader'); | |
| if (loader) loader.classList.add('hidden'); | |
| }, 1500); | |
| // Start Data Stream | |
| refreshAll(); | |
| postBeacon(); // Send fingerprint once on load | |
| startAutoRefresh(); | |
| // Clock | |
| setInterval(updateClock, 1000); | |
| updateClock(); | |
| // Initialize Graph (lazy load to ensure container visibility) | |
| setTimeout(initGraph, 2000); | |
| } | |
| /** | |
| * View Switcher | |
| */ | |
| function switchView(viewId) { | |
| // Update Nav | |
| document.querySelectorAll('.nav-item').forEach(el => { | |
| el.classList.remove('active'); | |
| if (el.dataset.view === viewId) el.classList.add('active'); | |
| }); | |
| // Update Views | |
| document.querySelectorAll('.view').forEach(el => { | |
| el.classList.remove('active'); | |
| }); | |
| const target = document.getElementById(`view-${viewId}`); | |
| if (target) { | |
| target.classList.add('active'); | |
| // Resize graph if showing forensics | |
| if (viewId === 'forensics' && state.graph) { | |
| state.graph.resize(); | |
| state.graph.fit(); | |
| } | |
| } | |
| state.activeView = viewId; | |
| } | |
| /** | |
| * Data Refresh Logic | |
| */ | |
| async function refreshAll() { | |
| await Promise.all([ | |
| fetchStats(), | |
| fetchCampaigns(), | |
| fetchHeartbeat(), | |
| fetchTelemetry() | |
| ]); | |
| showToast("Dashboard Synced", "success"); | |
| } | |
| function startAutoRefresh() { | |
| state.refreshIntervals.push(setInterval(fetchStats, 10000)); | |
| state.refreshIntervals.push(setInterval(fetchHeartbeat, 5000)); | |
| state.refreshIntervals.push(setInterval(fetchCampaigns, 15000)); | |
| state.refreshIntervals.push(setInterval(fetchLogs, 3000)); // Log stream | |
| } | |
| /** | |
| * API Fetch Wrapper | |
| */ | |
| async function apiCall(endpoint, method = 'GET', body = null) { | |
| try { | |
| const opts = { | |
| method, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': 'SENTINEL-KEY-v1-SECURE' | |
| } | |
| }; | |
| if (body) opts.body = JSON.stringify(body); | |
| const res = await fetch(`${state.apiBase}${endpoint}`, opts); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| return await res.json(); | |
| } catch (err) { | |
| console.error(`API Error [${endpoint}]:`, err); | |
| return null; | |
| } | |
| } | |
| // --- FEATURE: STATISTICS (TRUTH VERIFIED) --- | |
| async function fetchStats() { | |
| const data = await apiCall('/stats'); | |
| // TRUTH CHECK: If API is dead, show DEAD state. Do NOT fake it. | |
| if (!data) { | |
| updateElement('stat-interceptions', 'ERR'); | |
| updateElement('stat-savings', 'ERR'); | |
| updateElement('stat-risk', 'OFFLINE'); | |
| updateElement('stat-active', '0'); | |
| return; | |
| } | |
| // Map API response to UI strict mapping | |
| const interceptions = data.total_scams_detected || 0; | |
| const savings = data.estimated_savings_inr ? (data.estimated_savings_inr / 10000000).toFixed(2) : "0.00"; | |
| const risk = data.current_threat_level || "LOW"; | |
| // REAL: Active sessions comes from orchestration state | |
| const active = data.active_sessions_count !== undefined ? data.active_sessions_count : 0; | |
| updateElement('stat-interceptions', interceptions.toLocaleString()); | |
| updateElement('stat-savings', `₹${savings} Cr`); | |
| updateElement('stat-risk', risk); | |
| updateElement('stat-active', active); | |
| // Update Risk Bar | |
| const riskIndicator = document.getElementById('risk-indicator'); | |
| if (riskIndicator) { | |
| riskIndicator.className = `kpi-indicator ${risk.toLowerCase()}`; | |
| } | |
| } | |
| // --- FEATURE: HEARTBEAT --- | |
| async function fetchHeartbeat() { | |
| // Simulation fallback if API fails | |
| const data = await apiCall('/health/agents') || { | |
| agents: { | |
| scam_detector: { status: 'active' }, | |
| persona_engine: { status: 'active' }, | |
| orchestrator: { status: 'active' } | |
| } | |
| }; | |
| updateAgentStatus('detector', data.agents?.scam_detector?.status === 'active'); | |
| updateAgentStatus('brain', data.agents?.persona_engine?.status === 'active'); | |
| updateAgentStatus('siem', data.agents?.orchestrator?.status === 'active'); | |
| } | |
| function updateAgentStatus(id, isOnline) { | |
| const dot = document.getElementById(`dot-${id}`); | |
| const text = document.getElementById(`status-${id}`); | |
| if (dot) { | |
| dot.className = `agent-dot ${isOnline ? 'online' : 'offline'}`; | |
| } | |
| if (text) { | |
| text.innerText = isOnline ? 'ONLINE' : 'OFFLINE'; | |
| text.style.color = isOnline ? 'var(--success)' : 'var(--danger)'; | |
| } | |
| } | |
| // --- FEATURE: THREAT FEED & CAMPAIGNS --- | |
| async function refreshCampaigns() { | |
| const list = document.getElementById('campaign-list'); | |
| if (!list) return; | |
| list.innerHTML = '<div class="campaign-item loading"><i class="fas fa-spinner fa-spin"></i> Updating...</div>'; | |
| const data = await apiCall('/campaigns'); | |
| if (data && data.campaigns) { | |
| list.innerHTML = ''; | |
| data.campaigns.forEach(c => { | |
| const item = document.createElement('div'); | |
| item.className = 'campaign-item'; | |
| item.innerHTML = ` | |
| <div class="campaign-header"> | |
| <span class="campaign-id">${c.cluster_id}</span> | |
| <span class="campaign-severity ${c.severity.toLowerCase()}">${c.severity}</span> | |
| </div> | |
| <div class="campaign-target">${c.threat_type}</div> | |
| <div class="campaign-stats"> | |
| <span><i class="fas fa-bullseye"></i> ${c.active_threads || 1} Threads</span> | |
| <span><i class="fas fa-clock"></i> Active</span> | |
| </div> | |
| `; | |
| list.appendChild(item); | |
| }); | |
| // Allow Ticker to update too | |
| updateTicker(data.campaigns); | |
| updateElement('alert-count', data.campaigns.length); | |
| } else { | |
| list.innerHTML = '<div style="padding:15px; text-align:center; color:var(--text-muted)">No active campaigns.</div>'; | |
| } | |
| } | |
| function updateTicker(campaigns) { | |
| const ticker = document.getElementById('threat-ticker'); | |
| if (!ticker) return; | |
| ticker.innerHTML = ''; | |
| campaigns.forEach(c => { | |
| const item = document.createElement('div'); | |
| item.className = 'ticker-item'; | |
| item.innerHTML = ` | |
| <div class="ticker-icon"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| </div> | |
| <div class="ticker-content"> | |
| <div class="ticker-title">THREAT DETECTED: ${c.threat_type}</div> | |
| <div class="ticker-meta"> | |
| <span class="ticker-id">${c.cluster_id}</span> | |
| <span class="ticker-time">Just now</span> | |
| </div> | |
| </div> | |
| `; | |
| ticker.appendChild(item); | |
| }); | |
| } | |
| // --- FEATURE: FORENSICS GRAPH --- | |
| async function initGraph() { | |
| const container = document.getElementById('threat-graph'); | |
| if (!container) return; | |
| let elements = []; | |
| try { | |
| const data = await apiCall('/threat-graph'); | |
| if (data && data.elements) { | |
| elements = data.elements; | |
| } else { | |
| // Fallback / Mock | |
| elements = [ | |
| { data: { id: 'a', label: 'Suspect_X' }, classes: 'scammer' }, | |
| { data: { id: 'b', label: 'UPI_9982' }, classes: 'upi' }, | |
| { data: { id: 'c', label: 'Device_iPhone' }, classes: 'phone' }, | |
| { data: { id: 'd', label: 'Camp_KBC' }, classes: 'campaign' }, | |
| { data: { id: 'ab', source: 'a', target: 'b' } }, | |
| { data: { id: 'ac', source: 'a', target: 'c' } }, | |
| { data: { id: 'ad', source: 'a', target: 'd' } } | |
| ]; | |
| } | |
| } catch (e) { console.error("Graph load failed", e); } | |
| state.graph = cytoscape({ | |
| container: container, | |
| elements: elements, | |
| style: [ | |
| { | |
| selector: 'node', | |
| style: { | |
| 'background-color': '#666', | |
| 'label': 'data(label)', | |
| 'color': '#fff', | |
| 'font-size': '10px', | |
| 'width': '20px', | |
| 'height': '20px' | |
| } | |
| }, | |
| { selector: '.scammer', style: { 'background-color': '#ff4757', 'width': 30, 'height': 30 } }, | |
| { selector: '.upi', style: { 'background-color': '#ffc107' } }, | |
| { selector: '.phone', style: { 'background-color': '#00d4ff' } }, | |
| { selector: '.campaign', style: { 'background-color': '#a855f7' } }, | |
| { | |
| selector: 'edge', | |
| style: { | |
| 'width': 2, | |
| 'line-color': '#555', | |
| 'curve-style': 'bezier', | |
| 'target-arrow-color': '#555', | |
| 'target-arrow-shape': 'triangle' | |
| } | |
| } | |
| ], | |
| layout: { | |
| name: 'cose', | |
| animate: true | |
| } | |
| }); | |
| } | |
| function refreshGraph() { | |
| if (!state.graph) return initGraph(); | |
| state.graph.layout({ name: 'random' }).run(); | |
| setTimeout(() => { | |
| state.graph.layout({ name: 'cose', animate: true }).run(); | |
| }, 500); | |
| } | |
| // --- FEATURE: LOGS & BEACON (ADDED FOR API COMPLIANCE) --- | |
| async function fetchLogs() { | |
| const term = document.getElementById('terminal-output'); | |
| if (!term) return; | |
| // Simulate log stream or fetch if API exists | |
| const data = await apiCall('/logs') || { | |
| logs: [ | |
| { ts: new Date().toISOString(), level: 'INFO', msg: 'System heartbeat verified.' } | |
| ] | |
| }; | |
| if (data && data.logs) { | |
| data.logs.forEach(log => { | |
| const line = document.createElement('div'); | |
| line.className = `log-line ${log.level ? log.level.toLowerCase() : 'info'}`; | |
| line.innerText = `[${log.ts.split('T')[1].split('.')[0]}] ${log.msg}`; | |
| term.appendChild(line); | |
| term.scrollTop = term.scrollHeight; // Auto-scroll | |
| }); | |
| // Cap lines | |
| while (term.children.length > 50) term.removeChild(term.firstChild); | |
| } | |
| } | |
| async function postBeacon() { | |
| // Silent fingerprinting | |
| const fp = { | |
| ua: navigator.userAgent, | |
| lang: navigator.language, | |
| res: `${window.screen.width}x${window.screen.height}`, | |
| tz: Intl.DateTimeFormat().resolvedOptions().timeZone | |
| }; | |
| await apiCall('/telemetry/beacon', 'POST', fp); | |
| console.log("Sentinel Beacon Transmitted."); | |
| } | |
| // --- FEATURE: TELEMETRY & DOSSIER --- | |
| async function fetchTelemetry() { | |
| const tbody = document.getElementById('telemetry-body'); | |
| const count = document.getElementById('telemetry-count'); | |
| if (!tbody) return; | |
| const data = await apiCall('/telemetry'); | |
| if (data && data.tracked_ips) { | |
| tbody.innerHTML = ''; | |
| const entries = Object.entries(data.tracked_ips); | |
| count.innerText = `${entries.length} records`; | |
| entries.slice(0, 10).forEach(([ip, info]) => { | |
| const tr = document.createElement('tr'); | |
| const riskLevel = info.risk_score > 0.7 ? 'HIGH' : (info.risk_score > 0.4 ? 'MEDIUM' : 'LOW'); | |
| tr.innerHTML = ` | |
| <td><code>${ip}</code></td> | |
| <td>${info.forensics?.os || 'Unknown'}</td> | |
| <td>${info.forensics?.browser || 'Chrome'}</td> | |
| <td><span class="risk-badge ${riskLevel.toLowerCase()}">${riskLevel}</span></td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| } | |
| function generateDossier() { | |
| const sid = document.getElementById('dossier-id').value; | |
| if (!sid) { | |
| showToast("Please enter a valid Session ID", "error"); | |
| return; | |
| } | |
| showToast("Generating secure dossier...", "info"); | |
| setTimeout(() => { | |
| // Initiate download via API | |
| window.open(`/api/v1/dossier/${sid}`, '_blank'); | |
| showToast("Dossier Download Started", "success"); | |
| }, 1500); | |
| } | |
| // --- FEATURE: ENGAGEMENT --- | |
| async function handleTestInput(e) { | |
| if (e.key === 'Enter') { | |
| const input = document.getElementById('test-input'); | |
| const history = document.getElementById('test-history'); | |
| const msg = input.value.trim(); | |
| if (!msg) return; | |
| // User Line | |
| const userLine = document.createElement('div'); | |
| userLine.className = 'history-item input'; | |
| userLine.innerText = `scammer@test:~$ ${msg}`; | |
| history.appendChild(userLine); | |
| input.value = ''; | |
| // Loading | |
| const loadLine = document.createElement('div'); | |
| loadLine.className = 'history-item system'; | |
| loadLine.innerText = '[SYSTEM] Analyzing pattern...'; | |
| history.appendChild(loadLine); | |
| history.scrollTop = history.scrollHeight; | |
| // API Call | |
| const data = await apiCall('/analyze', 'POST', { | |
| message: msg, | |
| conversation_id: 'LAB_TEST' | |
| }); | |
| // Remove loading | |
| loadLine.remove(); | |
| if (data) { | |
| const resLine = document.createElement('div'); | |
| resLine.className = 'history-item output'; | |
| resLine.innerHTML = `> RESPONSE: "${data.honeypot_response?.message || data.honeypot_response || 'No response'}"<br>> CONFIDENCE: ${((data.confidence || data.confidence_score || 0) * 100).toFixed(1)}%`; | |
| history.appendChild(resLine); | |
| } else { | |
| const errLine = document.createElement('div'); | |
| errLine.className = 'history-item error'; | |
| errLine.innerText = '[ERROR] Analysis failed. System offline?'; | |
| history.appendChild(errLine); | |
| } | |
| history.scrollTop = history.scrollHeight; | |
| } | |
| } | |
| async function generateHoneytoken(type) { | |
| showToast(`Generating ${type.toUpperCase()} Trap...`, "info"); | |
| // Adjusted per request: /decoys/upi/pay used conceptually for generation or direct link | |
| // Currently pointing to a generator endpoint that returns the link | |
| const data = await apiCall('/decoys/generate'); | |
| const output = document.getElementById('honeytoken-output'); | |
| const code = document.getElementById('honeytoken-link'); | |
| if (output && code) { | |
| // If data is null (API fail), simulate | |
| const link = data?.decoy_portal || `https://secure-bank.Verify/upi/pay?t=${Date.now()}`; | |
| code.innerText = link; | |
| output.style.display = 'block'; | |
| } | |
| } | |
| function copyHoneytoken() { | |
| const code = document.getElementById('honeytoken-link').innerText; | |
| navigator.clipboard.writeText(code); | |
| showToast("Link copied to clipboard", "success"); | |
| } | |
| function exportLogs(format) { | |
| showToast(`Exporting logs in ${format.toUpperCase()} format...`, "success"); | |
| setTimeout(() => { | |
| // Using user specified export path | |
| window.open(`/audit-logs/export?format=${format}`, '_blank'); | |
| }, 1000); | |
| } | |
| // --- UTILS --- | |
| function updateElement(id, value) { | |
| const el = document.getElementById(id); | |
| if (el) el.innerText = value; | |
| } | |
| function updateClock() { | |
| const now = new Date(); | |
| const el = document.getElementById('live-clock'); | |
| if (el) el.innerText = now.toLocaleTimeString(); | |
| } | |
| function showToast(msg, type = 'info') { | |
| const toast = document.getElementById('toast'); | |
| const text = document.getElementById('toast-message'); | |
| const icon = document.getElementById('toast-icon'); | |
| if (toast && text && icon) { | |
| text.innerText = msg; | |
| toast.className = `toast show ${type}`; | |
| icon.className = type === 'success' ? 'fas fa-check-circle' : (type === 'error' ? 'fas fa-times-circle' : 'fas fa-info-circle'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| } | |
| function clearTerminal() { | |
| const term = document.getElementById('terminal-output'); | |
| if (term) term.innerHTML = '<div class="log-line system">[SYSTEM] Stream cleared.</div>'; | |
| } | |
| function saveApiKey() { | |
| const input = document.getElementById('guvi-api-key'); | |
| const status = document.getElementById('key-status'); | |
| const key = input.value.trim(); | |
| if (key) { | |
| // Save to LocalStorage for persistence | |
| localStorage.setItem('SENTINEL_GUVI_KEY', key); | |
| // UI Feedback | |
| status.innerText = '✓ Key Configured & Encrypted Locally'; | |
| status.classList.add('configured'); | |
| input.value = ''; // Clear for security | |
| showToast("API Key Saved Successfully", "success"); | |
| } else { | |
| showToast("Please enter a valid key", "error"); | |
| } | |
| } | |
| function setTheme(theme) { | |
| document.body.className = theme === 'minimal' ? 'theme-minimal' : ''; | |
| // Save preference | |
| localStorage.setItem('SENTINEL_THEME', theme); | |
| showToast(`Theme switched to ${theme.toUpperCase()}`, "info"); | |
| } | |
| // Check saved settings on load | |
| function loadSettings() { | |
| if (localStorage.getItem('SENTINEL_GUVI_KEY')) { | |
| const status = document.getElementById('key-status'); | |
| if (status) { | |
| status.innerText = '✓ Key Configured & Encrypted Locally'; | |
| status.classList.add('configured'); | |
| } | |
| } | |
| const savedTheme = localStorage.getItem('SENTINEL_THEME'); | |
| if (savedTheme) setTheme(savedTheme); | |
| } | |
| // --- PUBLIC INTERFACE --- | |
| return { | |
| init: () => { | |
| init(); | |
| loadSettings(); | |
| }, | |
| switchView, | |
| refreshAll, | |
| refreshCampaigns, | |
| refreshGraph, | |
| generateDossier, | |
| handleTestInput, | |
| generateHoneytoken, | |
| copyHoneytoken, | |
| exportLogs, | |
| clearTerminal, | |
| saveApiKey, | |
| setTheme | |
| }; | |
| })(); | |
| // START | |
| document.addEventListener('DOMContentLoaded', SentinelDash.init); | |