/**
* StatusPilot Ultra v6 — Fusion Engine
* Best of all 3 dashboards combined
*/
let appData = { status: [], config: null };
let charts = {};
let currentLocationFilter = 'All';
let searchQuery = '';
let isAuthPromptShowing = false;
// --- Axios 401 Interceptor ---
axios.interceptors.response.use(r => r, error => {
if (error.response && error.response.status === 401 && !isAuthPromptShowing) {
isAuthPromptShowing = true;
document.getElementById('master-auth-overlay').style.display = 'flex';
}
return Promise.reject(error);
});
// --- Init ---
async function init() {
updateClock();
setInterval(updateClock, 1000);
const auth = await checkAuth();
if (auth.required && !auth.authenticated) {
document.getElementById('master-auth-overlay').style.display = 'flex';
return;
}
unlock();
const lic = await checkLicense();
if (!lic.licensed) {
document.getElementById('license-overlay').style.display = 'flex';
return;
}
await loadData();
setInterval(loadData, 30000);
}
async function loadData() {
try { await Promise.all([loadConfig(), loadSummary(), loadStatus()]); }
catch (e) { console.error("Sync:", e); }
}
async function checkAuth() {
try { return (await axios.get('/api/auth/status')).data; }
catch { return { required: true, authenticated: false }; }
}
function unlock() {
isAuthPromptShowing = false;
document.getElementById('master-auth-overlay').style.display = 'none';
const app = document.getElementById('main-app');
app.style.filter = 'none'; app.style.opacity = '1'; app.style.pointerEvents = 'auto';
}
document.getElementById('master-login-btn').onclick = async () => {
const key = document.getElementById('master-key-input').value.trim();
if (!key) return;
try {
const res = await axios.post('/api/auth/login', { key });
if (res.data.success) {
setTimeout(async () => {
const s = await checkAuth();
if (s.authenticated) { unlock(); await init(); }
}, 500);
} else { alert("Invalid Master Key"); }
} catch { alert("Auth Node Offline"); }
};
async function checkLicense() {
try { return (await axios.get('/api/license/status')).data; }
catch { return { licensed: false }; }
}
// --- Config ---
async function loadConfig() {
const res = await axios.get('/api/config');
appData.config = res.data;
const locFilter = document.getElementById('location-filter');
const toolSelect = document.getElementById('test-tool-select');
const locs = new Set(['All', 'Global']);
appData.config.services_to_monitor.forEach(s => { if(s.location) locs.add(s.location); });
const prev = locFilter.value;
locFilter.innerHTML = '';
locs.forEach(l => {
const o = document.createElement('option');
o.value = l; o.textContent = l === 'All' ? 'All Regions' : l;
locFilter.appendChild(o);
});
if (prev) locFilter.value = prev;
toolSelect.innerHTML = '';
appData.config.services_to_monitor.forEach(s => {
const o = document.createElement('option');
o.value = s.name; o.textContent = s.name;
toolSelect.appendChild(o);
});
renderManageList();
document.getElementById('check-interval').value = appData.config.check_interval;
document.getElementById('alert-mode').value = appData.config.alert_mode || 'SMTP';
document.getElementById('smtp-user').value = appData.config.smtp_sender || '';
}
function renderManageList() {
const list = document.getElementById('manage-svc-list');
if (!list) return;
list.innerHTML = '';
appData.config.services_to_monitor.forEach(s => {
const item = document.createElement('div');
item.className = 'manage-item';
item.innerHTML = `
${s.name}
${s.url}
`;
list.appendChild(item);
});
list.querySelectorAll('.btn-remove').forEach(btn => {
btn.onclick = (e) => {
const t = e.target.closest('.btn-remove');
if (t) deleteService(t.getAttribute('data-name'));
};
});
}
async function deleteService(name) {
try {
const res = await axios.post('/api/config/delete', { name });
if (res.data.status === 'success') { await loadConfig(); await loadStatus(); }
else alert("Error: " + res.data.message);
} catch (e) { alert("Delete failed."); }
}
// --- Summary ---
async function loadSummary() {
const s = (await axios.get('/api/summary')).data;
document.getElementById('stat-monitors').textContent = s.total_monitors;
document.getElementById('stat-uptime').textContent = s.uptime_pct + '%';
document.getElementById('stat-incidents').textContent = s.incidents;
document.getElementById('stat-latency').textContent = s.avg_latency + 'ms';
}
// --- Status ---
async function loadStatus() {
appData.status = (await axios.get('/api/status')).data;
renderStatusGrid();
renderLatencyMatrix();
}
function renderLatencyMatrix() {
const container = document.getElementById('latency-matrix-bars');
if (!container) return;
container.innerHTML = '';
const sorted = [...appData.status].sort((a, b) => b.latency - a.latency).slice(0, 8);
const max = Math.max(...sorted.map(s => s.latency), 100);
sorted.forEach(s => {
const pct = Math.min((s.latency / max) * 100, 100);
const row = document.createElement('div');
row.className = 'latency-row';
row.innerHTML = `
${s.name}
${s.latency}ms
`;
container.appendChild(row);
});
}
function renderStatusGrid() {
const grid = document.getElementById('status-grid');
grid.innerHTML = '';
const filtered = appData.status.filter(s => {
const locOk = currentLocationFilter === 'All' || s.location === currentLocationFilter;
const searchOk = !searchQuery || s.name.toLowerCase().includes(searchQuery.toLowerCase());
return locOk && searchOk;
});
filtered.forEach(s => {
const row = document.createElement('div');
row.className = 'svc-row';
row.onclick = () => openDetails(s.name);
let barsHTML = '';
const history = [...Array(Math.max(0, 30 - s.timeline.length)).fill(null), ...s.timeline];
history.forEach(state => {
const cls = state === null ? 'none' : (state ? 'up' : 'down');
barsHTML += `
`;
});
barsHTML += '
';
const scopeClass = s.location === 'Security Node' ? 'security' : (s.location === 'Primary Web' ? 'web' : 'global');
row.innerHTML = `
${s.location}
${barsHTML}
${s.is_up ? 'ONLINE' : 'DOWN'}
${s.latency}ms
`;
grid.appendChild(row);
});
}
// --- Modal ---
async function openDetails(name) {
document.getElementById('modal-title').textContent = name;
document.getElementById('modal-subtitle').textContent = `Historical performance data for ${name}`;
document.getElementById('matrix-modal').classList.add('open');
try {
const res = await axios.get(`/api/history/${encodeURIComponent(name)}`);
renderModalCharts(res.data);
} catch (e) { console.error(e); }
}
function renderModalCharts(data) {
const labels = data.map(d => new Date(d.timestamp).toLocaleTimeString());
const latencies = data.map(d => d.latency);
const uptimes = data.map(d => d.is_up ? 100 : 0);
if (charts.detLat) charts.detLat.destroy();
if (charts.detUp) charts.detUp.destroy();
if (charts.detErr) charts.detErr.destroy();
const base = {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { display: false },
y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#64748b', font: { size: 9 } } }
}
};
charts.detUp = new Chart(document.getElementById('uptimeChart'), {
type: 'line',
data: { labels, datasets: [{ data: uptimes, borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,0.08)', fill: true, stepped: true, pointRadius: 0, borderWidth: 1.5 }] },
options: { ...base, scales: { ...base.scales, y: { ...base.scales.y, max: 105, min: -5 } } }
});
charts.detLat = new Chart(document.getElementById('latencyChart'), {
type: 'line',
data: { labels, datasets: [{ data: latencies, borderColor: '#0ea5e9', backgroundColor: 'rgba(14,165,233,0.08)', fill: true, tension: 0.4, pointRadius: 0, borderWidth: 1.5 }] },
options: base
});
const upCount = data.filter(d => d.is_up).length;
const downCount = data.filter(d => !d.is_up).length;
charts.detErr = new Chart(document.getElementById('errorChart'), {
type: 'doughnut',
data: { labels: ['Operational', 'Issues'], datasets: [{ data: [upCount, downCount], backgroundColor: ['#10b981', '#1e293b'], borderWidth: 0 }] },
options: { cutout: '75%', plugins: { legend: { position: 'bottom', labels: { color: '#64748b', padding: 12, font: { size: 10 } } } } }
});
renderIncidents(data);
}
function renderIncidents(data) {
const log = document.getElementById('incident-log');
if (!log) return;
log.innerHTML = '';
const fails = data.filter(d => !d.is_up).reverse().slice(0, 10);
if (fails.length === 0) {
log.innerHTML = '';
return;
}
fails.forEach(f => {
const el = document.createElement('div');
el.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:6px 10px; margin-bottom:4px; background:rgba(244,63,94,0.06); border-left:3px solid var(--danger); border-radius:4px;';
const msg = f.message || "Connection Error";
el.innerHTML = `Outage
${msg}
${new Date(f.timestamp).toLocaleTimeString()}`;
log.appendChild(el);
});
}
function updateClock() {
document.getElementById('live-clock').textContent = new Date().toLocaleTimeString();
}
// --- Event Handlers ---
// Modal close
document.getElementById('modal-close-btn').onclick = () => document.getElementById('matrix-modal').classList.remove('open');
document.getElementById('matrix-modal').onclick = (e) => { if (e.target === document.getElementById('matrix-modal')) document.getElementById('matrix-modal').classList.remove('open'); };
// Navigation
document.querySelectorAll('.sidebar-nav a').forEach(item => {
item.onclick = (e) => {
e.preventDefault();
document.querySelectorAll('.sidebar-nav a').forEach(n => n.classList.remove('active'));
item.classList.add('active');
const target = item.id.replace('nav-', 'section-');
document.querySelectorAll('.content-section').forEach(s => s.style.display = 'none');
document.getElementById(target).style.display = 'block';
};
});
// Location filter
document.getElementById('location-filter').onchange = (e) => { currentLocationFilter = e.target.value; renderStatusGrid(); };
// Search
document.getElementById('search-services').oninput = (e) => { searchQuery = e.target.value; renderStatusGrid(); };
// M365
document.getElementById('activate-m365-btn').onclick = async () => {
const sender = document.getElementById('m365-sender').value.trim();
const pass = document.getElementById('m365-pass').value.trim();
const recipient = document.getElementById('m365-recipient').value.trim();
if (!sender || !pass || !recipient) return alert("All M365 fields required.");
const config = { ...appData.config };
config.services_to_monitor.push({ name: "M365 Mail Flow", url: "https://outlook.office365.com", location: "Global", type: "m365_mail", m365_sender: sender, m365_password: pass, m365_recipient: recipient });
try { await axios.post('/api/config', config); alert("M365 Monitor Activated!"); loadConfig(); }
catch { alert("Failed to activate."); }
};
// Add service
document.getElementById('add-svc-btn').onclick = async () => {
const name = document.getElementById('new-svc-name').value.trim();
const url = document.getElementById('new-svc-url').value.trim();
const loc = document.getElementById('new-svc-loc').value.trim() || 'Global';
const type = document.getElementById('new-svc-type').value;
if (!name || !url) return alert("Name and URL required.");
const config = { ...appData.config };
config.services_to_monitor.push({ name, url, location: loc, type });
try { await axios.post('/api/config', config); alert("Node Provisioned!"); document.getElementById('new-svc-name').value = ''; document.getElementById('new-svc-url').value = ''; loadConfig(); loadStatus(); }
catch { alert("Error."); }
};
// Save settings
document.getElementById('save-settings-btn').onclick = async () => {
const config = { ...appData.config };
config.check_interval = parseInt(document.getElementById('check-interval').value);
config.alert_mode = document.getElementById('alert-mode').value;
config.smtp_sender = document.getElementById('smtp-user').value;
config.smtp_password = document.getElementById('smtp-pass').value;
try { await axios.post('/api/config', config); alert("Config Saved!"); loadConfig(); }
catch { alert("Save failed."); }
};
// Purge
const purgeBtn = document.getElementById('purge-stale-btn');
if (purgeBtn) purgeBtn.onclick = async () => { try { const r = await axios.post('/api/admin/purge_stale'); alert(`Purged ${r.data.purged_records} records.`); loadData(); } catch { alert("Purge failed."); } };
// Export
const exportBtn = document.getElementById('download-excel-btn');
if (exportBtn) exportBtn.onclick = () => window.location.href = '/api/report/excel';
init();