feat(panel): reskin backend — node sortIndex, presence/speed, /api/system, credentials, subscription aggregation (Node+Go parity)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
'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,
|
||||
};
|
||||
Reference in New Issue
Block a user