feat(panel): reskin backend — node sortIndex, presence/speed, /api/system, credentials, subscription aggregation (Node+Go parity)

This commit is contained in:
aaaamoooo
2026-10-20 18:09:07 +00:00
parent 7c21742810
commit 011eecc096
23 changed files with 3370 additions and 146 deletions
+81
View File
@@ -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,
};