From b9a43e78ee34cdf4d8a36bac57cfe84c4648a7ea Mon Sep 17 00:00:00 2001 From: kory Date: Mon, 28 Sep 2026 08:45:08 +0800 Subject: [PATCH] style(web): remove theme button on login page and add project brand logo to header --- internal/web/static/app.css | 12 +++++++++++- internal/web/static/app.js | 13 +------------ internal/web/static/icon.png | Bin 0 -> 2768 bytes internal/web/static/index.html | 14 +------------- internal/web/static/login.html | 2 +- web/static/app.css | 12 +++++++++++- web/static/app.js | 13 +------------ web/static/icon.png | Bin 0 -> 2768 bytes web/static/index.html | 14 +------------- web/static/login.html | 2 +- 10 files changed, 28 insertions(+), 54 deletions(-) create mode 100644 internal/web/static/icon.png create mode 100644 web/static/icon.png diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 9c7fca4..05e7ac3 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -1 +1,11 @@ -*{box-sizing:border-box}body{margin:0;background:#0b1020;color:#e7ebf3;font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}header{height:64px;padding:0 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #202a3d;background:#10182b;position:sticky;top:0;z-index:2}header strong{font-size:18px}header span{margin-left:12px;color:#7f8ba3;font-size:12px}main{max-width:1400px;margin:auto;padding:22px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.wide{grid-column:1/-1}.card{background:#111a2d;border:1px solid #24304a;border-radius:14px;padding:18px;box-shadow:0 8px 30px #0003}.cardhead{display:flex;align-items:center;justify-content:space-between}h2{margin:0 0 14px;font-size:16px}button,select,input,textarea{border:1px solid #34415d;border-radius:9px;background:#0d1526;color:#e7ebf3;padding:9px 12px}button{cursor:pointer}button:hover{background:#17233a}textarea{width:100%;height:110px;resize:vertical;font-family:ui-monospace,monospace;margin:8px 0}pre{white-space:pre-wrap;overflow:auto;background:#090e1a;border-radius:9px;padding:12px;color:#b9c5da}.devices{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}.device{padding:14px;border:1px solid #283650;border-radius:10px}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#4ade80;margin-right:7px}.offline .dot{background:#f87171}.muted{color:#7f8ba3}.tablewrap{overflow:auto}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:9px;border-bottom:1px solid #26324a;font-size:12px}th{color:#91a0bb}.login{display:grid;place-items:center;min-height:100vh}.login form{width:min(380px,90vw);background:#111a2d;border:1px solid #24304a;border-radius:16px;padding:28px}.login input,.login button{width:100%;margin-top:10px}.login h1{margin:0}.login p,#error{color:#8c9ab2}.status{font-size:12px;color:#9aabc5}@media(max-width:800px){.grid{grid-template-columns:1fr}.wide{grid-column:auto}main{padding:12px}header{padding:0 14px}} \ No newline at end of file +*{box-sizing:border-box} +:root{color-scheme:dark;--bg-body:#0b1020;--bg-card:#111a2d;--bg-sub:#0d1526;--border:#24304a;--border-strong:#34415d;--text-main:#e7ebf3;--text-muted:#7f8ba3;--text-status:#9aabc5;--pre-text:#b9c5da;--thead:#91a0bb;--dot-online:#4ade80;--dot-offline:#f87171;--shadow:0 8px 30px rgba(0,0,0,.2)} +:root[data-theme="light"]{color-scheme:light;--bg-body:#f4f6fa;--bg-card:#fff;--bg-sub:#eef2f7;--border:#d1d9e4;--border-strong:#b8c3d3;--text-main:#172033;--text-muted:#647089;--text-status:#58657a;--pre-text:#26344b;--thead:#53627a;--dot-online:#16833d;--dot-offline:#c62828;--shadow:0 8px 26px rgba(28,45,72,.08)} +html,body{min-height:100%}body{margin:0;background:var(--bg-body);color:var(--text-main);font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}header{min-height:64px;padding:0 24px;display:flex;align-items:center;justify-content:space-between;gap:14px;border-bottom:1px solid var(--border);background:var(--bg-card);position:sticky;top:0;z-index:2} +.brand-wrap{display:flex;align-items:center;gap:12px;min-width:0} +.brand-logo{width:36px;height:36px;border-radius:8px;object-fit:cover;flex:none;box-shadow:0 2px 8px rgba(0,0,0,.15)} +header strong{font-size:18px}header span{margin-left:12px;color:var(--text-muted);font-size:12px}.header-actions{display:flex;align-items:center;gap:8px;flex-shrink:0}main{max-width:1400px;margin:auto;padding:22px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.wide{grid-column:1/-1}.card{min-width:0;background:var(--bg-card);border:1px solid var(--border);border-radius:14px;padding:18px;box-shadow:var(--shadow)}.cardhead{display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0}.cardhead>h2{min-width:0}h2{margin:0 0 14px;font-size:16px}button,select,input,textarea{border:1px solid var(--border-strong);border-radius:9px;background:var(--bg-sub);color:var(--text-main);padding:9px 12px;font:inherit;max-width:100%}button{cursor:pointer}button:hover{filter:brightness(1.05)}button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--border-strong);outline-offset:2px}textarea{width:100%;height:110px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;margin:8px 0}pre{white-space:pre-wrap;overflow:auto;background:var(--bg-sub);border:1px solid var(--border);border-radius:9px;padding:12px;color:var(--pre-text)}.devices{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}.device{min-width:0;padding:14px;border:1px solid var(--border);border-radius:10px;background:var(--bg-sub)}.device b{display:block;min-width:0;overflow-wrap:anywhere;word-break:break-all}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--dot-online);vertical-align:middle;flex:none;margin-right:7px}.offline .dot{background:var(--dot-offline)}.muted{color:var(--text-muted)}.status{margin-top:3px;font-size:12px;color:var(--text-status);overflow-wrap:anywhere;word-break:break-word}.actions{display:flex;flex-wrap:wrap;gap:8px}.actions button{flex:1 1 150px}#device{min-width:180px}.tablewrap{width:100%;max-width:100%;overflow-x:auto;overscroll-behavior-x:contain}table{width:100%;min-width:760px;border-collapse:collapse}th,td{text-align:left;padding:9px;border-bottom:1px solid var(--border);font-size:12px;white-space:nowrap}th{color:var(--thead)}.login{display:grid;place-items:center;min-height:100vh;padding:20px}.login form{width:min(380px,90vw);background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:28px;box-shadow:var(--shadow)}.login input,.login button{width:100%;margin-top:10px}.login h1{margin:0}.login p,#error{color:var(--text-muted)} +@media(max-width:800px){.grid{grid-template-columns:1fr}.wide{grid-column:auto}main{padding:12px}header{padding:0 14px}.header-actions{gap:6px}.card{padding:15px}.cardhead{flex-wrap:wrap}.cardhead>#device{width:100%;min-width:0}.actions button{flex:1 1 140px}} +@media(max-width:680px){header{align-items:flex-start;padding-top:12px;padding-bottom:12px}header span{display:none}.brand-wrap{padding-top:2px}.brand-logo{width:32px;height:32px}.header-actions{flex-direction:column;align-items:stretch;width:112px}.header-actions button{width:100%;padding:8px 9px}.devices{grid-template-columns:1fr}.cardhead{flex-direction:column;align-items:stretch;gap:8px}.cardhead h2{margin-bottom:0}.cardhead button,#device{width:100%;min-width:0}.actions button{flex:1 1 calc(50% - 4px)}} +@media(max-width:450px){main{padding:9px}.card{padding:13px;border-radius:12px}.actions{display:grid;grid-template-columns:1fr 1fr}.actions button{width:100%;min-width:0}.login form{padding:22px}.header-actions{width:104px}} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important}} diff --git a/internal/web/static/app.js b/internal/web/static/app.js index abefbdf..5d6df6f 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -1,12 +1 @@ -const $=s=>document.querySelector(s); -async function api(path,opt={}){let r=await fetch("/panel/api"+path,opt);if(r.status===401){location.href="/panel/login";throw Error("unauthorized")}if(!r.ok)throw Error(await r.text());return r.json()} -function esc(v){return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]))} -async function loadDevices(){let ds=await api("/devices");$("#devices").innerHTML=ds.map(d=>`
${esc(d.name)}
${esc(d.status)} · ${d.is_local?"local":"remote"} · ${esc(d.arch||"")}/${esc(d.os||"")}
`).join("");$("#device").innerHTML=ds.map(d=>``).join("")} -async function loadAudits(){let a=await api("/audits?limit=100");$("#audits").innerHTML=a.map(x=>`${new Date(x.timestamp*1000).toLocaleString()}${esc(x.device)}${esc(x.action)}${esc(x.command)}${x.exit_code}${x.duration_ms}ms${esc(x.caller_ip)}`).join("")} -$("#refresh").onclick=loadDevices;$("#auditRefresh").onclick=loadAudits; -$("#run").onclick=async()=>{let o=$("#output");o.textContent="执行中…";try{let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})});o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms`}catch(e){o.textContent=e.message}}; -$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json"); -$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。"); -$("#rotateKey").onclick=async()=>{if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return;try{let x=await api("/key/rotate",{method:"POST"});$("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key}catch(e){$("#newKey").textContent=e.message}}; -$("#logout").onclick=async()=>{await fetch("/panel/logout",{method:"POST"});location.href="/panel/login"}; -loadDevices().then(loadAudits).catch(console.error); \ No newline at end of file +const $=s=>document.querySelector(s);(function(){const b=$("#themeToggle");if(!b)return;function sync(){const t=document.documentElement.dataset.theme||"dark";b.textContent=t==="light"?"深色模式":"浅色模式"}b.onclick=()=>{const t=document.documentElement.dataset.theme==="light"?"dark":"light";document.documentElement.dataset.theme=t;localStorage.setItem("vpc_theme",t);sync()};sync()})();async function api(path,opt={}){let r=await fetch("/panel/api"+path,opt);if(r.status===401){location.href="/panel/login";throw Error("unauthorized")}if(!r.ok)throw Error(await r.text());return r.json()}function esc(v){return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]))}async function loadDevices(){let ds=await api("/devices");$("#devices").innerHTML=ds.map(d=>'
'+esc(d.name)+'
'+esc(d.status)+' · '+(d.is_local?"local":"remote")+' · '+esc(d.arch||"")+'/'+esc(d.os||"")+'
').join("");$("#device").innerHTML=ds.map(d=>'').join("")}async function loadAudits(){let a=await api("/audits?limit=100");$("#audits").innerHTML=a.map(x=>''+new Date(x.timestamp*1000).toLocaleString()+''+esc(x.device)+''+esc(x.action)+''+esc(x.command)+''+x.exit_code+''+x.duration_ms+'ms'+esc(x.caller_ip)+'').join("")}$("#refresh").onclick=loadDevices;$("#auditRefresh").onclick=loadAudits;$("#run").onclick=async()=>{let o=$("#output");o.textContent="执行中…";try{let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})});o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms`}catch(e){o.textContent=e.message}};$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json");$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。");$("#rotateKey").onclick=async()=>{if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return;try{let x=await api("/key/rotate",{method:"POST"});$("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key}catch(e){$("#newKey").textContent=e.message}};$("#logout").onclick=async()=>{await fetch("/panel/logout",{method:"POST"});location.href="/panel/login"};loadDevices().then(loadAudits).catch(console.error); \ No newline at end of file diff --git a/internal/web/static/icon.png b/internal/web/static/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cc79e4cdcfab57f705e4409bcc68569ba42d9c58 GIT binary patch literal 2768 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 literal 0 HcmV?d00001 diff --git a/internal/web/static/index.html b/internal/web/static/index.html index e84dde9..c47b7dc 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -1,13 +1 @@ - - -VPS-Commander - -
VPS-CommanderPrivate Infrastructure Console
-
-
-

设备看板

-

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json
-

快速控制台

-

审计日志

时间设备动作命令/路径退出码耗时来源
-
-
\ No newline at end of file +VPS-Commander
VPS-CommanderPrivate Infrastructure Console

设备看板

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json

快速控制台

审计日志

时间设备动作命令/路径退出码耗时来源
diff --git a/internal/web/static/login.html b/internal/web/static/login.html index f5a8cb6..3774f26 100644 --- a/internal/web/static/login.html +++ b/internal/web/static/login.html @@ -1 +1 @@ -VPS-Commander Login

VPS-Commander

Private Infrastructure Console

\ No newline at end of file +VPS-Commander Login

VPS-Commander

Private Infrastructure Console

diff --git a/web/static/app.css b/web/static/app.css index 9c7fca4..05e7ac3 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -1 +1,11 @@ -*{box-sizing:border-box}body{margin:0;background:#0b1020;color:#e7ebf3;font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}header{height:64px;padding:0 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #202a3d;background:#10182b;position:sticky;top:0;z-index:2}header strong{font-size:18px}header span{margin-left:12px;color:#7f8ba3;font-size:12px}main{max-width:1400px;margin:auto;padding:22px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.wide{grid-column:1/-1}.card{background:#111a2d;border:1px solid #24304a;border-radius:14px;padding:18px;box-shadow:0 8px 30px #0003}.cardhead{display:flex;align-items:center;justify-content:space-between}h2{margin:0 0 14px;font-size:16px}button,select,input,textarea{border:1px solid #34415d;border-radius:9px;background:#0d1526;color:#e7ebf3;padding:9px 12px}button{cursor:pointer}button:hover{background:#17233a}textarea{width:100%;height:110px;resize:vertical;font-family:ui-monospace,monospace;margin:8px 0}pre{white-space:pre-wrap;overflow:auto;background:#090e1a;border-radius:9px;padding:12px;color:#b9c5da}.devices{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}.device{padding:14px;border:1px solid #283650;border-radius:10px}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#4ade80;margin-right:7px}.offline .dot{background:#f87171}.muted{color:#7f8ba3}.tablewrap{overflow:auto}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:9px;border-bottom:1px solid #26324a;font-size:12px}th{color:#91a0bb}.login{display:grid;place-items:center;min-height:100vh}.login form{width:min(380px,90vw);background:#111a2d;border:1px solid #24304a;border-radius:16px;padding:28px}.login input,.login button{width:100%;margin-top:10px}.login h1{margin:0}.login p,#error{color:#8c9ab2}.status{font-size:12px;color:#9aabc5}@media(max-width:800px){.grid{grid-template-columns:1fr}.wide{grid-column:auto}main{padding:12px}header{padding:0 14px}} \ No newline at end of file +*{box-sizing:border-box} +:root{color-scheme:dark;--bg-body:#0b1020;--bg-card:#111a2d;--bg-sub:#0d1526;--border:#24304a;--border-strong:#34415d;--text-main:#e7ebf3;--text-muted:#7f8ba3;--text-status:#9aabc5;--pre-text:#b9c5da;--thead:#91a0bb;--dot-online:#4ade80;--dot-offline:#f87171;--shadow:0 8px 30px rgba(0,0,0,.2)} +:root[data-theme="light"]{color-scheme:light;--bg-body:#f4f6fa;--bg-card:#fff;--bg-sub:#eef2f7;--border:#d1d9e4;--border-strong:#b8c3d3;--text-main:#172033;--text-muted:#647089;--text-status:#58657a;--pre-text:#26344b;--thead:#53627a;--dot-online:#16833d;--dot-offline:#c62828;--shadow:0 8px 26px rgba(28,45,72,.08)} +html,body{min-height:100%}body{margin:0;background:var(--bg-body);color:var(--text-main);font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}header{min-height:64px;padding:0 24px;display:flex;align-items:center;justify-content:space-between;gap:14px;border-bottom:1px solid var(--border);background:var(--bg-card);position:sticky;top:0;z-index:2} +.brand-wrap{display:flex;align-items:center;gap:12px;min-width:0} +.brand-logo{width:36px;height:36px;border-radius:8px;object-fit:cover;flex:none;box-shadow:0 2px 8px rgba(0,0,0,.15)} +header strong{font-size:18px}header span{margin-left:12px;color:var(--text-muted);font-size:12px}.header-actions{display:flex;align-items:center;gap:8px;flex-shrink:0}main{max-width:1400px;margin:auto;padding:22px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.wide{grid-column:1/-1}.card{min-width:0;background:var(--bg-card);border:1px solid var(--border);border-radius:14px;padding:18px;box-shadow:var(--shadow)}.cardhead{display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0}.cardhead>h2{min-width:0}h2{margin:0 0 14px;font-size:16px}button,select,input,textarea{border:1px solid var(--border-strong);border-radius:9px;background:var(--bg-sub);color:var(--text-main);padding:9px 12px;font:inherit;max-width:100%}button{cursor:pointer}button:hover{filter:brightness(1.05)}button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--border-strong);outline-offset:2px}textarea{width:100%;height:110px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;margin:8px 0}pre{white-space:pre-wrap;overflow:auto;background:var(--bg-sub);border:1px solid var(--border);border-radius:9px;padding:12px;color:var(--pre-text)}.devices{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}.device{min-width:0;padding:14px;border:1px solid var(--border);border-radius:10px;background:var(--bg-sub)}.device b{display:block;min-width:0;overflow-wrap:anywhere;word-break:break-all}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--dot-online);vertical-align:middle;flex:none;margin-right:7px}.offline .dot{background:var(--dot-offline)}.muted{color:var(--text-muted)}.status{margin-top:3px;font-size:12px;color:var(--text-status);overflow-wrap:anywhere;word-break:break-word}.actions{display:flex;flex-wrap:wrap;gap:8px}.actions button{flex:1 1 150px}#device{min-width:180px}.tablewrap{width:100%;max-width:100%;overflow-x:auto;overscroll-behavior-x:contain}table{width:100%;min-width:760px;border-collapse:collapse}th,td{text-align:left;padding:9px;border-bottom:1px solid var(--border);font-size:12px;white-space:nowrap}th{color:var(--thead)}.login{display:grid;place-items:center;min-height:100vh;padding:20px}.login form{width:min(380px,90vw);background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:28px;box-shadow:var(--shadow)}.login input,.login button{width:100%;margin-top:10px}.login h1{margin:0}.login p,#error{color:var(--text-muted)} +@media(max-width:800px){.grid{grid-template-columns:1fr}.wide{grid-column:auto}main{padding:12px}header{padding:0 14px}.header-actions{gap:6px}.card{padding:15px}.cardhead{flex-wrap:wrap}.cardhead>#device{width:100%;min-width:0}.actions button{flex:1 1 140px}} +@media(max-width:680px){header{align-items:flex-start;padding-top:12px;padding-bottom:12px}header span{display:none}.brand-wrap{padding-top:2px}.brand-logo{width:32px;height:32px}.header-actions{flex-direction:column;align-items:stretch;width:112px}.header-actions button{width:100%;padding:8px 9px}.devices{grid-template-columns:1fr}.cardhead{flex-direction:column;align-items:stretch;gap:8px}.cardhead h2{margin-bottom:0}.cardhead button,#device{width:100%;min-width:0}.actions button{flex:1 1 calc(50% - 4px)}} +@media(max-width:450px){main{padding:9px}.card{padding:13px;border-radius:12px}.actions{display:grid;grid-template-columns:1fr 1fr}.actions button{width:100%;min-width:0}.login form{padding:22px}.header-actions{width:104px}} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important}} diff --git a/web/static/app.js b/web/static/app.js index abefbdf..5d6df6f 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1,12 +1 @@ -const $=s=>document.querySelector(s); -async function api(path,opt={}){let r=await fetch("/panel/api"+path,opt);if(r.status===401){location.href="/panel/login";throw Error("unauthorized")}if(!r.ok)throw Error(await r.text());return r.json()} -function esc(v){return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]))} -async function loadDevices(){let ds=await api("/devices");$("#devices").innerHTML=ds.map(d=>`
${esc(d.name)}
${esc(d.status)} · ${d.is_local?"local":"remote"} · ${esc(d.arch||"")}/${esc(d.os||"")}
`).join("");$("#device").innerHTML=ds.map(d=>``).join("")} -async function loadAudits(){let a=await api("/audits?limit=100");$("#audits").innerHTML=a.map(x=>`${new Date(x.timestamp*1000).toLocaleString()}${esc(x.device)}${esc(x.action)}${esc(x.command)}${x.exit_code}${x.duration_ms}ms${esc(x.caller_ip)}`).join("")} -$("#refresh").onclick=loadDevices;$("#auditRefresh").onclick=loadAudits; -$("#run").onclick=async()=>{let o=$("#output");o.textContent="执行中…";try{let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})});o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms`}catch(e){o.textContent=e.message}}; -$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json"); -$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。"); -$("#rotateKey").onclick=async()=>{if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return;try{let x=await api("/key/rotate",{method:"POST"});$("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key}catch(e){$("#newKey").textContent=e.message}}; -$("#logout").onclick=async()=>{await fetch("/panel/logout",{method:"POST"});location.href="/panel/login"}; -loadDevices().then(loadAudits).catch(console.error); \ No newline at end of file +const $=s=>document.querySelector(s);(function(){const b=$("#themeToggle");if(!b)return;function sync(){const t=document.documentElement.dataset.theme||"dark";b.textContent=t==="light"?"深色模式":"浅色模式"}b.onclick=()=>{const t=document.documentElement.dataset.theme==="light"?"dark":"light";document.documentElement.dataset.theme=t;localStorage.setItem("vpc_theme",t);sync()};sync()})();async function api(path,opt={}){let r=await fetch("/panel/api"+path,opt);if(r.status===401){location.href="/panel/login";throw Error("unauthorized")}if(!r.ok)throw Error(await r.text());return r.json()}function esc(v){return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]))}async function loadDevices(){let ds=await api("/devices");$("#devices").innerHTML=ds.map(d=>'
'+esc(d.name)+'
'+esc(d.status)+' · '+(d.is_local?"local":"remote")+' · '+esc(d.arch||"")+'/'+esc(d.os||"")+'
').join("");$("#device").innerHTML=ds.map(d=>'').join("")}async function loadAudits(){let a=await api("/audits?limit=100");$("#audits").innerHTML=a.map(x=>''+new Date(x.timestamp*1000).toLocaleString()+''+esc(x.device)+''+esc(x.action)+''+esc(x.command)+''+x.exit_code+''+x.duration_ms+'ms'+esc(x.caller_ip)+'').join("")}$("#refresh").onclick=loadDevices;$("#auditRefresh").onclick=loadAudits;$("#run").onclick=async()=>{let o=$("#output");o.textContent="执行中…";try{let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})});o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms`}catch(e){o.textContent=e.message}};$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json");$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。");$("#rotateKey").onclick=async()=>{if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return;try{let x=await api("/key/rotate",{method:"POST"});$("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key}catch(e){$("#newKey").textContent=e.message}};$("#logout").onclick=async()=>{await fetch("/panel/logout",{method:"POST"});location.href="/panel/login"};loadDevices().then(loadAudits).catch(console.error); \ No newline at end of file diff --git a/web/static/icon.png b/web/static/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cc79e4cdcfab57f705e4409bcc68569ba42d9c58 GIT binary patch literal 2768 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 literal 0 HcmV?d00001 diff --git a/web/static/index.html b/web/static/index.html index e84dde9..c47b7dc 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -1,13 +1 @@ - - -VPS-Commander - -
VPS-CommanderPrivate Infrastructure Console
-
-
-

设备看板

-

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json
-

快速控制台

-

审计日志

时间设备动作命令/路径退出码耗时来源
-
-
\ No newline at end of file +VPS-Commander
VPS-CommanderPrivate Infrastructure Console

设备看板

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json

快速控制台

审计日志

时间设备动作命令/路径退出码耗时来源
diff --git a/web/static/login.html b/web/static/login.html index f5a8cb6..3774f26 100644 --- a/web/static/login.html +++ b/web/static/login.html @@ -1 +1 @@ -VPS-Commander Login

VPS-Commander

Private Infrastructure Console

\ No newline at end of file +VPS-Commander Login

VPS-Commander

Private Infrastructure Console