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=>`${esc(d.name)} `).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=>''+esc(d.name)+' ').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 0000000..cc79e4c
Binary files /dev/null and b/internal/web/static/icon.png differ
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
-
-
-
-
-
设备看板 刷新
-Actions 向导 当前 Hub 的 OpenAPI Schema:
复制 Schema 地址 复制 System Prompt 轮换 API Key /openapi.json
-
快速控制台 执行
-
审计日志 刷新
-
-
\ No newline at end of file
+VPS-Commander
设备看板 刷新
Actions 向导 当前 Hub 的 OpenAPI Schema:
复制 Schema 地址 复制 System Prompt 轮换 API Key
/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
\ No newline at end of file
+VPS-Commander Login
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=>`${esc(d.name)} `).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=>''+esc(d.name)+' ').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 0000000..cc79e4c
Binary files /dev/null and b/web/static/icon.png differ
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
-
-
-
-
-
设备看板 刷新
-Actions 向导 当前 Hub 的 OpenAPI Schema:
复制 Schema 地址 复制 System Prompt 轮换 API Key /openapi.json
-
快速控制台 执行
-
审计日志 刷新
-
-
\ No newline at end of file
+VPS-Commander
设备看板 刷新
Actions 向导 当前 Hub 的 OpenAPI Schema:
复制 Schema 地址 复制 System Prompt 轮换 API Key
/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
\ No newline at end of file
+VPS-Commander Login