/* ============================================================
TELEMETRY DASHBOARD — live ops frontend
============================================================ */
(() => {
'use strict';
const API_BASE = '/api/telemetry';
const POLL_OVERVIEW = 5000;
const POLL_HISTORY = 60000;
const POLL_TAIL = 5000;
const POLL_GEO = 60000;
const POLL_ALERTS = 10000;
const POLL_VISITORS = 15000;
const POLL_COMMITS = 120000;
// ─── State ──────────────────────────────────────────
const state = {
lastOverview: null,
lastHistory: null,
lastAlertSig: '',
lastSync: 0,
alarm: false,
audioCtx: null,
seenTailKeys: new Set(),
};
// ─── Utils ──────────────────────────────────────────
const $ = (id) => document.getElementById(id);
const fmt = {
bytes(n) {
if (!n) return '0 B';
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), u.length - 1);
return (n / Math.pow(1024, i)).toFixed(1) + ' ' + u[i];
},
bps(n) {
return fmt.bytes(n) + '/s';
},
uptime(s) {
if (!s) return '—';
s = Math.floor(s);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
if (d > 0) return `${d}d ${h}h ${m}m`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
},
numGrp(n) {
return (n || 0).toLocaleString('en-GB');
},
iso(iso) {
if (!iso) return '—';
try {
const d = new Date(iso);
return d.toLocaleString('en-GB', { hour12: false });
} catch { return iso; }
},
ago(iso) {
if (!iso) return '—';
try {
const s = Math.floor((Date.now() - new Date(iso).getTime()) / 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`;
} catch { return iso; }
},
escape(s) {
return String(s || '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
}
};
async function fetchJSON(url) {
try {
const r = await fetch(url, { credentials: 'same-origin' });
if (!r.ok) throw new Error('HTTP ' + r.status);
return await r.json();
} catch (e) {
console.warn('[telemetry] fetch failed', url, e);
return null;
}
}
// ─── Canvas gauge ───────────────────────────────────
function drawGauge(canvas, value, label) {
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const size = canvas.clientWidth;
canvas.width = size * dpr;
canvas.height = size * dpr;
ctx.scale(dpr, dpr);
const cx = size / 2, cy = size / 2;
const r = size / 2 - 6;
ctx.clearRect(0, 0, size, size);
// Background arc
ctx.beginPath();
ctx.arc(cx, cy, r, Math.PI * 0.75, Math.PI * 2.25);
ctx.lineWidth = 6;
ctx.strokeStyle = '#1a1a1a';
ctx.stroke();
// Tick marks
for (let i = 0; i <= 10; i++) {
const a = Math.PI * 0.75 + (Math.PI * 1.5 * i / 10);
const r1 = r - 2, r2 = r + 4;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * r1, cy + Math.sin(a) * r1);
ctx.lineTo(cx + Math.cos(a) * r2, cy + Math.sin(a) * r2);
ctx.lineWidth = 1;
ctx.strokeStyle = '#2a2a2a';
ctx.stroke();
}
// Value arc
const pct = Math.max(0, Math.min(100, value)) / 100;
const endA = Math.PI * 0.75 + Math.PI * 1.5 * pct;
const color = value > 90 ? '#ff2d2d' : value > 80 ? '#c9a227' : '#00cc33';
ctx.beginPath();
ctx.arc(cx, cy, r, Math.PI * 0.75, endA);
ctx.lineWidth = 6;
ctx.strokeStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 8;
ctx.stroke();
ctx.shadowBlur = 0;
// Needle
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(endA) * (r - 12), cy + Math.sin(endA) * (r - 12));
ctx.lineWidth = 2;
ctx.strokeStyle = color;
ctx.stroke();
// Hub
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
// ─── Sparkline ──────────────────────────────────────
function drawSpark(canvas, values, { color = '#00cc33', fill = true, max = null } = {}) {
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth;
const h = canvas.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, w, h);
if (!values || values.length < 2) {
ctx.fillStyle = '#333';
ctx.font = '10px JetBrains Mono';
ctx.fillText('awaiting history...', 6, h / 2 + 3);
return;
}
const peak = max !== null ? max : Math.max(...values, 1);
const step = w / (values.length - 1);
ctx.beginPath();
values.forEach((v, i) => {
const x = i * step;
const y = h - (v / peak) * (h - 4) - 2;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = color;
ctx.lineWidth = 1.2;
ctx.shadowColor = color;
ctx.shadowBlur = 4;
ctx.stroke();
ctx.shadowBlur = 0;
if (fill) {
ctx.lineTo(w, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fillStyle = color + '22';
ctx.fill();
}
}
// ─── Disk ASCII bars ────────────────────────────────
function renderDisks(disks) {
const el = $('tmDisks');
if (!el) return;
if (!disks || !disks.length) { el.innerHTML = '
NO MOUNTS
'; return; }
const bars = 18;
el.innerHTML = disks.map(d => {
const filled = Math.round(d.pct / 100 * bars);
const cls = d.pct > 90 ? 'crit' : d.pct > 85 ? 'warn' : '';
const bar = '█'.repeat(filled) + '░'.repeat(bars - filled);
return `
${fmt.escape(d.mount)}
${d.used_gb}G / ${d.total_gb}G · ${d.pct}%
[${bar}]
`;
}).join('');
}
// ─── Services ──────────────────────────────────────
function renderServices(services) {
const el = $('tmServicesBody');
if (!el) return;
if (!services || !services.length) { el.innerHTML = '| NO SERVICES |
'; return; }
const sorted = [...services].sort((a, b) => {
const rank = { down: 0, unknown: 1, up: 2 };
return (rank[a.status] ?? 3) - (rank[b.status] ?? 3);
});
el.innerHTML = sorted.map(s => `
| ${s.status.toUpperCase()} |
${fmt.escape(s.name)} |
${fmt.uptime(s.uptime_seconds)} |
${s.memory_mb ? s.memory_mb.toFixed(0) + ' MB' : '—'} |
${s.cpu_percent ? s.cpu_percent.toFixed(1) + '%' : '—'} |
`).join('');
const upCount = services.filter(s => s.status === 'up').length;
const badge = $('tmSvcBadge');
if (badge) badge.textContent = `${upCount}/${services.length} UP`;
}
// ─── Crons ─────────────────────────────────────────
function renderCrons(crons) {
const el = $('tmCronsBody');
if (!el) return;
if (!crons || !crons.length) { el.innerHTML = '| NO CRONS |
'; return; }
el.innerHTML = crons.map((c, i) => `
| ${fmt.escape(c.name)} |
${fmt.escape(c.schedule)} |
${fmt.ago(c.last_run_iso)} |
${c.last_status === 'ok' ? '✓ OK' : c.last_status === 'fail' ? '✗ FAIL' : '? UNKNOWN'} |
${fmt.escape(c.last_output_tail || '(no output)')} |
`).join('');
el.querySelectorAll('[data-cron-idx]').forEach(row => {
row.addEventListener('click', () => {
const idx = row.getAttribute('data-cron-idx');
const exp = el.querySelector(`[data-cron-exp="${idx}"]`);
if (exp) exp.style.display = exp.style.display === 'none' ? 'table-row' : 'none';
});
});
}
// ─── Nginx ─────────────────────────────────────────
function renderNginx(nginx) {
const s = $('tmNginxStats');
const t = $('tmTopPages');
if (!s || !t) return;
if (!nginx || nginx.total_requests === 0) {
s.innerHTML = 'NO ACCESS LOG DATA YET
';
t.innerHTML = '';
return;
}
s.innerHTML = `
${fmt.numGrp(nginx.total_requests)}REQUESTS / 24h
${fmt.bytes(nginx.total_bytes)}BANDWIDTH
${nginx.avg_rpm}AVG REQ / MIN
${fmt.numGrp(nginx.status_4xx_count)}4xx ERRORS
${fmt.numGrp(nginx.status_5xx_count)}5xx ERRORS
`;
const top = (list, head) => `
${head}
${list.slice(0, 8).map(x => `- ${fmt.escape(x.path || x.ip)}${fmt.numGrp(x.count)}
`).join('')}
`;
t.innerHTML = top(nginx.top_pages || [], 'TOP PAGES') + top(nginx.top_ips_redacted || [], 'TOP IPs (REDACTED)');
}
// ─── Security ──────────────────────────────────────
function renderSecurity(sec) {
const s = $('tmSecurityStats');
const j = $('tmJails');
if (!s) return;
if (!sec) { s.innerHTML = 'NO DATA
'; return; }
s.innerHTML = `
${sec.ufw_rules_count}UFW RULES
${sec.fail2ban_bans_total}BANNED IPs
${sec.ssh_attempts_24h}SSH BRUTEFORCE 24h
${sec.last_reboot_iso ? fmt.ago(sec.last_reboot_iso) : '—'}LAST REBOOT
`;
if (j) {
if (!sec.fail2ban_jails || !sec.fail2ban_jails.length) {
j.innerHTML = 'no fail2ban jails detected
';
} else {
j.innerHTML = 'JAILS
' +
sec.fail2ban_jails.map(x => `${fmt.escape(x.name)}${x.banned}
`).join('');
}
}
}
// ─── SSL ──────────────────────────────────────────
function renderSSL(ssl) {
const el = $('tmSsl');
if (!el) return;
if (!ssl || !ssl.length) { el.innerHTML = 'NO CERT DATA
'; return; }
el.innerHTML = ssl.map(s => {
const cls = s.days_left < 0 ? 'fail' : s.days_left < 14 ? 'crit' : s.days_left < 30 ? 'warn' : '';
const daysTxt = s.days_left < 0 ? 'FAIL' : s.days_left + 'd';
return `
${fmt.escape(s.domain)}
${daysTxt}
`;
}).join('');
}
// ─── Stack ─────────────────────────────────────────
function renderStack(stack) {
const el = $('tmStack');
if (!el) return;
if (!stack) { el.innerHTML = 'NO DATA
'; return; }
const keys = [
['OS', stack.os], ['kernel', stack.kernel],
['python', stack.python_version], ['node', stack.node_version],
['nginx', stack.nginx_version], ['docker', stack.docker_version],
['ffmpeg', stack.ffmpeg_version], ['arch', stack.uname],
];
el.innerHTML = keys.filter(([_, v]) => v).map(([k, v]) =>
`${k}${fmt.escape(v)}
`
).join('');
}
// ─── Repos ─────────────────────────────────────────
function renderRepos(repos) {
const el = $('tmRepos');
if (!el) return;
if (!repos || !repos.length) { el.innerHTML = 'NO REPOS
'; return; }
el.innerHTML = repos.map(r => `
${fmt.escape(r.name)}
${fmt.escape(r.last_commit_sha)}${r.dirty ? ' ● DIRTY' : ''}
${fmt.escape(r.last_commit_msg)}
${fmt.ago(r.last_commit_iso)} · ${fmt.escape(r.path)}
`).join('');
}
// ─── Tail ──────────────────────────────────────────
function renderTail(lines) {
const el = $('tmTail');
if (!el) return;
if (!lines || !lines.length) return;
// Filter to only new lines
const newOnes = [];
for (const l of lines) {
const key = `${l.time}|${l.ip_masked}|${l.path}|${l.status}`;
if (!state.seenTailKeys.has(key)) {
state.seenTailKeys.add(key);
newOnes.push(l);
}
}
if (state.seenTailKeys.size > 200) {
// Trim
const arr = [...state.seenTailKeys].slice(-100);
state.seenTailKeys = new Set(arr);
}
if (!newOnes.length && el.querySelector('.tm-loading')) {
el.innerHTML = '(no recent requests)
';
return;
}
// Clear loading once we have data
if (el.querySelector('.tm-loading')) el.innerHTML = '';
newOnes.forEach(l => {
const stCls = l.status >= 500 ? 'st-5xx' : l.status >= 400 ? 'st-4xx' : l.status >= 300 ? 'st-3xx' : 'st-2xx';
const div = document.createElement('div');
div.className = 'tm-tail-line';
div.innerHTML = `${fmt.escape(l.time.split(' ')[0].split(':').slice(1).join(':'))} ${fmt.escape(l.ip_masked)} ${fmt.escape(l.method)} ${fmt.escape(l.path)} ${l.status} ${fmt.bytes(l.size)}`;
el.appendChild(div);
});
// Trim DOM to last 40 lines
while (el.children.length > 40) el.removeChild(el.firstChild);
// Scroll to bottom
el.scrollTop = el.scrollHeight;
}
// ─── Geo ───────────────────────────────────────────
function renderGeo(geo) {
const el = $('tmGeo');
const top = $('tmGeoTop');
if (!el) return;
if (!geo || !geo.length) {
el.innerHTML = 'GEO INTEL UNAVAILABLE
(GeoIP mmdb not installed on VPS)
';
if (top) top.innerHTML = '';
return;
}
const max = geo[0].count;
// Simple horizontal bar chart instead of a world map (keeps dep-free, no external svg)
el.innerHTML = `
${geo.slice(0, 15).map(g => {
const pct = Math.round(g.count / max * 100);
return `
${fmt.escape(g.country_code)}
${fmt.escape(g.country_name)}
${fmt.numGrp(g.count)}
`;
}).join('')}
`;
if (top) {
top.innerHTML = geo.slice(0, 12).map(g =>
`${fmt.escape(g.country_code)}${fmt.numGrp(g.count)}
`
).join('');
}
}
// ─── Burn rate ──────────────────────────────────────
function renderBurn(overview, history) {
const el = $('tmBurn');
if (!el) return;
if (!overview) { el.innerHTML = 'NO DATA
'; return; }
// Disk burn: find root /, compute daily growth from history if available
const root = (overview.system.disk_per_mount || []).find(d => d.mount === '/') || (overview.system.disk_per_mount || [])[0];
const lines = [];
if (root) {
const freeGb = root.total_gb - root.used_gb;
lines.push(`ROOT DISK FREE${freeGb} GB of ${root.total_gb} GB (${root.pct}% used)
`);
}
// Network today — sum of last 24h net_tx from history
if (history && history.net_tx && history.net_tx.length) {
const sumTx = history.net_tx.reduce((a, b) => a + b, 0);
const sumRx = history.net_rx.reduce((a, b) => a + b, 0);
const avgInterval = 300; // 5min
const totalTx = sumTx * avgInterval;
const totalRx = sumRx * avgInterval;
lines.push(`BANDWIDTH 24h↑ ${fmt.bytes(totalTx)} · ↓ ${fmt.bytes(totalRx)}
`);
}
lines.push(`LOAD AVG${overview.system.load_1.toFixed(2)} / ${overview.system.load_5.toFixed(2)} / ${overview.system.load_15.toFixed(2)} (${overview.system.ncpu} CPU)
`);
lines.push(`KERNEL${fmt.escape(overview.system.kernel)} @ ${fmt.escape(overview.system.hostname)}
`);
el.innerHTML = lines.join('');
}
// ─── Ticker ─────────────────────────────────────────
function renderTicker(commits) {
const el = $('tmTicker');
if (!el) return;
if (!commits || !commits.length) { el.innerHTML = 'NO COMMITS
'; return; }
el.innerHTML = `${commits.map(c =>
`${fmt.escape(c.sha)}${fmt.escape(c.msg)}${fmt.ago(c.iso)}`
).join('')}
`;
}
// ─── Alerts ─────────────────────────────────────────
function renderAlerts(alerts) {
const banner = $('tmAlerts');
const content = $('tmAlertsContent');
if (!banner || !content) return;
if (!alerts || !alerts.length) {
banner.classList.remove('visible', 'amber');
state.lastAlertSig = '';
return;
}
const sig = alerts.map(a => a.level + a.message).join('|');
const hasRed = alerts.some(a => a.level === 'red');
banner.classList.add('visible');
banner.classList.toggle('amber', !hasRed);
if (sig !== state.lastAlertSig) {
content.innerHTML = alerts.map(a =>
`${fmt.escape(a.message)}`
).join('') + alerts.map(a =>
`${fmt.escape(a.message)}`
).join(''); // doubled for smooth marquee
state.lastAlertSig = sig;
if (state.alarm) playBeep(hasRed);
}
}
// ─── Alarm beep (Web Audio) ─────────────────────────
function playBeep(urgent) {
try {
if (!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const ctx = state.audioCtx;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.connect(g); g.connect(ctx.destination);
o.type = 'square';
o.frequency.value = urgent ? 880 : 440;
g.gain.setValueAtTime(0.15, ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
o.start(ctx.currentTime);
o.stop(ctx.currentTime + 0.22);
} catch (e) { }
}
// ─── Overview render ────────────────────────────────
function applyOverview(data) {
if (!data) return;
state.lastOverview = data;
state.lastSync = Date.now();
const sys = data.system || {};
// CPU
drawGauge($('tmCpuGauge'), sys.cpu_percent);
const cpuVal = $('tmCpuVal');
if (cpuVal) {
cpuVal.textContent = sys.cpu_percent + '%';
cpuVal.className = 'tm-gauge-value ' + (sys.cpu_percent > 90 ? 'crit' : sys.cpu_percent > 80 ? 'warn' : '');
}
const cpuLoad = $('tmCpuLoad');
if (cpuLoad) cpuLoad.textContent = `load ${sys.load_1?.toFixed(2)} · ${sys.ncpu} cores`;
const cpuBadge = $('tmCpuBadge');
if (cpuBadge) cpuBadge.textContent = `${sys.ncpu} CPU`;
// MEM
drawGauge($('tmMemGauge'), sys.mem_percent);
const memVal = $('tmMemVal');
if (memVal) {
memVal.textContent = sys.mem_percent + '%';
memVal.className = 'tm-gauge-value ' + (sys.mem_percent > 90 ? 'crit' : sys.mem_percent > 80 ? 'warn' : '');
}
const memSize = $('tmMemSize');
if (memSize) memSize.textContent = `${fmt.bytes(sys.mem_used_bytes)} / ${fmt.bytes(sys.mem_total_bytes)}`;
const memBadge = $('tmMemBadge');
if (memBadge) memBadge.textContent = fmt.bytes(sys.mem_total_bytes);
// Disks
renderDisks(sys.disk_per_mount);
// Net
const rx = $('tmNetRx'); if (rx) rx.textContent = fmt.bps(sys.net_rx_bps);
const tx = $('tmNetTx'); if (tx) tx.textContent = fmt.bps(sys.net_tx_bps);
const up = $('tmUptime'); if (up) up.textContent = `uptime ${fmt.uptime(sys.uptime_seconds)}`;
// Tables & sections
renderServices(data.services);
renderCrons(data.crons);
renderNginx(data.nginx_24h);
renderSecurity(data.security);
renderSSL(data.ssl);
renderStack(data.stack);
renderRepos(data.repos);
renderBurn(data, state.lastHistory);
}
function applyHistory(data) {
if (!data) return;
state.lastHistory = data;
drawSpark($('tmCpuSpark'), data.cpu, { color: '#00cc33', max: 100 });
drawSpark($('tmMemSpark'), data.mem, { color: '#c9a227', max: 100 });
// Combined net: overlay
const canvas = $('tmNetSpark');
if (canvas && (data.net_rx?.length || data.net_tx?.length)) {
drawSpark(canvas, data.net_rx, { color: '#14F195', fill: true });
// Overlay tx on top without clearing
const ctx = canvas.getContext('2d');
ctx.globalCompositeOperation = 'source-over';
// redraw-ish: tx overlay
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth, h = canvas.clientHeight;
const values = data.net_tx || [];
if (values.length > 1) {
const peak = Math.max(...(data.net_rx || []), ...values, 1);
const step = w / (values.length - 1);
ctx.beginPath();
values.forEach((v, i) => {
const x = i * step;
const y = h - (v / peak) * (h - 4) - 2;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = '#00cc33';
ctx.lineWidth = 1.2;
ctx.shadowColor = '#00cc33';
ctx.shadowBlur = 4;
ctx.stroke();
ctx.shadowBlur = 0;
}
}
// Re-render burn with fresh history
if (state.lastOverview) renderBurn(state.lastOverview, data);
}
function applyVisitors(data) {
if (!data) return;
const a = $('tmVisitorsActive'); if (a) a.textContent = fmt.numGrp(data.active);
const r = $('tmVisitorsRpm'); if (r) r.textContent = data.req_per_min;
}
// ─── Live indicator ─────────────────────────────────
function tickLive() {
const secs = state.lastSync ? Math.floor((Date.now() - state.lastSync) / 1000) : 0;
const el = $('tmLastSync');
const dot = $('tmLiveDot');
if (el) el.textContent = state.lastSync ? secs : '—';
if (dot) {
dot.className = 'tm-live-dot ' + (secs < 30 ? '' : secs < 90 ? 'amber' : 'red');
}
}
// ─── Polling loops ──────────────────────────────────
async function pollOverview() {
const d = await fetchJSON(API_BASE + '/overview');
if (d) applyOverview(d);
}
async function pollHistory() {
const d = await fetchJSON(API_BASE + '/history');
if (d) applyHistory(d);
}
async function pollTail() {
const d = await fetchJSON(API_BASE + '/nginx-tail');
if (d) renderTail(d);
}
async function pollGeo() {
const d = await fetchJSON(API_BASE + '/geo');
renderGeo(d || []);
}
async function pollAlerts() {
const d = await fetchJSON(API_BASE + '/alerts');
if (Array.isArray(d)) renderAlerts(d);
}
async function pollVisitors() {
const d = await fetchJSON(API_BASE + '/visitors');
if (d) applyVisitors(d);
}
async function pollCommits() {
const d = await fetchJSON(API_BASE + '/commits');
if (d) renderTicker(d);
}
// ─── Sound toggle ───────────────────────────────────
function bindSoundToggle() {
const btn = $('tmSoundToggle');
if (!btn) return;
btn.addEventListener('click', () => {
state.alarm = !state.alarm;
btn.classList.toggle('on', state.alarm);
btn.textContent = state.alarm ? '🔊 ALARM ON' : '🔇 ALARM OFF';
if (state.alarm) {
// Init audio context on user gesture
if (!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
playBeep(false);
}
});
}
// ─── Boot ──────────────────────────────────────────
function hideBoot() {
const boot = $('tmBoot');
if (boot) setTimeout(() => { boot.classList.add('hidden'); setTimeout(() => boot.remove(), 600); }, 1500);
}
async function init() {
hideBoot();
bindSoundToggle();
// Initial fetches — parallel
await Promise.all([pollOverview(), pollHistory(), pollCommits(), pollGeo(), pollVisitors(), pollAlerts(), pollTail()]);
// Intervals
setInterval(pollOverview, POLL_OVERVIEW);
setInterval(pollHistory, POLL_HISTORY);
setInterval(pollTail, POLL_TAIL);
setInterval(pollGeo, POLL_GEO);
setInterval(pollAlerts, POLL_ALERTS);
setInterval(pollVisitors, POLL_VISITORS);
setInterval(pollCommits, POLL_COMMITS);
setInterval(tickLive, 1000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();