// 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;
}