diff --git a/panel-go/golden/golden.json b/panel-go/golden/golden.json index 8346b00..0244ca6 100644 --- a/panel-go/golden/golden.json +++ b/panel-go/golden/golden.json @@ -1,6 +1,6 @@ { "meta": { - "capturedAt": "2026-10-20T21:35:58.550Z", + "capturedAt": "2026-10-20T21:46:16.902Z", "note": "Go panel golden baseline; see README.md in this directory." }, "sections": { @@ -3584,7 +3584,7 @@ }, "errors": { "badSubscription": { - "body": "\n\n\n \n \n \n \n amo\n \n \n\n\n
\n
\n
amo
\n
\n \n \n \n
\n

\n \n
\n
\n\n
\n \n\n
\n
\n amo / 系统状态\n \n \n \n \n
\n
\n
\n
\n\n
\n
\n

\n
\n
\n
\n
\n
\n \n \n\n\n", + "body": "\n\n\n \n \n \n \n amo\n \n \n\n\n
\n
\n
amo
\n
\n \n \n \n
\n

\n \n
\n
\n\n
\n \n\n
\n
\n amo / 系统状态\n \n \n \n
\n
\n
\n
\n\n
\n
\n

\n
\n
\n
\n
\n
\n \n \n\n\n", "status": 200 }, "loginFailure": { diff --git a/public/app.js b/public/app.js index 99eb2f9..2341f46 100644 --- a/public/app.js +++ b/public/app.js @@ -1,7 +1,7 @@ 'use strict'; -/* amo 控制台前端。六个页面:系统状态 / 服务器 / 客户与订阅 / 转发与节点 / - 安装与命令 / 设置与运维。hash 路由,45 秒自动刷新(弹窗打开时暂停)。 */ +/* amo 控制台前端。五个页面:系统状态 / 节点管理 / 用户订阅 / 入站&出站 / + 设置。hash 路由,45 秒自动刷新(弹窗打开时暂停)。 */ const state = { session: null, page: 'overview', overview: null, system: null, @@ -62,14 +62,7 @@ const statusText = { }; const roleText = { relay: '入口 / 转发', exit: '出口 / 落地', hybrid: '综合节点' }; const topologyText = { forward: '转发线路', direct: '单机直连' }; -const auditText = { - create_server: '添加设备', update_server: '编辑设备', delete_server: '删除设备', create_enrollment: '生成注册令牌', enroll_agent: 'Agent 注册', - reconcile_server: '设备配置对账', set_tls_domain: '节点证书同步', create_customer: '添加客户', update_customer: '编辑客户', reset_customer_usage: '重置客户流量', delete_customer: '删除客户', - create_node: '创建节点', update_node: '编辑节点', deploy_node: '部署节点', repair_node: '修复节点', remove_node: '停用节点', reorder_nodes: '节点排序', - delete_node: '删除节点', assign_customer: '分配客户', unassign_customer: '移除分配', suspend_customer_resources: '暂停客户资源', resume_customer_resources: '恢复客户资源', - retry_cleanup: '重试遗留清理', force_forget_server: '强制遗忘离线设备', update_account: '修改管理员账号', suspend_ip_limit: 'IP 超限停用', rotate_subscription: '重置订阅链接', - regenerate_credentials: '重新生成凭据', monthly_reset: '按月重置流量' -}; + const suspendReasonText = { traffic_limit: '额度耗尽', expired: '已到期', ip_limit: '节点 IP 超限', manual: '手动停用' }; const ICONS = { @@ -264,8 +257,7 @@ async function load(page = state.page) { const [servers, customers, routes, protocols] = await Promise.all([api('/api/servers'), api('/api/customers'), api('/api/nodes'), api('/api/protocols')]); state.servers = servers.servers; state.customers = customers.customers; state.customerSummary = customers.summary || null; state.nodes = routes.nodes; state.nodeClients = routes.nodeClients; state.protocols = protocols.profiles; state.realityPresets = protocols.realityPresets || []; - } else if (page === 'commands') { /* 静态页 */ } - else if (page === 'operations') { + } else if (page === 'operations') { const [jobs, overview] = await Promise.all([api('/api/jobs'), api('/api/overview')]); state.jobs = jobs.jobs; state.overview = overview; state.version = overview.version || state.version; $('#version-label').textContent = `v${state.version}`; @@ -274,8 +266,8 @@ async function load(page = state.page) { } catch (error) { toast(error.message, true); } } -const PAGES = ['overview', 'servers', 'customers', 'chains', 'commands', 'operations']; -const TITLES = { overview: '系统状态', servers: '服务器', customers: '客户与订阅', chains: '转发与节点', commands: '安装与命令', operations: '设置与运维' }; +const PAGES = ['overview', 'servers', 'customers', 'chains', 'operations']; +const TITLES = { overview: '系统状态', servers: '节点管理', customers: '用户订阅', chains: '入站&出站', operations: '设置' }; function pageFromHash() { const page = location.hash.replace(/^#/, '').split('?')[0]; return PAGES.includes(page) ? page : 'overview'; } function setPage(page) { if (!PAGES.includes(page)) page = 'overview'; @@ -370,7 +362,6 @@ function renderOverview() { const meanUp = history.length ? history.reduce((a, p) => a + (p.upBps || 0), 0) / history.length : 0; const meanDown = history.length ? history.reduce((a, p) => a + (p.downBps || 0), 0) / history.length : 0; const degraded = (state.overview && state.overview.degraded) || []; - const events = ((state.overview && state.overview.activity) || []).map((event) => `
${esc(auditText[event.action] || event.action)} ${esc(event.actor)} · ${esc(event.target)}
`).join(''); return `
${cards}
@@ -401,8 +392,7 @@ function renderOverview() {
IP 地址
IPv4${esc((sys.ips && sys.ips.ipv4) || '—')}
IPv6${esc((sys.ips && sys.ips.ipv6) || '—')}
- ${degraded.length ? `
${degraded.length} 个节点异常:${degraded.map((n) => esc(n.name)).join('、')}。到「转发与节点」查看并修复。
` : ''} -

最近活动

${events || '
暂无活动
'}
`; + ${degraded.length ? `
${degraded.length} 个节点异常:${degraded.map((n) => esc(n.name)).join('、')}。到「入站&出站」查看并修复。
` : ''}`; } /* ---------- 服务器 ---------- */ @@ -488,7 +478,7 @@ function renderServers() { ${rows || emptyRow(14, '没有匹配设备')}${pagerHTML('servers', filtered.length)}`; } -/* ---------- 客户与订阅 ---------- */ +/* ---------- 用户订阅 ---------- */ function protocolFamily(protocol) { const id = String(protocol || ''); if (id.startsWith('vless')) return { cls: '', label: 'VLESS' }; @@ -841,7 +831,7 @@ function uninstallModal() {
`); } -/* ---------- 转发与节点 ---------- */ +/* ---------- 入站&出站 ---------- */ function protoBadges(node) { const fam = (id) => { const v = String(id || ''); @@ -893,9 +883,9 @@ function renderNodes() { ${speed > 0 ? `↑ ${fmtSpeed(node.speedUpBps)} / ↓ ${fmtSpeed(node.speedDownBps)}` : '—'} ∞`; }).join(''); - return `
+ return `
- ${state.nodes.length} 个节点在线 ${active}${degraded ? `降级 ${degraded}` : ''}
+ ${state.nodes.length} 个入站在线 ${active}${degraded ? `降级 ${degraded}` : ''}
${rows || emptyRow(13, '还没有节点')}
ID⇅菜单启用备注节点端口⇅协议客户端流量⇅速度到期时间
${pagerHTML('nodes', filtered.length)}
`; } @@ -916,7 +906,7 @@ function nodeForm(item = null) {
出口与转发传输
-
+
@@ -1036,8 +1026,8 @@ function nodeMenuHTML(node) { return items.join(''); } -/* ---------- 安装与命令 ---------- */ -function renderCommands() { +/* ---------- 设置(命令速查已合并至此页) ---------- */ +function commandsHTML() { const groups = [ ['面板机', [['更新控制面', 'ng update'], ['检查面板 HTTPS', 'ng cert'], ['备份面板', 'ng backup']]], ['入口机 / 中转机', [['更新 Agent', 'ng-agent update'], ['申请或续签节点证书', 'ng-agent cert'], ['安装 sing-box 引擎', 'ng-agent engine install'], ['检查证书、引擎与双向计数', 'ng-agent doctor']]], @@ -1046,7 +1036,6 @@ function renderCommands() { return `
${groups.map(([title, commands]) => `

${esc(title)}

${commands.map(([label, command]) => `
${esc(label)}${esc(command)}
`).join('')}
`).join('')}
`; } -/* ---------- 设置与运维 ---------- */ function renderOperations() { const rows = state.jobs.slice(0, 100).map((job) => `${esc(job.action === 'apply_resource' ? '应用资源' : '移除资源')}
${esc(job.id)}
${esc((serverById(job.serverId) || {}).name || job.serverId)}${esc(statusText[job.status] || job.status)}${job.attempts}${fmtDate(job.updatedAt)}${esc(job.error || '')}`).join(''); return `
@@ -1065,11 +1054,12 @@ function renderOperations() {
Caddy HTTPS证书自动申请与续签
运行中
ng update\nng domain new.example.com\nng cert
-

任务队列

${rows || emptyRow(6, '任务队列为空')}
任务设备状态尝试更新时间错误
`; + ${commandsHTML()} +

任务队列

${rows || emptyRow(6, '任务队列为空')}
任务设备状态尝试更新时间错误
`; } function render() { - const views = { overview: renderOverview, servers: renderServers, customers: renderCustomers, chains: renderNodes, commands: renderCommands, operations: renderOperations }; + const views = { overview: renderOverview, servers: renderServers, customers: renderCustomers, chains: renderNodes, operations: renderOperations }; $('#content').innerHTML = views[state.page](); } diff --git a/public/index.html b/public/index.html index 9797592..2bb09a0 100644 --- a/public/index.html +++ b/public/index.html @@ -28,11 +28,10 @@
amo
@@ -41,7 +40,6 @@
amo / 系统状态 -
diff --git a/public/styles.css b/public/styles.css index 49f1304..7ba8715 100644 --- a/public/styles.css +++ b/public/styles.css @@ -65,7 +65,6 @@ input,select,textarea{font-family:var(--font);color:var(--text)} .crumb{color:var(--text-3);font-size:13px;white-space:nowrap} .crumb b{color:var(--text);font-weight:600;font-size:15px} .topbar .sp{flex:1} -.top-search{background:var(--card);border:1px solid var(--border);border-radius:6px;color:var(--text-2);padding:5px 12px;font-size:13px;width:230px} .theme-btn{border:1px solid var(--border);background:var(--card);color:var(--text-2);border-radius:6px;padding:5px 11px;font-size:12.5px;cursor:pointer;white-space:nowrap} .theme-btn:hover{border-color:var(--primary);color:var(--primary-text)} .content{padding:24px;width:100%} @@ -137,13 +136,6 @@ select.search{width:auto} .stop-up-a{stop-color:var(--primary);stop-opacity:.26} .stop-up-b{stop-color:var(--primary);stop-opacity:0} .notice{border:1px solid var(--orange);background:var(--orange-soft);color:var(--orange-text);border-radius:6px;padding:8px 12px;font-size:13px;margin-bottom:16px} -.activity-list{display:flex;flex-direction:column} -.activity-list .event{display:flex;align-items:center;gap:10px;padding:7px 16px;border-bottom:1px solid var(--border-soft);font-size:13px} -.activity-list .event:last-child{border-bottom:none} -.activity-list .event b{font-weight:500} -.activity-list .event small{color:var(--text-3)} -.activity-list .event time{margin-left:auto;color:var(--text-3);font-size:12px;white-space:nowrap} -.event-dot{width:7px;height:7px;border-radius:50%;background:var(--primary);flex:none} /* ---------- 表格通用 ---------- */ .table-wrap{overflow-x:auto} @@ -152,6 +144,9 @@ table{width:100%;border-collapse:collapse} table.w-servers{min-width:1390px} table.w-customers{min-width:1120px} table.w-nodes{min-width:1180px} +table.w-jobs td{white-space:nowrap;padding-top:5px;padding-bottom:5px} +table.w-jobs th:last-child{width:22%} +table.w-jobs td:last-child{white-space:normal;min-width:120px;word-break:break-word} thead th{text-align:left;font-size:12.5px;font-weight:600;color:var(--text-2);letter-spacing:.3px;padding:7px 12px;border-bottom:1px solid var(--border-soft);background:var(--th-bg);white-space:nowrap} thead th .sarr{color:var(--text-3);font-size:10px;margin-left:3px} thead th.ctr,tbody td.ctr{text-align:center} @@ -322,9 +317,9 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri .mono{font-family:var(--mono);font-size:12.5px;word-break:break-all} /* 页签 */ -.seg{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden} -.seg button{padding:6px 16px;background:var(--card);border:none;color:var(--text-2);cursor:pointer;font:inherit;font-size:13px} -.seg button.active{background:var(--primary);color:#fff} +.segctl{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden} +.segctl button{padding:6px 16px;background:var(--card);border:none;color:var(--text-2);cursor:pointer;font:inherit;font-size:13px} +.segctl button.active{background:var(--primary);color:#fff} .tabs{display:flex;gap:2px;border-bottom:1px solid var(--border-soft);margin-bottom:14px} .tabs button{border:none;background:transparent;padding:7px 14px;font-size:13.5px;color:var(--text-2);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px} .tabs button.active{color:var(--primary-text);border-bottom-color:var(--primary);font-weight:500} @@ -379,7 +374,8 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri .warn-note{border:1px solid var(--orange);background:var(--orange-soft);color:var(--orange-text);border-radius:6px;padding:8px 12px;font-size:13px} /* 命令页 / 运维页 */ -.command-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px} +.command-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;margin-bottom:16px} +.command-grid .panel{margin-bottom:0} .command-item{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border-soft)} .command-item:last-child{border-bottom:none} .command-item>div{flex:1;min-width:0} @@ -406,4 +402,4 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri .toast.error{border-left-color:var(--red)} @media (max-width:1100px){.stat-grid{grid-template-columns:repeat(2,1fr)}.mid-grid{grid-template-columns:1fr}.info-bar{flex-direction:column;gap:14px}.info-bar .seg{padding:0}.info-bar .seg+.seg{border-left:none}.stats{grid-template-columns:repeat(2,1fr)}.stats.six{grid-template-columns:repeat(3,1fr)}} -@media (max-width:760px){.sider{display:none}.form-grid{grid-template-columns:1fr}.top-search{display:none}.content{padding:8px}} +@media (max-width:760px){.sider{display:none}.form-grid{grid-template-columns:1fr}.content{padding:8px}}