Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions web/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,16 @@ th[data-sort].sorted-desc:after{border-top-color:var(--accent);opacity:1}
.config-form input[name=password],.config-form textarea[name=rule]{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.config-form button:disabled{opacity:.5;cursor:not-allowed}
.config-editor-card{scroll-margin-top:70px}
.client-cmd{display:flex;flex-direction:column;gap:.6rem;border:1px dashed var(--border);border-radius:8px;background:var(--bg);padding:.75rem}
.client-cmd-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem}
.client-cmd-head strong{font-size:13px}
.client-cmd-head .icon-text{width:auto;flex:none}
.client-cmd-addr{display:flex;flex-direction:column;gap:.3rem;color:var(--text-dim);font-size:12px}
.client-cmd-addr input{height:36px;border:1px solid var(--border);background:var(--bg-alt);color:var(--text);border-radius:6px;padding:0 .65rem;outline:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.client-cmd-addr input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent)}
.client-cmd-text{margin:0;padding:.6rem .7rem;border-radius:6px;background:var(--bg-alt);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-all;max-height:220px;overflow-y:auto}
.client-cmd-methods{width:100%}
.client-cmd-methods button{flex:1;padding:0 .4rem;white-space:nowrap}

@media (min-width:981px){
.config-editor-card{position:sticky;top:70px;max-height:calc(100dvh - 86px);overflow-y:auto;scrollbar-gutter:stable}
Expand Down
5 changes: 3 additions & 2 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="description" content="云监控,ServerStatus中文版,ServerStatus,ServerStatus cppla" />
<title>云监控</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="stylesheet" href="css/app.css?v=20260812-2" />
<link rel="stylesheet" href="css/app.css?v=20260812-4" />
</head>
<body>
<header class="topbar">
Expand Down Expand Up @@ -189,6 +189,7 @@ <h2 id="configEditorTitle">新增节点</h2>
<button type="button" id="resetConfigFormBtn" class="icon-text">清空</button>
</div>
</form>
<div id="clientCmd" class="client-cmd" style="display:none;"></div>
</section>
</div>
</section>
Expand All @@ -207,6 +208,6 @@ <h3 id="detailTitle" class="modal-title">节点详情</h3>
<a href="https://github.com/cppla/ServerStatus" target="_blank" rel="noopener">ServerStatus中文版</a>
</footer>

<script src="js/app.js?v=20260812-4" defer></script>
<script src="js/app.js?v=20260812-7" defer></script>
</body>
</html>
117 changes: 117 additions & 0 deletions web/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const S = {
enabled: false,
connected: false,
config: null,
agentAddr: '',
clientServer: '',
clientCmdMethod: 'shell',
selectedType: 'servers',
selectedIndex: -1,
queries: { servers:'', monitors:'', sslcerts:'', watchdog:'' },
Expand Down Expand Up @@ -814,6 +817,7 @@ async function ensureAdminChecked(){
try{
const health = await api('/api/health', { auth:false });
S.admin.enabled = !!health.enabled;
S.admin.agentAddr = health.agent?.address || '';
setAdminStatus(health.enabled ? '管理 API 已启用,输入 token 后可编辑配置。' : '管理 API 未启用:请在容器环境变量设置 ADMIN_TOKEN。', health.enabled ? '' : 'err');
if(S.admin.enabled && S.admin.token) await loadConfig();
}catch(err){
Expand Down Expand Up @@ -919,6 +923,55 @@ function normalizeAdminConfig(config){
});
return normalized;
}
function agentPort(){
const addr = S.admin.agentAddr || '';
const idx = addr.lastIndexOf(':');
const port = idx >= 0 ? addr.slice(idx + 1) : addr;
return /^\d+$/.test(port) ? port : '35601';
}
function defaultClientServer(){
return window.location.hostname || '127.0.0.1';
}
function currentClientServer(){
if(!S.admin.clientServer) S.admin.clientServer = defaultClientServer();
return S.admin.clientServer;
}
function shellSafe(v){
const s = String(v ?? '');
if(!s) return "''";
return /^[A-Za-z0-9_@:./-]+$/.test(s) ? s : "'" + s.replace(/'/g, "'\\''") + "'";
}
function clientInstallCommand(user, pass){
const server = shellSafe(currentClientServer());
const port = shellSafe(agentPort());
const userS = shellSafe(user);
const passS = shellSafe(pass);
const method = S.admin.clientCmdMethod || 'shell';
if(method === 'compose'){
return [
`wget -qO docker-compose-client.yml --header='Accept: application/vnd.github.raw' \\`,
` 'https://api.github.com/repos/cppla/ServerStatus/contents/docker-compose-client.yml?ref=master'`,
`SERVER=${server} PORT=${port} USER=${userS} PASSWORD=${passS} \\`,
` docker compose -f docker-compose-client.yml up -d --force-recreate`,
].join('\n');
}
if(method === 'run'){
return [
`docker run -d --restart=always --name=serverstatus-client \\`,
` --network=host --pid=host \\`,
` -e SERVER=${server} \\`,
` -e PORT=${port} \\`,
` -e USER=${userS} \\`,
` -e PASSWORD=${passS} \\`,
` cppla/serverstatus:client`,
].join('\n');
}
return [
`wget -qO client-linux.py --header='Accept: application/vnd.github.raw' \\`,
` 'https://api.github.com/repos/cppla/ServerStatus/contents/clients/client-linux.py?ref=master'`,
`nohup python3 client-linux.py SERVER=${server} PORT=${port} USER=${userS} PASSWORD=${passS} >/dev/null 2>&1 &`
].join('\n');
}
function activeConfigDef(){
return CONFIG_TYPES[S.admin.selectedType] || CONFIG_TYPES.servers;
}
Expand Down Expand Up @@ -991,6 +1044,37 @@ function renderConfigEditor(item){
resetTrafficBtn.style.display = canResetTraffic ? '' : 'none';
resetTrafficBtn.disabled = !canResetTraffic || S.admin.saving;
$('deleteConfigItemBtn').disabled = !editing;
renderClientCmd();
}
function renderClientCmd(){
const box = $('clientCmd');
if(!box) return;
if(S.admin.selectedType !== 'servers'){ box.style.display = 'none'; return; }
box.style.display = '';
box.innerHTML = [
'<div class="client-cmd-head">',
'<strong>客户端安装命令</strong>',
'<button type="button" class="icon-text" id="copyClientCmdBtn" title="复制到剪贴板">复制</button>',
'</div>',
'<div class="segmented client-cmd-methods" id="clientCmdMethods" role="group" aria-label="安装方式">',
'<button type="button" data-method="shell" class="active">Shell</button>',
'<button type="button" data-method="compose">Docker Compose</button>',
'<button type="button" data-method="run">Docker Run</button>',
'</div>',
'<label class="client-cmd-addr"><span>服务端地址(客户端 SERVER)</span><input id="clientCmdServer" type="text" spellcheck="false" placeholder="服务器 IP 或域名" /></label>',
'<pre class="client-cmd-text" id="clientCmdText"></pre>'
].join('');
$('clientCmdServer').value = currentClientServer();
document.querySelectorAll('#clientCmdMethods [data-method]').forEach(btn => btn.classList.toggle('active', btn.dataset.method === (S.admin.clientCmdMethod || 'shell')));
refreshClientCmd();
}
function refreshClientCmd(){
const textEl = $('clientCmdText');
if(!textEl) return;
const form = $('configForm').elements;
const user = form.username ? form.username.value.trim() : '';
const pass = form.password ? form.password.value : '';
textEl.textContent = clientInstallCommand(user, pass);
}
function fieldHTML(field, item){
const value = item[field.name] ?? field.default ?? '';
Expand Down Expand Up @@ -1118,6 +1202,39 @@ function bindAdmin(){
});
$('resetConfigFormBtn').addEventListener('click', clearConfigForm);
$('resetTrafficBtn').addEventListener('click', () => resetServerTraffic(S.admin.selectedIndex));
$('clientCmd').addEventListener('input', e => {
if(e.target.id === 'clientCmdServer'){
S.admin.clientServer = e.target.value.trim();
refreshClientCmd();
}
});
$('clientCmd').addEventListener('click', async e => {
const methodBtn = e.target.closest('#clientCmdMethods [data-method]');
if(methodBtn){
S.admin.clientCmdMethod = methodBtn.dataset.method;
document.querySelectorAll('#clientCmdMethods [data-method]').forEach(b => b.classList.toggle('active', b === methodBtn));
refreshClientCmd();
return;
}
const btn = e.target.closest('#copyClientCmdBtn');
if(!btn) return;
const text = $('clientCmdText')?.textContent || '';
if(!text) return;
try{
await navigator.clipboard.writeText(text);
}catch(_err){
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
}
const prev = btn.textContent;
btn.textContent = '已复制';
setTimeout(() => { btn.textContent = prev; }, 1200);
});
$('configForm').addEventListener('input', () => { if(S.admin.selectedType === 'servers') refreshClientCmd(); });
$('adminReload').addEventListener('click', async () => {
try{ await api('/api/reload', { method:'POST' }); setAdminStatus('配置重载已触发。', 'ok'); }
catch(err){ setAdminStatus('重载失败:' + err.message, 'err'); }
Expand Down