// PingQL Dashboard — shared utilities // Auth is now cookie-based. No localStorage needed. const API_BASE = window.location.origin; function logout() { window.location.href = '/dashboard/logout'; } // requireAuth is a no-op now — server redirects to /dashboard if not authed function requireAuth() { return true; } async function api(path, opts = {}) { const res = await fetch(`${API_BASE}${path}`, { ...opts, credentials: 'same-origin', // send cookie automatically headers: { 'Content-Type': 'application/json', ...opts.headers, }, body: opts.body ? JSON.stringify(opts.body) : undefined, }); if (res.status === 401) { window.location.href = '/dashboard'; throw new Error('Unauthorized'); } const data = await res.json(); if (!res.ok) throw new Error(data.error || 'API error'); return data; } // Format relative time function formatAgo(ms) { const s = Math.floor(ms / 1000); if (s < 60) return `${s}s ago`; if (s < 3600) return `${Math.floor(s / 60)}m ago`; if (s < 86400)return `${Math.floor(s / 3600)}h ago`; return `${Math.floor(s / 86400)}d ago`; } function timeAgo(date) { const ts = new Date(date).getTime(); const elapsed = Date.now() - ts; return `${formatAgo(elapsed)}`; } // Tick all live timestamps every second setInterval(() => { document.querySelectorAll('.timestamp[data-ts]').forEach(el => { const elapsed = Date.now() - Number(el.dataset.ts); el.textContent = formatAgo(elapsed); }); }, 1000); // Render a tiny sparkline SVG from latency values function sparkline(values, width = 120, height = 32) { if (!values.length) return ''; const max = Math.max(...values, 1); const min = Math.min(...values, 0); const range = max - min || 1; const step = width / Math.max(values.length - 1, 1); const points = values.map((v, i) => { const x = i * step; const y = height - ((v - min) / range) * (height - 4) - 2; return `${x},${y}`; }).join(' '); return ``; } // Status badge function statusBadge(up) { if (up === true) return ''; if (up === false) return ''; return ''; } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // Subscribe to live ping updates for a monitor via SSE (fetch-based for auth header support) // Returns an AbortController — call .abort() to close function watchMonitor(monitorId, onPing) { const ac = new AbortController(); async function connect() { try { const res = await fetch(`/monitors/${monitorId}/stream`, { credentials: 'same-origin', signal: ac.signal, }); if (!res.ok || !res.body) return; const reader = res.body.getReader(); const dec = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); const lines = buf.split('\n'); buf = lines.pop() ?? ''; for (const line of lines) { if (line.startsWith('data: ')) { try { onPing(JSON.parse(line.slice(6))); } catch {} } } } } catch (e) { if (e.name === 'AbortError') return; // Reconnect after a short delay on unexpected disconnect setTimeout(connect, 3000); } } connect(); return ac; }