'use strict'; // Presence & speed derivation (panel-go/presence.go mirrors this file). // // Signals (agent protocol unchanged): // - Agent usage reports carry cumulative per-user counters aggregated per // server (NOT per node) and cumulative per-inbound (per node) counters. // A processed sample whose delta is > 0 marks activity: per-server for // the customer (serverUsage slot lastActiveAt + customer.lastOnlineAt), // per-node for the inbound (node.lastActiveAt). // - AnyTLS observations confirm customer activity and also stamp // customer.lastOnlineAt at ingest time. // A customer counts as "online on node N" when they hold an active // binding on N and their last activity on N's server is inside the // window. With several nodes on one server this attributes the activity // to each of them — the reports cannot separate nodes on a server. const PRESENCE_WINDOW_MS = 180000; // 3 minutes const SPEED_STALE_MS = 150000; function parseMs(value) { if (!value) return 0; const ms = Date.parse(value); return Number.isFinite(ms) ? ms : 0; } function customerLastActiveAt(customer) { return customer.lastOnlineAt || null; } function customerOnline(customer, nowMs) { const ms = parseMs(customer.lastOnlineAt); return ms > 0 && nowMs - ms <= PRESENCE_WINDOW_MS; } // Sum of the customer's per-server speeds that are still fresh. function customerSpeedBps(customer, nowMs) { let up = 0; let down = 0; for (const slot of Object.values(customer.serverUsage || {})) { if (!slot || !slot.speedAt) continue; if (nowMs - parseMs(slot.speedAt) > SPEED_STALE_MS) continue; up += Number(slot.speedUpBps || 0); down += Number(slot.speedDownBps || 0); } return { up, down }; } function nodeSpeedBps(node, nowMs) { if (!node.speedAt || nowMs - parseMs(node.speedAt) > SPEED_STALE_MS) return { up: 0, down: 0 }; return { up: Number(node.speedUpBps || 0), down: Number(node.speedDownBps || 0) }; } // Customers (ids) with an active binding on this node whose last // activity on the node's server is inside the presence window. The // per-server usage slot is authoritative; when the customer has never // reported usage from this server, the customer-level lastOnlineAt is // used as a fallback (AnyTLS observations stamp it without creating a // usage slot, and they are validated against this server at ingest). function nodeOnlineCustomerIds(data, node, nowMs) { const out = []; for (const nc of data.nodeClients) { if (nc.nodeId !== node.id || nc.status !== 'active') continue; const customer = data.customers.find((item) => item.id === nc.customerId); if (!customer) continue; const slot = (customer.serverUsage || {})[`srv:${node.serverId}`]; const ms = slot && slot.lastActiveAt ? parseMs(slot.lastActiveAt) : parseMs(customer.lastOnlineAt); if (ms > 0 && nowMs - ms <= PRESENCE_WINDOW_MS) out.push(customer.id); } return out; } function countOnlineCustomers(data, nowMs) { return data.customers.filter((customer) => customerOnline(customer, nowMs)).length; } module.exports = { PRESENCE_WINDOW_MS, SPEED_STALE_MS, parseMs, customerLastActiveAt, customerOnline, customerSpeedBps, nodeSpeedBps, nodeOnlineCustomerIds, countOnlineCustomers, };