// Real fleet — Sitnov monitoring // Groups: Websites, Lovable.dev, Cloudflare, Dgx-spark, Godento servers // Initial group list — used to seed the `makeNodes` demo data. Real groups // at runtime come from `deriveGroups(nodes)` so that newly-added groups // (via the admin panel) show up immediately in the sidebar / 3D / heatmap. const GROUPS = ['Websites', 'Lovable', 'Cloudflare', 'Dgx-spark', 'Godento']; function deriveGroups(nodes) { // Preserve first-seen order; fall back to the hard-coded default when the // node list is empty (before the first API poll). if (!nodes || !nodes.length) return GROUPS.slice(); const seen = new Set(); const out = []; for (const n of nodes) { if (n.group && !seen.has(n.group)) { seen.add(n.group); out.push(n.group); } } return out; } const SERVER_LIST = [ // Websites { url: 'https://citydent.bg', group: 'Websites' }, { url: 'https://godento.com', group: 'Websites' }, { url: 'https://smiledento.com', group: 'Websites' }, { url: 'https://app.godento.com/booking/', group: 'Websites' }, { url: 'https://smileon46.com', group: 'Websites' }, // Lovable.dev { url: 'https://go.gotrelu.com', group: 'Lovable' }, { url: 'https://smile.sitnov.work', group: 'Lovable' }, { url: 'https://nsitnov.com', group: 'Lovable' }, { url: 'https://drsitnov.com', group: 'Lovable' }, { url: 'https://citydentesthetics.com', group: 'Lovable' }, { url: 'https://timeoff.sitnov.work', group: 'Lovable' }, { url: 'https://xray-go.sitnov.work', group: 'Lovable' }, { url: 'https://myrestart.live', group: 'Lovable' }, { url: 'https://coveandpinestays.com', group: 'Lovable' }, { url: 'https://nutrisunny.com', group: 'Lovable' }, { url: 'https://scan-wise.app', group: 'Lovable' }, { url: 'https://sqlgodento.com', group: 'Lovable' }, { url: 'https://sqlgodento.dev', group: 'Lovable' }, { url: 'https://camps-sports.com/', group: 'Lovable' }, // Cloudflare { url: 'https://api.sitnov.work/dashboard', group: 'Cloudflare' }, { url: 'https://sitsms.sitnov.work', group: 'Cloudflare' }, { url: 'https://godento.sitnov.work', group: 'Cloudflare' }, { url: 'https://drugs.sitnov.work/', group: 'Cloudflare' }, { url: 'https://face.sitnov.work/', group: 'Cloudflare' }, { url: 'https://coworking.sitnov.work', group: 'Cloudflare' }, { url: 'https://dicompressor.sitnov.work', group: 'Cloudflare' }, { url: 'https://aprilvision.sitnov.work/training.html', group: 'Cloudflare' }, { url: 'https://agentdento.com', group: 'Cloudflare' }, { url: 'https://askdento.com', group: 'Cloudflare' }, { url: 'https://agent.askdento.com/', group: 'Cloudflare' }, { url: 'https://boats.askdento.com/', group: 'Cloudflare' }, { url: 'https://dc8n8n1.askdento.com/sse', group: 'Cloudflare' }, { url: 'https://dukovi.askdento.com/', group: 'Cloudflare' }, { url: 'https://citydent2.askdento.com/', group: 'Cloudflare' }, { url: 'https://n8nmcp.askdento.com', group: 'Cloudflare' }, { url: 'https://invoices.askdento.com/', group: 'Cloudflare' }, { url: 'https://dc8.askdento.com/', group: 'Cloudflare' }, // Dgx-spark { url: 'https://amd.sitnov.work', group: 'Dgx-spark' }, { url: 'https://bushkalova-api.sitnov.work', group: 'Dgx-spark' }, { url: 'https://librechat.sitnov.work/login', group: 'Dgx-spark' }, { url: 'https://macstudioinferencer.sitnov.work', group: 'Dgx-spark' }, { url: 'https://macstudiolm.sitnov.work', group: 'Dgx-spark' }, { url: 'https://storage.sitnov.work', group: 'Dgx-spark' }, { url: 'https://xray.sitnov.work', group: 'Dgx-spark' }, { url: 'https://godento.cloudflareaccess.com/', group: 'Dgx-spark' }, // Godento servers (with ports) { url: 'https://enter.godento.com:8077/', group: 'Godento' }, { url: 'https://eodent.godento.com:8050/', group: 'Godento' }, { url: 'https://enter3.godento.com:7078/', group: 'Godento' }, { url: 'https://enter4.godento.com:6030/', group: 'Godento' }, { url: 'https://enter6.godento.com:6530/', group: 'Godento' }, { url: 'https://enter7.godento.com:6046/', group: 'Godento' }, { url: 'https://enter9.godento.com:7032/', group: 'Godento' }, { url: 'https://dc8.godento.com:7040/', group: 'Godento' }, { url: 'https://enter8.godento.com:7103/', group: 'Godento' }, ]; // seeded PRNG function mulberry32(a) { return function() { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const rand = mulberry32(7); function parseUrl(raw) { try { const u = new URL(raw); return { protocol: u.protocol.replace(':',''), host: u.host, // incl. port hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? '443' : '80'), path: (u.pathname + (u.search || '')) || '/', }; } catch (e) { return { protocol: 'https', host: raw, hostname: raw, port: '443', path: '/' }; } } function makeNodes() { const nodes = []; SERVER_LIST.forEach((entry, i) => { const p = parseUrl(entry.url); // deterministic latency band per group const groupLat = { Websites: 85, Lovable: 120, Cloudflare: 45, 'Dgx-spark': 180, Godento: 95 }[entry.group] || 100; const jitter = (rand() - 0.5) * groupLat * 0.6; const latency = Math.max(15, groupLat + jitter); const uptime = rand() * 60 * 24 * 180; // up to 180 days nodes.push({ id: `n-${String(i + 1).padStart(3, '0')}`, url: entry.url, hostname: p.host, displayName: p.hostname.replace(/^www\./, ''), path: p.path, protocol: p.protocol, port: p.port, group: entry.group, service: entry.group.toLowerCase(), role: entry.group === 'Godento' ? 'server' : entry.group === 'Dgx-spark' ? 'compute' : 'web', status: 'ok', httpCode: 200, latency, uptime, // network throughput synthesized from group baselines netIn: 20 + rand() * 80, netOut: 20 + rand() * 80, // ssl sslDaysLeft: Math.floor(20 + rand() * 340), checkedAt: Date.now(), latencyHist: [], netHist: [], }); }); // all endpoints start as healthy — real state would come from live probes // use the "simulate incident" button in Tweaks to demo critical/offline visuals // seed history for (const n of nodes) { for (let i = 0; i < 60; i++) { const base = n.status === 'off' ? 0 : n.latency; n.latencyHist.push(Math.max(0, base + (rand() - 0.5) * base * 0.3)); n.netHist.push(Math.max(0, (n.netIn + n.netOut) + (rand() - 0.5) * 30)); } } return nodes; } function makeAlerts(nodes) { const now = Date.now(); const alerts = []; // SSL warnings — find soonest expiring const soonExpire = [...nodes].sort((a,b) => a.sslDaysLeft - b.sslDaysLeft).slice(0, 2); soonExpire.forEach((n, i) => { if (n.sslDaysLeft < 30) { alerts.push({ id: 'a-ssl-' + i, ts: now - 1000 * 60 * (60 + i * 20), severity: 'warn', node: n.id, title: `TLS cert expires in ${n.sslDaysLeft}d`, body: `${n.hostname} · auto-renew pending`, ack: i > 0 }); } }); if (nodes[3]) alerts.push({ id: 'a-003', ts: now - 1000 * 60 * 18, severity: 'info', node: nodes[3].id, title: 'Probe completed', body: 'all endpoints healthy', ack: true }); if (nodes[31]) alerts.push({ id: 'a-006', ts: now - 1000 * 60 * 131, severity: 'info', node: nodes[31].id, title: 'DNS record changed', body: 'CNAME updated · verified', ack: true }); return alerts.sort((a,b) => b.ts - a.ts); } const LOG_SOURCES = ['probe', 'dns', 'tls', 'http', 'uptime', 'cf-edge', 'agent', 'alertmanager', 'scheduler', 'n8n']; const LOG_TEMPLATES = [ ['I', 'HEAD / → 200 in %dms'], ['I', 'GET /healthz → 200 ok'], ['I', 'TLS handshake ok · expires in %dd'], ['I', 'DNS A → %d.%d.%d.%d (cache hit)'], ['D', 'probe scheduled next in %ds'], ['W', 'HEAD / → 200 slow (%dms > budget)'], ['W', 'retry attempt=%d backoff=%dms'], ['W', 'TLS cert rotation pending'], ['E', 'HEAD / → 503 service unavailable'], ['E', 'connect ETIMEDOUT after %dms'], ['I', 'uptime %dh · %d checks · %d%% success'], ['D', 'traceroute %d hops · edge cf-sof'], ]; function fmtTpl(tpl) { return tpl.replace(/%d/g, () => Math.floor(rand() * 999)); } function seedLogs(nodes) { const lines = []; const now = Date.now(); for (let i = 0; i < 80; i++) { const [lvl, tpl] = LOG_TEMPLATES[Math.floor(rand() * LOG_TEMPLATES.length)]; const node = nodes[Math.floor(rand() * nodes.length)]; lines.push({ ts: now - (80 - i) * 1200 - Math.floor(rand() * 400), lvl, src: LOG_SOURCES[Math.floor(rand() * LOG_SOURCES.length)], node: node.id, msg: fmtTpl(tpl), }); } return lines; } function fmtUptime(mins) { const d = Math.floor(mins / (60 * 24)); const h = Math.floor((mins % (60 * 24)) / 60); const m = Math.floor(mins % 60); if (d > 0) return `${d}d ${h}h`; if (h > 0) return `${h}h ${m}m`; return `${m}m`; } function fmtTs(ts) { const d = new Date(ts); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; } function fmtRel(ts) { const s = Math.floor((Date.now() - ts) / 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`; } // Live probe API client — backend agent runs HEAD requests on an interval async function fetchProbeNodes() { const res = await fetch('/api/nodes', { cache: 'no-store' }); if (!res.ok) throw new Error(`probe api ${res.status}`); return res.json(); } async function triggerProbe(id) { const res = await fetch(`/api/probe/${encodeURIComponent(id)}`, { method: 'POST' }); if (!res.ok) throw new Error(`probe api ${res.status}`); return res.json(); } Object.assign(window, { GROUPS, SERVER_LIST, deriveGroups, makeNodes, makeAlerts, seedLogs, LOG_SOURCES, LOG_TEMPLATES, fmtTpl, fmtUptime, fmtTs, fmtRel, fetchProbeNodes, triggerProbe, });