Files
RustProxyAdmin/templates/admin/nodes.html
T
2026-09-30 16:11:51 +08:00

136 lines
6.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}节点管理 - RustProxyAdmin{% endblock %}
{% block page_title %}节点管理{% endblock %}
{% block page_desc %}服务器节点管理与 Worker 配对{% endblock %}
{% block content %}
<div class="toolbar">
<button class="btn" onclick="showAdd()">+ 添加节点</button>
<span class="muted" style="font-size:12px">本机节点不可删除;Worker token 仅在创建/重置时显示一次</span>
</div>
<div class="card"><table class="tbl" id="tbl">
<thead><tr><th>ID</th><th>名称</th><th>类型</th><th>公网地址</th><th>状态</th><th>最近心跳</th><th>版本</th><th>负载 CPU/内存</th><th>操作</th></tr></thead>
<tbody id="rows"></tbody>
</table></div>
<div id="modal" class="modal" style="display:none"><div class="modal-box">
<h3 id="mTitle">添加节点</h3>
<div id="mBody"></div>
<div style="margin-top:12px;text-align:right">
<button class="btn ghost" onclick="hideModal()">关闭</button>
<button class="btn" id="mOk" onclick="saveNode()">保存</button>
</div>
</div></div>
<div id="cmdModal" class="modal" style="display:none"><div class="modal-box">
<h3>Worker 启动命令</h3>
<p class="muted" style="font-size:12px" id="cmdNote"></p>
<pre id="cmdPre" style="white-space:pre-wrap;word-break:break-all;background:#0d1117;padding:12px;border-radius:6px"></pre>
<div style="margin-top:12px;text-align:right">
<button class="btn" onclick="copyCmd()">复制</button>
<button class="btn ghost" onclick="document.getElementById('cmdModal').style.display='none'">我已保存</button>
</div>
</div></div>
<script>
let editing = null;
async function load() {
const [m, n2] = await Promise.all([api('GET', '/api/monitor/nodes'), api('GET', '/api/nodes')]);
const extra = {};
for (const n of n2.items) extra[n.id] = n;
const tb = document.getElementById('rows');
tb.innerHTML = '';
for (const n of m.items) {
const host = (extra[n.id] || {}).public_host || '';
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${n.id}</td>
<td>${esc(n.name)}</td>
<td>${n.is_local ? '<span class="badge">本机</span>' : 'Worker'}</td>
<td class="muted">${esc(host || '—')}</td>
<td>${n.online ? '<span class="badge ok">在线</span>' : '<span class="badge danger">离线</span>'}</td>
<td class="muted" style="font-size:12px">${esc(n.last_heartbeat_at || (n.is_local ? '本机' : '无'))}</td>
<td class="muted">${esc(n.version || '—')}</td>
<td class="muted" style="font-size:12px">${n.cpu != null ? n.cpu.toFixed(1) + '% / ' + n.mem.toFixed(1) + '%' : '—'}</td>
<td class="ops"></td>`;
const ops = tr.querySelector('.ops');
if (!n.is_local) {
const b1 = document.createElement('button'); b1.className = 'btn sm'; b1.textContent = '编辑';
b1.onclick = () => showEdit(n.id); ops.appendChild(b1);
const b2 = document.createElement('button'); b2.className = 'btn sm warn'; b2.textContent = '重置 Token';
b2.onclick = () => resetToken(n.id); ops.appendChild(b2);
const b3 = document.createElement('button'); b3.className = 'btn sm danger'; b3.textContent = '删除';
b3.onclick = () => delNode(n.id); ops.appendChild(b3);
}
tb.appendChild(tr);
}
}
function hideModal() { document.getElementById('modal').style.display = 'none'; }
function showAdd() {
editing = null;
document.getElementById('mTitle').textContent = '添加节点';
document.getElementById('mBody').innerHTML = `
<label>节点名称<input id="fName" class="inp" maxlength="64" placeholder="如 worker-tokyo"></label>
<label>公网地址(可选)<input id="fHost" class="inp" placeholder="如 203.0.113.10"></label>
<label>Master 公网地址<input id="fMaster" class="inp" placeholder="如 https://admin.example.com"></label>
<label>备注(可选)<input id="fMeta" class="inp"></label>`;
document.getElementById('modal').style.display = 'block';
}
async function showEdit(id) {
const d = await api('GET', '/api/nodes');
const n = d.items.find(x => x.id === id);
if (!n) return;
editing = id;
document.getElementById('mTitle').textContent = '编辑节点 #' + id;
document.getElementById('mBody').innerHTML = `
<label>节点名称<input id="fName" class="inp" maxlength="64" value="${esc(n.name)}"></label>
<label>公网地址<input id="fHost" class="inp" value="${esc(n.public_host || '')}"></label>
<label>备注<input id="fMeta" class="inp" value="${esc(n.meta || '')}"></label>
<label>启用<select id="fEnabled" class="inp"><option value="1"${n.enabled ? ' selected' : ''}>启用</option><option value="0"${n.enabled ? '' : ' selected'}>停用</option></select></label>`;
document.getElementById('modal').style.display = 'block';
}
async function saveNode() {
const name = document.getElementById('fName').value.trim();
if (!name) { alert('名称不能为空'); return; }
if (editing == null) {
const d = await api('POST', '/api/nodes', {
name, public_host: document.getElementById('fHost').value.trim() || null,
meta: document.getElementById('fMeta').value.trim() || null,
master_url: document.getElementById('fMaster').value.trim() || null,
});
hideModal();
showCmd(d.worker_cmd, d.note);
} else {
await api('PUT', '/api/nodes/' + editing, {
name, public_host: document.getElementById('fHost').value.trim(),
meta: document.getElementById('fMeta').value.trim(),
enabled: parseInt(document.getElementById('fEnabled').value),
});
hideModal();
}
load();
}
async function resetToken(id) {
if (!confirm('重置后旧 token 立即失效,Worker 需用新命令重启。继续?')) return;
const master = prompt('Master 公网地址(用于生成启动命令)', '');
const d = await api('POST', '/api/nodes/' + id + '/reset_token', { master_url: master || null });
showCmd(d.worker_cmd, d.note);
load();
}
async function delNode(id) {
if (!confirm('删除节点 #' + id + '?该节点下的条目配置不会被删除。')) return;
await api('DELETE', '/api/nodes/' + id);
load();
}
function showCmd(cmd, note) {
document.getElementById('cmdPre').textContent = cmd;
document.getElementById('cmdNote').textContent = note || '';
document.getElementById('cmdModal').style.display = 'block';
}
function copyCmd() {
navigator.clipboard.writeText(document.getElementById('cmdPre').textContent);
alert('已复制');
}
load();
</script>
{% endblock %}