${degraded.length} 个节点异常:${degraded.map((n) => esc(n.name)).join('、')}。到「转发与节点」查看并修复。
` : ''}
+
- | ${esc(server.name)}${esc(server.publicAddress)}${server.publicAddressV6 ? ` · ${esc(server.publicAddressV6)}` : ''} | ${esc(roleText[server.role] || server.role)} | ${esc(server.region || '未分组')} |
- ${tags(server.labels)} | ${status(server.status)}${server.lastSeenAt ? `最后上报 ${fmtDate(server.lastSeenAt)}` : '等待 Agent 注册'} · Agent ${esc(server.agentVersion || '未上报版本')}${resources(server.system)}${server.tlsDomain ? `节点证书 ${esc(server.tlsDomain)} · ${server.engine?.certificates?.includes(server.tlsDomain) ? '已确认' : '待确认'}` : ''}${server.role !== 'exit' && server.engine ? `AnyTLS 引擎 · ${server.engine.singBoxInstalled ? '已安装' : '按需安装(ng-agent engine install)'}` : ''}${oldUsageAgent(server.agentVersion) ? '旧版流量采集有问题,请在该机运行 ng-agent-update' : ''}${server.usage ? `流量统计 ${fmtDate(server.usage.lastReportAt)} · ${server.usage.sampleCount} 项` : '尚无流量统计上报'}${server.usage?.error ? `统计异常:${esc(server.usage.error)}` : ''}${server.engine && server.engine.status === 'error' ? `引擎故障:${esc(server.engine.detail)}` : ''}${server.pendingCleanup ? `待清理 ${server.pendingCleanup} 项` : ''} |
- ${rowActions(server.id, ``, [
- ['agent-uninstall','SSH 卸载'], ...(server.pendingCleanup ? [['retry-cleanup','重试清理']] : []),
- server.pendingCleanup && server.status !== 'online' ? ['forget-server','遗忘离线设备'] : ['delete-server','删除设备']
- ])} |
`).join('');
- return `统一管理入口转发机、出口落地机与单机节点。Agent 主动连接控制面,不保存设备 SSH 密码;重装 Agent 会自动对账并恢复已有资源。
-
+ |
+ |
+ |
+ |
+ ${esc(server.name)} |
+ ${addr} |
+ ${server.status === 'online' ? '在线' : '离线'}${server.pendingCleanup ? ` ${ICONS.warn}` : ''} |
+ ${cpuPct == null ? '—' : `${cpuPct.toFixed(1)}%`} |
+ ${memPct == null ? '—' : `${memPct.toFixed(1)}%`} |
+ ${esc(server.agentVersion || '—')}${server.agentVersion && versionLessThan(server.agentVersion, state.version) ? '有可用更新' : ''} |
+ — |
+ ${system.uptimeSeconds ? fmtUptime(system.uptimeSeconds) : '—'} |
+ ${ICONS.users}${clients.total}${clients.online} |
+ — |
+ ${server.lastSeenAt ? relTime(server.lastSeenAt) : '等待注册'} |
`;
+ return expanded ? row + serverDetailRow(server) : row;
+ }).join('');
+ return `${stats}
+ | ${esc(item.name)}${esc(item.group || '未分组')} | ${status(item.status)}${item.status === 'suspended' ? `${esc(({ traffic_limit:'额度耗尽', expired:'已到期', ip_limit:'节点 IP 超限', manual:'手动停用' })[item.suspendReason] || '待恢复')}` : ''} |
- ${fmtBytes(item.usedBytes)} / ${item.trafficLimitBytes ? fmtBytes(item.trafficLimitBytes) : '不限'}上行 ${fmtBytes(item.usedUplinkBytes)} · 下行 ${fmtBytes(item.usedDownlinkBytes)} ${item.lastUsageAt ? `最后计量 ${fmtDate(item.lastUsageAt)}` : '尚未收到入口流量统计'} |
- ${item.expiresAt ? fmtDate(item.expiresAt) : '不限期'} | 节点 IP ${item.observedIpCount || 0} / ${item.ipLimit || '不限'}订阅客户端约 ${item.subscriptionClientCount || 0} / ${item.deviceLimit || '不限'} | ${tags(item.tags)} |
- ${rowActions(item.id, ``, [['assign-nodes','分配节点'],['reset-usage','流量清零'],['toggle-customer',item.status === 'active' ? '停用' : '启用'],['delete-customer','删除客户']])} |
`;
+ const groups = [...new Set(state.customers.map((c) => c.group).filter(Boolean))];
+ let filtered = state.customers.filter((item) => {
+ if (state.groupFilter && item.group !== state.groupFilter) return false;
+ if (state.customerNodeFilter && !state.nodeClients.some((nc) => nc.customerId === item.id && nc.nodeId === state.customerNodeFilter)) return false;
+ return [item.name, item.group, item.notes, ...(item.tags || [])].join(' ').toLowerCase().includes(q);
+ });
+ const { pageItems } = pageSlice(filtered, 'customers');
+ const filterNode = state.customerNodeFilter ? nodeById(state.customerNodeFilter) : null;
+ const rows = pageItems.map((item) => {
+ const bindings = customerBindings(item.id);
+ const first = bindings[0];
+ const firstNode = first ? nodeById(first.nodeId) : null;
+ const fam = firstNode ? protocolFamily(firstNode.protocol) : null;
+ const speedTotal = (item.speedUpBps || 0) + (item.speedDownBps || 0);
+ const manualOff = item.status === 'suspended' && item.suspendReason === 'manual';
+ return `流量按入口单向累计,Agent 约每分钟上报,按客户跨节点汇总。额度、到期或节点 IP 触发暂停后,调整限制会自动恢复;待 Agent 完成恢复任务,订阅才可更新。
-