67d0f00525
1. 设置页加载时自动触发版本检查 2. POST /api/updates/backup — 手动创建备份 3. 备份管理区新增「+ 创建备份」按钮 4. 顶栏版本按钮: checkTopUpdate 每10min轮询 Co-Authored-By: Claude <noreply@anthropic.com>
254 lines
15 KiB
HTML
254 lines
15 KiB
HTML
<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");
|
||
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";
|
||
document.getElementById("update-notes").innerHTML = 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) {
|
||
document.getElementById("update-notes").innerHTML += " <span style='font-size:.75rem;color:var(--text-dim)'>上次检查: "+new Date(d.last_check*1000).toLocaleString()+"</span>";
|
||
}
|
||
} 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||
|
||
// ── 创建对话框 ──
|
||
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>
|