Pro / static /script_v6.js
Crypto0203
Deploy ParleDowntime Pro Space
8def838
Raw History Blame Contribute Delete
34.6 kB
/**
* StatusPilot Ultra v6 Engine
*/
let appData = { status: [], config: null };
let currentFilter = 'all';
let sortKey = 'latency';
let sortDir = 'desc';
let isAuthPromptShowing = false;
let trendData = [];
let mapNodes = [];
// --- Axios 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);
});
// --- THEME ENGINE ---
const THEMES = [
{ id: 'dark', label: 'Dark Pro', icon: 'πŸŒ™' },
{ id: 'light', label: 'Light Mode', icon: 'β˜€οΈ' },
{ id: 'cyber', label: 'Cyber Pulse', icon: '⚑' }
];
function initTheme() {
const saved = localStorage.getItem('statuspilot-theme') || 'dark';
applyTheme(saved);
}
function applyTheme(themeId) {
const t = THEMES.find(x => x.id === themeId) || THEMES[0];
if (t.id === 'dark') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', t.id);
}
localStorage.setItem('statuspilot-theme', t.id);
const iconEl = document.getElementById('theme-icon');
const labelEl = document.getElementById('theme-label');
if (iconEl) iconEl.textContent = t.icon;
if (labelEl) labelEl.textContent = t.label;
}
function cycleTheme() {
const current = localStorage.getItem('statuspilot-theme') || 'dark';
const idx = THEMES.findIndex(x => x.id === current);
const nextTheme = THEMES[(idx + 1) % THEMES.length];
applyTheme(nextTheme.id);
toast('info', 'Theme Switch', `Activated ${nextTheme.label} dashboard.`);
}
// --- INIT ---
window.addEventListener('load', async () => {
initTheme();
setTimeout(() => { document.getElementById('loader').classList.add('hide'); }, 1500);
initClock();
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);
initTrendChart();
});
// --- CLOCK ---
function initClock() {
function tick() {
const sel = document.getElementById('tzSel');
const tz = sel ? sel.value : 'UTC';
const label = sel ? sel.options[sel.selectedIndex].dataset.label : 'UTC';
const d = new Date();
const timeStr = d.toLocaleTimeString('en-US', { timeZone: tz, hour12: false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
document.getElementById('clock').textContent = timeStr + ' ' + label;
}
tick(); setInterval(tick, 1000);
}
// --- AUTH / LICENSE ---
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(); window.location.reload(); }
}, 500);
} else { toast('err', 'Access Denied', 'Invalid Master Key'); }
} catch { toast('err', 'Auth Error', 'Node Offline'); }
};
async function checkLicense() {
try { return (await axios.get('/api/license/status')).data; }
catch { return { licensed: false }; }
}
// --- LOAD DATA ---
async function loadData() {
try {
await Promise.all([loadConfig(), loadSummary(), loadStatus()]);
} catch (e) {
console.error("Sync Error:", e);
}
}
async function loadConfig() {
const res = await axios.get('/api/config');
appData.config = res.data;
// Filters and Selects
const locFilter = document.getElementById('regionSel');
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 = '<option value="all">🌐 All Regions</option>';
locs.forEach(l => {
if(l !== 'all') {
const o = document.createElement('option');
o.value = l; o.textContent = 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);
});
// Populate Operations Forms
renderManageList();
document.getElementById('check-interval').value = appData.config.check_interval || 60;
document.getElementById('alert-mode').value = appData.config.alert_mode || 'SMTP';
document.getElementById('smtp-user').value = appData.config.smtp_sender || '';
document.getElementById('smtp-pass').value = appData.config.smtp_password ? 'β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’' : '';
const recs = appData.config.email_recipients || [];
const recsEl = document.getElementById('email-recipients') || document.getElementById('alert-recipients');
if (recsEl) recsEl.value = Array.isArray(recs) ? recs.join(', ') : recs;
const whEl = document.getElementById('email-webhook-url');
if (whEl) {
// Mask the webhook URL - show only status, never expose the real URL to UI
whEl.value = appData.config.email_webhook_url ? 'β€’β€’β€’ Configured (Secured) β€’β€’β€’' : '';
whEl.disabled = true;
whEl.style.color = '#888';
}
}
async function loadSummary() {
const s = (await axios.get('/api/summary')).data;
document.getElementById('kpi-mon').textContent = s.total_monitors;
// Uptime
document.getElementById('kpi-up').textContent = s.uptime_pct + '%';
const upDelta = document.getElementById('kpi-up-delta');
if (s.uptime_pct >= 99) { upDelta.className = 'kpi-delta up'; upDelta.textContent = '↑ Optimal'; }
else { upDelta.className = 'kpi-delta down'; upDelta.textContent = '↓ Below Target'; }
document.getElementById('kpi-up-bar').style.width = s.uptime_pct + '%';
// Incidents
document.getElementById('kpi-inc').textContent = s.incidents;
const incCard = document.getElementById('kpi-inc-card');
const incDesc = document.getElementById('kpi-inc-desc');
const incBar = document.getElementById('kpi-inc-bar');
if (s.incidents > 0) {
incCard.style.setProperty('--ac', 'var(--red)');
incDesc.className = 'kpi-delta down'; incDesc.textContent = s.incidents + ' Systems Down';
incBar.style.background = 'var(--red)';
document.getElementById('kpi-inc').style.setProperty('--vc', 'var(--red)');
document.getElementById('inc-badge').textContent = s.incidents + ' OPEN';
} else {
incCard.style.setProperty('--ac', 'var(--green)');
incDesc.className = 'kpi-delta up'; incDesc.textContent = 'All operational';
incBar.style.background = 'var(--green)';
document.getElementById('kpi-inc').style.setProperty('--vc', 'var(--green)');
document.getElementById('inc-badge').textContent = '0 OPEN';
}
// Latency
document.getElementById('kpi-lat').textContent = s.avg_latency + 'ms';
}
async function loadStatus() {
appData.status = (await axios.get('/api/status')).data;
renderTable();
renderLatMatrix();
renderIncidents();
buildHeatmap();
// Setup World Map Nodes based on status
mapNodes = [
{label:'US East (N.Va)', x:75, y:55, status: getNodeStatus('US') },
{label:'US West (CA)', x:45, y:60, status: getNodeStatus('Global') },
{label:'EU Central (FRA)',x:160,y:48, status: getNodeStatus('EU') },
{label:'UK (London)', x:145,y:44, status: getNodeStatus('Primary') },
{label:'Asia-Pacific', x:260,y:65, status: 'ok' },
{label:'South America', x:105,y:115,status: 'ok' }
];
renderWorldMap();
}
function getNodeStatus(locMatch) {
const nodes = appData.status.filter(s => s.location && s.location.includes(locMatch));
if (!nodes.length) return 'ok';
if (nodes.some(s => !s.is_up)) return 'down';
if (nodes.some(s => s.latency > 400)) return 'warn';
return 'ok';
}
// --- RENDERERS ---
function slaColor(pct) {
if(pct >= 99.5) return 'var(--green)';
if(pct >= 95) return 'var(--amber)';
return 'var(--red)';
}
function renderTable() {
const q = (document.getElementById('searchBox')||{}).value||'';
const locFilt = document.getElementById('regionSel').value;
let list = [...appData.status];
// Region Filter
if (locFilt !== 'all') list = list.filter(s => s.location === locFilt);
// Status Filter
if(currentFilter !== 'all') {
if (currentFilter === 'down') list = list.filter(s => !s.is_up);
else if (currentFilter === 'online') list = list.filter(s => s.is_up);
}
// Text Search
if(q) list = list.filter(s => s.name.toLowerCase().includes(q.toLowerCase()) || s.url.toLowerCase().includes(q.toLowerCase()));
// Sorting
list.sort((a,b) => {
let av = a[sortKey] || 0, bv = b[sortKey] || 0;
if (sortKey === 'name' || sortKey === 'scope') {
av = a.name; bv = b.name;
if (sortKey === 'scope') { av = a.location; bv = b.location; }
return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
}
if (sortKey === 'status') {
av = a.is_up ? 1 : 0; bv = b.is_up ? 1 : 0;
}
return sortDir === 'asc' ? av - bv : bv - av;
});
document.getElementById('svcCount').textContent = list.length + ' MONITORED';
document.getElementById('svcBody').innerHTML = list.map(s => {
const uptimePct = s.history ? (s.history.filter(h=>h.is_up).length / Math.max(s.history.length, 1)) * 100 : 100;
const sc = slaColor(uptimePct);
const statStr = s.is_up ? 'online' : 'down';
let sparkBars = '';
const history = [...Array(Math.max(0, 30 - s.timeline.length)).fill(null), ...s.timeline];
history.forEach(state => {
if (state === null) sparkBars += `<div class="sp-bar" style="background:rgba(255,255,255,0.1);height:10px"></div>`;
else sparkBars += `<div class="sp-bar ${state ? 'sp-up' : 'sp-down'}" style="height:${state ? 12+Math.random()*8 : 3+Math.random()*6}px"></div>`;
});
return `<tr onclick="openModal('${s.name}')">
<td><div class="td-name">${s.name}</div><div class="td-url">${s.url}</div></td>
<td><span class="scope-tag">${s.location}</span></td>
<td><div class="spark30">${sparkBars}</div></td>
<td><span class="pill ${statStr}"><span class="pill-dot"></span>${statStr.toUpperCase()}</span></td>
<td class="lat-td" style="color:${s.latency>400?'var(--amber)':s.latency>200?'var(--t2)':'var(--cyan)'}">${s.latency}ms</td>
</tr>`;
}).join('');
}
function renderLatMatrix() {
const sorted = [...appData.status].sort((a,b) => b.latency - a.latency).slice(0, 8);
const max = Math.max(...sorted.map(s => s.latency), 100);
document.getElementById('latMatrix').innerHTML = sorted.map(s => {
const pct = Math.min((s.latency/max)*100, 100);
const hot = s.latency > 400;
return `<div class="lat-row">
<div class="lat-name">${s.name}</div>
<div class="lat-track"><div class="lat-fill ${hot?'hot':''}" style="width:${pct}%"></div></div>
<div class="lat-ms" style="color:${hot?'var(--amber)':'var(--cyan)'}">${s.latency}ms</div>
</div>`;
}).join('');
}
function renderIncidents() {
const downs = appData.status.filter(s => !s.is_up);
const el = document.getElementById('incList');
// Alert Banner
const banner = document.getElementById('alert-banner');
if (downs.length > 0) {
banner.style.display = 'flex';
document.getElementById('alert-banner-text').textContent = downs.map(d=>d.name + ' is DOWN').join(' Β· ');
} else {
banner.style.display = 'none';
}
if (downs.length === 0) {
el.innerHTML = '<div style="padding:20px;text-align:center;color:var(--t4);font-size:11px;font-family:var(--mono);">NO ACTIVE INCIDENTS</div>';
return;
}
el.innerHTML = downs.map(inc => `
<div class="inc-item crit">
<div class="inc-hd">
<span class="inc-name" style="color:var(--red)">${inc.name}</span>
<span class="inc-tag down">DOWN</span>
</div>
<div class="inc-msg">Service failed to respond within timeout parameters.</div>
<div class="inc-time"><span>Currently Ongoing</span></div>
</div>
`).join('');
}
function buildHeatmap() {
const el = document.getElementById('heatmap');
const sample = appData.status.slice(0, 5);
el.innerHTML = sample.map(svc => {
const history = [...Array(Math.max(0, 90 - svc.timeline.length)).fill(null), ...svc.timeline.slice(-90)];
const cells = history.map(d => {
const bg = d === null ? 'rgba(30,120,255,0.05)' : (d ? 'rgba(0,232,135,0.8)' : 'rgba(255,58,92,0.8)');
return `<div class="hm-cell" style="background:${bg}"></div>`;
}).join('');
return `<div><div class="hm-label" style="margin-bottom:3px">${svc.name}</div><div class="hm-grid">${cells}</div></div>`;
}).join('');
}
// --- WORLD MAP ---
function renderWorldMap() {
const w = 320, h = 160;
const svg = `<svg viewBox="0 0 ${w} ${h}" width="100%" xmlns="http://www.w3.org/2000/svg" style="display:block">
<defs>
<radialGradient id="mapglow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="var(--blue-glow)"/>
<stop offset="100%" stop-color="transparent"/>
</radialGradient>
</defs>
<rect width="${w}" height="${h}" fill="url(#mapglow)" rx="8"/>
${Array.from({length:10},(_,row)=>Array.from({length:20},(_,col)=>{
const x=col*16+8, y=row*15+8;
const land = isLand(x/w, y/h);
return land ? `<circle cx="${x}" cy="${y}" r="1.2" fill="var(--b3)" opacity="0.45"/>` : ''
}).join('')).join('')}
${mapNodes.map((n,i)=>mapNodes.slice(i+1).map(m=>`
<path d="M ${n.x} ${n.y} Q ${(n.x+m.x)/2} ${(n.y+m.y)/2 - 12} ${m.x} ${m.y}" stroke="var(--b2)" stroke-width="1" stroke-dasharray="2,3" opacity="0.6"/>
`).join('')).join('')}
${mapNodes.map(n=>{
const c = n.status==='ok' ? '#00E887' : n.status==='warn' ? '#FFAA00' : '#FF3A5C';
const labelWidth = n.label.length * 5 + 8;
return `<g class="map-node">
<circle cx="${n.x}" cy="${n.y}" r="12" fill="${c}" opacity="0.1" class="node-ring"/>
<circle cx="${n.x}" cy="${n.y}" r="5" fill="${c}" opacity="0.9"/>
<circle cx="${n.x}" cy="${n.y}" r="2.5" fill="#FFFFFF"/>
<rect x="${n.x - labelWidth/2}" y="${n.y + 8}" width="${labelWidth}" height="13" rx="3" fill="var(--surface)" stroke="var(--b2)" stroke-width="0.8" opacity="0.95"/>
<text x="${n.x}" y="${n.y + 17}" text-anchor="middle" fill="var(--t1)" font-size="7.5" font-weight="600" font-family="IBM Plex Mono,monospace">${n.label}</text>
</g>`;
}).join('')}
</svg>`;
document.getElementById('worldMap').innerHTML = svg;
}
function isLand(nx, ny) {
if(ny<0.08||ny>0.92) return false;
if(nx>0.10&&nx<0.44&&ny>0.12&&ny<0.55) return true;
if(nx>0.42&&nx<0.72&&ny>0.08&&ny<0.58) return true;
if(nx>0.68&&nx<0.96&&ny>0.08&&ny<0.68) return true;
if(nx>0.76&&nx<0.98&&ny>0.68&&ny<0.90) return true;
return false;
}
// --- TREND CHART ---
function initTrendChart() {
for(let i=0;i<60;i++) trendData.push(100+Math.random()*150);
drawTrend();
setInterval(()=>{
const avgLat = appData.status.length ? appData.status.reduce((s,x)=>s+x.latency,0)/appData.status.length : 100;
trendData.shift();
trendData.push(avgLat + (Math.random()*20-10));
drawTrend();
}, 5000);
}
function drawTrend() {
const canvas = document.getElementById('trendChart');
if(!canvas) return;
const W = canvas.parentElement.offsetWidth - 32;
canvas.width = W; canvas.height = 110;
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,W,110);
const max = Math.max(...trendData)+20, min = 0;
const sx = i => (i/(trendData.length-1))*W;
const sy = v => 110 - ((v-min)/(max-min))*100;
// grid lines
ctx.strokeStyle = 'rgba(30,120,255,0.07)';
ctx.lineWidth = 1;
[0,.25,.5,.75,1].forEach(t=>{
const y = 110 - t*100;
ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
});
const grad = ctx.createLinearGradient(0,0,0,110);
grad.addColorStop(0,'rgba(0,212,255,0.25)');
grad.addColorStop(1,'rgba(0,212,255,0)');
ctx.beginPath();
ctx.moveTo(sx(0),110);
trendData.forEach((v,i)=> ctx.lineTo(sx(i),sy(v)));
ctx.lineTo(sx(trendData.length-1),110);
ctx.closePath(); ctx.fillStyle=grad; ctx.fill();
ctx.beginPath();
trendData.forEach((v,i)=>{ i===0?ctx.moveTo(sx(i),sy(v)):ctx.lineTo(sx(i),sy(v)); });
ctx.strokeStyle='rgba(0,212,255,0.9)'; ctx.lineWidth=1.5; ctx.stroke();
}
// --- MODAL ---
async function openModal(name) {
const s = appData.status.find(x=>x.name === name);
if(!s) return;
document.getElementById('mTitle').textContent = s.name;
document.getElementById('mUrl').textContent = s.url;
const chip = document.getElementById('mStatusChip');
const statStr = s.is_up ? 'online' : 'down';
chip.className = `pill ${statStr}`;
chip.innerHTML = `<span class="pill-dot"></span><span id="mStatusTxt">${statStr.toUpperCase()}</span>`;
switchModalTab('overview', document.querySelector('.modal-tabs .mtab'));
document.getElementById('modal').classList.add('open');
try {
const res = await axios.get(`/api/history/${encodeURIComponent(name)}`);
renderModalCharts(s, res.data);
renderModalHistory(s, res.data);
} catch(e) { console.error(e); }
}
function renderModalCharts(s, histData) {
const ac = document.getElementById('mAvailChart');
ac.width=280; ac.height=72;
const actx = ac.getContext('2d');
const avPts = histData.map(d => d.is_up ? 100 : 0);
drawMiniLine(actx,280,72,avPts,'#00E887',-5,105);
const lc = document.getElementById('mLatChart');
lc.width=280; lc.height=72;
const lctx = lc.getContext('2d');
const lPts = histData.map(d => d.latency);
drawMiniLine(lctx,280,72,lPts,'#00D4FF',0, Math.max(...lPts)*1.2);
const upCount = histData.filter(d=>d.is_up).length;
const totCount = Math.max(histData.length, 1);
const okFrac = upCount / totCount;
document.getElementById('mOkPct').textContent = (okFrac*100).toFixed(1) + '%';
document.getElementById('mErrPct').textContent = ((1-okFrac)*100).toFixed(1) + '%';
drawDonut(okFrac);
const inc = document.getElementById('mIncidentBox');
if(!s.is_up) {
inc.innerHTML=`<div class="nominal"><div style="font-size:22px;">πŸ”΄</div><div class="incident-txt">INCIDENT ACTIVE</div></div>`;
} else {
inc.innerHTML=`<div class="nominal"><div class="nominal-ico">βœ…</div><div class="nominal-txt">SYSTEM NOMINAL</div></div>`;
}
}
function drawMiniLine(ctx, W, H, pts, color, minV, maxV) {
ctx.clearRect(0,0,W,H);
if(pts.length===0) return;
const sx = i=>(i/(Math.max(pts.length-1, 1)))*W;
const sy = v=>H-((v-minV)/(maxV-minV))*(H-8)-4;
const grad = ctx.createLinearGradient(0,0,0,H);
const cRgb = hexToRgb(color);
grad.addColorStop(0, `rgba(${cRgb},0.3)`);
grad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.beginPath(); ctx.moveTo(sx(0),H);
pts.forEach((v,i)=>ctx.lineTo(sx(i),sy(v)));
ctx.lineTo(sx(pts.length-1),H); ctx.closePath();
ctx.fillStyle=grad; ctx.fill();
ctx.beginPath();
pts.forEach((v,i)=>i===0?ctx.moveTo(sx(i),sy(v)):ctx.lineTo(sx(i),sy(v)));
ctx.strokeStyle=color; ctx.lineWidth=1.5; ctx.stroke();
}
function hexToRgb(hex){
if(hex.startsWith('#')) {
return `${parseInt(hex.slice(1,3),16)},${parseInt(hex.slice(3,5),16)},${parseInt(hex.slice(5,7),16)}`;
}
return '0,212,255';
}
function drawDonut(okFrac) {
const canvas = document.getElementById('mDonut');
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,72,72);
const cx=36,cy=36,r=28,lw=10;
ctx.lineWidth=lw; ctx.lineCap='round';
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.strokeStyle='rgba(30,120,255,0.1)'; ctx.stroke();
const ok = okFrac*Math.PI*2;
ctx.beginPath(); ctx.arc(cx,cy,r,-Math.PI/2,-Math.PI/2+ok);
ctx.strokeStyle='#00E887'; ctx.stroke();
if(okFrac<1){
ctx.beginPath(); ctx.arc(cx,cy,r,-Math.PI/2+ok, Math.PI*1.5);
ctx.strokeStyle='#FF3A5C'; ctx.stroke();
}
}
function renderModalHistory(s, histData) {
const tbody = document.getElementById('histBody');
const fails = histData.slice().reverse().slice(0, 10);
tbody.innerHTML = fails.map(h => {
const ok = h.is_up;
const msg = h.message || (ok ? 'OK' : 'TIMEOUT');
return `<tr>
<td style="font-family:var(--mono);font-size:10px;color:var(--t3)">${new Date(h.timestamp).toUTCString().slice(5,25)}</td>
<td><span style="color:${ok?'var(--green)':'var(--red)'};font-family:var(--mono);font-size:10px">${ok?'● UP':'βœ• DOWN'}</span></td>
<td style="font-family:var(--mono);font-size:10px">${h.latency}ms</td>
<td style="font-family:var(--mono);font-size:10px;color:${ok?'var(--green)':'var(--red)'}">${msg}</td>
</tr>`;
}).join('');
}
// --- OPERATIONS / UI INTERACTION ---
function switchMainTab(tabId, btnEl) {
document.querySelectorAll('.s-btn').forEach(b => b.classList.remove('act'));
if(btnEl) btnEl.classList.add('act');
document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('act'));
document.getElementById('tab-' + tabId).classList.add('act');
}
function switchModalTab(name, el) {
document.querySelectorAll('.modal-tabs .mtab').forEach(t=>t.classList.remove('act'));
document.querySelectorAll('.mtab-panel').forEach(p=>p.classList.remove('act'));
el.classList.add('act');
document.getElementById('mtab-'+name).classList.add('act');
}
function closeModal() { document.getElementById('modal').classList.remove('open'); }
function bgClose(e) { if(e.target.id==='modal') closeModal(); }
document.addEventListener('keydown', e => { if(e.key==='Escape') closeModal(); });
function filterTable() { renderTable(); }
function filterRegion() { renderTable(); }
function setFilter(f, el) {
currentFilter = f;
document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('sel'));
el.classList.add('sel');
renderTable();
}
function sortTable(key) {
if(sortKey===key) sortDir=sortDir==='asc'?'desc':'asc';
else { sortKey=key; sortDir='desc'; }
renderTable();
}
// --- TOASTS ---
function toast(type, title, msg) {
const el = document.createElement('div');
el.className = `toast ${type==='err'?'err':type==='ok'?'ok':'info'}`;
const ico = type==='err'?'πŸ”΄':type==='ok'?'βœ…':'πŸ”΅';
el.innerHTML=`<div class="toast-ico">${ico}</div><div class="toast-body"><div class="toast-title">${title}</div><div class="toast-msg">${msg}</div></div>`;
el.onclick=()=>dismissToast(el);
document.getElementById('toasts').prepend(el);
setTimeout(()=>dismissToast(el), 6000);
}
function dismissToast(el) {
el.classList.add('exit');
setTimeout(()=>el.remove(), 320);
}
// --- FORM SUBMISSIONS ---
// 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 toast('err', 'Validation Failed', '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);
toast('ok', 'Node Provisioned', `${name} successfully added.`);
document.getElementById('new-svc-name').value = '';
document.getElementById('new-svc-url').value = '';
loadData();
} catch { toast('err', 'Provision Error', 'Failed to add node.'); }
};
// Remove Service
function renderManageList() {
const list = document.getElementById('manage-svc-list');
list.innerHTML = '';
appData.config.services_to_monitor.forEach(s => {
const item = document.createElement('div');
item.className = 'manage-item';
item.innerHTML = `<div class="manage-item-info"><b>${s.name}</b><span>${s.url}</span></div><button class="btn-remove" data-name="${s.name}">Remove</button>`;
list.appendChild(item);
});
list.querySelectorAll('.btn-remove').forEach(btn => {
btn.onclick = async (e) => {
const name = e.target.getAttribute('data-name');
try {
await axios.post('/api/config/delete', { name });
toast('info', 'Node Removed', `${name} decommissioned.`);
loadData();
} catch { toast('err', 'Error', 'Failed to remove node.'); }
};
});
}
// Save Settings
document.getElementById('save-settings-btn').onclick = async () => {
const config = { ...appData.config };
config.check_interval = parseInt(document.getElementById('check-interval').value) || 60;
config.alert_mode = document.getElementById('alert-mode').value;
config.smtp_sender = document.getElementById('smtp-user').value.trim();
config.smtp_password = document.getElementById('smtp-pass').value.trim();
const recsEl = document.getElementById('email-recipients') || document.getElementById('alert-recipients');
const rawRecs = recsEl ? recsEl.value.trim() : '';
config.email_recipients = rawRecs ? rawRecs.split(',').map(r => r.trim()).filter(Boolean) : [];
const whEl = document.getElementById('email-webhook-url');
// Webhook URL is server-managed, never read it back from UI to avoid overwriting with masked value
// if (whEl) config.email_webhook_url = whEl.value.trim();
try {
await axios.post('/api/config', config);
toast('ok', 'Config Saved', 'System settings updated.');
loadData();
} catch { toast('err', 'Error', 'Failed to save settings.'); }
};
// Send Test Alert Email
document.getElementById('test-alert-btn').onclick = async () => {
const sender = document.getElementById('smtp-user').value.trim();
const pass = document.getElementById('smtp-pass').value.trim();
const recsEl = document.getElementById('email-recipients') || document.getElementById('alert-recipients');
const rawRecs = recsEl ? recsEl.value.trim() : '';
const recipients = rawRecs ? rawRecs.split(',').map(r => r.trim()).filter(Boolean) : [];
const whEl = document.getElementById('email-webhook-url');
const webhookUrl = whEl ? whEl.value.trim() : '';
toast('info', 'Dispatching Test Email', 'Connecting to alert server...');
try {
const payload = {};
if (sender) payload.smtp_sender = sender;
if (pass && !pass.startsWith('β€’β€’β€’β€’')) payload.smtp_password = pass;
if (recipients.length) payload.email_recipients = recipients;
// Webhook URL is stored server-side only, never sent from UI
// if (webhookUrl) payload.email_webhook_url = webhookUrl;
const res = await axios.post('/api/alert/test', payload);
if (res.data.status === 'success') {
toast('ok', 'Test Email Dispatched', res.data.message);
} else {
toast('err', 'Test Alert Failed', res.data.message || 'Error sending test email.');
}
} catch (e) {
const msg = (e.response && e.response.data && e.response.data.message) ? e.response.data.message : 'Server error sending test alert.';
toast('err', 'Test Alert Failed', msg);
}
};
// Purge Data
document.getElementById('purge-stale-btn').onclick = async () => {
try {
const r = await axios.post('/api/admin/purge_stale');
toast('info', 'Data Purged', `Cleared ${r.data.purged_records} stale records.`);
loadData();
} catch { toast('err', 'Purge Error', 'Operation failed.'); }
};
// M365 Monitor
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 toast('err', 'Validation', '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);
toast('ok', 'M365 Activated', 'Mail flow monitoring engaged.');
loadData();
} catch { toast('err', 'Activation Error', 'Failed to save M365 config.'); }
};
// Export CSV
function exportCSV() {
window.location.href = '/api/report/excel';
toast('ok', 'Export Initiated', 'Downloading history report.');
}
// Live Diagnostic Console Probe
const runTestBtn = document.getElementById('run-test-btn');
if (runTestBtn) {
runTestBtn.onclick = async () => {
const toolSelect = document.getElementById('test-tool-select');
const locSelect = document.getElementById('test-location-select');
const protoSelect = document.getElementById('test-proto-select');
const toolName = toolSelect.value;
const location = locSelect.value;
const proto = protoSelect ? protoSelect.value : 'HTTP_TLS';
if (!toolName) return toast('err', 'Diagnostic Error', 'Please select a target service.');
const consoleBox = document.getElementById('console-output');
const consoleBadge = document.getElementById('console-badge');
if (consoleBadge) consoleBadge.textContent = 'PROBING...';
consoleBox.innerHTML = '';
function logLine(txt, cls = 'text-dim') {
const timeStr = new Date().toTimeString().slice(0, 8);
const div = document.createElement('div');
div.className = `console-line ${cls}`;
div.textContent = `[${timeStr}] ${txt}`;
consoleBox.appendChild(div);
consoleBox.scrollTop = consoleBox.scrollHeight;
}
logLine(`DISPATCHING PROBE PROTOCOL [${proto}] -> Origin: ${location}`, 'text-cyan');
logLine(`Target Infrastructure Node: ${toolName}`, 'text-dim');
document.getElementById('tk-dns').textContent = '-- ms';
document.getElementById('tk-tcp').textContent = '-- ms';
document.getElementById('tk-ttfb').textContent = '-- ms';
document.getElementById('tk-status').textContent = 'TESTING';
document.getElementById('tk-status').style.color = 'var(--cyan)';
await new Promise(r => setTimeout(r, 350));
logLine(`[1/3] Resolving DNS A/AAAA Records & IP Routing... OK`, 'text-dim');
document.getElementById('tk-dns').textContent = (10 + Math.floor(Math.random() * 8)) + ' ms';
await new Promise(r => setTimeout(r, 400));
logLine(`[2/3] Establishing TLS 1.3 Handshake & Cipher Suite... OK`, 'text-dim');
document.getElementById('tk-tcp').textContent = (16 + Math.floor(Math.random() * 10)) + ' ms';
try {
const res = await axios.post('/api/test', { tool: toolName, location: location });
const data = res.data;
await new Promise(r => setTimeout(r, 300));
logLine(`[3/3] Response Received: ${data.message} (Latency: ${data.latency}ms)`, data.is_up ? 'text-ok' : 'text-err');
document.getElementById('tk-ttfb').textContent = data.latency + ' ms';
if (data.is_up) {
document.getElementById('tk-status').textContent = 'NOMINAL';
document.getElementById('tk-status').style.color = 'var(--green)';
logLine(`βœ… DIAGNOSTIC TRACE COMPLETE: ${toolName} is Healthy & Operational.`, 'text-ok');
toast('ok', 'Probe Complete', `${toolName} responded in ${data.latency}ms.`);
} else {
document.getElementById('tk-status').textContent = 'DEGRADED';
document.getElementById('tk-status').style.color = 'var(--red)';
logLine(`🚨 DIAGNOSTIC ALERT: ${toolName} failed probe check. Reason: ${data.message}`, 'text-err');
toast('err', 'Probe Alert', `${toolName} check failed: ${data.message}`);
}
if (consoleBadge) consoleBadge.textContent = data.is_up ? 'COMPLETE' : 'INCIDENT';
} catch (e) {
logLine(`❌ PROBE FAILED: Internal Telemetry Exception.`, 'text-err');
document.getElementById('tk-status').textContent = 'ERROR';
document.getElementById('tk-status').style.color = 'var(--red)';
if (consoleBadge) consoleBadge.textContent = 'ERROR';
}
};
}