Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- core/templates.py +289 -4
core/templates.py
CHANGED
|
@@ -351,8 +351,9 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
|
|
| 351 |
|
| 352 |
/* Account & Env Styles */
|
| 353 |
.account-card .acc-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
|
| 354 |
-
.acc-title {{ font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; }}
|
| 355 |
-
.
|
|
|
|
| 356 |
.acc-status {{ font-size: 12px; font-weight: 600; }}
|
| 357 |
.acc-actions {{ display: flex; gap: 8px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
|
| 358 |
.acc-body {{ }}
|
|
@@ -600,7 +601,8 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
|
|
| 600 |
@media (max-width: 800px) {{
|
| 601 |
.grid-3, .grid-env {{ grid-template-columns: 1fr; }}
|
| 602 |
.header {{ flex-direction: column; align-items: flex-start; gap: 16px; }}
|
| 603 |
-
.header-actions {{ width: 100%;
|
|
|
|
| 604 |
.ep-table td {{ display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }}
|
| 605 |
.ep-desc {{ margin-left: 0; }}
|
| 606 |
}}
|
|
@@ -614,8 +616,11 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
|
|
| 614 |
<div class="subtitle">多账户代理面板</div>
|
| 615 |
</div>
|
| 616 |
<div class="header-actions">
|
|
|
|
| 617 |
<a href="/public/log/html" class="btn" target="_blank">📄 公开日志</a>
|
| 618 |
<a href="/{main.PATH_PREFIX}/admin/log/html?key={main.ADMIN_KEY}" class="btn" target="_blank">🔧 管理日志</a>
|
|
|
|
|
|
|
| 619 |
<button class="btn" onclick="showEditConfig()" id="edit-btn">✏️ 编辑配置</button>
|
| 620 |
</div>
|
| 621 |
</div>
|
|
@@ -627,7 +632,10 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
|
|
| 627 |
|
| 628 |
<div class="section">
|
| 629 |
<div class="section-title">账户状态 ({len(multi_account_mgr.accounts)} 个)</div>
|
| 630 |
-
<div style="color: #6b6b6b; font-size: 12px; margin-bottom: 12px; padding-left: 4px;">
|
|
|
|
|
|
|
|
|
|
| 631 |
<div class="account-grid">
|
| 632 |
{accounts_html if accounts_html else '<div class="card"><p style="color: #6b6b6b; font-size: 14px; text-align:center;">暂无账户</p></div>'}
|
| 633 |
</div>
|
|
@@ -808,6 +816,16 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
|
|
| 808 |
<td><span class="ep-path">/public/log/html</span></td>
|
| 809 |
<td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
|
| 810 |
</tr>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
<tr>
|
| 812 |
<td><span class="method m-get">GET</span></td>
|
| 813 |
<td><span class="ep-path">/docs</span></td>
|
|
@@ -846,6 +864,7 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
|
|
| 846 |
</div>
|
| 847 |
</div>
|
| 848 |
|
|
|
|
| 849 |
<script>
|
| 850 |
let currentConfig = null;
|
| 851 |
|
|
@@ -985,6 +1004,90 @@ def generate_admin_html(request: Request, multi_account_mgr, show_hide_tip: bool
|
|
| 985 |
}}
|
| 986 |
}}
|
| 987 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
// 点击模态框外部关闭
|
| 989 |
document.getElementById('jsonModal').addEventListener('click', function(e) {{
|
| 990 |
if (e.target === this) {{
|
|
@@ -2094,3 +2197,185 @@ async def get_public_logs_html():
|
|
| 2094 |
</html>
|
| 2095 |
"""
|
| 2096 |
return HTMLResponse(content=html_content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
/* Account & Env Styles */
|
| 353 |
.account-card .acc-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
|
| 354 |
+
.acc-title {{ font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; overflow: hidden; }}
|
| 355 |
+
.acc-title span:last-child {{ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 120px; }}
|
| 356 |
+
.status-dot {{ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }}
|
| 357 |
.acc-status {{ font-size: 12px; font-weight: 600; }}
|
| 358 |
.acc-actions {{ display: flex; gap: 8px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f5f5f5; }}
|
| 359 |
.acc-body {{ }}
|
|
|
|
| 601 |
@media (max-width: 800px) {{
|
| 602 |
.grid-3, .grid-env {{ grid-template-columns: 1fr; }}
|
| 603 |
.header {{ flex-direction: column; align-items: flex-start; gap: 16px; }}
|
| 604 |
+
.header-actions {{ width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }}
|
| 605 |
+
.header-actions .btn {{ justify-content: center; text-align: center; }}
|
| 606 |
.ep-table td {{ display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }}
|
| 607 |
.ep-desc {{ margin-left: 0; }}
|
| 608 |
}}
|
|
|
|
| 616 |
<div class="subtitle">多账户代理面板</div>
|
| 617 |
</div>
|
| 618 |
<div class="header-actions">
|
| 619 |
+
<a href="/public/uptime/html" class="btn" target="_blank">📊 状态监控</a>
|
| 620 |
<a href="/public/log/html" class="btn" target="_blank">📄 公开日志</a>
|
| 621 |
<a href="/{main.PATH_PREFIX}/admin/log/html?key={main.ADMIN_KEY}" class="btn" target="_blank">🔧 管理日志</a>
|
| 622 |
+
<button class="btn" onclick="document.getElementById('fileInput').click()">📥 批量上传</button>
|
| 623 |
+
<input type="file" id="fileInput" accept=".json" multiple style="display:none" onchange="handleFileUpload(event)">
|
| 624 |
<button class="btn" onclick="showEditConfig()" id="edit-btn">✏️ 编辑配置</button>
|
| 625 |
</div>
|
| 626 |
</div>
|
|
|
|
| 632 |
|
| 633 |
<div class="section">
|
| 634 |
<div class="section-title">账户状态 ({len(multi_account_mgr.accounts)} 个)</div>
|
| 635 |
+
<div style="color: #6b6b6b; font-size: 12px; margin-bottom: 12px; padding-left: 4px;">
|
| 636 |
+
过期时间为12小时,可以自行修改时间,脚本可能有误差。<br>
|
| 637 |
+
批量上传格式:<code style="font-size: 11px;">[{{"secure_c_ses": "...", "csesidx": "...", "config_id": "...", "id": "account_1"}}]</code>(id 可选)
|
| 638 |
+
</div>
|
| 639 |
<div class="account-grid">
|
| 640 |
{accounts_html if accounts_html else '<div class="card"><p style="color: #6b6b6b; font-size: 14px; text-align:center;">暂无账户</p></div>'}
|
| 641 |
</div>
|
|
|
|
| 816 |
<td><span class="ep-path">/public/log/html</span></td>
|
| 817 |
<td><span class="ep-desc">公开日志查看器 (HTML)</span></td>
|
| 818 |
</tr>
|
| 819 |
+
<tr>
|
| 820 |
+
<td><span class="method m-get">GET</span></td>
|
| 821 |
+
<td><span class="ep-path">/public/uptime</span></td>
|
| 822 |
+
<td><span class="ep-desc">实时状态监控 (JSON)</span></td>
|
| 823 |
+
</tr>
|
| 824 |
+
<tr>
|
| 825 |
+
<td><span class="method m-get">GET</span></td>
|
| 826 |
+
<td><span class="ep-path">/public/uptime/html</span></td>
|
| 827 |
+
<td><span class="ep-desc">实时状态监控页面 (HTML)</span></td>
|
| 828 |
+
</tr>
|
| 829 |
<tr>
|
| 830 |
<td><span class="method m-get">GET</span></td>
|
| 831 |
<td><span class="ep-path">/docs</span></td>
|
|
|
|
| 864 |
</div>
|
| 865 |
</div>
|
| 866 |
|
| 867 |
+
|
| 868 |
<script>
|
| 869 |
let currentConfig = null;
|
| 870 |
|
|
|
|
| 1004 |
}}
|
| 1005 |
}}
|
| 1006 |
|
| 1007 |
+
// 批量上传相关函数
|
| 1008 |
+
async function handleFileUpload(event) {{
|
| 1009 |
+
const files = event.target.files;
|
| 1010 |
+
if (!files.length) return;
|
| 1011 |
+
|
| 1012 |
+
let newAccounts = [];
|
| 1013 |
+
for (const file of files) {{
|
| 1014 |
+
try {{
|
| 1015 |
+
const text = await file.text();
|
| 1016 |
+
const data = JSON.parse(text);
|
| 1017 |
+
if (Array.isArray(data)) {{
|
| 1018 |
+
newAccounts.push(...data);
|
| 1019 |
+
}} else {{
|
| 1020 |
+
newAccounts.push(data);
|
| 1021 |
+
}}
|
| 1022 |
+
}} catch (e) {{
|
| 1023 |
+
alert(`文件 ${{file.name}} 解析失败: ${{e.message}}`);
|
| 1024 |
+
event.target.value = '';
|
| 1025 |
+
return;
|
| 1026 |
+
}}
|
| 1027 |
+
}}
|
| 1028 |
+
|
| 1029 |
+
if (!newAccounts.length) {{
|
| 1030 |
+
alert('未找到有效账户数据');
|
| 1031 |
+
event.target.value = '';
|
| 1032 |
+
return;
|
| 1033 |
+
}}
|
| 1034 |
+
|
| 1035 |
+
try {{
|
| 1036 |
+
// 获取现有配置
|
| 1037 |
+
const configResp = await fetch('/{main.PATH_PREFIX}/admin/accounts-config?key={main.ADMIN_KEY}');
|
| 1038 |
+
const configData = await handleApiResponse(configResp);
|
| 1039 |
+
const existing = configData.accounts || [];
|
| 1040 |
+
|
| 1041 |
+
// 构建ID到索引的映射
|
| 1042 |
+
const idToIndex = new Map();
|
| 1043 |
+
existing.forEach((acc, idx) => {{
|
| 1044 |
+
if (acc.id) idToIndex.set(acc.id, idx);
|
| 1045 |
+
}});
|
| 1046 |
+
|
| 1047 |
+
// 合并:相同ID覆盖,新ID追加
|
| 1048 |
+
let added = 0;
|
| 1049 |
+
let updated = 0;
|
| 1050 |
+
for (const acc of newAccounts) {{
|
| 1051 |
+
if (!acc.secure_c_ses || !acc.csesidx || !acc.config_id) continue;
|
| 1052 |
+
const accId = acc.id || `account_${{existing.length + added + 1}}`;
|
| 1053 |
+
acc.id = accId;
|
| 1054 |
+
|
| 1055 |
+
if (idToIndex.has(accId)) {{
|
| 1056 |
+
// 覆盖已存在的账户
|
| 1057 |
+
existing[idToIndex.get(accId)] = acc;
|
| 1058 |
+
updated++;
|
| 1059 |
+
}} else {{
|
| 1060 |
+
// 追加新账户
|
| 1061 |
+
existing.push(acc);
|
| 1062 |
+
idToIndex.set(accId, existing.length - 1);
|
| 1063 |
+
added++;
|
| 1064 |
+
}}
|
| 1065 |
+
}}
|
| 1066 |
+
|
| 1067 |
+
if (added === 0 && updated === 0) {{
|
| 1068 |
+
alert('没有有效账户可导入');
|
| 1069 |
+
event.target.value = '';
|
| 1070 |
+
return;
|
| 1071 |
+
}}
|
| 1072 |
+
|
| 1073 |
+
// 保存合并后的配置
|
| 1074 |
+
const response = await fetch('/{main.PATH_PREFIX}/admin/accounts-config?key={main.ADMIN_KEY}', {{
|
| 1075 |
+
method: 'PUT',
|
| 1076 |
+
headers: {{'Content-Type': 'application/json'}},
|
| 1077 |
+
body: JSON.stringify(existing)
|
| 1078 |
+
}});
|
| 1079 |
+
|
| 1080 |
+
const result = await handleApiResponse(response);
|
| 1081 |
+
alert(`导入完成!\\n新增: ${{added}} 个\\n覆盖: ${{updated}} 个\\n当前账户数: ${{result.account_count}}`);
|
| 1082 |
+
event.target.value = '';
|
| 1083 |
+
setTimeout(refreshPage, 1000);
|
| 1084 |
+
}} catch (error) {{
|
| 1085 |
+
console.error('导入失败:', error);
|
| 1086 |
+
alert('导入失败: ' + error.message);
|
| 1087 |
+
event.target.value = '';
|
| 1088 |
+
}}
|
| 1089 |
+
}}
|
| 1090 |
+
|
| 1091 |
// 点击模态框外部关闭
|
| 1092 |
document.getElementById('jsonModal').addEventListener('click', function(e) {{
|
| 1093 |
if (e.target === this) {{
|
|
|
|
| 2197 |
</html>
|
| 2198 |
"""
|
| 2199 |
return HTMLResponse(content=html_content)
|
| 2200 |
+
|
| 2201 |
+
|
| 2202 |
+
async def get_uptime_html():
|
| 2203 |
+
"""Uptime 实时监控页面(类似 Uptime Kuma)"""
|
| 2204 |
+
html_content = """
|
| 2205 |
+
<!DOCTYPE html>
|
| 2206 |
+
<html lang="zh-CN">
|
| 2207 |
+
<head>
|
| 2208 |
+
<meta charset="UTF-8">
|
| 2209 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 2210 |
+
<title>Gemini Status</title>
|
| 2211 |
+
<style>
|
| 2212 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 2213 |
+
body {
|
| 2214 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 2215 |
+
background: #f5f5f7;
|
| 2216 |
+
color: #1d1d1f;
|
| 2217 |
+
min-height: 100vh;
|
| 2218 |
+
padding: 20px;
|
| 2219 |
+
}
|
| 2220 |
+
.container { max-width: 1200px; margin: 0 auto; }
|
| 2221 |
+
h1 {
|
| 2222 |
+
font-size: 24px;
|
| 2223 |
+
font-weight: 600;
|
| 2224 |
+
margin-bottom: 8px;
|
| 2225 |
+
color: #1d1d1f;
|
| 2226 |
+
}
|
| 2227 |
+
.subtitle { color: #86868b; font-size: 14px; margin-bottom: 24px; }
|
| 2228 |
+
.update-time { color: #86868b; font-size: 12px; margin-bottom: 16px; }
|
| 2229 |
+
.grid {
|
| 2230 |
+
display: grid;
|
| 2231 |
+
grid-template-columns: repeat(2, 1fr);
|
| 2232 |
+
gap: 16px;
|
| 2233 |
+
}
|
| 2234 |
+
.card {
|
| 2235 |
+
background: #fff;
|
| 2236 |
+
border: 1px solid #e5e5e5;
|
| 2237 |
+
border-radius: 12px;
|
| 2238 |
+
padding: 16px;
|
| 2239 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
| 2240 |
+
}
|
| 2241 |
+
.card:hover { border-color: #d4d4d4; }
|
| 2242 |
+
.card-header {
|
| 2243 |
+
display: flex;
|
| 2244 |
+
justify-content: space-between;
|
| 2245 |
+
align-items: center;
|
| 2246 |
+
margin-bottom: 12px;
|
| 2247 |
+
}
|
| 2248 |
+
.service-name { font-weight: 600; font-size: 14px; color: #1d1d1f; }
|
| 2249 |
+
.status-badge {
|
| 2250 |
+
padding: 2px 8px;
|
| 2251 |
+
border-radius: 12px;
|
| 2252 |
+
font-size: 11px;
|
| 2253 |
+
font-weight: 600;
|
| 2254 |
+
}
|
| 2255 |
+
.status-up { background: #d1fae5; color: #065f46; }
|
| 2256 |
+
.status-down { background: #fee2e2; color: #991b1b; }
|
| 2257 |
+
.status-unknown { background: #f3f4f6; color: #6b7280; }
|
| 2258 |
+
.stats {
|
| 2259 |
+
display: flex;
|
| 2260 |
+
gap: 16px;
|
| 2261 |
+
margin-bottom: 12px;
|
| 2262 |
+
font-size: 12px;
|
| 2263 |
+
color: #86868b;
|
| 2264 |
+
}
|
| 2265 |
+
.stat-value { color: #1d1d1f; font-weight: 600; }
|
| 2266 |
+
.heartbeat-bar {
|
| 2267 |
+
display: flex;
|
| 2268 |
+
gap: 2px;
|
| 2269 |
+
height: 24px;
|
| 2270 |
+
align-items: flex-end;
|
| 2271 |
+
}
|
| 2272 |
+
.beat {
|
| 2273 |
+
flex: 1;
|
| 2274 |
+
min-width: 4px;
|
| 2275 |
+
max-width: 8px;
|
| 2276 |
+
border-radius: 2px;
|
| 2277 |
+
transition: all 0.2s;
|
| 2278 |
+
position: relative;
|
| 2279 |
+
}
|
| 2280 |
+
.beat:hover { opacity: 0.8; transform: scaleY(1.1); }
|
| 2281 |
+
.beat.up { background: #34c759; height: 100%; }
|
| 2282 |
+
.beat.down { background: #ff3b30; height: 100%; }
|
| 2283 |
+
.beat.empty { background: #e5e5ea; height: 40%; }
|
| 2284 |
+
.beat .tooltip {
|
| 2285 |
+
position: absolute;
|
| 2286 |
+
bottom: 100%;
|
| 2287 |
+
left: 50%;
|
| 2288 |
+
transform: translateX(-50%);
|
| 2289 |
+
background: #1d1d1f;
|
| 2290 |
+
color: #fff;
|
| 2291 |
+
padding: 6px 10px;
|
| 2292 |
+
border-radius: 6px;
|
| 2293 |
+
font-size: 11px;
|
| 2294 |
+
white-space: nowrap;
|
| 2295 |
+
opacity: 0;
|
| 2296 |
+
pointer-events: none;
|
| 2297 |
+
transition: opacity 0.15s;
|
| 2298 |
+
margin-bottom: 6px;
|
| 2299 |
+
z-index: 100;
|
| 2300 |
+
}
|
| 2301 |
+
.beat .tooltip::after {
|
| 2302 |
+
content: '';
|
| 2303 |
+
position: absolute;
|
| 2304 |
+
top: 100%;
|
| 2305 |
+
left: 50%;
|
| 2306 |
+
transform: translateX(-50%);
|
| 2307 |
+
border: 5px solid transparent;
|
| 2308 |
+
border-top-color: #1d1d1f;
|
| 2309 |
+
}
|
| 2310 |
+
.beat:hover .tooltip { opacity: 1; }
|
| 2311 |
+
@media (max-width: 768px) {
|
| 2312 |
+
.grid { grid-template-columns: 1fr; }
|
| 2313 |
+
.beat { min-width: 3px; max-width: 6px; }
|
| 2314 |
+
}
|
| 2315 |
+
</style>
|
| 2316 |
+
</head>
|
| 2317 |
+
<body>
|
| 2318 |
+
<div class="container">
|
| 2319 |
+
<h1>Gemini Status</h1>
|
| 2320 |
+
<p class="subtitle">服务状态监控</p>
|
| 2321 |
+
<p class="update-time" id="update-time">更新中...</p>
|
| 2322 |
+
<div class="grid" id="services"></div>
|
| 2323 |
+
</div>
|
| 2324 |
+
<script>
|
| 2325 |
+
async function loadStatus() {
|
| 2326 |
+
try {
|
| 2327 |
+
const res = await fetch('/public/uptime');
|
| 2328 |
+
const data = await res.json();
|
| 2329 |
+
renderServices(data);
|
| 2330 |
+
document.getElementById('update-time').textContent = '更新于 ' + data.updated_at;
|
| 2331 |
+
} catch (e) {
|
| 2332 |
+
document.getElementById('services').innerHTML = '<div class="card">加载失败</div>';
|
| 2333 |
+
}
|
| 2334 |
+
}
|
| 2335 |
+
|
| 2336 |
+
function renderServices(data) {
|
| 2337 |
+
const container = document.getElementById('services');
|
| 2338 |
+
let html = '';
|
| 2339 |
+
for (const [id, svc] of Object.entries(data.services)) {
|
| 2340 |
+
const statusClass = svc.status === 'up' ? 'status-up' : svc.status === 'down' ? 'status-down' : 'status-unknown';
|
| 2341 |
+
const statusText = svc.status === 'up' ? '正常' : svc.status === 'down' ? '故障' : '未知';
|
| 2342 |
+
|
| 2343 |
+
// 生成心跳条
|
| 2344 |
+
let beats = '';
|
| 2345 |
+
const maxBeats = 60;
|
| 2346 |
+
const heartbeats = svc.heartbeats || [];
|
| 2347 |
+
for (let i = 0; i < maxBeats; i++) {
|
| 2348 |
+
if (i < heartbeats.length) {
|
| 2349 |
+
const beat = heartbeats[i];
|
| 2350 |
+
const status = beat.success ? '成功' : '失败';
|
| 2351 |
+
beats += `<div class="beat ${beat.success ? 'up' : 'down'}"><span class="tooltip">${beat.time} · ${status}</span></div>`;
|
| 2352 |
+
} else {
|
| 2353 |
+
beats += '<div class="beat empty"></div>';
|
| 2354 |
+
}
|
| 2355 |
+
}
|
| 2356 |
+
|
| 2357 |
+
html += `
|
| 2358 |
+
<div class="card">
|
| 2359 |
+
<div class="card-header">
|
| 2360 |
+
<span class="service-name">${svc.name}</span>
|
| 2361 |
+
<span class="status-badge ${statusClass}">${statusText}</span>
|
| 2362 |
+
</div>
|
| 2363 |
+
<div class="stats">
|
| 2364 |
+
<span>可用率 <span class="stat-value">${svc.uptime}%</span></span>
|
| 2365 |
+
<span>请求 <span class="stat-value">${svc.total}</span></span>
|
| 2366 |
+
<span>成功 <span class="stat-value">${svc.success}</span></span>
|
| 2367 |
+
</div>
|
| 2368 |
+
<div class="heartbeat-bar">${beats}</div>
|
| 2369 |
+
</div>
|
| 2370 |
+
`;
|
| 2371 |
+
}
|
| 2372 |
+
container.innerHTML = html;
|
| 2373 |
+
}
|
| 2374 |
+
|
| 2375 |
+
loadStatus();
|
| 2376 |
+
setInterval(loadStatus, 5000);
|
| 2377 |
+
</script>
|
| 2378 |
+
</body>
|
| 2379 |
+
</html>
|
| 2380 |
+
"""
|
| 2381 |
+
return HTMLResponse(content=html_content)
|