Pro / static /script.js
Crypto0203
Deploy ParleDowntime Pro Space
8def838
Raw History Blame Contribute Delete
20.1 kB
/**
* StatusPilot Ultra Pro v5
* Logic & Infrastructure Interface
*/
let appData = {
status: [],
config: null,
summary: null
};
let charts = {};
let currentLocationFilter = 'All';
let isAuthPromptShowing = false;
// Global Axios Interceptor for 401s
axios.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
console.warn("Session expired or unauthorized. Showing prompt.");
if (!isAuthPromptShowing) {
showAuthOverlay();
}
}
return Promise.reject(error);
}
);
function showAuthOverlay() {
// Only show if not already visible
if (isAuthPromptShowing) return;
isAuthPromptShowing = true;
const overlay = document.getElementById('master-auth-overlay');
if (overlay) {
overlay.style.display = 'flex';
overlay.style.zIndex = "999999"; // Ensure it's on top
}
const mainApp = document.getElementById('main-app');
if (mainApp) {
mainApp.style.filter = 'blur(20px)';
mainApp.style.opacity = '0.3';
mainApp.style.pointerEvents = 'none';
}
}
// Configuration
const REFRESH_INTERVAL = 30000;
const ACCENT_COLOR = '#10b981';
const PRIMARY_COLOR = '#3b82f6';
// --- Initialization ---
async function init() {
updateClock();
setInterval(updateClock, 1000);
const authStatus = await checkAuthStatus();
if (authStatus.required && !authStatus.authenticated) {
document.getElementById('master-auth-overlay').style.display = 'flex';
return;
}
unlockDashboard();
const license = await checkLicense();
if (!license.licensed) {
document.getElementById('license-overlay').style.display = 'flex';
return;
}
await loadData();
setInterval(loadData, REFRESH_INTERVAL);
}
async function loadData() {
try {
await Promise.all([
loadConfig(),
loadSummary(),
loadStatus()
]);
} catch (err) {
console.error("Critical Sync Error:", err);
}
}
// --- Auth & Security ---
async function checkAuthStatus() {
try {
const res = await axios.get('/api/auth/status');
return res.data;
} catch (err) { return { required: true, authenticated: false }; }
}
async function unlockDashboard() {
isAuthPromptShowing = false;
document.getElementById('master-auth-overlay').style.display = 'none';
const mainApp = document.getElementById('main-app');
mainApp.style.filter = 'none';
mainApp.style.opacity = '1';
mainApp.style.pointerEvents = 'auto';
}
document.getElementById('master-login-btn').onclick = async () => {
const keyInput = document.getElementById('master-key-input');
const key = keyInput.value.trim();
const btn = document.getElementById('master-login-btn');
const err = document.getElementById('master-error');
if (!key) return;
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> SYNCING...';
btn.disabled = true;
err.style.display = 'none';
try {
const res = await axios.post('/api/auth/login', { key });
if (res.data.success) {
console.log("Login successful, verifying session...");
// CRITICAL: Wait a moment for the cookie to settle
setTimeout(async () => {
const status = await checkAuthStatus();
if (status.authenticated) {
btn.textContent = 'SYNC SUCCESSFUL';
unlockDashboard();
await init(); // Re-run init to load data
} else {
throw new Error("Session verification failed after login.");
}
}, 500);
} else {
err.textContent = "Invalid Master Access Key";
err.style.display = 'block';
btn.textContent = 'Unlock Data Center';
btn.disabled = false;
}
} catch (e) {
console.error(e);
err.textContent = "Security Node Offline or Cookie Blocked";
err.style.display = 'block';
btn.textContent = 'Unlock Data Center';
btn.disabled = false;
}
};
async function checkLicense() {
try {
const res = await axios.get('/api/license/status');
return res.data;
} catch (err) { return { licensed: false }; }
}
document.getElementById('activate-btn').onclick = async () => {
const email = document.getElementById('lic-email').value;
const key = document.getElementById('lic-key').value;
try {
const res = await axios.post('/api/license/activate', { email, key: key.trim().toUpperCase() });
if (res.data.success) init();
else {
const err = document.getElementById('lic-error');
err.textContent = res.data.message;
err.style.display = 'block';
}
} catch (e) { console.error(e); }
};
// --- Data Fetching ---
async function loadConfig() {
const res = await axios.get('/api/config');
appData.config = res.data;
// Fill selects
const locFilter = document.getElementById('location-filter');
const toolSelect = document.getElementById('test-tool-select');
const locations = new Set(['All', 'Global']);
appData.config.services_to_monitor.forEach(s => {
if(s.location) locations.add(s.location);
});
const prevLoc = locFilter.value;
locFilter.innerHTML = '';
locations.forEach(loc => {
const opt = document.createElement('option');
opt.value = loc; opt.textContent = loc === 'All' ? 'GLOBAL NETWORK' : loc.toUpperCase();
locFilter.appendChild(opt);
});
if(prevLoc) locFilter.value = prevLoc;
toolSelect.innerHTML = '';
appData.config.services_to_monitor.forEach(s => {
const opt = document.createElement('option');
opt.value = s.name; opt.textContent = s.name;
toolSelect.appendChild(opt);
});
// Populate Management List
renderManagementList();
// Settings fields
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 renderManagementList() {
const list = document.getElementById('manage-svc-list');
if (!list) return;
list.innerHTML = '';
appData.config.services_to_monitor.forEach((s, index) => {
const item = document.createElement('div');
item.className = 'manage-item';
item.innerHTML = `
<div style="display:flex; flex-direction:column;">
<b style="font-size:0.9rem;">${s.name}</b>
<span style="font-size:0.7rem; color:var(--text-muted);">${s.url}</span>
</div>
<button class="btn-delete" onclick="deleteService(${index}, '${s.name}')">
<i class="fas fa-trash-alt"></i> REMOVE
</button>
`;
list.appendChild(item);
});
}
async function deleteService(index, name) {
if (!confirm(`🚨 DECOMMISSION NODE: Are you sure you want to remove ${name} from active monitoring?`)) return;
try {
// Switching to POST for decommissioning to avoid DELETE method blocks in cloud proxies
const res = await axios.post('/api/config/delete', { name });
if (res.data.status === 'success') {
console.log(`Node ${name} successfully decommissioned.`);
await loadConfig();
await loadStatus();
} else {
throw new Error(res.data.message || "Unknown server error");
}
} catch (e) {
console.error(e);
const msg = e.response?.data?.message || e.message;
alert(`OPERATIONAL ERROR: ${msg}\n\nPlease ensure you are logged in and the node still exists.`);
}
}
async function loadSummary() {
const res = await axios.get('/api/summary');
const s = res.data;
animateValue('stat-monitors', s.total_monitors);
animateValue('stat-uptime', s.uptime_pct, '%');
animateValue('stat-incidents', s.incidents);
animateValue('stat-latency', s.avg_latency, 'ms');
}
async function loadStatus() {
const res = await axios.get('/api/status');
appData.status = res.data;
renderStatusGrid();
renderGlobalMatrix();
}
// --- UI Rendering ---
function renderStatusGrid() {
const grid = document.getElementById('status-grid');
grid.innerHTML = '';
const filtered = appData.status.filter(s => currentLocationFilter === 'All' || s.location === currentLocationFilter);
if (filtered.length === 0) {
grid.innerHTML = '<div style="text-align:center; padding:3rem; color:var(--text-muted);">No services matching filter.</div>';
return;
}
filtered.forEach(s => {
const row = document.createElement('div');
row.className = 'status-row';
row.onclick = () => openDetails(s.name);
let timelineHTML = '<div class="uptime-timeline">';
const history = [...Array(30 - s.timeline.length).fill(null), ...s.timeline];
history.forEach(state => {
const cls = state === null ? 'none' : (state ? 'up' : 'down');
timelineHTML += `<div class="timeline-bar ${cls}"></div>`;
});
timelineHTML += '</div>';
row.innerHTML = `
<div class="svc-info">
<h3>${s.name}</h3>
<span>${s.url}</span>
</div>
<div><span class="location-badge">${s.location}</span></div>
${timelineHTML}
<div class="status-text ${s.is_up ? 'up' : 'down'}">${s.is_up ? 'Operational' : 'Major Outage'}</div>
<div class="latency">${s.latency}ms</div>
`;
grid.appendChild(row);
});
}
function renderGlobalMatrix() {
const ctx = document.getElementById('globalLatencyMatrix');
if (!ctx) return;
const labels = appData.status.map(s => s.name);
const data = appData.status.map(s => s.latency);
if (charts.globalMatrix) charts.globalMatrix.destroy();
charts.globalMatrix = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Latency (ms)',
data: data,
backgroundColor: data.map(v => v > 500 ? 'rgba(239, 68, 68, 0.4)' : 'rgba(59, 130, 246, 0.4)'),
borderColor: data.map(v => v > 500 ? '#ef4444' : '#3b82f6'),
borderWidth: 1,
borderRadius: 8
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } },
y: { grid: { display: false }, ticks: { color: '#fff', font: { weight: '600' } } }
}
}
});
}
// --- Analytics Modal ---
async function openDetails(name) {
document.getElementById('modal-title').textContent = name;
document.getElementById('matrix-modal').style.display = 'block';
try {
const res = await axios.get(`/api/history/${encodeURIComponent(name)}`);
const history = res.data;
renderMatrixCharts(history);
renderIncidentLog(history);
} catch (e) { console.error(e); }
}
function renderMatrixCharts(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 chartOps = {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { display: false } },
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } }
}
};
charts.detLat = new Chart(document.getElementById('latencyChart'), {
type: 'line',
data: {
labels: labels,
datasets: [{
data: latencies,
borderColor: PRIMARY_COLOR,
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 0
}]
},
options: chartOps
});
charts.detUp = new Chart(document.getElementById('uptimeChart'), {
type: 'line',
data: {
labels: labels,
datasets: [{
data: uptimes,
borderColor: ACCENT_COLOR,
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
stepped: true,
pointRadius: 0
}]
},
options: { ...chartOps, scales: { ...chartOps.scales, y: { ...chartOps.scales.y, max: 100, min: 0 } } }
});
charts.detErr = new Chart(document.getElementById('errorChart'), {
type: 'doughnut',
data: {
labels: ['Success', 'Failure'],
datasets: [{
data: [data.filter(d => d.is_up).length, data.filter(d => !d.is_up).length],
backgroundColor: [ACCENT_COLOR, '#ef4444'],
borderWidth: 0
}]
},
options: { cutout: '70%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
});
}
function renderIncidentLog(data) {
const log = document.getElementById('incident-log');
log.innerHTML = '';
const fails = data.filter(d => !d.is_up).reverse();
if (fails.length === 0) {
log.innerHTML = '<p style="color: var(--accent); font-weight: 600;">System is healthy. No recent incidents.</p>';
return;
}
fails.forEach(f => {
const item = document.createElement('div');
item.style.padding = '10px 0';
item.style.borderBottom = '1px solid var(--border)';
item.innerHTML = `<span style="color:#ef4444; font-weight:700;">DOWN</span> <span style="margin-left:10px;">${new Date(f.timestamp).toLocaleString()}</span>`;
log.appendChild(item);
});
}
// --- Helpers & Utilities ---
function animateValue(id, value, suffix = '') {
const el = document.getElementById(id);
const start = parseInt(el.textContent) || 0;
const duration = 1000;
const startTime = performance.now();
function update(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const current = start + (value - start) * progress;
if (suffix === '%') el.textContent = current.toFixed(1) + suffix;
else if (suffix === 'ms') el.textContent = Math.round(current) + suffix;
else el.textContent = Math.round(current);
if (progress < 1) requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
function updateClock() {
const el = document.getElementById('live-clock');
const now = new Date();
el.textContent = now.toLocaleTimeString() + ' | ' + now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function showToast(msg, type = 'success') {
// Simple alert for now, can be upgraded to toast
console.log(`[${type.toUpperCase()}] ${msg}`);
}
// --- Event Handlers ---
document.querySelectorAll('.nav-item').forEach(item => {
item.onclick = (e) => {
e.preventDefault();
document.querySelectorAll('.nav-item').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';
};
});
document.getElementById('location-filter').onchange = (e) => {
currentLocationFilter = e.target.value;
renderStatusGrid();
};
document.querySelector('.close-modal').onclick = () => {
document.getElementById('matrix-modal').style.display = 'none';
};
document.getElementById('purge-stale-btn').onclick = async () => {
if (!confirm("🚨 WARNING: This will permanently delete history for services no longer in your config. Proceed?")) return;
try {
const res = await axios.post('/api/admin/purge_stale');
alert(`Cleanup complete! Purged ${res.data.purged_records} records.`);
loadData();
} catch (e) { alert("Failed to purge stale data."); }
};
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("Configuration updated successfully!");
loadData();
} catch (e) { alert("Failed to save configuration."); }
};
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("Service Name and Resource URL are required.");
const config = { ...appData.config };
const newService = {
name,
url,
location: loc,
type: type
};
// Add specific fields based on type
if (type === 'http_code') newService.expected_code = 200;
if (type === 'json_status') {
newService.key_to_check = 'status';
newService.expected_value = 'up';
}
config.services_to_monitor.push(newService);
try {
await axios.post('/api/config', config);
alert(`Node Activated: ${name}`);
document.getElementById('new-svc-name').value = '';
document.getElementById('new-svc-url').value = '';
loadConfig();
loadStatus();
} catch (e) { alert("Failed to provision new service node."); }
};
document.getElementById('run-test-btn').onclick = async () => {
const tool = document.getElementById('test-tool-select').value;
const location = document.getElementById('test-location-select').value;
const results = document.getElementById('test-results');
results.innerHTML = '<div style="text-align:center; padding:2rem;"><i class="fas fa-circle-notch fa-spin"></i> Pinging remote node...</div>';
try {
const res = await axios.post('/api/test', { tool, location });
const d = res.data;
results.innerHTML = `
<div class="glass-panel" style="border-color: ${d.is_up ? 'var(--accent)' : 'var(--danger)'}">
<h3 style="color: ${d.is_up ? 'var(--accent)' : 'var(--danger)'}">${d.is_up ? 'TRACEROUTE SUCCESSFUL' : 'NODE UNREACHABLE'}</h3>
<div style="margin-top: 1rem; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div><b>Latency:</b> ${d.latency}ms</div>
<div><b>Location:</b> ${d.location}</div>
<div style="grid-column: 1/-1;"><b>Response:</b> ${d.message}</div>
</div>
</div>
`;
} catch (e) { results.innerHTML = '<div class="glass-panel" style="color:var(--danger)">Diagnostic timeout. Node might be offline.</div>'; }
};
document.getElementById('download-excel-btn').onclick = () => {
window.location.href = '/api/report/excel';
};
// Start
init();