aws-runtime-bridge 1.9.1 → 1.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +5 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -4
- package/dist/index.js +20 -4
- package/dist/middleware/auth.d.ts +8 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +44 -0
- package/dist/routes/ai-sources.js +18 -14
- package/dist/routes/dashboard.d.ts +8 -0
- package/dist/routes/dashboard.d.ts.map +1 -0
- package/dist/routes/dashboard.js +1347 -0
- package/dist/routes/instance.d.ts +1 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +53 -4
- package/dist/routes/instance.test.js +4 -0
- package/dist/routes/processes.js +1 -1
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +37 -0
- package/dist/routes/sessions.js +7 -7
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +143 -33
- package/dist/services/agent-process-manager.d.ts +12 -0
- package/dist/services/agent-process-manager.d.ts.map +1 -1
- package/dist/services/agent-process-manager.js +47 -13
- package/dist/services/auto-register.d.ts.map +1 -1
- package/dist/services/auto-register.js +4 -5
- package/dist/services/mcp-launch-binding-queue.d.ts +5 -0
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
- package/dist/services/mcp-launch-binding-queue.js +17 -0
- package/dist/services/mcp-launch-binding-queue.test.js +16 -1
- package/dist/services/orphan-monitor.js +2 -2
- package/dist/services/panel-auth.d.ts +26 -0
- package/dist/services/panel-auth.d.ts.map +1 -0
- package/dist/services/panel-auth.js +105 -0
- package/dist/services/process-detector.d.ts +16 -9
- package/dist/services/process-detector.d.ts.map +1 -1
- package/dist/services/process-detector.js +136 -66
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +106 -6
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +20 -0
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +71 -2
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts +4 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/constants.js +5 -1
- package/package/aws-client-agent-mcp/dist/constants.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +17 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +124 -28
- package/package/aws-client-agent-mcp/dist/http-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js +108 -2
- package/package/aws-client-agent-mcp/dist/http-client.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +5 -0
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +53 -3
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +79 -28
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +35 -0
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +9 -0
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 仪表盘路由
|
|
3
|
+
*
|
|
4
|
+
* 提供 Bridge 状态看板的静态页面和数据聚合 API
|
|
5
|
+
* 支持中/英文界面,默认中文
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from "express";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import { getRegistrationState } from "../services/auto-register.js";
|
|
10
|
+
import { getRuntimeBindingPublicState, getRuntimePairingCode } from "../services/runtime-binding.js";
|
|
11
|
+
import { getAgentProcessManager } from "../services/agent-process-manager.js";
|
|
12
|
+
import { collectSystemMetrics } from "../services/system-metrics.js";
|
|
13
|
+
import { loadPersistedSessions } from "../services/terminal-persistence.js";
|
|
14
|
+
import { createLogger } from "../utils/logger.js";
|
|
15
|
+
import { getBridgeVersion } from "../config.js";
|
|
16
|
+
import { sdkSessions } from "./terminal.js";
|
|
17
|
+
import { createSession, validateSession, destroySession, validateCredentials, } from "../services/panel-auth.js";
|
|
18
|
+
const log = createLogger("dashboard");
|
|
19
|
+
// ── 内存缓存(两级) ─────────────────────────────────────
|
|
20
|
+
// 一级:完整响应缓存,匹配前端轮询间隔(10s),避免大部分请求走 I/O
|
|
21
|
+
let dashboardCache = null;
|
|
22
|
+
const CACHE_TTL = 10000; // 10 秒,匹配前端 refresh 间隔
|
|
23
|
+
// 二级:慢指标缓存(系统指标/进程扫描),需要 PowerShell WMI 调用(Windows 下尤其昂贵)
|
|
24
|
+
// 请求处理流程绝不等待慢操作——始终从缓存读取,后台定时+首次请求时双触发刷新
|
|
25
|
+
let metricsCache = null;
|
|
26
|
+
let processCache = null;
|
|
27
|
+
const SLOW_CACHE_TTL = 30000; // 30 秒
|
|
28
|
+
// ── 后台缓存预热 ─────────────────────────────────────────
|
|
29
|
+
// 启动时立即 + 每 30 秒后台刷新,确保慢指标永不阻塞请求
|
|
30
|
+
async function warmSlideCaches() {
|
|
31
|
+
collectSystemMetrics()
|
|
32
|
+
.then((data) => { metricsCache = { data, ts: Date.now() }; })
|
|
33
|
+
.catch(() => { });
|
|
34
|
+
getAgentProcessManager().discoverAllProcesses()
|
|
35
|
+
.then((data) => { processCache = { data, ts: Date.now() }; })
|
|
36
|
+
.catch(() => { });
|
|
37
|
+
// ★ 清理注册表中的陈旧记录(已终止且进程不再存活的条目)
|
|
38
|
+
getAgentProcessManager().cleanupStaleRegistry().catch(() => { });
|
|
39
|
+
}
|
|
40
|
+
// 立即执行首次预热
|
|
41
|
+
warmSlideCaches();
|
|
42
|
+
// 每 30 秒定期刷新
|
|
43
|
+
setInterval(warmSlideCaches, SLOW_CACHE_TTL);
|
|
44
|
+
const bridgeVersion = getBridgeVersion();
|
|
45
|
+
/**
|
|
46
|
+
* 从 lifecycle-state.json 中读取最新的一条生命周期操作记录。
|
|
47
|
+
* 用于给 dashboard 展示 update/restart 状态。
|
|
48
|
+
*/
|
|
49
|
+
async function loadLatestLifecycleOperation() {
|
|
50
|
+
const fs = await import("node:fs/promises");
|
|
51
|
+
const path = await import("node:path");
|
|
52
|
+
const os = await import("node:os");
|
|
53
|
+
const lifecycleFile = path.join(String(process.env.AWS_RUNTIME_HOME_DIR || os.homedir()).trim() || os.homedir(), ".agentswork", "runtime-bridge", "lifecycle-state.json");
|
|
54
|
+
try {
|
|
55
|
+
const raw = await fs.readFile(lifecycleFile, "utf8");
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
const ops = Array.isArray(parsed.operations) ? parsed.operations : [];
|
|
58
|
+
if (ops.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
// 按 updatedAt 降序取最新
|
|
61
|
+
ops.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
62
|
+
return ops[0] || null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export const dashboardRouter = Router();
|
|
69
|
+
// ── Cookie 解析 ────────────────────────────────────────────
|
|
70
|
+
const COOKIE_NAME = "panel_sid";
|
|
71
|
+
function parseCookies(cookieHeader) {
|
|
72
|
+
const result = {};
|
|
73
|
+
if (!cookieHeader)
|
|
74
|
+
return result;
|
|
75
|
+
for (const part of cookieHeader.split(";")) {
|
|
76
|
+
const idx = part.indexOf("=");
|
|
77
|
+
if (idx === -1)
|
|
78
|
+
continue;
|
|
79
|
+
const key = part.slice(0, idx).trim();
|
|
80
|
+
const val = part.slice(idx + 1).trim();
|
|
81
|
+
if (key)
|
|
82
|
+
result[key] = val;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function getSessionToken(req) {
|
|
87
|
+
const cookies = parseCookies(req.headers.cookie);
|
|
88
|
+
return cookies[COOKIE_NAME];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 面板认证中间件
|
|
92
|
+
* 检查请求是否携带有效的面板会话 cookie,无效时直接返回 401 JSON(API 请求)或重定向到登录页。
|
|
93
|
+
*/
|
|
94
|
+
function requirePanelAuth(req, res) {
|
|
95
|
+
const token = getSessionToken(req);
|
|
96
|
+
if (token && validateSession(token)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
// AJAX / API 请求:返回 JSON 401
|
|
100
|
+
const acceptsJson = req.xhr || req.headers.accept?.includes("application/json");
|
|
101
|
+
if (acceptsJson) {
|
|
102
|
+
res.status(401).json({ ok: false, error: "unauthorized", needLogin: true });
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
// 页面请求:重定向到登录页
|
|
106
|
+
res.redirect("/login");
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
// ── 登录/登出路由 ──────────────────────────────────────────
|
|
110
|
+
/** POST /runtime/login 登录验证 */
|
|
111
|
+
dashboardRouter.post("/runtime/login", (req, res) => {
|
|
112
|
+
const { username, password } = req.body || {};
|
|
113
|
+
if (!username || !password) {
|
|
114
|
+
res.status(400).json({ ok: false, error: "username and password required" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!validateCredentials(String(username), String(password))) {
|
|
118
|
+
res.status(401).json({ ok: false, error: "invalid credentials" });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const token = createSession();
|
|
122
|
+
// 设置 HttpOnly cookie,24 小时过期
|
|
123
|
+
res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400`);
|
|
124
|
+
res.json({ ok: true });
|
|
125
|
+
});
|
|
126
|
+
/** POST /runtime/logout 登出 */
|
|
127
|
+
dashboardRouter.post("/runtime/logout", (req, res) => {
|
|
128
|
+
const token = getSessionToken(req);
|
|
129
|
+
if (token) {
|
|
130
|
+
destroySession(token);
|
|
131
|
+
}
|
|
132
|
+
res.setHeader("Set-Cookie", `${COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`);
|
|
133
|
+
res.json({ ok: true });
|
|
134
|
+
});
|
|
135
|
+
/**
|
|
136
|
+
* GET /runtime/dashboard
|
|
137
|
+
* 聚合 bridge 所有状态数据。使用 2 秒缓存避免频繁 I/O,需要面板登录会话。
|
|
138
|
+
*/
|
|
139
|
+
dashboardRouter.get("/runtime/dashboard", async (req, res) => {
|
|
140
|
+
if (!requirePanelAuth(req, res))
|
|
141
|
+
return;
|
|
142
|
+
// 缓存命中
|
|
143
|
+
if (dashboardCache && Date.now() - dashboardCache.ts < CACHE_TTL) {
|
|
144
|
+
return void res.json(dashboardCache.data);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
// 慢指标始终从缓存读取(后台预热 + 首次 miss 时同步触发预热,但不阻塞请求)
|
|
148
|
+
const metrics = metricsCache?.data ?? null;
|
|
149
|
+
const processesData = processCache?.data ?? null;
|
|
150
|
+
// 若缓存尚未就绪(首次启动预热未完成),在后台触发一次填充,但不等待
|
|
151
|
+
if (!metricsCache) {
|
|
152
|
+
warmSlideCaches();
|
|
153
|
+
}
|
|
154
|
+
const [binding, registration, persistedSessions] = await Promise.all([
|
|
155
|
+
Promise.resolve(getRuntimeBindingPublicState()),
|
|
156
|
+
Promise.resolve(getRegistrationState()),
|
|
157
|
+
loadPersistedSessions().catch(() => []),
|
|
158
|
+
]);
|
|
159
|
+
const manager = getAgentProcessManager();
|
|
160
|
+
const registryRecords = manager.getAllManagedProcesses();
|
|
161
|
+
const activeSessions = Array.from(sdkSessions.entries()).map(([sessionId, entry]) => ({
|
|
162
|
+
sessionId,
|
|
163
|
+
agentId: entry.agentId,
|
|
164
|
+
workspacePath: entry.workspacePath,
|
|
165
|
+
runtimeStatus: entry.currentRuntimeStatus || "running",
|
|
166
|
+
runtimeActionType: entry.currentRuntimeActionType,
|
|
167
|
+
runtimeActionLabel: entry.currentRuntimeActionLabel,
|
|
168
|
+
providerId: entry.providerId,
|
|
169
|
+
}));
|
|
170
|
+
let processStats = { managed: 0, orphan: 0, unknown: 0 };
|
|
171
|
+
let processList = [];
|
|
172
|
+
if (processesData) {
|
|
173
|
+
processStats = {
|
|
174
|
+
managed: processesData.filter((p) => p.managementStatus === "managed").length,
|
|
175
|
+
orphan: processesData.filter((p) => p.managementStatus === "orphan").length,
|
|
176
|
+
unknown: processesData.filter((p) => p.managementStatus === "unknown").length,
|
|
177
|
+
};
|
|
178
|
+
const registryMap = new Map(registryRecords.map((r) => [r.pid, r]));
|
|
179
|
+
processList = processesData.map((p) => {
|
|
180
|
+
const record = registryMap.get(p.pid);
|
|
181
|
+
return {
|
|
182
|
+
agentId: p.agentId || record?.agentId,
|
|
183
|
+
pid: p.pid,
|
|
184
|
+
managementStatus: p.managementStatus,
|
|
185
|
+
sessionId: record?.sessionId,
|
|
186
|
+
state: record?.state,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
const sortOrder = { managed: 0, orphan: 1, unknown: 2 };
|
|
190
|
+
processList.sort((a, b) => (sortOrder[a.managementStatus] ?? 9) - (sortOrder[b.managementStatus] ?? 9));
|
|
191
|
+
}
|
|
192
|
+
const activeSessionIds = new Set(activeSessions.map((s) => s.sessionId));
|
|
193
|
+
const persistedOnly = persistedSessions.filter((s) => !activeSessionIds.has(s.sessionId));
|
|
194
|
+
const bindingPublic = binding;
|
|
195
|
+
const latestLifecycleOperation = await loadLatestLifecycleOperation();
|
|
196
|
+
const body = {
|
|
197
|
+
ok: true,
|
|
198
|
+
collectedAt: new Date().toISOString(),
|
|
199
|
+
bridge: {
|
|
200
|
+
version: bridgeVersion,
|
|
201
|
+
hostname: os.hostname(),
|
|
202
|
+
platform: os.platform(),
|
|
203
|
+
arch: os.arch(),
|
|
204
|
+
uptime: Math.floor(process.uptime()),
|
|
205
|
+
nodeVersion: process.version,
|
|
206
|
+
},
|
|
207
|
+
binding: {
|
|
208
|
+
status: bindingPublic.status,
|
|
209
|
+
paired: bindingPublic.paired,
|
|
210
|
+
instanceId: bindingPublic.instanceId,
|
|
211
|
+
userId: bindingPublic.userId,
|
|
212
|
+
schedulerBaseUrl: bindingPublic.schedulerBaseUrl,
|
|
213
|
+
pairingCode: getRuntimePairingCode(),
|
|
214
|
+
},
|
|
215
|
+
registration: {
|
|
216
|
+
registered: registration.registered,
|
|
217
|
+
instanceId: registration.instanceId,
|
|
218
|
+
lastAttempt: registration.lastAttempt?.toISOString(),
|
|
219
|
+
error: registration.error,
|
|
220
|
+
},
|
|
221
|
+
metrics,
|
|
222
|
+
sessions: {
|
|
223
|
+
active: activeSessions,
|
|
224
|
+
activeCount: activeSessions.length,
|
|
225
|
+
persisted: persistedOnly,
|
|
226
|
+
persistedCount: persistedOnly.length,
|
|
227
|
+
total: activeSessions.length + persistedOnly.length,
|
|
228
|
+
},
|
|
229
|
+
processes: {
|
|
230
|
+
...processStats,
|
|
231
|
+
total: processStats.managed + processStats.orphan + processStats.unknown,
|
|
232
|
+
list: processList,
|
|
233
|
+
registryCount: registryRecords.length,
|
|
234
|
+
},
|
|
235
|
+
lifecycleOperation: latestLifecycleOperation,
|
|
236
|
+
};
|
|
237
|
+
// 写入一级缓存(10 秒 TTL,匹配前端轮询间隔,避免连续请求都吃 I/O)
|
|
238
|
+
dashboardCache = { data: body, ts: Date.now() };
|
|
239
|
+
res.json(body);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const err = error;
|
|
243
|
+
log.error("Dashboard aggregation failed:", err.message);
|
|
244
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
/**
|
|
248
|
+
* GET /login 登录页面
|
|
249
|
+
*/
|
|
250
|
+
dashboardRouter.get("/login", (_req, res) => {
|
|
251
|
+
res.type("text/html; charset=utf-8");
|
|
252
|
+
res.send(LOGIN_HTML);
|
|
253
|
+
});
|
|
254
|
+
/**
|
|
255
|
+
* GET / 仪表盘主页
|
|
256
|
+
* 需要面板登录会话,未登录时跳转到 /login。
|
|
257
|
+
* 支持 ?lang=en 切换英文,默认中文
|
|
258
|
+
*/
|
|
259
|
+
dashboardRouter.get("/", (req, res) => {
|
|
260
|
+
const token = getSessionToken(req);
|
|
261
|
+
if (!token || !validateSession(token)) {
|
|
262
|
+
res.redirect("/login");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const lang = req.query.lang === "en" ? "en" : "zh";
|
|
266
|
+
res.type("text/html; charset=utf-8");
|
|
267
|
+
res.send(DASHBOARD_HTML.replace("{{LANG}}", lang));
|
|
268
|
+
});
|
|
269
|
+
// ======================================================================
|
|
270
|
+
// DASHBOARD HTML — 单页自包含,支持中文/英文界面,默认中文
|
|
271
|
+
// ======================================================================
|
|
272
|
+
const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
273
|
+
<html lang="{{LANG}}">
|
|
274
|
+
<head>
|
|
275
|
+
<meta charset="UTF-8">
|
|
276
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
277
|
+
<title>Bridge Dashboard</title>
|
|
278
|
+
<style>
|
|
279
|
+
:root {
|
|
280
|
+
--bg: #0f1117;
|
|
281
|
+
--surface: #1a1d2e;
|
|
282
|
+
--surface2: #232738;
|
|
283
|
+
--border: #2d3248;
|
|
284
|
+
--text: #e1e4f0;
|
|
285
|
+
--text2: #8b90a8;
|
|
286
|
+
--accent: #5b8def;
|
|
287
|
+
--accent2: #43c6a8;
|
|
288
|
+
--red: #f05454;
|
|
289
|
+
--orange: #f0a030;
|
|
290
|
+
--green: #43c6a8;
|
|
291
|
+
--green-bg: rgba(67,198,168,0.12);
|
|
292
|
+
--red-bg: rgba(240,84,84,0.12);
|
|
293
|
+
--orange-bg: rgba(240,160,48,0.12);
|
|
294
|
+
--radius: 10px;
|
|
295
|
+
}
|
|
296
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
297
|
+
body {
|
|
298
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
|
|
299
|
+
background: var(--bg);
|
|
300
|
+
color: var(--text);
|
|
301
|
+
padding: 24px;
|
|
302
|
+
min-height: 100vh;
|
|
303
|
+
}
|
|
304
|
+
.header {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
justify-content: space-between;
|
|
308
|
+
margin-bottom: 24px;
|
|
309
|
+
flex-wrap: wrap;
|
|
310
|
+
gap: 12px;
|
|
311
|
+
}
|
|
312
|
+
.header h1 {
|
|
313
|
+
font-size: 22px;
|
|
314
|
+
font-weight: 600;
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
gap: 10px;
|
|
318
|
+
}
|
|
319
|
+
.header h1 .dot {
|
|
320
|
+
display: inline-block;
|
|
321
|
+
width: 10px; height: 10px;
|
|
322
|
+
border-radius: 50%;
|
|
323
|
+
background: var(--green);
|
|
324
|
+
animation: pulse 2s ease-in-out infinite;
|
|
325
|
+
}
|
|
326
|
+
@keyframes pulse {
|
|
327
|
+
0%, 100% { opacity: 1; }
|
|
328
|
+
50% { opacity: 0.4; }
|
|
329
|
+
}
|
|
330
|
+
.header-right {
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
gap: 12px;
|
|
334
|
+
font-size: 13px;
|
|
335
|
+
color: var(--text2);
|
|
336
|
+
}
|
|
337
|
+
.header-right .refresh-time { color: var(--text2); }
|
|
338
|
+
.header-right button, .header-right .lang-btn {
|
|
339
|
+
background: var(--surface2);
|
|
340
|
+
border: 1px solid var(--border);
|
|
341
|
+
color: var(--text);
|
|
342
|
+
padding: 6px 14px;
|
|
343
|
+
border-radius: 6px;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
font-size: 13px;
|
|
346
|
+
transition: background .15s;
|
|
347
|
+
}
|
|
348
|
+
.header-right button:hover, .header-right .lang-btn:hover {
|
|
349
|
+
background: var(--border);
|
|
350
|
+
}
|
|
351
|
+
.lang-btn.active {
|
|
352
|
+
background: var(--accent);
|
|
353
|
+
border-color: var(--accent);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.grid {
|
|
357
|
+
display: grid;
|
|
358
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
359
|
+
gap: 16px;
|
|
360
|
+
margin-bottom: 20px;
|
|
361
|
+
}
|
|
362
|
+
.card {
|
|
363
|
+
background: var(--surface);
|
|
364
|
+
border: 1px solid var(--border);
|
|
365
|
+
border-radius: var(--radius);
|
|
366
|
+
padding: 18px;
|
|
367
|
+
}
|
|
368
|
+
.card-title {
|
|
369
|
+
font-size: 12px;
|
|
370
|
+
font-weight: 600;
|
|
371
|
+
text-transform: uppercase;
|
|
372
|
+
letter-spacing: 0.5px;
|
|
373
|
+
color: var(--text2);
|
|
374
|
+
margin-bottom: 12px;
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 8px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.stat-row {
|
|
381
|
+
display: flex;
|
|
382
|
+
justify-content: space-between;
|
|
383
|
+
align-items: center;
|
|
384
|
+
padding: 6px 0;
|
|
385
|
+
font-size: 13px;
|
|
386
|
+
border-bottom: 1px solid var(--border);
|
|
387
|
+
}
|
|
388
|
+
.stat-row:last-child { border-bottom: none; }
|
|
389
|
+
.stat-label { color: var(--text2); }
|
|
390
|
+
.stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
391
|
+
|
|
392
|
+
.status-green { color: var(--green); }
|
|
393
|
+
.status-orange { color: var(--orange); }
|
|
394
|
+
.status-red { color: var(--red); }
|
|
395
|
+
.status-muted { color: var(--text2); }
|
|
396
|
+
|
|
397
|
+
.section-title {
|
|
398
|
+
font-size: 15px;
|
|
399
|
+
font-weight: 600;
|
|
400
|
+
margin: 24px 0 12px;
|
|
401
|
+
display: flex;
|
|
402
|
+
align-items: center;
|
|
403
|
+
gap: 8px;
|
|
404
|
+
}
|
|
405
|
+
.section-title .count {
|
|
406
|
+
font-size: 12px;
|
|
407
|
+
color: var(--text2);
|
|
408
|
+
font-weight: 400;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.table-wrap {
|
|
412
|
+
background: var(--surface);
|
|
413
|
+
border: 1px solid var(--border);
|
|
414
|
+
border-radius: var(--radius);
|
|
415
|
+
overflow-x: auto;
|
|
416
|
+
}
|
|
417
|
+
table {
|
|
418
|
+
width: 100%;
|
|
419
|
+
border-collapse: collapse;
|
|
420
|
+
font-size: 13px;
|
|
421
|
+
}
|
|
422
|
+
th {
|
|
423
|
+
text-align: left;
|
|
424
|
+
padding: 10px 14px;
|
|
425
|
+
font-size: 11px;
|
|
426
|
+
font-weight: 600;
|
|
427
|
+
text-transform: uppercase;
|
|
428
|
+
letter-spacing: 0.5px;
|
|
429
|
+
color: var(--text2);
|
|
430
|
+
background: var(--surface2);
|
|
431
|
+
border-bottom: 1px solid var(--border);
|
|
432
|
+
white-space: nowrap;
|
|
433
|
+
}
|
|
434
|
+
td {
|
|
435
|
+
padding: 9px 14px;
|
|
436
|
+
border-bottom: 1px solid var(--border);
|
|
437
|
+
white-space: nowrap;
|
|
438
|
+
}
|
|
439
|
+
tr:last-child td { border-bottom: none; }
|
|
440
|
+
tr:hover { background: rgba(255,255,255,0.03); }
|
|
441
|
+
|
|
442
|
+
.bar-wrap {
|
|
443
|
+
display: flex;
|
|
444
|
+
align-items: center;
|
|
445
|
+
gap: 8px;
|
|
446
|
+
}
|
|
447
|
+
.bar {
|
|
448
|
+
flex: 1;
|
|
449
|
+
height: 6px;
|
|
450
|
+
background: var(--surface2);
|
|
451
|
+
border-radius: 3px;
|
|
452
|
+
overflow: hidden;
|
|
453
|
+
}
|
|
454
|
+
.bar-fill {
|
|
455
|
+
height: 100%;
|
|
456
|
+
border-radius: 3px;
|
|
457
|
+
transition: width 0.5s ease;
|
|
458
|
+
}
|
|
459
|
+
.bar-fill.green { background: var(--green); }
|
|
460
|
+
.bar-fill.orange { background: var(--orange); }
|
|
461
|
+
.bar-fill.red { background: var(--red); }
|
|
462
|
+
.bar-label { font-size: 12px; min-width: 48px; text-align: right; font-variant-numeric: tabular-nums; }
|
|
463
|
+
|
|
464
|
+
.tag {
|
|
465
|
+
display: inline-block;
|
|
466
|
+
padding: 1px 8px;
|
|
467
|
+
border-radius: 4px;
|
|
468
|
+
font-size: 11px;
|
|
469
|
+
font-weight: 600;
|
|
470
|
+
}
|
|
471
|
+
.tag.managed { background: var(--green-bg); color: var(--green); }
|
|
472
|
+
.tag.orphan { background: var(--orange-bg); color: var(--orange); }
|
|
473
|
+
.tag.unknown { background: rgba(139,144,168,0.15); color: var(--text2); }
|
|
474
|
+
|
|
475
|
+
.empty { color: var(--text2); text-align: center; padding: 24px; font-size: 13px; }
|
|
476
|
+
|
|
477
|
+
.toggle-header { cursor: pointer; user-select: none; }
|
|
478
|
+
.toggle-header:hover { opacity: 0.8; }
|
|
479
|
+
.toggle-icon { display: inline-block; transition: transform .2s; font-size: 10px; margin-right: 4px; }
|
|
480
|
+
.toggle-icon.open { transform: rotate(90deg); }
|
|
481
|
+
|
|
482
|
+
.orphan-banner {
|
|
483
|
+
background: var(--orange-bg);
|
|
484
|
+
border: 1px solid var(--orange);
|
|
485
|
+
border-radius: var(--radius);
|
|
486
|
+
padding: 12px 16px;
|
|
487
|
+
margin-bottom: 12px;
|
|
488
|
+
font-size: 13px;
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 10px;
|
|
492
|
+
}
|
|
493
|
+
.orphan-banner .icon { font-size: 16px; }
|
|
494
|
+
.orphan-banner .text { flex: 1; }
|
|
495
|
+
.orphan-banner .action-hint { color: var(--orange); font-weight: 600; }
|
|
496
|
+
|
|
497
|
+
.action-btn {
|
|
498
|
+
background: var(--surface2);
|
|
499
|
+
border: 1px solid var(--border);
|
|
500
|
+
color: var(--text);
|
|
501
|
+
padding: 2px 10px;
|
|
502
|
+
border-radius: 4px;
|
|
503
|
+
cursor: pointer;
|
|
504
|
+
font-size: 11px;
|
|
505
|
+
transition: background .15s;
|
|
506
|
+
}
|
|
507
|
+
.action-btn:hover { background: var(--border); }
|
|
508
|
+
.action-btn.terminate { border-color: var(--red); color: var(--red); }
|
|
509
|
+
.action-btn.terminate:hover { background: var(--red-bg); }
|
|
510
|
+
|
|
511
|
+
.summary-line {
|
|
512
|
+
display: flex;
|
|
513
|
+
gap: 16px;
|
|
514
|
+
align-items: center;
|
|
515
|
+
margin-bottom: 12px;
|
|
516
|
+
font-size: 13px;
|
|
517
|
+
flex-wrap: wrap;
|
|
518
|
+
}
|
|
519
|
+
.summary-line .item { display: flex; align-items: center; gap: 4px; }
|
|
520
|
+
.summary-line .dot {
|
|
521
|
+
display: inline-block;
|
|
522
|
+
width: 8px; height: 8px;
|
|
523
|
+
border-radius: 50%;
|
|
524
|
+
}
|
|
525
|
+
.summary-line .dot.green { background: var(--green); }
|
|
526
|
+
.summary-line .dot.orange { background: var(--orange); }
|
|
527
|
+
.summary-line .dot.gray { background: var(--text2); }
|
|
528
|
+
|
|
529
|
+
.version-area {
|
|
530
|
+
display: inline-flex;
|
|
531
|
+
align-items: center;
|
|
532
|
+
gap: 6px;
|
|
533
|
+
font-size: 13px;
|
|
534
|
+
color: var(--text2);
|
|
535
|
+
}
|
|
536
|
+
.update-indicator {
|
|
537
|
+
font-size: 12px;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
white-space: nowrap;
|
|
540
|
+
transition: color .15s;
|
|
541
|
+
}
|
|
542
|
+
.update-indicator:hover { opacity: 0.8; }
|
|
543
|
+
.update-indicator.available { color: var(--accent); }
|
|
544
|
+
.update-indicator.installing { color: var(--orange); cursor: default; }
|
|
545
|
+
.update-indicator.success { color: var(--green); }
|
|
546
|
+
.update-indicator.failed { color: var(--red); }
|
|
547
|
+
|
|
548
|
+
@media (max-width: 640px) {
|
|
549
|
+
body { padding: 12px; }
|
|
550
|
+
.grid { grid-template-columns: 1fr; }
|
|
551
|
+
th, td { padding: 7px 10px; }
|
|
552
|
+
.header h1 { font-size: 18px; }
|
|
553
|
+
}
|
|
554
|
+
</style>
|
|
555
|
+
</head>
|
|
556
|
+
<body>
|
|
557
|
+
|
|
558
|
+
<div class="header">
|
|
559
|
+
<h1><span class="dot"></span>Bridge Dashboard</h1>
|
|
560
|
+
<div class="header-right">
|
|
561
|
+
<span class="refresh-time" id="refreshTime">-</span>
|
|
562
|
+
<span id="bridgeHostname">-</span>
|
|
563
|
+
<span class="version-area">
|
|
564
|
+
<span id="bridgeVersion">v-</span>
|
|
565
|
+
<span id="updateIndicator" style="display:none"></span>
|
|
566
|
+
</span>
|
|
567
|
+
<span class="lang-btn" id="langBtn" onclick="toggleLang()">中文</span>
|
|
568
|
+
<button onclick="refresh()" id="refreshBtn">↻</button>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<div class="grid" id="statusCards"></div>
|
|
573
|
+
|
|
574
|
+
<div class="section-title" id="sectionSysRes">System Resources <span class="count" id="sysMetricsCount"></span></div>
|
|
575
|
+
<div class="grid" id="metricsGrid"></div>
|
|
576
|
+
|
|
577
|
+
<!-- Summary badges for process categories -->
|
|
578
|
+
<div class="summary-line" id="summaryLine">
|
|
579
|
+
<span class="item"><span class="dot green"></span> <span id="summaryManaged">0 managed</span></span>
|
|
580
|
+
<span class="item"><span class="dot orange"></span> <span id="summaryOrphan">0 orphan</span></span>
|
|
581
|
+
<span class="item"><span class="dot gray"></span> <span id="summaryUnknown">0 unknown</span></span>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<!-- Active Sessions (primary table, includes PID from process list) -->
|
|
585
|
+
<div class="section-title" id="sectionSessions">Active Sessions <span class="count" id="sessionCount"></span></div>
|
|
586
|
+
<div class="table-wrap" id="sessionTable"><div class="empty" id="loadingSessions">Loading...</div></div>
|
|
587
|
+
|
|
588
|
+
<!-- Orphan Processes (shown only when orphans exist) -->
|
|
589
|
+
<div id="orphanContainer" style="display:none">
|
|
590
|
+
<div class="section-title" id="sectionOrphans">Orphan Processes <span class="count" id="orphanCount"></span></div>
|
|
591
|
+
<div class="orphan-banner">
|
|
592
|
+
<span class="icon">⚠</span>
|
|
593
|
+
<span class="text" id="orphanBannerText">These processes are running but no longer managed by bridge</span>
|
|
594
|
+
<span class="action-hint">Terminate to clean up</span>
|
|
595
|
+
</div>
|
|
596
|
+
<div class="table-wrap" id="orphanTable"><div class="empty">No orphans</div></div>
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
<!-- Unknown Processes (collapsible, hidden by default) -->
|
|
600
|
+
<div id="unknownContainer">
|
|
601
|
+
<div class="section-title toggle-header" id="sectionUnknown" onclick="toggleUnknown()">
|
|
602
|
+
<span class="toggle-icon" id="unknownIcon">▶</span><span id="unknownLabel"> Other processes</span> <span class="count" id="unknownCount"></span>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="table-wrap" id="unknownTable" style="display:none"><div class="empty" id="loadingUnknown">Loading...</div></div>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<!-- Persisted Sessions -->
|
|
608
|
+
<div class="section-title" id="sectionPersisted">Persisted Sessions <span class="count" id="persistedCount"></span></div>
|
|
609
|
+
<div class="table-wrap" id="persistedTable"><div class="empty" id="loadingPersisted">Loading...</div></div>
|
|
610
|
+
|
|
611
|
+
<script>
|
|
612
|
+
// ── 多语言翻译表 ──────────────────────────────────────────
|
|
613
|
+
const T = {
|
|
614
|
+
zh: {
|
|
615
|
+
langName: "English",
|
|
616
|
+
refreshBtn: "\u21bb \u5237\u65b0",
|
|
617
|
+
loading: "\u52a0\u8f7d\u4e2d...",
|
|
618
|
+
noProcesses: "\u65e0\u8fdb\u7a0b",
|
|
619
|
+
noSessions: "\u65e0\u6d3b\u8dc3\u4f1a\u8bdd",
|
|
620
|
+
noPersisted: "\u65e0\u6301\u4e45\u5316\u4f1a\u8bdd",
|
|
621
|
+
metricsUnavailable: "\u7cfb\u7edf\u6307\u6807\u4e0d\u53ef\u7528",
|
|
622
|
+
managed: "\u7ba1\u7406",
|
|
623
|
+
orphan: "\u5b64\u513f",
|
|
624
|
+
managedLabel: "\u7ba1\u7406\u4e2d",
|
|
625
|
+
// 状态卡片
|
|
626
|
+
cardBridge: "Bridge",
|
|
627
|
+
cardRegistration: "\u6ce8\u518c",
|
|
628
|
+
cardBinding: "\u7ed1\u5b9a",
|
|
629
|
+
cardSessions: "\u4f1a\u8bdd\u4e0e\u8fdb\u7a0b",
|
|
630
|
+
fldVersion: "\u7248\u672c",
|
|
631
|
+
fldHost: "\u4e3b\u673a",
|
|
632
|
+
fldPlatform: "\u5e73\u53f0",
|
|
633
|
+
fldUptime: "\u8fd0\u884c\u65f6\u957f",
|
|
634
|
+
fldNode: "Node",
|
|
635
|
+
fldStatus: "\u72b6\u6001",
|
|
636
|
+
fldInstanceId: "\u5b9e\u4f8b ID",
|
|
637
|
+
fldLastAttempt: "\u4e0a\u6b21\u5c1d\u8bd5",
|
|
638
|
+
fldError: "\u9519\u8bef",
|
|
639
|
+
fldScheduler: "\u8c03\u5ea6\u5668",
|
|
640
|
+
fldPairingCode: "\u5339\u914d\u7801",
|
|
641
|
+
fldActiveSessions: "\u6d3b\u8dc3\u4f1a\u8bdd",
|
|
642
|
+
fldPersistedSessions: "\u6301\u4e45\u5316\u4f1a\u8bdd",
|
|
643
|
+
fldManagedProcesses: "\u7ba1\u7406\u8fdb\u7a0b",
|
|
644
|
+
fldOrphanProcesses: "\u5b64\u513f\u8fdb\u7a0b",
|
|
645
|
+
fldRegistryEntries: "\u6ce8\u518c\u8868\u8bb0\u5f55",
|
|
646
|
+
// 资源
|
|
647
|
+
secSysRes: "\u7cfb\u7edf\u8d44\u6e90",
|
|
648
|
+
secSessions: "\u6d3b\u8dc3\u4f1a\u8bdd",
|
|
649
|
+
secPersisted: "\u6301\u4e45\u5316\u4f1a\u8bdd",
|
|
650
|
+
secOrphans: "\u5b64\u513f\u8fdb\u7a0b",
|
|
651
|
+
secUnknown: "\u5176\u4ed6\u8fdb\u7a0b",
|
|
652
|
+
orphanBanner: "\u4ee5\u4e0b\u8fdb\u7a0b\u4ecd\u5728\u8fd0\u884c\u4f46\u8131\u79bb\u4e86 bridge \u7ba1\u7406",
|
|
653
|
+
btnTerminate: "\u7ec8\u6b62",
|
|
654
|
+
showUnknown: "\u663e\u793a",
|
|
655
|
+
hideUnknown: "\u9690\u85cf",
|
|
656
|
+
noOrphans: "\u65e0\u5b64\u513f\u8fdb\u7a0b",
|
|
657
|
+
fldUsage: "\u4f7f\u7528\u7387",
|
|
658
|
+
fldCores: "\u5185\u6838\u6570",
|
|
659
|
+
fldModel: "\u578b\u53f7",
|
|
660
|
+
fldUsed: "\u5df2\u7528",
|
|
661
|
+
fldFree: "\u5269\u4f59",
|
|
662
|
+
fldTotal: "\u603b\u8ba1",
|
|
663
|
+
fldDownload: "\u4e0b\u884c",
|
|
664
|
+
fldUpload: "\u4e0a\u884c",
|
|
665
|
+
fldTotalDown: "\u603b\u4e0b\u884c",
|
|
666
|
+
fldTotalUp: "\u603b\u4e0a\u884c",
|
|
667
|
+
// 表格
|
|
668
|
+
thAgentId: "Agent ID",
|
|
669
|
+
thPid: "PID",
|
|
670
|
+
thStatus: "\u72b6\u6001",
|
|
671
|
+
thSessionId: "\u4f1a\u8bdd ID",
|
|
672
|
+
thState: "\u72b6\u6001",
|
|
673
|
+
thAction: "\u884c\u52a8",
|
|
674
|
+
thWorkspace: "\u5de5\u4f5c\u533a",
|
|
675
|
+
thProvider: "\u63d0\u4f9b\u65b9",
|
|
676
|
+
thCommand: "\u547d\u4ee4",
|
|
677
|
+
thStarted: "\u5f00\u59cb\u65f6\u95f4",
|
|
678
|
+
countManaged: "\u7ba1\u7406\u4e2d",
|
|
679
|
+
// 状态文本
|
|
680
|
+
stHealthy: "\u5065\u5eb7",
|
|
681
|
+
stError: "\u5f02\u5e38",
|
|
682
|
+
stRunning: "\u8fd0\u884c\u4e2d",
|
|
683
|
+
stStopped: "\u5df2\u505c\u6b62",
|
|
684
|
+
stPaired: "\u5df2\u7ed1\u5b9a",
|
|
685
|
+
stUnpaired: "\u672a\u7ed1\u5b9a",
|
|
686
|
+
stRegistered: "\u5df2\u6ce8\u518c",
|
|
687
|
+
stNotRegistered: "\u672a\u6ce8\u518c",
|
|
688
|
+
stWarning: "\u8b66\u544a",
|
|
689
|
+
stNone: "\u65e0",
|
|
690
|
+
},
|
|
691
|
+
en: {
|
|
692
|
+
langName: "\u4e2d\u6587",
|
|
693
|
+
refreshBtn: "\u21bb Refresh",
|
|
694
|
+
loading: "Loading...",
|
|
695
|
+
noProcesses: "No processes",
|
|
696
|
+
noSessions: "No active sessions",
|
|
697
|
+
noPersisted: "No persisted sessions",
|
|
698
|
+
metricsUnavailable: "System metrics unavailable",
|
|
699
|
+
managed: "managed",
|
|
700
|
+
orphan: "orphan",
|
|
701
|
+
managedLabel: "managed",
|
|
702
|
+
cardBridge: "Bridge",
|
|
703
|
+
cardRegistration: "Registration",
|
|
704
|
+
cardBinding: "Binding",
|
|
705
|
+
cardSessions: "Sessions & Processes",
|
|
706
|
+
fldVersion: "Version",
|
|
707
|
+
fldHost: "Host",
|
|
708
|
+
fldPlatform: "Platform",
|
|
709
|
+
fldUptime: "Uptime",
|
|
710
|
+
fldNode: "Node",
|
|
711
|
+
fldStatus: "Status",
|
|
712
|
+
fldInstanceId: "Instance ID",
|
|
713
|
+
fldLastAttempt: "Last Attempt",
|
|
714
|
+
fldError: "Error",
|
|
715
|
+
fldScheduler: "Scheduler",
|
|
716
|
+
fldPairingCode: "Pairing Code",
|
|
717
|
+
fldActiveSessions: "Active Sessions",
|
|
718
|
+
fldPersistedSessions: "Persisted Sessions",
|
|
719
|
+
fldManagedProcesses: "Managed Processes",
|
|
720
|
+
fldOrphanProcesses: "Orphan Processes",
|
|
721
|
+
fldRegistryEntries: "Registry Entries",
|
|
722
|
+
secSysRes: "System Resources",
|
|
723
|
+
secSessions: "Active Sessions",
|
|
724
|
+
secPersisted: "Persisted Sessions",
|
|
725
|
+
secOrphans: "Orphan Processes",
|
|
726
|
+
secUnknown: "Other Processes",
|
|
727
|
+
orphanBanner: "These processes are running but no longer managed by bridge",
|
|
728
|
+
btnTerminate: "Terminate",
|
|
729
|
+
showUnknown: "Show",
|
|
730
|
+
hideUnknown: "Hide",
|
|
731
|
+
noOrphans: "No orphan processes",
|
|
732
|
+
fldUsage: "Usage",
|
|
733
|
+
fldCores: "Cores",
|
|
734
|
+
fldModel: "Model",
|
|
735
|
+
fldUsed: "Used",
|
|
736
|
+
fldFree: "Free",
|
|
737
|
+
fldTotal: "Total",
|
|
738
|
+
fldDownload: "Download",
|
|
739
|
+
fldUpload: "Upload",
|
|
740
|
+
fldTotalDown: "Total Down",
|
|
741
|
+
fldTotalUp: "Total Up",
|
|
742
|
+
thAgentId: "Agent ID",
|
|
743
|
+
thPid: "PID",
|
|
744
|
+
thStatus: "Status",
|
|
745
|
+
thSessionId: "Session ID",
|
|
746
|
+
thState: "State",
|
|
747
|
+
thAction: "Action",
|
|
748
|
+
thWorkspace: "Workspace",
|
|
749
|
+
thProvider: "Provider",
|
|
750
|
+
thCommand: "Command",
|
|
751
|
+
thStarted: "Started",
|
|
752
|
+
countManaged: "managed",
|
|
753
|
+
stHealthy: "Healthy",
|
|
754
|
+
stError: "Error",
|
|
755
|
+
stRunning: "Running",
|
|
756
|
+
stStopped: "Stopped",
|
|
757
|
+
stPaired: "Paired",
|
|
758
|
+
stUnpaired: "Unpaired",
|
|
759
|
+
stRegistered: "Registered",
|
|
760
|
+
stNotRegistered: "Not Registered",
|
|
761
|
+
stWarning: "Warning",
|
|
762
|
+
stNone: "None",
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// ── 语言偏好 ──────────────────────────────────────────────
|
|
767
|
+
let lang = localStorage.getItem("dashboard-lang");
|
|
768
|
+
if (!lang) {
|
|
769
|
+
// url 参数优先,其次浏览器语言,默认 zh
|
|
770
|
+
const urlLang = new URLSearchParams(location.search).get("lang");
|
|
771
|
+
lang = urlLang === "en" ? "en" : navigator.language.startsWith("zh") ? "zh" : "en";
|
|
772
|
+
}
|
|
773
|
+
localStorage.setItem("dashboard-lang", lang);
|
|
774
|
+
document.documentElement.lang = lang === "en" ? "en" : "zh-CN";
|
|
775
|
+
|
|
776
|
+
function t(k) {
|
|
777
|
+
var _a;
|
|
778
|
+
return ((_a = T[lang]) === null || _a === void 0 ? void 0 : _a[k]) || T.zh[k] || k;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function toggleLang() {
|
|
782
|
+
lang = lang === "zh" ? "en" : "zh";
|
|
783
|
+
localStorage.setItem("dashboard-lang", lang);
|
|
784
|
+
document.documentElement.lang = lang === "en" ? "en" : "zh-CN";
|
|
785
|
+
document.getElementById("langBtn").textContent = t("langName");
|
|
786
|
+
if (lastData) render(lastData);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ── 工具函数 ──────────────────────────────────────────────
|
|
790
|
+
let lastData = null;
|
|
791
|
+
|
|
792
|
+
function fmtPercent(v) {
|
|
793
|
+
if (v == null || !Number.isFinite(v)) return "-";
|
|
794
|
+
return v.toFixed(1) + "%";
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function fmtBytes(b) {
|
|
798
|
+
if (!b || !Number.isFinite(b)) return "-";
|
|
799
|
+
if (b < 1024) return b + " B";
|
|
800
|
+
if (b < 1024*1024) return (b / 1024).toFixed(1) + " KB";
|
|
801
|
+
if (b < 1024*1024*1024) return (b / (1024*1024)).toFixed(1) + " MB";
|
|
802
|
+
return (b / (1024*1024*1024)).toFixed(2) + " GB";
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function fmtDuration(seconds) {
|
|
806
|
+
if (!seconds || seconds < 0) return "-";
|
|
807
|
+
const d = Math.floor(seconds / 86400);
|
|
808
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
809
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
810
|
+
const s = seconds % 60;
|
|
811
|
+
const parts = [];
|
|
812
|
+
if (d > 0) parts.push(d + "d");
|
|
813
|
+
if (h > 0) parts.push(h + "h");
|
|
814
|
+
if (m > 0) parts.push(m + "m");
|
|
815
|
+
parts.push(s + "s");
|
|
816
|
+
return parts.join(" ");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function statusClass(v) {
|
|
820
|
+
if (v === true || v === "healthy" || v === "paired" || v === "running" || v === "managed" || v === "registered") return "status-green";
|
|
821
|
+
if (v === false || v === "error" || v === "unpaired" || v === "stopped" || v === "orphan") return v === "orphan" ? "status-orange" : "status-red";
|
|
822
|
+
if (v === "warning" || v === "unknown") return "status-orange";
|
|
823
|
+
return "status-muted";
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function statusText(v) {
|
|
827
|
+
if (v === true) return t("stHealthy");
|
|
828
|
+
if (v === "healthy") return t("stHealthy");
|
|
829
|
+
if (v === false) return t("stError");
|
|
830
|
+
if (v === "paired") return t("stPaired");
|
|
831
|
+
if (v === "unpaired") return t("stUnpaired");
|
|
832
|
+
if (v === "running") return t("stRunning");
|
|
833
|
+
if (v === "stopped") return t("stStopped");
|
|
834
|
+
if (v === "registered") return t("stRegistered");
|
|
835
|
+
if (v === "warning") return t("stWarning");
|
|
836
|
+
return String(v ?? "-");
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function barColor(pct) {
|
|
840
|
+
if (pct < 60) return "green";
|
|
841
|
+
if (pct < 85) return "orange";
|
|
842
|
+
return "red";
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ── 渲染 ──────────────────────────────────────────────────
|
|
846
|
+
function render(data) {
|
|
847
|
+
lastData = data;
|
|
848
|
+
document.getElementById("refreshTime").textContent = new Date(data.collectedAt).toLocaleString();
|
|
849
|
+
document.getElementById("bridgeHostname").textContent = data.bridge?.hostname || "-";
|
|
850
|
+
document.getElementById("bridgeVersion").textContent = "v" + (data.bridge?.version || "-");
|
|
851
|
+
document.getElementById("langBtn").textContent = t("langName");
|
|
852
|
+
document.getElementById("refreshBtn").textContent = t("refreshBtn");
|
|
853
|
+
|
|
854
|
+
// Section titles
|
|
855
|
+
document.getElementById("sectionSysRes").firstChild.textContent = t("secSysRes");
|
|
856
|
+
document.getElementById("sectionSessions").firstChild.textContent = t("secSessions");
|
|
857
|
+
document.getElementById("unknownLabel").textContent = " " + t("secUnknown");
|
|
858
|
+
document.getElementById("sectionOrphans").firstChild.textContent = t("secOrphans");
|
|
859
|
+
document.getElementById("sectionPersisted").firstChild.textContent = t("secPersisted");
|
|
860
|
+
document.getElementById("orphanBannerText").textContent = t("orphanBanner");
|
|
861
|
+
if (document.getElementById("unknownTable").style.display !== "none") {
|
|
862
|
+
document.getElementById("unknownIcon").classList.add("open");
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Status cards
|
|
866
|
+
const b = data.binding || {};
|
|
867
|
+
const r = data.registration || {};
|
|
868
|
+
const cards = [
|
|
869
|
+
{ title: t("cardBridge"), items: [
|
|
870
|
+
[t("fldVersion"), data.bridge?.version || "-"],
|
|
871
|
+
[t("fldHost"), data.bridge?.hostname || "-"],
|
|
872
|
+
[t("fldPlatform"), (data.bridge?.platform || "") + " " + (data.bridge?.arch || "")],
|
|
873
|
+
[t("fldUptime"), fmtDuration(data.bridge?.uptime)],
|
|
874
|
+
[t("fldNode"), data.bridge?.nodeVersion || "-"],
|
|
875
|
+
]},
|
|
876
|
+
{ title: t("cardRegistration"), items: [
|
|
877
|
+
[t("fldStatus"), '<span class="' + statusClass(r.registered) + '">' + (r.registered ? t("stRegistered") : t("stNotRegistered")) + "</span>"],
|
|
878
|
+
[t("fldInstanceId"), r.instanceId || "-"],
|
|
879
|
+
[t("fldLastAttempt"), r.lastAttempt ? new Date(r.lastAttempt).toLocaleString() : "-"],
|
|
880
|
+
[t("fldError"), r.error ? '<span class="status-red">' + r.error + "</span>" : t("stNone")],
|
|
881
|
+
]},
|
|
882
|
+
{ title: t("cardBinding"), items: [
|
|
883
|
+
[t("fldStatus"), '<span class="' + statusClass(b.paired) + '">' + (b.paired ? t("stPaired") : t("stUnpaired")) + "</span>"],
|
|
884
|
+
[t("fldInstanceId"), b.instanceId || "-"],
|
|
885
|
+
[t("fldScheduler"), b.schedulerBaseUrl || "-"],
|
|
886
|
+
[t("fldPairingCode"), b.pairingCode || "-"],
|
|
887
|
+
]},
|
|
888
|
+
{ title: t("cardSessions"), items: [
|
|
889
|
+
[t("fldActiveSessions"), '<span class="' + ((data.sessions?.activeCount ?? 0) > 0 ? "status-green" : "status-muted") + '">' + (data.sessions?.activeCount ?? 0) + "</span>"],
|
|
890
|
+
[t("fldPersistedSessions"), (data.sessions?.persistedCount ?? 0)],
|
|
891
|
+
[t("fldManagedProcesses"), data.processes?.managed ?? 0],
|
|
892
|
+
[t("fldOrphanProcesses"), '<span class="' + ((data.processes?.orphan ?? 0) > 0 ? "status-orange" : "status-muted") + '">' + (data.processes?.orphan ?? 0) + "</span>"],
|
|
893
|
+
[t("fldRegistryEntries"), data.processes?.registryCount ?? 0],
|
|
894
|
+
]},
|
|
895
|
+
];
|
|
896
|
+
|
|
897
|
+
document.getElementById("statusCards").innerHTML = cards.map(c => {
|
|
898
|
+
const rows = c.items.map(([k, v]) =>
|
|
899
|
+
'<div class="stat-row"><span class="stat-label">' + k + '</span><span class="stat-value">' + v + '</span></div>'
|
|
900
|
+
).join("");
|
|
901
|
+
return '<div class="card"><div class="card-title">' + c.title + '</div>' + rows + '</div>';
|
|
902
|
+
}).join("");
|
|
903
|
+
|
|
904
|
+
// Metrics
|
|
905
|
+
const m = data.metrics;
|
|
906
|
+
const metricsGrid = document.getElementById("metricsGrid");
|
|
907
|
+
if (m && m.ok) {
|
|
908
|
+
const cpuPct = m.cpu?.usagePercent ?? 0;
|
|
909
|
+
const memPct = m.memory?.usagePercent ?? 0;
|
|
910
|
+
const storagePct = m.storage?.usagePercent ?? 0;
|
|
911
|
+
metricsGrid.innerHTML = [
|
|
912
|
+
{ title: "CPU", usage: cpuPct, lines: [
|
|
913
|
+
[t("fldUsage"), fmtPercent(cpuPct)],
|
|
914
|
+
[t("fldCores"), m.cpu?.coreCount ?? "-"],
|
|
915
|
+
[t("fldModel"), (m.cpu?.model || "-").substring(0, 40)],
|
|
916
|
+
]},
|
|
917
|
+
{ title: "Memory", usage: memPct, lines: [
|
|
918
|
+
[t("fldUsage"), fmtPercent(memPct)],
|
|
919
|
+
[t("fldUsed"), fmtBytes(m.memory?.usedBytes)],
|
|
920
|
+
[t("fldFree"), fmtBytes(m.memory?.freeBytes)],
|
|
921
|
+
[t("fldTotal"), fmtBytes(m.memory?.totalBytes)],
|
|
922
|
+
]},
|
|
923
|
+
{ title: "Storage (" + (m.storage?.path || "") + ")", usage: storagePct, lines: [
|
|
924
|
+
[t("fldUsage"), fmtPercent(storagePct)],
|
|
925
|
+
[t("fldFree"), m.storage ? m.storage.freeGiB + " GiB" : "-"],
|
|
926
|
+
[t("fldTotal"), m.storage ? m.storage.totalGiB + " GiB" : "-"],
|
|
927
|
+
]},
|
|
928
|
+
{ title: "Network", usage: null, lines: [
|
|
929
|
+
[t("fldDownload"), fmtBytes(m.network?.downloadBytesPerSecond) + "/s"],
|
|
930
|
+
[t("fldUpload"), fmtBytes(m.network?.uploadBytesPerSecond) + "/s"],
|
|
931
|
+
[t("fldTotalDown"), fmtBytes(m.network?.downloadTotalBytes)],
|
|
932
|
+
[t("fldTotalUp"), fmtBytes(m.network?.uploadTotalBytes)],
|
|
933
|
+
]},
|
|
934
|
+
].map(c => {
|
|
935
|
+
const barHtml = c.usage != null
|
|
936
|
+
? '<div class="bar-wrap"><div class="bar"><div class="bar-fill ' + barColor(c.usage) + '" style="width:' + Math.min(c.usage, 100) + '%"></div></div><span class="bar-label">' + fmtPercent(c.usage) + '</span></div>'
|
|
937
|
+
: "";
|
|
938
|
+
const rows = c.lines.map(([k, v]) =>
|
|
939
|
+
'<div class="stat-row"><span class="stat-label">' + k + '</span><span class="stat-value">' + v + '</span></div>'
|
|
940
|
+
).join("");
|
|
941
|
+
return '<div class="card"><div class="card-title">' + c.title + '</div>' + barHtml + rows + '</div>';
|
|
942
|
+
}).join("");
|
|
943
|
+
} else {
|
|
944
|
+
metricsGrid.innerHTML = '<div class="card"><div class="empty">' + t("metricsUnavailable") + '</div></div>';
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Build process lookup maps from process list
|
|
948
|
+
const plist = data.processes?.list || [];
|
|
949
|
+
const pidByAgentId = {};
|
|
950
|
+
const managedProcs = [];
|
|
951
|
+
const orphanProcs = [];
|
|
952
|
+
const unknownProcs = [];
|
|
953
|
+
for (const p of plist) {
|
|
954
|
+
if (p.agentId) pidByAgentId[p.agentId] = p.pid;
|
|
955
|
+
if (p.managementStatus === 'managed') managedProcs.push(p);
|
|
956
|
+
else if (p.managementStatus === 'orphan') orphanProcs.push(p);
|
|
957
|
+
else unknownProcs.push(p);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Summary badges for process categories
|
|
961
|
+
document.getElementById("summaryManaged").textContent = managedProcs.length + " " + t("managed");
|
|
962
|
+
document.getElementById("summaryOrphan").textContent = orphanProcs.length + " " + t("orphan");
|
|
963
|
+
document.getElementById("summaryUnknown").textContent = unknownProcs.length + " unknown";
|
|
964
|
+
|
|
965
|
+
// Active sessions (primary table, with PID from process cross-reference)
|
|
966
|
+
const sessions = data.sessions?.active || [];
|
|
967
|
+
document.getElementById("sessionCount").textContent = "(" + sessions.length + ")";
|
|
968
|
+
if (sessions.length === 0) {
|
|
969
|
+
document.getElementById("sessionTable").innerHTML = '<div class="empty">' + t("noSessions") + '</div>';
|
|
970
|
+
} else {
|
|
971
|
+
document.getElementById("sessionTable").innerHTML =
|
|
972
|
+
'<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thPid") + '</th><th>' + t("thStatus") + '</th><th>' + t("thAction") + '</th><th>' + t("thWorkspace") + '</th><th>' + t("thProvider") + '</th></tr></thead><tbody>' +
|
|
973
|
+
sessions.map(s => {
|
|
974
|
+
const pid = pidByAgentId[s.agentId];
|
|
975
|
+
const st = s.runtimeStatus || "running";
|
|
976
|
+
return '<tr><td>' + s.agentId + '</td><td>' + (pid ? '<code>' + pid + '</code>' : '<span class="status-muted">-</span>') + '</td>'
|
|
977
|
+
+ '<td><span class="' + statusClass(st) + '">' + (t("st" + st.charAt(0).toUpperCase() + st.slice(1)) || st) + '</span></td>'
|
|
978
|
+
+ '<td>' + (s.runtimeActionLabel || '-') + '</td>'
|
|
979
|
+
+ '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.split(/[/\\\\]/).pop() : "-") + '</td>'
|
|
980
|
+
+ '<td>' + (s.providerId || "-") + '</td></tr>';
|
|
981
|
+
}).join("") +
|
|
982
|
+
'</tbody></table>';
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Orphan processes (shown only when orphans exist)
|
|
986
|
+
const orphanContainer = document.getElementById("orphanContainer");
|
|
987
|
+
if (orphanProcs.length > 0) {
|
|
988
|
+
orphanContainer.style.display = '';
|
|
989
|
+
document.getElementById("orphanCount").textContent = "(" + orphanProcs.length + ")";
|
|
990
|
+
document.getElementById("orphanTable").innerHTML =
|
|
991
|
+
'<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thPid") + '</th></tr></thead><tbody>' +
|
|
992
|
+
orphanProcs.map(p =>
|
|
993
|
+
'<tr><td>' + (p.agentId || '<span class="status-muted">-</span>') + '</td><td><code>' + p.pid + '</code></td></tr>'
|
|
994
|
+
).join("") +
|
|
995
|
+
'</tbody></table>';
|
|
996
|
+
} else {
|
|
997
|
+
orphanContainer.style.display = 'none';
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Unknown processes (collapsible, hidden by default)
|
|
1001
|
+
document.getElementById("unknownCount").textContent = "(" + unknownProcs.length + ")";
|
|
1002
|
+
const unknownTable = document.getElementById("unknownTable");
|
|
1003
|
+
if (unknownProcs.length === 0) {
|
|
1004
|
+
unknownTable.innerHTML = '<div class="empty">' + t("noProcesses") + '</div>';
|
|
1005
|
+
} else {
|
|
1006
|
+
unknownTable.innerHTML =
|
|
1007
|
+
'<table><thead><tr><th>' + t("thPid") + '</th></tr></thead><tbody>' +
|
|
1008
|
+
unknownProcs.map(p =>
|
|
1009
|
+
'<tr><td><code>' + p.pid + '</code></td></tr>'
|
|
1010
|
+
).join("") +
|
|
1011
|
+
'</tbody></table>';
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Persisted sessions
|
|
1015
|
+
const persisted = data.sessions?.persisted || [];
|
|
1016
|
+
document.getElementById("persistedCount").textContent = "(" + persisted.length + ")";
|
|
1017
|
+
if (persisted.length === 0) {
|
|
1018
|
+
document.getElementById("persistedTable").innerHTML = '<div class="empty">' + t("noPersisted") + '</div>';
|
|
1019
|
+
} else {
|
|
1020
|
+
document.getElementById("persistedTable").innerHTML =
|
|
1021
|
+
'<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thStatus") + '</th><th>' + t("thWorkspace") + '</th><th>' + t("thCommand") + '</th><th>' + t("thStarted") + '</th></tr></thead><tbody>' +
|
|
1022
|
+
persisted.map(s =>
|
|
1023
|
+
'<tr><td>' + s.agentId + '</td><td>' + (s.status || "stopped") + '</td>'
|
|
1024
|
+
+ '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.split(/[/\\\\]/).pop() : "-") + '</td>'
|
|
1025
|
+
+ '<td>' + (s.command || "-") + '</td>'
|
|
1026
|
+
+ '<td>' + (s.startedAt ? new Date(s.startedAt).toLocaleString() : "-") + '</td></tr>'
|
|
1027
|
+
).join("") +
|
|
1028
|
+
'</tbody></table>';
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ── Toggle unknown processes ────────────────────────────
|
|
1033
|
+
function toggleUnknown() {
|
|
1034
|
+
const table = document.getElementById("unknownTable");
|
|
1035
|
+
const icon = document.getElementById("unknownIcon");
|
|
1036
|
+
const isHidden = table.style.display === "none";
|
|
1037
|
+
table.style.display = isHidden ? "" : "none";
|
|
1038
|
+
icon.classList.toggle("open", isHidden);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ── 请求与轮询 ──────────────────────────────────────────
|
|
1042
|
+
async function refresh() {
|
|
1043
|
+
try {
|
|
1044
|
+
const resp = await fetch("/runtime/dashboard");
|
|
1045
|
+
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
|
1046
|
+
const data = await resp.json();
|
|
1047
|
+
render(data);
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
document.getElementById("refreshTime").textContent = "Error: " + err.message;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// 每 10 秒自动刷新
|
|
1054
|
+
setInterval(refresh, 10000);
|
|
1055
|
+
refresh();
|
|
1056
|
+
|
|
1057
|
+
// ── 更新检查与安装 ──────────────────────────────────────
|
|
1058
|
+
const UPDATE_LABELS = {
|
|
1059
|
+
zh: {
|
|
1060
|
+
label_available: "→ {v}",
|
|
1061
|
+
label_installing: "安装中...",
|
|
1062
|
+
label_restart: "重启",
|
|
1063
|
+
label_failed: "更新失败",
|
|
1064
|
+
},
|
|
1065
|
+
en: {
|
|
1066
|
+
label_available: "→ {v}",
|
|
1067
|
+
label_installing: "installing...",
|
|
1068
|
+
label_restart: "restart",
|
|
1069
|
+
label_failed: "failed",
|
|
1070
|
+
},
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
function ul(key) {
|
|
1074
|
+
const m = UPDATE_LABELS[lang] || UPDATE_LABELS.zh;
|
|
1075
|
+
return m[key] || UPDATE_LABELS.zh[key] || key;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
let updateState = {
|
|
1079
|
+
installing: false,
|
|
1080
|
+
operationId: null,
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// 检查更新
|
|
1084
|
+
async function checkUpdate() {
|
|
1085
|
+
try {
|
|
1086
|
+
const resp = await fetch("/runtime/check-update");
|
|
1087
|
+
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
|
1088
|
+
const data = await resp.json();
|
|
1089
|
+
if (data.ok && data.hasUpdate) {
|
|
1090
|
+
showUpdateAvailable(data);
|
|
1091
|
+
}
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
console.warn("Update check failed:", err.message);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function showUpdateAvailable(data) {
|
|
1098
|
+
if (updateState.installing) return;
|
|
1099
|
+
const indicator = document.getElementById("updateIndicator");
|
|
1100
|
+
indicator.className = "update-indicator available";
|
|
1101
|
+
indicator.textContent = ul("label_available").replace("{v}", data.latestVersion);
|
|
1102
|
+
indicator.style.display = "";
|
|
1103
|
+
indicator.onclick = doInstallUpdate;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
async function doInstallUpdate() {
|
|
1107
|
+
const indicator = document.getElementById("updateIndicator");
|
|
1108
|
+
updateState.installing = true;
|
|
1109
|
+
indicator.className = "update-indicator installing";
|
|
1110
|
+
indicator.textContent = ul("label_installing");
|
|
1111
|
+
indicator.onclick = null;
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
const resp = await fetch("/runtime/update-bridge", {
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
headers: { "Content-Type": "application/json" },
|
|
1117
|
+
body: JSON.stringify({ requestedByUserId: "dashboard-ui" }),
|
|
1118
|
+
});
|
|
1119
|
+
const data = await resp.json();
|
|
1120
|
+
if (!data.ok && data.error) throw new Error(data.error);
|
|
1121
|
+
updateState.operationId = data.operationId;
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
indicator.className = "update-indicator failed";
|
|
1124
|
+
indicator.textContent = ul("label_failed");
|
|
1125
|
+
indicator.onclick = checkUpdate;
|
|
1126
|
+
updateState.installing = false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function doRestartBridge() {
|
|
1131
|
+
const indicator = document.getElementById("updateIndicator");
|
|
1132
|
+
indicator.textContent = ul("label_restart") + "...";
|
|
1133
|
+
indicator.onclick = null;
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
await fetch("/runtime/restart-bridge", {
|
|
1137
|
+
method: "POST",
|
|
1138
|
+
headers: { "Content-Type": "application/json" },
|
|
1139
|
+
body: JSON.stringify({ preserveSessions: true, requestedByUserId: "dashboard-ui" }),
|
|
1140
|
+
});
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
// Bridge may shut down before responding — ignore
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Override render to track lifecycle operation status
|
|
1147
|
+
const __origRender = render;
|
|
1148
|
+
render = function(data) {
|
|
1149
|
+
__origRender(data);
|
|
1150
|
+
|
|
1151
|
+
const op = data.lifecycleOperation;
|
|
1152
|
+
if (!op || !updateState.installing) return;
|
|
1153
|
+
|
|
1154
|
+
const indicator = document.getElementById("updateIndicator");
|
|
1155
|
+
|
|
1156
|
+
if (op.operationType === "update") {
|
|
1157
|
+
if (op.status === "succeeded") {
|
|
1158
|
+
indicator.className = "update-indicator success";
|
|
1159
|
+
indicator.textContent = ul("label_restart");
|
|
1160
|
+
indicator.onclick = doRestartBridge;
|
|
1161
|
+
updateState.installing = false;
|
|
1162
|
+
} else if (op.status === "failed") {
|
|
1163
|
+
indicator.className = "update-indicator failed";
|
|
1164
|
+
indicator.textContent = ul("label_failed");
|
|
1165
|
+
indicator.onclick = checkUpdate;
|
|
1166
|
+
updateState.installing = false;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
// Check for updates on page load
|
|
1172
|
+
checkUpdate();
|
|
1173
|
+
// Also check periodically (every 5 minutes)
|
|
1174
|
+
setInterval(checkUpdate, 300000);
|
|
1175
|
+
</script>
|
|
1176
|
+
</body>
|
|
1177
|
+
</html>`;
|
|
1178
|
+
// ======================================================================
|
|
1179
|
+
// LOGIN HTML — 登录页面
|
|
1180
|
+
// ======================================================================
|
|
1181
|
+
const LOGIN_HTML = `<!DOCTYPE html>
|
|
1182
|
+
<html lang="zh-CN">
|
|
1183
|
+
<head>
|
|
1184
|
+
<meta charset="UTF-8">
|
|
1185
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1186
|
+
<title>Bridge Dashboard - Login</title>
|
|
1187
|
+
<style>
|
|
1188
|
+
:root {
|
|
1189
|
+
--bg: #0f1117;
|
|
1190
|
+
--surface: #1a1d2e;
|
|
1191
|
+
--surface2: #232738;
|
|
1192
|
+
--border: #2d3248;
|
|
1193
|
+
--text: #e1e4f0;
|
|
1194
|
+
--text2: #8b90a8;
|
|
1195
|
+
--accent: #5b8def;
|
|
1196
|
+
--accent-hover: #4a7de0;
|
|
1197
|
+
--red: #f05454;
|
|
1198
|
+
--radius: 10px;
|
|
1199
|
+
}
|
|
1200
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
1201
|
+
body {
|
|
1202
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
|
|
1203
|
+
background: var(--bg);
|
|
1204
|
+
color: var(--text);
|
|
1205
|
+
min-height: 100vh;
|
|
1206
|
+
display: flex;
|
|
1207
|
+
align-items: center;
|
|
1208
|
+
justify-content: center;
|
|
1209
|
+
}
|
|
1210
|
+
.login-card {
|
|
1211
|
+
background: var(--surface);
|
|
1212
|
+
border: 1px solid var(--border);
|
|
1213
|
+
border-radius: var(--radius);
|
|
1214
|
+
padding: 40px 36px;
|
|
1215
|
+
width: 360px;
|
|
1216
|
+
max-width: 90vw;
|
|
1217
|
+
}
|
|
1218
|
+
.login-card h1 {
|
|
1219
|
+
font-size: 20px;
|
|
1220
|
+
font-weight: 600;
|
|
1221
|
+
margin-bottom: 4px;
|
|
1222
|
+
text-align: center;
|
|
1223
|
+
}
|
|
1224
|
+
.login-card p.subtitle {
|
|
1225
|
+
font-size: 13px;
|
|
1226
|
+
color: var(--text2);
|
|
1227
|
+
text-align: center;
|
|
1228
|
+
margin-bottom: 28px;
|
|
1229
|
+
}
|
|
1230
|
+
.form-group {
|
|
1231
|
+
margin-bottom: 18px;
|
|
1232
|
+
}
|
|
1233
|
+
.form-group label {
|
|
1234
|
+
display: block;
|
|
1235
|
+
font-size: 13px;
|
|
1236
|
+
font-weight: 500;
|
|
1237
|
+
margin-bottom: 6px;
|
|
1238
|
+
color: var(--text2);
|
|
1239
|
+
}
|
|
1240
|
+
.form-group input {
|
|
1241
|
+
width: 100%;
|
|
1242
|
+
padding: 10px 12px;
|
|
1243
|
+
background: var(--surface2);
|
|
1244
|
+
border: 1px solid var(--border);
|
|
1245
|
+
border-radius: 6px;
|
|
1246
|
+
color: var(--text);
|
|
1247
|
+
font-size: 14px;
|
|
1248
|
+
outline: none;
|
|
1249
|
+
transition: border-color .15s;
|
|
1250
|
+
}
|
|
1251
|
+
.form-group input:focus {
|
|
1252
|
+
border-color: var(--accent);
|
|
1253
|
+
}
|
|
1254
|
+
.form-group input::placeholder {
|
|
1255
|
+
color: var(--text2);
|
|
1256
|
+
opacity: 0.6;
|
|
1257
|
+
}
|
|
1258
|
+
button {
|
|
1259
|
+
width: 100%;
|
|
1260
|
+
padding: 10px;
|
|
1261
|
+
background: var(--accent);
|
|
1262
|
+
border: none;
|
|
1263
|
+
border-radius: 6px;
|
|
1264
|
+
color: #fff;
|
|
1265
|
+
font-size: 14px;
|
|
1266
|
+
font-weight: 600;
|
|
1267
|
+
cursor: pointer;
|
|
1268
|
+
transition: background .15s;
|
|
1269
|
+
margin-top: 4px;
|
|
1270
|
+
}
|
|
1271
|
+
button:hover { background: var(--accent-hover); }
|
|
1272
|
+
button:disabled {
|
|
1273
|
+
opacity: 0.5;
|
|
1274
|
+
cursor: not-allowed;
|
|
1275
|
+
}
|
|
1276
|
+
.error-msg {
|
|
1277
|
+
background: rgba(240,84,84,0.12);
|
|
1278
|
+
color: var(--red);
|
|
1279
|
+
padding: 8px 12px;
|
|
1280
|
+
border-radius: 6px;
|
|
1281
|
+
font-size: 13px;
|
|
1282
|
+
margin-bottom: 16px;
|
|
1283
|
+
display: none;
|
|
1284
|
+
}
|
|
1285
|
+
.error-msg.visible { display: block; }
|
|
1286
|
+
</style>
|
|
1287
|
+
</head>
|
|
1288
|
+
<body>
|
|
1289
|
+
<div class="login-card">
|
|
1290
|
+
<h1>Bridge Dashboard</h1>
|
|
1291
|
+
<p class="subtitle">Please log in to continue</p>
|
|
1292
|
+
<div class="error-msg" id="errorMsg"></div>
|
|
1293
|
+
<form id="loginForm" onsubmit="return doLogin(event)">
|
|
1294
|
+
<div class="form-group">
|
|
1295
|
+
<label for="username">Username</label>
|
|
1296
|
+
<input type="text" id="username" name="username" placeholder="Username" autocomplete="username" autofocus>
|
|
1297
|
+
</div>
|
|
1298
|
+
<div class="form-group">
|
|
1299
|
+
<label for="password">Password</label>
|
|
1300
|
+
<input type="password" id="password" name="password" placeholder="Password" autocomplete="current-password">
|
|
1301
|
+
</div>
|
|
1302
|
+
<button type="submit" id="loginBtn">Log In</button>
|
|
1303
|
+
</form>
|
|
1304
|
+
</div>
|
|
1305
|
+
<script>
|
|
1306
|
+
async function doLogin(e) {
|
|
1307
|
+
e.preventDefault();
|
|
1308
|
+
const btn = document.getElementById("loginBtn");
|
|
1309
|
+
const errorEl = document.getElementById("errorMsg");
|
|
1310
|
+
errorEl.classList.remove("visible");
|
|
1311
|
+
btn.disabled = true;
|
|
1312
|
+
btn.textContent = "Logging in...";
|
|
1313
|
+
|
|
1314
|
+
try {
|
|
1315
|
+
const resp = await fetch("/runtime/login", {
|
|
1316
|
+
method: "POST",
|
|
1317
|
+
headers: { "Content-Type": "application/json" },
|
|
1318
|
+
body: JSON.stringify({
|
|
1319
|
+
username: document.getElementById("username").value,
|
|
1320
|
+
password: document.getElementById("password").value,
|
|
1321
|
+
}),
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
if (resp.ok) {
|
|
1325
|
+
window.location.href = "/";
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const data = await resp.json();
|
|
1330
|
+
errorEl.textContent = data.error === "invalid credentials" ? "Invalid username or password" : "Login failed";
|
|
1331
|
+
errorEl.classList.add("visible");
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
errorEl.textContent = "Network error, please try again";
|
|
1334
|
+
errorEl.classList.add("visible");
|
|
1335
|
+
} finally {
|
|
1336
|
+
btn.disabled = false;
|
|
1337
|
+
btn.textContent = "Log In";
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// 回车键自动提交
|
|
1342
|
+
document.getElementById("loginForm").addEventListener("keydown", function(e) {
|
|
1343
|
+
if (e.key === "Enter") doLogin(e);
|
|
1344
|
+
});
|
|
1345
|
+
</script>
|
|
1346
|
+
</body>
|
|
1347
|
+
</html>`;
|