Spaces:
Sleeping
Sleeping
Download static/script_v6.js from ParleDowntime/Pro: direct link, hf CLI and curl.
- Browser
- Download file 34.6 kB
-
https://huggingface.co/spaces/ParleDowntime/Pro/resolve/main/static/script_v6.js
- Command line
-
hf download hf://spaces/ParleDowntime/Pro/static/script_v6.js
-
curl -L -o script_v6.js https://huggingface.co/spaces/ParleDowntime/Pro/resolve/main/static/script_v6.js
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'; | |
| } | |
| }; | |
| } | |