Files
SenSu/static/web_panel/pages/settings.html
T
qinglong 94d56d8210 fix: 版本检查 — 后端设置 checked 字段 + 前端 info 合并显示
- UpdateService.status 增加 checked/current_version/latest_version
- _check_version 成功后设置 checked=True
- 设置页 update-notes 合并版本信息和时间戳 (替代 innerHTML 覆盖)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 21:29:35 +08:00

256 lines
15 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.
<div class="plugin-page-root">
<h2><svg width="22" height="22" viewBox="0 0 24 24" style="vertical-align:-4px;margin-right:6px"><path fill="currentColor" d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94L14.4 2.81a.484.484 0 00-.41-.3h-3.98c-.2 0-.38.13-.45.32l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.19.24.31.44.31h3.98c.2 0 .38-.13.45-.32l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>框架设置</h2>
<!-- ── 版本与更新 ── -->
<div class="card" style="margin-bottom:24px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0"><svg width="20" height="20" viewBox="0 0 24 24" style="vertical-align:-4px;margin-right:6px"><path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>版本与更新</h3>
</div>
<div id="update-area">
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<span style="font-size:1.1rem;font-weight:600">SenSu <span id="ver-current">v?</span></span>
<span id="ver-badge" style="font-size:.85rem;padding:2px 8px;border-radius:12px;background:var(--md-sys-color-surface-container-lowest)">检查中...</span>
<button class="btn btn-sm btn-filled" onclick="checkUpdate()">检查更新</button>
<button id="btn-apply" class="btn btn-sm btn-filled" style="display:none;background:var(--success,#4caf50)" onclick="applyUpdate()">⬆ 立即更新</button>
</div>
<div id="update-notes" style="margin-top:8px;font-size:.85rem;color:var(--text-dim)"></div>
</div>
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h4 style="margin:0"><svg width="18" height="18" viewBox="0 0 24 24" style="vertical-align:-3px;margin-right:4px"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V6h5.17l2 2H20v10z"/></svg>备份管理</h4>
<button class="btn btn-sm btn-outlined" onclick="loadBackups()">刷新列表</button>
<button class="btn btn-sm btn-filled" onclick="createBackup()" style="margin-left:8px">+ 创建备份</button>
</div>
<div id="backup-list"><span style="color:var(--text-dim);font-size:.85rem">加载中...</span></div>
</div>
</div>
<!-- ── API Key 管理 ── -->
<div class="card" style="margin-bottom:24px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="margin:0"><svg width="20" height="20" viewBox="0 0 24 24" style="vertical-align:-4px;margin-right:6px"><path fill="currentColor" d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>API Key 管理</h3>
<button class="btn btn-filled" onclick="showCreateKey()">+ 创建 Key</button>
</div>
<p style="color:var(--text-dim);font-size:.85rem;margin-bottom:12px">
API Key 用于服务器间安全通信。支持权限范围控制和过期时间。
</p>
<div id="apikey-list" style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse">
<thead><tr style="text-align:left;border-bottom:1px solid var(--border)">
<th style="padding:8px">名称</th><th style="padding:8px">Key</th><th style="padding:8px">权限</th><th style="padding:8px">用量</th><th style="padding:8px">过期</th><th style="padding:8px">操作</th>
</tr></thead>
<tbody id="apikey-tbody"><tr><td colspan="6" style="padding:16px;text-align:center;color:var(--text-dim)">加载中...</td></tr></tbody>
</table>
</div>
</div>
<!-- ── 创建对话框 (hidden) ── -->
<div id="create-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:1000;display:none;align-items:center;justify-content:center">
<div class="card" style="min-width:420px;max-width:500px">
<h3 style="margin:0 0 16px 0">创建 API Key</h3>
<div class="form-group" style="margin-bottom:12px">
<label style="display:block;margin-bottom:4px;font-size:.85rem">名称</label>
<input id="new-key-name" class="input" placeholder="例如: 监控插件" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
</div>
<div class="form-group" style="margin-bottom:12px">
<label style="display:block;margin-bottom:4px;font-size:.85rem">权限范围</label>
<select id="new-key-preset" class="input" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
<option value="readonly">只读 — 仅查询系统状态</option>
<option value="monitor" selected>监控 — 查询 + 事件订阅</option>
<option value="full">管理 — 全部权限</option>
</select>
</div>
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;margin-bottom:4px;font-size:.85rem">过期时间 (秒, 0=永不过期)</label>
<input id="new-key-ttl" class="input" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
<span style="font-size:.75rem;color:var(--text-dim)">常用: 86400(1天) 604800(7天) 2592000(30天)</span>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn btn-outlined" onclick="hideCreateKey()">取消</button>
<button class="btn btn-filled" onclick="doCreateKey()">创建</button>
</div>
<div id="create-result" style="margin-top:12px;display:none">
<div style="background:var(--md-sys-color-surface-container-lowest);padding:12px;border-radius:var(--shape-xs);word-break:break-all">
<strong style="color:var(--success)">✓ Key 已创建 (仅显示一次):</strong>
<code id="new-key-display" style="display:block;margin-top:4px;font-size:.9rem;color:var(--primary)"></code>
</div>
</div>
</div>
</div>
<style>
.input:focus { outline:1px solid var(--primary) }
#apikey-tbody td { padding:8px; border-bottom:1px solid var(--md-sys-color-surface-container-lowest) }
#apikey-tbody tr:hover { background:var(--md-sys-color-surface-container-lowest) }
.backup-row { display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--md-sys-color-surface-container-lowest);gap:8px }
.backup-name { font-size:.85rem;word-break:break-all;flex:1;min-width:0 }
</style>
<script>
// ── 版本与更新 ──
async function loadVersion() {
try {
var r = await fetch("./api/updates"), d = await r.json();
document.getElementById("ver-current").textContent = d.current_version || d.latest_version || "?";
var badge = document.getElementById("ver-badge");
var notesHtml = "";
if (d.update_available) {
badge.textContent = "↻ " + d.latest_version + " 可用";
badge.style.background = "var(--success,#4caf50)";
badge.style.color = "#fff";
document.getElementById("btn-apply").style.display = "inline-block";
notesHtml = d.latest_version ? "最新版本: <b>"+esc(d.latest_version)+"</b>" : "";
} else if (d.checked) {
badge.textContent = "✓ 已是最新";
badge.style.background = "var(--md-sys-color-surface-container-lowest)";
badge.style.color = "var(--text)";
document.getElementById("btn-apply").style.display = "none";
} else {
badge.textContent = "⏳ 等待检查...";
}
if (d.last_check) {
notesHtml += (notesHtml ? " | " : "") + "上次检查: "+new Date(d.last_check*1000).toLocaleString();
}
document.getElementById("update-notes").innerHTML = notesHtml;
} catch(e) {}
}
async function createBackup() {
try {
var r = await fetch("./api/updates/backup", {method:"POST"}), d = await r.json();
if (d.ok) alert("✓ 备份已创建: " + d.name);
else alert("✗ " + (d.error||"失败"));
loadBackups();
} catch(e) { alert("✗ " + e); }
}
async function checkUpdate() {
document.getElementById("ver-badge").textContent = "⏳ 检查中...";
await fetch("./api/updates/check", {method:"POST"});
setTimeout(loadVersion, 1500);
}
async function applyUpdate() {
if (!confirm("确认更新 SenSu?更新前会自动备份当前版本。")) return;
document.getElementById("btn-apply").disabled = true;
document.getElementById("btn-apply").textContent = "⏳ 更新中...";
try {
var r = await fetch("./api/updates/apply", {method:"POST"}), d = await r.json();
if (d.ok) alert("✓ " + d.msg);
else alert("✗ " + (d.error||"失败"));
} catch(e) { alert("✗ " + e); }
loadVersion().then(function(){ checkUpdate(); });
}
// ── 备份管理 ──
async function loadBackups() {
var el = document.getElementById("backup-list");
try {
var r = await fetch("./api/updates/backups"), d = await r.json();
if (!d.backups.length) { el.innerHTML = '<span style="color:var(--text-dim);font-size:.85rem">暂无备份</span>'; return; }
el.innerHTML = d.backups.map(function(b,i){
return '<div class="backup-row"><span class="backup-name">'+esc(b.name)+'</span><span style="font-size:.8rem;color:var(--text-dim)">'+b.size_mb+'MB | '+b.time+'</span>'+
'<button class="btn btn-sm btn-outlined" onclick="doRollback(\''+esc(b.name)+'\')">回滚</button></div>';
}).join("");
} catch(e) { el.innerHTML = '<span style="color:var(--error)">加载失败</span>'; }
}
async function doRollback(name) {
if (!confirm("确认回滚到 "+name+"? 当前版本将被覆盖。")) return;
try {
var r = await fetch("./api/updates/rollback", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({name:name})}), d = await r.json();
if (d.ok) alert("✓ " + d.msg);
else alert("✗ " + (d.error||"失败"));
} catch(e) { alert("✗ " + e); }
}
// ── 加载 API Keys ──
async function loadKeys() {
var tbody = document.getElementById("apikey-tbody");
try {
var resp = await fetch("./api/apikeys");
var data = await resp.json();
if (!data.keys || !data.keys.length) {
tbody.innerHTML = '<tr><td colspan="6" style="padding:16px;text-align:center;color:var(--text-dim)">暂无 API Key</td></tr>';
return;
}
var rows = [];
data.keys.forEach(function(k) {
var expStr = k.expires_at > 0 ? new Date(k.expires_at*1000).toLocaleDateString() : "永不过期";
if (k.expired) expStr = '<span style="color:var(--error)">已过期</span>';
var permLabel = k.permissions && k.permissions[0] === "admin" ? "管理" : (k.permissions && k.permissions.length <= 2 ? "只读" : "监控");
rows.push(
'<tr>' +
'<td>' + esc(k.name) + '</td>' +
'<td><code>' + esc(k.key) + '</code></td>' +
'<td><span class="badge">' + permLabel + '</span></td>' +
'<td>' + (k.request_count || 0) + '</td>' +
'<td>' + expStr + '</td>' +
'<td><button class="btn btn-sm btn-outlined" style="color:var(--error);border-color:var(--error)" onclick="deleteKey(\'' + esc(k.full_key) + '\',\'' + esc(k.name) + '\')">删除</button></td>' +
'</tr>'
);
});
tbody.innerHTML = rows.join("");
} catch(e) {
tbody.innerHTML = '<tr><td colspan="6" style="padding:16px;text-align:center;color:var(--error)">加载失败: ' + esc(String(e)) + '</td></tr>';
}
}
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
// ── 创建对话框 ──
function showCreateKey() {
document.getElementById("create-dialog").style.display = "flex";
document.getElementById("create-result").style.display = "none";
document.getElementById("new-key-name").value = "";
document.getElementById("new-key-ttl").value = "0";
}
function hideCreateKey() {
document.getElementById("create-dialog").style.display = "none";
}
async function doCreateKey() {
var name = document.getElementById("new-key-name").value.trim();
if (!name) { alert("请输入名称"); return; }
var preset = document.getElementById("new-key-preset").value;
var ttl = parseInt(document.getElementById("new-key-ttl").value) || 0;
try {
var resp = await fetch("./api/apikeys", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({name: name, preset: preset, ttl: ttl})
});
var data = await resp.json();
if (data.ok) {
document.getElementById("create-result").style.display = "block";
document.getElementById("new-key-display").textContent = data.key;
loadKeys();
} else {
alert("创建失败: " + (data.error || "未知错误"));
}
} catch(e) { alert("请求失败: " + e); }
}
// ── 删除 ──
async function deleteKey(fullKey, name) {
if (!confirm("确认删除 Key '" + name + "' 此操作不可撤销。")) return;
try {
var resp = await fetch("./api/apikeys", {
method: "DELETE",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({key: fullKey})
});
var data = await resp.json();
if (data.ok) loadKeys();
else alert("删除失败: " + (data.error || "未知错误"));
} catch(e) { alert("请求失败: " + e); }
}
// ── 初始加载 ──
loadKeys();
loadVersion().then(function(){ checkUpdate(); });
loadBackups();
</script>
</div>