// =====================================================
// SentinelAI Command Center - Main Controller
// =====================================================
// ---- State ----
const state = {
startTime: Date.now(),
scanQueue: 0,
threatCount: 247,
scanCount: 1832,
vulnCount: 54,
assetCount: 128,
alerts: [],
logs: [],
discoveries: [],
targets: [],
sparklineData: [],
scanning: false
};
// ---- Threat Map Data (simulated world coordinates) ----
const threatZones = [
{ name: 'North America', lat: 39.8, lng: -98.6, threats: 45, level: 'high' },
{ name: 'Europe', lat: 50.1, lng: 9.8, threats: 38, level: 'medium' },
{ name: 'East Asia', lat: 35.9, lng: 104.2, threats: 52, level: 'critical' },
{ name: 'South Asia', lat: 20.6, lng: 78.9, threats: 21, level: 'medium' },
{ name: 'Middle East', lat: 29.5, lng: 45.5, threats: 15, level: 'high' },
{ name: 'South America', lat: -14.2, lng: -51.9, threats: 12, level: 'low' },
{ name: 'Africa', lat: 6.6, lng: 20.9, threats: 8, level: 'low' },
{ name: 'Oceania', lat: -25.3, lng: 133.8, threats: 6, level: 'low' },
{ name: 'Russia', lat: 61.5, lng: 105.3, threats: 33, level: 'high' },
{ name: 'SE Asia', lat: 1.4, lng: 103.8, threats: 18, level: 'medium' },
];
// ---- Alert Templates ----
const alertTemplates = [
{ severity: 'critical', icon: 'shield-alert', messages: [
'SQL Injection attempt detected on /api/v2/users',
'Brute force attack on SSH port 22 from 185.x.x.x',
'Data exfiltration anomaly detected - 2.3GB outbound',
'Zero-day exploit CVE-2024-XXXX detected in wild',
]},
{ severity: 'high', icon: 'alert-triangle', messages: [
'Unusual login pattern from geographic outlier',
'DDoS attack mitigation active - 15Gbps filtered',
'Exposed .git repository found on subdomain',
'Credential stuffing attack on login endpoint',
]},
{ severity: 'medium', icon: 'info', messages: [
'New subdomain discovered: dev.internal.target.com',
'SSL certificate expiring in 14 days',
'Port 8080 newly opened on monitored asset',
'API rate limit threshold reached for key X',
]},
{ severity: 'low', icon: 'bell', messages: [
'Scheduled scan completed for sector A',
'DNS record change detected',
'New HTTP header observed on endpoint',
'Cookie security flag missing on staging',
]}
];
// ---- Discovery Templates ----
const discoveryTemplates = [
{ type: 'subdomain', icon: 'globe', color: 'cyan', msg: 'Subdomain found: {sub}.target.com' },
{ type: 'port', icon: 'plug', color: 'amber', msg: 'Open port {port} detected on {ip}' },
{ type: 'vuln', icon: 'bug', color: 'red', msg: 'XSS vulnerability in /search?q=' },
{ type: 'dork', icon: 'search', color: 'purple', msg: 'Google Dork match: ext:sql site:target.com' },
{ type: 'config', icon: 'file-warning', color: 'orange', msg: 'Exposed config file: /.env' },
{ type: 'api', icon: 'code', color: 'blue', msg: 'Unauthenticated API endpoint: /api/v1/admin' },
];
// ---- Target Data ----
const targetData = [
{ name: 'prod-server-01', ip: '10.0.1.15', hits: 847, severity: 'critical' },
{ name: 'web-gateway', ip: '10.0.1.1', hits: 623, severity: 'high' },
{ name: 'db-cluster-master', ip: '10.0.2.10', hits: 412, severity: 'high' },
{ name: 'api-load-balancer', ip: '10.0.1.50', hits: 289, severity: 'medium' },
{ name: 'cdn-edge-node-03', ip: '10.0.3.22', hits: 156, severity: 'medium' },
{ name: 'staging-env', ip: '10.0.4.5', hits: 98, severity: 'low' },
{ name: 'dev-sandbox', ip: '10.0.5.12', hits: 34, severity: 'low' },
];
// ---- Initialize ----
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
initCounters();
initAlerts();
initDiscoveries();
initTargets();
initSparkline();
initThreatMap();
startClock();
startOperationalMode();
});
// ---- Clock & Uptime ----
function startClock() {
function update() {
const now = new Date();
const timeEl = document.getElementById('systemTime');
if (timeEl) timeEl.textContent = now.toUTCString().split(' ')[4] + ' UTC';
const uptimeEl = document.getElementById('uptimeCounter');
if (uptimeEl) {
const diff = Date.now() - state.startTime;
const d = Math.floor(diff / 86400000);
const h = Math.floor((diff % 86400000) / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
uptimeEl.textContent = `${d}d ${h}h ${m}m`;
}
}
update();
setInterval(update, 1000);
}
// ---- Counter Animation ----
function animateCounter(el, target, duration = 1500) {
if (!el) return;
const start = parseInt(el.textContent) || 0;
const startTime = performance.now();
function step(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
el.textContent = Math.floor(start + (target - start) * eased).toLocaleString();
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
function initCounters() {
animateCounter(document.getElementById('threatCount'), state.threatCount);
animateCounter(document.getElementById('scanCount'), state.scanCount);
animateCounter(document.getElementById('vulnCount'), state.vulnCount);
animateCounter(document.getElementById('assetCount'), state.assetCount);
}
// ---- Alerts ----
async function initAlerts() {
await fetchAndAddAlerts();
setInterval(fetchAndAddAlerts, 8000);
}
function addAlert() {
const template = alertTemplates[Math.floor(Math.random() * alertTemplates.length)];
const message = template.messages[Math.floor(Math.random() * template.messages.length)];
const severity = template.severity;
const icon = template.icon;
const now = new Date();
const timeStr = now.toTimeString().split(' ')[0];
const colorMap = {
critical: { bg: 'bg-red-500/10', border: 'border-red-500/20', text: 'text-red-400', badge: 'bg-red-500/20 text-red-400' },
high: { bg: 'bg-amber-500/10', border: 'border-amber-500/20', text: 'text-amber-400', badge: 'bg-amber-500/20 text-amber-400' },
medium: { bg: 'bg-cyan-500/10', border: 'border-cyan-500/20', text: 'text-cyan-400', badge: 'bg-cyan-500/20 text-cyan-400' },
low: { bg: 'bg-slate-500/10', border: 'border-slate-700/30', text: 'text-slate-400', badge: 'bg-slate-500/20 text-slate-400' },
};
const c = colorMap[severity] || colorMap.low;
const alertObj = { severity, message, time: timeStr, icon, colors: c };
state.alerts.unshift(alertObj);
if (state.alerts.length > 20) state.alerts.pop();
renderAlerts();
updateAlertCount();
}
function renderAlerts() {
const list = document.getElementById('alertsList');
if (!list) return;
list.innerHTML = state.alerts.map((a, i) => `
${a.message}
${a.severity}
${a.time}
`).join('');
lucide.createIcons();
}
function updateAlertCount() {
const el = document.getElementById('alertCount');
if (el) {
const critCount = state.alerts.filter(a => a.severity === 'critical' || a.severity === 'high').length;
el.textContent = critCount;
}
}
// ---- Activity Log ----
function addLog(message, type = 'info') {
const now = new Date();
const timeStr = now.toTimeString().split(' ')[0];
const colorMap = {
info: 'text-slate-400',
success: 'text-green-400',
warning: 'text-amber-400',
error: 'text-red-400',
system: 'text-cyan-400',
};
const prefixMap = {
info: 'INF',
success: 'OK ',
warning: 'WRN',
error: 'ERR',
system: 'SYS',
};
state.logs.unshift({ message, type, time: timeStr });
if (state.logs.length > 50) state.logs.pop();
renderLogs();
}
function renderLogs() {
const el = document.getElementById('activityLog');
if (!el) return;
const colorMap = {
info: 'text-slate-400',
success: 'text-green-400',
warning: 'text-amber-400',
error: 'text-red-400',
system: 'text-cyan-400',
};
const prefixMap = {
info: 'INF',
success: 'OK ',
warning: 'WRN',
error: 'ERR',
system: 'SYS',
};
el.innerHTML = state.logs.map(l => `
${l.time}
[${prefixMap[l.type] || 'INF'}]
${l.message}
`).join('');
}
function clearLogs() {
state.logs = [];
renderLogs();
showToast('Logs cleared');
}
// ---- Discoveries ----
function initDiscoveries() {
// Discoveries are already populated by fetchAndAddAlerts, no need for simulated ones.
setInterval(fetchAndAddAlerts, 12000);
}
function addDiscovery() {
const template = discoveryTemplates[Math.floor(Math.random() * discoveryTemplates.length)];
const subs = ['dev', 'staging', 'api', 'admin', 'cdn', 'mail', 'vpn', 'git', 'app', 'test'];
const ports = [22, 80, 443, 3306, 5432, 8080, 8443, 27017, 6379, 9200];
const ips = ['10.0.1.15', '10.0.2.10', '192.168.1.1', '172.16.0.5'];
let msg = template.msg
.replace('{sub}', subs[Math.floor(Math.random() * subs.length)])
.replace('{port}', ports[Math.floor(Math.random() * ports.length)])
.replace('{ip}', ips[Math.floor(Math.random() * ips.length)]);
const now = new Date();
const timeStr = now.toTimeString().split(' ')[0].substring(0, 5);
state.discoveries.unshift({ type: template.type, icon: template.icon, color: template.color, msg, time: timeStr });
if (state.discoveries.length > 15) state.discoveries.pop();
renderDiscoveries();
}
function renderDiscoveries() {
const el = document.getElementById('discoveriesList');
if (!el) return;
const colorClasses = {
cyan: 'text-cyan-400 bg-cyan-500/10',
amber: 'text-amber-400 bg-amber-500/10',
red: 'text-red-400 bg-red-500/10',
purple: 'text-purple-400 bg-purple-500/10',
orange: 'text-orange-400 bg-orange-500/10',
blue: 'text-blue-400 bg-blue-500/10',
};
el.innerHTML = state.discoveries.map(d => {
const cc = colorClasses[d.color] || colorClasses.cyan;
const [textC, bgC] = cc.split(' ');
return `
`;
}).join('');
lucide.createIcons();
}
// ---- Top Targets ----
async function initTargets() {
// Utiliser des IPs réelles
const ips = ['8.8.8.8', '1.1.1.1', '208.67.222.222', '9.9.9.9', '8.8.4.4', '1.0.0.1', '208.67.220.220'];
const results = await SentinelAPI.batchLookup(ips);
state.targets = results.map((r, i) => ({
name: r.org ? r.org.split(' ')[0] : `Target-${i+1}`,
ip: r.ip || ips[i],
hits: Math.floor(Math.random() * 800 + 100),
severity: r.country ? 'medium' : 'low',
}));
renderTargets();
}
function renderTargets() {
const el = document.getElementById('topTargetsList');
if (!el) return;
const sevColors = {
critical: 'text-red-400 bg-red-500/10 border-red-500/20',
high: 'text-amber-400 bg-amber-500/10 border-amber-500/20',
medium: 'text-cyan-400 bg-cyan-500/10 border-cyan-500/20',
low: 'text-slate-400 bg-slate-500/10 border-slate-700/30',
};
const maxHits = Math.max(...state.targets.map(t => t.hits));
el.innerHTML = state.targets.map(t => {
const sc = sevColors[t.severity] || sevColors.low;
const [textC, bgC, borderC] = sc.split(' ');
const barWidth = (t.hits / maxHits * 100);
const barColor = t.severity === 'critical' ? 'bg-red-500' : t.severity === 'high' ? 'bg-amber-500' : t.severity === 'medium' ? 'bg-cyan-500' : 'bg-slate-500';
return `
`;
}).join('');
lucide.createIcons();
}
// ---- Threat Map (Leaflet) ----
let threatMap = null;
let pulseRings = [];
function initThreatMap() {
const mapEl = document.getElementById('threatMap');
if (!mapEl) return;
// Initialize Leaflet map with dark theme
threatMap = L.map('threatMap', {
center: [20, 0],
zoom: 2,
minZoom: 2,
maxZoom: 8,
zoomControl: false,
attributionControl: false,
worldCopyJump: true,
scrollWheelZoom: true,
});
// Dark theme tiles (CartoDB Dark Matter)
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
subdomains: 'abcd',
maxZoom: 19,
}).addTo(threatMap);
// Zoom control bottom-right
L.control.zoom({ position: 'bottomright' }).addTo(threatMap);
// Color map for threat levels
const colorMap = {
critical: '#dc2626',
high: '#f59e0b',
medium: '#06b6d4',
low: '#64748b',
};
// Add threat markers
threatZones.forEach(zone => {
const color = colorMap[zone.level] || colorMap.low;
const intensity = Math.min(zone.threats / 60, 1);
const markerRadius = 6 + intensity * 6;
// Glow circle (semi-transparent fill)
L.circle([zone.lat, zone.lng], {
radius: 400000 + intensity * 600000,
fillColor: color,
color: color,
weight: 0,
opacity: 0,
fillOpacity: 0.08,
}).addTo(threatMap);
// Main marker
const marker = L.circleMarker([zone.lat, zone.lng], {
radius: markerRadius,
fillColor: color,
color: '#0c1222',
weight: 2,
opacity: 1,
fillOpacity: 0.9,
}).addTo(threatMap);
// Popup
marker.bindPopup(`
${zone.name}
${zone.level} · ${zone.threats} threats
`);
// Tooltip on hover
marker.bindTooltip(`${zone.name} — ${zone.threats} threats`, {
direction: 'top',
offset: [0, -markerRadius - 4],
className: 'threat-tooltip',
});
// Pulse ring (animated)
const pulseRing = L.circle([zone.lat, zone.lng], {
radius: 100000,
fillColor: color,
color: color,
weight: 1.5,
opacity: 0.5,
fillOpacity: 0,
}).addTo(threatMap);
pulseRings.push({ ring: pulseRing, color, phase: Math.random() });
});
// Draw connection lines between zones
const connections = [[0, 2], [1, 8], [2, 9], [3, 4], [0, 1]];
connections.forEach(([a, b]) => {
const za = threatZones[a];
const zb = threatZones[b];
L.polyline([[za.lat, za.lng], [zb.lat, zb.lng]], {
color: '#06b6d4',
weight: 1,
opacity: 0.12,
dashArray: '4, 6',
}).addTo(threatMap);
});
// Fix map size after layout settles
setTimeout(() => { if (threatMap) threatMap.invalidateSize(); }, 100);
// Start pulse animation
animatePulseRings();
}
function animatePulseRings() {
if (!threatMap) return;
pulseRings.forEach(p => {
p.phase = (p.phase + 0.008) % 1;
const radius = 800000 * p.phase + 80000;
const opacity = 0.5 * (1 - p.phase);
p.ring.setRadius(radius);
p.ring.setStyle({ opacity: opacity, fillOpacity: 0 });
});
requestAnimationFrame(animatePulseRings);
}
// ---- Sparkline Chart ----
function initSparkline() {
for (let i = 0; i < 60; i++) {
state.sparklineData.push(Math.random() * 40 + 30);
}
drawSparkline();
setInterval(() => {
state.sparklineData.shift();
state.sparklineData.push(Math.random() * 40 + 30);
drawSparkline();
}, 1000);
}
function drawSparkline() {
const canvas = document.getElementById('sparkline');
if (!canvas) return;
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = 50;
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
const data = state.sparklineData;
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
ctx.clearRect(0, 0, w, h);
// Fill gradient
const gradient = ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, 'rgba(6, 182, 212, 0.2)');
gradient.addColorStop(1, 'rgba(6, 182, 212, 0)');
ctx.beginPath();
ctx.moveTo(0, h);
data.forEach((val, i) => {
const x = (i / (data.length - 1)) * w;
const y = h - ((val - min) / range) * (h - 4) - 2;
if (i === 0) ctx.lineTo(x, y);
else ctx.lineTo(x, y);
});
ctx.lineTo(w, h);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
// Line
ctx.beginPath();
data.forEach((val, i) => {
const x = (i / (data.length - 1)) * w;
const y = h - ((val - min) / range) * (h - 4) - 2;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.strokeStyle = 'rgba(6, 182, 212, 0.7)';
ctx.lineWidth = 1.5;
ctx.stroke();
// Endpoint dot
const lastVal = data[data.length - 1];
const lastX = w;
const lastY = h - ((lastVal - min) / range) * (h - 4) - 2;
ctx.fillStyle = '#22d3ee';
ctx.beginPath();
ctx.arc(lastX - 1, lastY, 2.5, 0, Math.PI * 2);
ctx.fill();
}
// ---- System Health Simulation ----
function updateSystemHealth() {
const metrics = [
{ bar: 'cpuBar', val: 'cpuVal', base: 45, range: 30 },
{ bar: 'memBar', val: 'memVal', base: 60, range: 20 },
{ bar: 'netBar', val: 'netVal', base: 25, range: 35 },
{ bar: 'diskBar', val: 'diskVal', base: 55, range: 10 },
];
metrics.forEach(m => {
const value = Math.floor(m.base + Math.random() * m.range);
const barEl = document.getElementById(m.bar);
const valEl = document.getElementById(m.val);
if (barEl) barEl.style.width = `${value}%`;
if (valEl) valEl.textContent = `${value}%`;
});
}
// ---- Scan Simulation ----
function startScan(type) {
if (state.scanning) {
showToast('Scan already in progress');
return;
}
const target = document.getElementById('scanTarget');
const targetValue = target && target.value.trim() ? target.value.trim() : 'target.local';
state.scanning = true;
const progressDiv = document.getElementById('scanProgress');
const stageEl = document.getElementById('scanStage');
const percentEl = document.getElementById('scanPercent');
const barEl = document.getElementById('scanBar');
const queueEl = document.getElementById('scanQueue');
if (progressDiv) progressDiv.classList.remove('hidden');
state.scanQueue++;
if (queueEl) queueEl.textContent = state.scanQueue;
const typeNames = { recon: 'Reconnaissance', vuln: 'Vulnerability', dork: 'Google Dork', full: 'Full Audit' };
const stages = {
recon: ['Resolving target...', 'Port scanning...', 'Service detection...', 'OS fingerprinting...', 'Generating report...'],
vuln: ['Loading vulnerability DB...', 'Testing injection points...', 'Checking misconfigurations...', 'Running exploit checks...', 'Compiling results...'],
dork: ['Connecting to Google...', 'Building dork queries...', 'Analyzing results...', 'Checking cached pages...', 'Aggregating findings...'],
full: ['Initializing full audit...', 'Phase 1: Recon...', 'Phase 2: Vuln scanning...', 'Phase 3: Dork analysis...', 'Phase 4: Report generation...'],
};
const scanStages = stages[type] || stages.recon;
let currentStage = 0;
let progress = 0;
addLog(`Starting ${typeNames[type] || type} scan on ${targetValue}`, 'system');
const interval = setInterval(() => {
progress += Math.random() * 8 + 2;
if (progress >= 100) progress = 100;
const stageIndex = Math.min(Math.floor(progress / 25), scanStages.length - 1);
if (stageIndex !== currentStage) {
currentStage = stageIndex;
addLog(scanStages[currentStage], 'info');
}
if (stageEl) stageEl.textContent = scanStages[currentStage];
if (percentEl) percentEl.textContent = `${Math.floor(progress)}%`;
if (barEl) barEl.style.width = `${progress}%`;
if (progress >= 100) {
clearInterval(interval);
state.scanning = false;
state.scanQueue--;
if (queueEl) queueEl.textContent = state.scanQueue;
state.scanCount += Math.floor(Math.random() * 5) + 1;
animateCounter(document.getElementById('scanCount'), state.scanCount, 500);
addLog(`${typeNames[type] || type} scan complete for ${targetValue}`, 'success');
showToast(`${typeNames[type]} scan complete!`);
// Add some discoveries from the scan
addDiscovery();
addDiscovery();
setTimeout(() => {
if (progressDiv) progressDiv.classList.add('hidden');
if (barEl) barEl.style.width = '0%';
}, 2000);
}
}, 300);
}
// ---- Operational Mode (Real APIs) ----
let cveFeedInterval = null;
async function startOperationalMode() {
// System health updates (browser-side metrics)
updateSystemHealth();
setInterval(updateSystemHealth, 3000);
// Initial logs
addLog('SentinelAI Command Center initialized — OPERATIONAL MODE', 'system');
addLog('Real API services: ipinfo.io · NVD CVE · Google DNS', 'success');
addLog('All monitoring subsystems online', 'success');
addLog('GDorks engine loaded — 36 dork templates ready', 'info');
addLog('Network scanner module active — ipinfo.io + DNS resolve', 'info');
// Fetch initial CVE feed from NVD
addLog('Fetching real-time CVE data from NVD...', 'system');
await fetchCVeFeed();
// Poll NVD every 60s for new CVEs (respecting rate limits)
cveFeedInterval = setInterval(() => fetchCVeFeed(), 60000);
// Update threat level based on real data
setInterval(updateThreatLevelFromData, 15000);
// Resize handler
window.addEventListener('resize', () => {
if (threatMap) threatMap.invalidateSize();
drawSparkline();
});
}
// ---- Fetch real CVE data from NVD ----
async function fetchCVeFeed() {
try {
const keywords = ['injection', 'xss', 'ransomware', 'zero-day', 'exploit', 'authentication'];
const kw = keywords[Math.floor(Math.random() * keywords.length)];
const cves = await SentinelAPI.searchCVEs(kw, 5);
if (cves.length === 0) return;
cves.forEach(cve => {
const timeStr = new Date(cve.published).toTimeString().split(' ')[0];
const sevMap = { critical: 'critical', high: 'high', medium: 'medium', low: 'low' };
const sev = sevMap[cve.severity] || 'medium';
// Add to alerts if high/critical
if (sev === 'critical' || sev === 'high') {
const alertObj = {
severity: sev,
message: `${cve.id}: ${cve.description.substring(0, 80)}...`,
time: timeStr,
icon: sev === 'critical' ? 'shield-alert' : 'alert-triangle',
colors: sev === 'critical'
? { bg: 'bg-red-500/10', border: 'border-red-500/20', text: 'text-red-400', badge: 'bg-red-500/20 text-red-400' }
: { bg: 'bg-amber-500/10', border: 'border-amber-500/20', text: 'text-amber-400', badge: 'bg-amber-500/20 text-amber-400' },
};
state.alerts.unshift(alertObj);
if (state.alerts.length > 20) state.alerts.pop();
}
// Add to discoveries
state.discoveries.unshift({
type: 'vuln',
icon: 'bug',
color: sev === 'critical' ? 'red' : sev === 'high' ? 'amber' : 'cyan',
msg: `${cve.id} (CVSS ${cve.score}) — ${cve.description.substring(0, 60)}...`,
time: timeStr.substring(0, 5),
});
if (state.discoveries.length > 15) state.discoveries.pop();
state.threatCount++;
addLog(`NVD: ${cve.id} — ${cve.severity.toUpperCase()} CVSS ${cve.score}`, sev === 'critical' ? 'error' : 'warning');
});
renderAlerts();
updateAlertCount();
renderDiscoveries();
animateCounter(document.getElementById('threatCount'), state.threatCount, 500);
} catch (e) {
addLog('NVD API暂时不可用 — retrying...', 'warning');
}
}
// ---- Update threat level from real data ----
function updateThreatLevelFromData() {
const critCount = state.alerts.filter(a => a.severity === 'critical').length;
const highCount = state.alerts.filter(a => a.severity === 'high').length;
let level = 'LOW';
if (critCount > 2) level = 'HIGH';
else if (critCount > 0 || highCount > 3) level = 'MEDIUM';
const indicator = document.getElementById('threatLevel');
const textEl = document.getElementById('threatLevelText');
if (indicator && textEl) {
textEl.textContent = level;
indicator.className = 'threat-indicator flex items-center gap-2 px-3 py-1.5 rounded-lg border';
if (level === 'LOW') {
indicator.classList.add('bg-green-500/10', 'border-green-500/20');
textEl.className = 'text-xs font-semibold text-green-400 font-mono';
indicator.querySelector('.w-2').className = 'w-2 h-2 bg-green-400 rounded-full animate-pulse';
} else if (level === 'MEDIUM') {
indicator.classList.add('bg-amber-500/10', 'border-amber-500/20');
textEl.className = 'text-xs font-semibold text-amber-400 font-mono';
indicator.querySelector('.w-2').className = 'w-2 h-2 bg-amber-400 rounded-full animate-pulse';
} else {
indicator.classList.add('bg-red-500/10', 'border-red-500/20');
textEl.className = 'text-xs font-semibold text-red-400 font-mono';
indicator.querySelector('.w-2').className = 'w-2 h-2 bg-red-400 rounded-full animate-pulse';
}
}
}
// ---- Mobile Menu Toggle ----
function toggleMobileMenu() {
const menu = document.getElementById('mobileMenu');
if (menu) menu.classList.toggle('hidden');
}
// ---- Toast ----
function showToast(message) {
const toast = document.getElementById('ccToast');
const msgEl = document.getElementById('ccToastMsg');
if (!toast || !msgEl) return;
msgEl.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}