/**
 * 仪表盘路由
 *
 * 提供 Bridge 状态看板的静态页面和数据聚合 API
 * 支持中/英文界面，默认中文
 */
import { Router } from "express";
import os from "node:os";
import { getRegistrationState } from "../services/auto-register.js";
import { getRuntimeBindingPublicState, getRuntimePairingCode } from "../services/runtime-binding.js";
import { getAgentProcessManager } from "../services/agent-process-manager.js";
import { collectSystemMetrics } from "../services/system-metrics.js";
import { loadPersistedSessions } from "../services/terminal-persistence.js";
import { createLogger } from "../utils/logger.js";
import { getBridgeVersion } from "../config.js";
import { sdkSessions } from "./terminal.js";
import { createSession, validateSession, destroySession, validateCredentials, } from "../services/panel-auth.js";
const log = createLogger("dashboard");
// ── 内存缓存（两级） ─────────────────────────────────────
// 一级：完整响应缓存，匹配前端轮询间隔（10s），避免大部分请求走 I/O
let dashboardCache = null;
const CACHE_TTL = 10000; // 10 秒，匹配前端 refresh 间隔
// 二级：慢指标缓存（系统指标/进程扫描），需要 PowerShell WMI 调用（Windows 下尤其昂贵）
// 请求处理流程绝不等待慢操作——始终从缓存读取，后台定时+首次请求时双触发刷新
let metricsCache = null;
let processCache = null;
const SLOW_CACHE_TTL = 30000; // 30 秒
// ── 后台缓存预热 ─────────────────────────────────────────
// 启动时立即 + 每 30 秒后台刷新，确保慢指标永不阻塞请求
async function warmSlideCaches() {
    collectSystemMetrics()
        .then((data) => { metricsCache = { data, ts: Date.now() }; })
        .catch(() => { });
    getAgentProcessManager().discoverAllProcesses()
        .then((data) => { processCache = { data, ts: Date.now() }; })
        .catch(() => { });
    // ★ 清理注册表中的陈旧记录（已终止且进程不再存活的条目）
    getAgentProcessManager().cleanupStaleRegistry().catch(() => { });
}
// 立即执行首次预热
warmSlideCaches();
// 每 30 秒定期刷新
setInterval(warmSlideCaches, SLOW_CACHE_TTL);
const bridgeVersion = getBridgeVersion();
/**
 * 从 lifecycle-state.json 中读取最新的一条生命周期操作记录。
 * 用于给 dashboard 展示 update/restart 状态。
 */
async function loadLatestLifecycleOperation() {
    const fs = await import("node:fs/promises");
    const path = await import("node:path");
    const os = await import("node:os");
    const lifecycleFile = path.join(String(process.env.AWS_RUNTIME_HOME_DIR || os.homedir()).trim() || os.homedir(), ".agentswork", "runtime-bridge", "lifecycle-state.json");
    try {
        const raw = await fs.readFile(lifecycleFile, "utf8");
        const parsed = JSON.parse(raw);
        const ops = Array.isArray(parsed.operations) ? parsed.operations : [];
        if (ops.length === 0)
            return null;
        // 按 updatedAt 降序取最新
        ops.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
        return ops[0] || null;
    }
    catch {
        return null;
    }
}
export const dashboardRouter = Router();
// ── Cookie 解析 ────────────────────────────────────────────
const COOKIE_NAME = "panel_sid";
function parseCookies(cookieHeader) {
    const result = {};
    if (!cookieHeader)
        return result;
    for (const part of cookieHeader.split(";")) {
        const idx = part.indexOf("=");
        if (idx === -1)
            continue;
        const key = part.slice(0, idx).trim();
        const val = part.slice(idx + 1).trim();
        if (key)
            result[key] = val;
    }
    return result;
}
function getSessionToken(req) {
    const cookies = parseCookies(req.headers.cookie);
    return cookies[COOKIE_NAME];
}
/**
 * 面板认证中间件
 * 检查请求是否携带有效的面板会话 cookie，无效时直接返回 401 JSON（API 请求）或重定向到登录页。
 */
function requirePanelAuth(req, res) {
    const token = getSessionToken(req);
    if (token && validateSession(token)) {
        return true;
    }
    // AJAX / API 请求：返回 JSON 401
    const acceptsJson = req.xhr || req.headers.accept?.includes("application/json");
    if (acceptsJson) {
        res.status(401).json({ ok: false, error: "unauthorized", needLogin: true });
        return false;
    }
    // 页面请求：重定向到登录页
    res.redirect("/login");
    return false;
}
// ── 登录/登出路由 ──────────────────────────────────────────
/** POST /runtime/login 登录验证 */
dashboardRouter.post("/runtime/login", (req, res) => {
    const { username, password } = req.body || {};
    if (!username || !password) {
        res.status(400).json({ ok: false, error: "username and password required" });
        return;
    }
    if (!validateCredentials(String(username), String(password))) {
        res.status(401).json({ ok: false, error: "invalid credentials" });
        return;
    }
    const token = createSession();
    // 设置 HttpOnly cookie，24 小时过期
    res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400`);
    res.json({ ok: true });
});
/** POST /runtime/logout 登出 */
dashboardRouter.post("/runtime/logout", (req, res) => {
    const token = getSessionToken(req);
    if (token) {
        destroySession(token);
    }
    res.setHeader("Set-Cookie", `${COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`);
    res.json({ ok: true });
});
/**
 * GET /runtime/dashboard
 * 聚合 bridge 所有状态数据。使用 2 秒缓存避免频繁 I/O，需要面板登录会话。
 */
dashboardRouter.get("/runtime/dashboard", async (req, res) => {
    if (!requirePanelAuth(req, res))
        return;
    // 缓存命中
    if (dashboardCache && Date.now() - dashboardCache.ts < CACHE_TTL) {
        return void res.json(dashboardCache.data);
    }
    try {
        // 慢指标始终从缓存读取（后台预热 + 首次 miss 时同步触发预热，但不阻塞请求）
        const metrics = metricsCache?.data ?? null;
        const processesData = processCache?.data ?? null;
        // 若缓存尚未就绪（首次启动预热未完成），在后台触发一次填充，但不等待
        if (!metricsCache) {
            warmSlideCaches();
        }
        const [binding, registration, persistedSessions] = await Promise.all([
            Promise.resolve(getRuntimeBindingPublicState()),
            Promise.resolve(getRegistrationState()),
            loadPersistedSessions().catch(() => []),
        ]);
        const manager = getAgentProcessManager();
        const registryRecords = manager.getAllManagedProcesses();
        const activeSessions = Array.from(sdkSessions.entries()).map(([sessionId, entry]) => ({
            sessionId,
            agentId: entry.agentId,
            workspacePath: entry.workspacePath,
            runtimeStatus: entry.currentRuntimeStatus || "running",
            runtimeActionType: entry.currentRuntimeActionType,
            runtimeActionLabel: entry.currentRuntimeActionLabel,
            providerId: entry.providerId,
        }));
        let processStats = { managed: 0, orphan: 0, unknown: 0 };
        let processList = [];
        if (processesData) {
            processStats = {
                managed: processesData.filter((p) => p.managementStatus === "managed").length,
                orphan: processesData.filter((p) => p.managementStatus === "orphan").length,
                unknown: processesData.filter((p) => p.managementStatus === "unknown").length,
            };
            const registryMap = new Map(registryRecords.map((r) => [r.pid, r]));
            processList = processesData.map((p) => {
                const record = registryMap.get(p.pid);
                return {
                    agentId: p.agentId || record?.agentId,
                    pid: p.pid,
                    managementStatus: p.managementStatus,
                    sessionId: record?.sessionId,
                    state: record?.state,
                };
            });
            const sortOrder = { managed: 0, orphan: 1, unknown: 2 };
            processList.sort((a, b) => (sortOrder[a.managementStatus] ?? 9) - (sortOrder[b.managementStatus] ?? 9));
        }
        const activeSessionIds = new Set(activeSessions.map((s) => s.sessionId));
        const persistedOnly = persistedSessions.filter((s) => !activeSessionIds.has(s.sessionId));
        const bindingPublic = binding;
        const latestLifecycleOperation = await loadLatestLifecycleOperation();
        const body = {
            ok: true,
            collectedAt: new Date().toISOString(),
            bridge: {
                version: bridgeVersion,
                hostname: os.hostname(),
                platform: os.platform(),
                arch: os.arch(),
                uptime: Math.floor(process.uptime()),
                nodeVersion: process.version,
            },
            binding: {
                status: bindingPublic.status,
                paired: bindingPublic.paired,
                instanceId: bindingPublic.instanceId,
                userId: bindingPublic.userId,
                schedulerBaseUrl: bindingPublic.schedulerBaseUrl,
                pairingCode: getRuntimePairingCode(),
            },
            registration: {
                registered: registration.registered,
                instanceId: registration.instanceId,
                lastAttempt: registration.lastAttempt?.toISOString(),
                error: registration.error,
            },
            metrics,
            sessions: {
                active: activeSessions,
                activeCount: activeSessions.length,
                persisted: persistedOnly,
                persistedCount: persistedOnly.length,
                total: activeSessions.length + persistedOnly.length,
            },
            processes: {
                ...processStats,
                total: processStats.managed + processStats.orphan + processStats.unknown,
                list: processList,
                registryCount: registryRecords.length,
            },
            lifecycleOperation: latestLifecycleOperation,
        };
        // 写入一级缓存（10 秒 TTL，匹配前端轮询间隔，避免连续请求都吃 I/O）
        dashboardCache = { data: body, ts: Date.now() };
        res.json(body);
    }
    catch (error) {
        const err = error;
        log.error("Dashboard aggregation failed:", err.message);
        res.status(500).json({ ok: false, error: err.message });
    }
});
/**
 * GET /login 登录页面
 */
dashboardRouter.get("/login", (_req, res) => {
    res.type("text/html; charset=utf-8");
    res.send(LOGIN_HTML);
});
/**
 * GET / 仪表盘主页
 * 需要面板登录会话，未登录时跳转到 /login。
 * 支持 ?lang=en 切换英文，默认中文
 */
dashboardRouter.get("/", (req, res) => {
    const token = getSessionToken(req);
    if (!token || !validateSession(token)) {
        res.redirect("/login");
        return;
    }
    const lang = req.query.lang === "en" ? "en" : "zh";
    res.type("text/html; charset=utf-8");
    res.send(DASHBOARD_HTML.replace("{{LANG}}", lang));
});
// ======================================================================
// DASHBOARD HTML — 单页自包含，支持中文/英文界面，默认中文
// ======================================================================
const DASHBOARD_HTML = `<!DOCTYPE html>
<html lang="{{LANG}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bridge Dashboard</title>
<style>
  :root {
    --bg: #0f1117;
    --surface: #1a1d2e;
    --surface2: #232738;
    --border: #2d3248;
    --text: #e1e4f0;
    --text2: #8b90a8;
    --accent: #5b8def;
    --accent2: #43c6a8;
    --red: #f05454;
    --orange: #f0a030;
    --green: #43c6a8;
    --green-bg: rgba(67,198,168,0.12);
    --red-bg: rgba(240,84,84,0.12);
    --orange-bg: rgba(240,160,48,0.12);
    --radius: 10px;
  }
  * { margin:0; padding:0; box-sizing:border-box; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
    background: var(--bg);
    color: var(--text);
    padding: 24px;
    min-height: 100vh;
  }
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 24px;
    flex-wrap: wrap;
    gap: 12px;
  }
  .header h1 {
    font-size: 22px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .header h1 .dot {
    display: inline-block;
    width: 10px; height: 10px;
    border-radius: 50%;
    background: var(--green);
    animation: pulse 2s ease-in-out infinite;
  }
  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
  }
  .header-right {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 13px;
    color: var(--text2);
  }
  .header-right .refresh-time { color: var(--text2); }
  .header-right button, .header-right .lang-btn {
    background: var(--surface2);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 6px 14px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 13px;
    transition: background .15s;
  }
  .header-right button:hover, .header-right .lang-btn:hover {
    background: var(--border);
  }
  .lang-btn.active {
    background: var(--accent);
    border-color: var(--accent);
  }

  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 16px;
    margin-bottom: 20px;
  }
  .card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 18px;
  }
  .card-title {
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--text2);
    margin-bottom: 12px;
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .stat-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 6px 0;
    font-size: 13px;
    border-bottom: 1px solid var(--border);
  }
  .stat-row:last-child { border-bottom: none; }
  .stat-label { color: var(--text2); }
  .stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }

  .status-green { color: var(--green); }
  .status-orange { color: var(--orange); }
  .status-red { color: var(--red); }
  .status-muted { color: var(--text2); }

  .section-title {
    font-size: 15px;
    font-weight: 600;
    margin: 24px 0 12px;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .section-title .count {
    font-size: 12px;
    color: var(--text2);
    font-weight: 400;
  }

  .table-wrap {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow-x: auto;
  }
  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
  }
  th {
    text-align: left;
    padding: 10px 14px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--text2);
    background: var(--surface2);
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
  }
  td {
    padding: 9px 14px;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
  }
  tr:last-child td { border-bottom: none; }
  tr:hover { background: rgba(255,255,255,0.03); }

  .bar-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .bar {
    flex: 1;
    height: 6px;
    background: var(--surface2);
    border-radius: 3px;
    overflow: hidden;
  }
  .bar-fill {
    height: 100%;
    border-radius: 3px;
    transition: width 0.5s ease;
  }
  .bar-fill.green { background: var(--green); }
  .bar-fill.orange { background: var(--orange); }
  .bar-fill.red { background: var(--red); }
  .bar-label { font-size: 12px; min-width: 48px; text-align: right; font-variant-numeric: tabular-nums; }

  .tag {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 4px;
    font-size: 11px;
    font-weight: 600;
  }
  .tag.managed { background: var(--green-bg); color: var(--green); }
  .tag.orphan { background: var(--orange-bg); color: var(--orange); }
  .tag.unknown { background: rgba(139,144,168,0.15); color: var(--text2); }

  .empty { color: var(--text2); text-align: center; padding: 24px; font-size: 13px; }

  .toggle-header { cursor: pointer; user-select: none; }
  .toggle-header:hover { opacity: 0.8; }
  .toggle-icon { display: inline-block; transition: transform .2s; font-size: 10px; margin-right: 4px; }
  .toggle-icon.open { transform: rotate(90deg); }

  .orphan-banner {
    background: var(--orange-bg);
    border: 1px solid var(--orange);
    border-radius: var(--radius);
    padding: 12px 16px;
    margin-bottom: 12px;
    font-size: 13px;
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .orphan-banner .icon { font-size: 16px; }
  .orphan-banner .text { flex: 1; }
  .orphan-banner .action-hint { color: var(--orange); font-weight: 600; }

  .action-btn {
    background: var(--surface2);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 2px 10px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 11px;
    transition: background .15s;
  }
  .action-btn:hover { background: var(--border); }
  .action-btn.terminate { border-color: var(--red); color: var(--red); }
  .action-btn.terminate:hover { background: var(--red-bg); }

  .summary-line {
    display: flex;
    gap: 16px;
    align-items: center;
    margin-bottom: 12px;
    font-size: 13px;
    flex-wrap: wrap;
  }
  .summary-line .item { display: flex; align-items: center; gap: 4px; }
  .summary-line .dot {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
  }
  .summary-line .dot.green { background: var(--green); }
  .summary-line .dot.orange { background: var(--orange); }
  .summary-line .dot.gray { background: var(--text2); }

  .version-area {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: var(--text2);
  }
  .update-indicator {
    font-size: 12px;
    cursor: pointer;
    white-space: nowrap;
    transition: color .15s;
  }
  .update-indicator:hover { opacity: 0.8; }
  .update-indicator.available { color: var(--accent); }
  .update-indicator.installing { color: var(--orange); cursor: default; }
  .update-indicator.success { color: var(--green); }
  .update-indicator.failed { color: var(--red); }

  @media (max-width: 640px) {
    body { padding: 12px; }
    .grid { grid-template-columns: 1fr; }
    th, td { padding: 7px 10px; }
    .header h1 { font-size: 18px; }
  }
</style>
</head>
<body>

<div class="header">
  <h1><span class="dot"></span>Bridge Dashboard</h1>
  <div class="header-right">
    <span class="refresh-time" id="refreshTime">-</span>
    <span id="bridgeHostname">-</span>
    <span class="version-area">
      <span id="bridgeVersion">v-</span>
      <span id="updateIndicator" style="display:none"></span>
    </span>
    <span class="lang-btn" id="langBtn" onclick="toggleLang()">中文</span>
    <button onclick="refresh()" id="refreshBtn">&#x21bb;</button>
  </div>
</div>

<div class="grid" id="statusCards"></div>

<div class="section-title" id="sectionSysRes">System Resources <span class="count" id="sysMetricsCount"></span></div>
<div class="grid" id="metricsGrid"></div>

<!-- Summary badges for process categories -->
<div class="summary-line" id="summaryLine">
  <span class="item"><span class="dot green"></span> <span id="summaryManaged">0 managed</span></span>
  <span class="item"><span class="dot orange"></span> <span id="summaryOrphan">0 orphan</span></span>
  <span class="item"><span class="dot gray"></span> <span id="summaryUnknown">0 unknown</span></span>
</div>

<!-- Active Sessions (primary table, includes PID from process list) -->
<div class="section-title" id="sectionSessions">Active Sessions <span class="count" id="sessionCount"></span></div>
<div class="table-wrap" id="sessionTable"><div class="empty" id="loadingSessions">Loading...</div></div>

<!-- Orphan Processes (shown only when orphans exist) -->
<div id="orphanContainer" style="display:none">
  <div class="section-title" id="sectionOrphans">Orphan Processes <span class="count" id="orphanCount"></span></div>
  <div class="orphan-banner">
    <span class="icon">&#x26A0;</span>
    <span class="text" id="orphanBannerText">These processes are running but no longer managed by bridge</span>
    <span class="action-hint">Terminate to clean up</span>
  </div>
  <div class="table-wrap" id="orphanTable"><div class="empty">No orphans</div></div>
</div>

<!-- Unknown Processes (collapsible, hidden by default) -->
<div id="unknownContainer">
  <div class="section-title toggle-header" id="sectionUnknown" onclick="toggleUnknown()">
    <span class="toggle-icon" id="unknownIcon">&#x25B6;</span><span id="unknownLabel"> Other processes</span> <span class="count" id="unknownCount"></span>
  </div>
  <div class="table-wrap" id="unknownTable" style="display:none"><div class="empty" id="loadingUnknown">Loading...</div></div>
</div>

<!-- Persisted Sessions -->
<div class="section-title" id="sectionPersisted">Persisted Sessions <span class="count" id="persistedCount"></span></div>
<div class="table-wrap" id="persistedTable"><div class="empty" id="loadingPersisted">Loading...</div></div>

<script>
// ── 多语言翻译表 ──────────────────────────────────────────
const T = {
  zh: {
    langName: "English",
    refreshBtn: "\u21bb \u5237\u65b0",
    loading: "\u52a0\u8f7d\u4e2d...",
    noProcesses: "\u65e0\u8fdb\u7a0b",
    noSessions: "\u65e0\u6d3b\u8dc3\u4f1a\u8bdd",
    noPersisted: "\u65e0\u6301\u4e45\u5316\u4f1a\u8bdd",
    metricsUnavailable: "\u7cfb\u7edf\u6307\u6807\u4e0d\u53ef\u7528",
    managed: "\u7ba1\u7406",
    orphan: "\u5b64\u513f",
    managedLabel: "\u7ba1\u7406\u4e2d",
    // 状态卡片
    cardBridge: "Bridge",
    cardRegistration: "\u6ce8\u518c",
    cardBinding: "\u7ed1\u5b9a",
    cardSessions: "\u4f1a\u8bdd\u4e0e\u8fdb\u7a0b",
    fldVersion: "\u7248\u672c",
    fldHost: "\u4e3b\u673a",
    fldPlatform: "\u5e73\u53f0",
    fldUptime: "\u8fd0\u884c\u65f6\u957f",
    fldNode: "Node",
    fldStatus: "\u72b6\u6001",
    fldInstanceId: "\u5b9e\u4f8b ID",
    fldLastAttempt: "\u4e0a\u6b21\u5c1d\u8bd5",
    fldError: "\u9519\u8bef",
    fldScheduler: "\u8c03\u5ea6\u5668",
    fldPairingCode: "\u5339\u914d\u7801",
    fldActiveSessions: "\u6d3b\u8dc3\u4f1a\u8bdd",
    fldPersistedSessions: "\u6301\u4e45\u5316\u4f1a\u8bdd",
    fldManagedProcesses: "\u7ba1\u7406\u8fdb\u7a0b",
    fldOrphanProcesses: "\u5b64\u513f\u8fdb\u7a0b",
    fldRegistryEntries: "\u6ce8\u518c\u8868\u8bb0\u5f55",
    // 资源
    secSysRes: "\u7cfb\u7edf\u8d44\u6e90",
    secSessions: "\u6d3b\u8dc3\u4f1a\u8bdd",
    secPersisted: "\u6301\u4e45\u5316\u4f1a\u8bdd",
    secOrphans: "\u5b64\u513f\u8fdb\u7a0b",
    secUnknown: "\u5176\u4ed6\u8fdb\u7a0b",
    orphanBanner: "\u4ee5\u4e0b\u8fdb\u7a0b\u4ecd\u5728\u8fd0\u884c\u4f46\u8131\u79bb\u4e86 bridge \u7ba1\u7406",
    btnTerminate: "\u7ec8\u6b62",
    showUnknown: "\u663e\u793a",
    hideUnknown: "\u9690\u85cf",
    noOrphans: "\u65e0\u5b64\u513f\u8fdb\u7a0b",
    fldUsage: "\u4f7f\u7528\u7387",
    fldCores: "\u5185\u6838\u6570",
    fldModel: "\u578b\u53f7",
    fldUsed: "\u5df2\u7528",
    fldFree: "\u5269\u4f59",
    fldTotal: "\u603b\u8ba1",
    fldDownload: "\u4e0b\u884c",
    fldUpload: "\u4e0a\u884c",
    fldTotalDown: "\u603b\u4e0b\u884c",
    fldTotalUp: "\u603b\u4e0a\u884c",
    // 表格
    thAgentId: "Agent ID",
    thPid: "PID",
    thStatus: "\u72b6\u6001",
    thSessionId: "\u4f1a\u8bdd ID",
    thState: "\u72b6\u6001",
    thAction: "\u884c\u52a8",
    thWorkspace: "\u5de5\u4f5c\u533a",
    thProvider: "\u63d0\u4f9b\u65b9",
    thCommand: "\u547d\u4ee4",
    thStarted: "\u5f00\u59cb\u65f6\u95f4",
    countManaged: "\u7ba1\u7406\u4e2d",
    // 状态文本
    stHealthy: "\u5065\u5eb7",
    stError: "\u5f02\u5e38",
    stRunning: "\u8fd0\u884c\u4e2d",
    stStopped: "\u5df2\u505c\u6b62",
    stPaired: "\u5df2\u7ed1\u5b9a",
    stUnpaired: "\u672a\u7ed1\u5b9a",
    stRegistered: "\u5df2\u6ce8\u518c",
    stNotRegistered: "\u672a\u6ce8\u518c",
    stWarning: "\u8b66\u544a",
    stNone: "\u65e0",
  },
  en: {
    langName: "\u4e2d\u6587",
    refreshBtn: "\u21bb Refresh",
    loading: "Loading...",
    noProcesses: "No processes",
    noSessions: "No active sessions",
    noPersisted: "No persisted sessions",
    metricsUnavailable: "System metrics unavailable",
    managed: "managed",
    orphan: "orphan",
    managedLabel: "managed",
    cardBridge: "Bridge",
    cardRegistration: "Registration",
    cardBinding: "Binding",
    cardSessions: "Sessions & Processes",
    fldVersion: "Version",
    fldHost: "Host",
    fldPlatform: "Platform",
    fldUptime: "Uptime",
    fldNode: "Node",
    fldStatus: "Status",
    fldInstanceId: "Instance ID",
    fldLastAttempt: "Last Attempt",
    fldError: "Error",
    fldScheduler: "Scheduler",
    fldPairingCode: "Pairing Code",
    fldActiveSessions: "Active Sessions",
    fldPersistedSessions: "Persisted Sessions",
    fldManagedProcesses: "Managed Processes",
    fldOrphanProcesses: "Orphan Processes",
    fldRegistryEntries: "Registry Entries",
    secSysRes: "System Resources",
    secSessions: "Active Sessions",
    secPersisted: "Persisted Sessions",
    secOrphans: "Orphan Processes",
    secUnknown: "Other Processes",
    orphanBanner: "These processes are running but no longer managed by bridge",
    btnTerminate: "Terminate",
    showUnknown: "Show",
    hideUnknown: "Hide",
    noOrphans: "No orphan processes",
    fldUsage: "Usage",
    fldCores: "Cores",
    fldModel: "Model",
    fldUsed: "Used",
    fldFree: "Free",
    fldTotal: "Total",
    fldDownload: "Download",
    fldUpload: "Upload",
    fldTotalDown: "Total Down",
    fldTotalUp: "Total Up",
    thAgentId: "Agent ID",
    thPid: "PID",
    thStatus: "Status",
    thSessionId: "Session ID",
    thState: "State",
    thAction: "Action",
    thWorkspace: "Workspace",
    thProvider: "Provider",
    thCommand: "Command",
    thStarted: "Started",
    countManaged: "managed",
    stHealthy: "Healthy",
    stError: "Error",
    stRunning: "Running",
    stStopped: "Stopped",
    stPaired: "Paired",
    stUnpaired: "Unpaired",
    stRegistered: "Registered",
    stNotRegistered: "Not Registered",
    stWarning: "Warning",
    stNone: "None",
  }
};

// ── 语言偏好 ──────────────────────────────────────────────
let lang = localStorage.getItem("dashboard-lang");
if (!lang) {
  // url 参数优先，其次浏览器语言，默认 zh
  const urlLang = new URLSearchParams(location.search).get("lang");
  lang = urlLang === "en" ? "en" : navigator.language.startsWith("zh") ? "zh" : "en";
}
localStorage.setItem("dashboard-lang", lang);
document.documentElement.lang = lang === "en" ? "en" : "zh-CN";

function t(k) {
  var _a;
  return ((_a = T[lang]) === null || _a === void 0 ? void 0 : _a[k]) || T.zh[k] || k;
}

function toggleLang() {
  lang = lang === "zh" ? "en" : "zh";
  localStorage.setItem("dashboard-lang", lang);
  document.documentElement.lang = lang === "en" ? "en" : "zh-CN";
  document.getElementById("langBtn").textContent = t("langName");
  if (lastData) render(lastData);
}

// ── 工具函数 ──────────────────────────────────────────────
let lastData = null;

function fmtPercent(v) {
  if (v == null || !Number.isFinite(v)) return "-";
  return v.toFixed(1) + "%";
}

function fmtBytes(b) {
  if (!b || !Number.isFinite(b)) return "-";
  if (b < 1024) return b + " B";
  if (b < 1024*1024) return (b / 1024).toFixed(1) + " KB";
  if (b < 1024*1024*1024) return (b / (1024*1024)).toFixed(1) + " MB";
  return (b / (1024*1024*1024)).toFixed(2) + " GB";
}

function fmtDuration(seconds) {
  if (!seconds || seconds < 0) return "-";
  const d = Math.floor(seconds / 86400);
  const h = Math.floor((seconds % 86400) / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  const parts = [];
  if (d > 0) parts.push(d + "d");
  if (h > 0) parts.push(h + "h");
  if (m > 0) parts.push(m + "m");
  parts.push(s + "s");
  return parts.join(" ");
}

function statusClass(v) {
  if (v === true || v === "healthy" || v === "paired" || v === "running" || v === "managed" || v === "registered") return "status-green";
  if (v === false || v === "error" || v === "unpaired" || v === "stopped" || v === "orphan") return v === "orphan" ? "status-orange" : "status-red";
  if (v === "warning" || v === "unknown") return "status-orange";
  return "status-muted";
}

function statusText(v) {
  if (v === true) return t("stHealthy");
  if (v === "healthy") return t("stHealthy");
  if (v === false) return t("stError");
  if (v === "paired") return t("stPaired");
  if (v === "unpaired") return t("stUnpaired");
  if (v === "running") return t("stRunning");
  if (v === "stopped") return t("stStopped");
  if (v === "registered") return t("stRegistered");
  if (v === "warning") return t("stWarning");
  return String(v ?? "-");
}

function barColor(pct) {
  if (pct < 60) return "green";
  if (pct < 85) return "orange";
  return "red";
}

// ── 渲染 ──────────────────────────────────────────────────
function render(data) {
  lastData = data;
  document.getElementById("refreshTime").textContent = new Date(data.collectedAt).toLocaleString();
  document.getElementById("bridgeHostname").textContent = data.bridge?.hostname || "-";
  document.getElementById("bridgeVersion").textContent = "v" + (data.bridge?.version || "-");
  document.getElementById("langBtn").textContent = t("langName");
  document.getElementById("refreshBtn").textContent = t("refreshBtn");

  // Section titles
  document.getElementById("sectionSysRes").firstChild.textContent = t("secSysRes");
  document.getElementById("sectionSessions").firstChild.textContent = t("secSessions");
  document.getElementById("unknownLabel").textContent = " " + t("secUnknown");
  document.getElementById("sectionOrphans").firstChild.textContent = t("secOrphans");
  document.getElementById("sectionPersisted").firstChild.textContent = t("secPersisted");
  document.getElementById("orphanBannerText").textContent = t("orphanBanner");
  if (document.getElementById("unknownTable").style.display !== "none") {
    document.getElementById("unknownIcon").classList.add("open");
  }

  // Status cards
  const b = data.binding || {};
  const r = data.registration || {};
  const cards = [
    { title: t("cardBridge"), items: [
      [t("fldVersion"), data.bridge?.version || "-"],
      [t("fldHost"), data.bridge?.hostname || "-"],
      [t("fldPlatform"), (data.bridge?.platform || "") + " " + (data.bridge?.arch || "")],
      [t("fldUptime"), fmtDuration(data.bridge?.uptime)],
      [t("fldNode"), data.bridge?.nodeVersion || "-"],
    ]},
    { title: t("cardRegistration"), items: [
      [t("fldStatus"), '<span class="' + statusClass(r.registered) + '">' + (r.registered ? t("stRegistered") : t("stNotRegistered")) + "</span>"],
      [t("fldInstanceId"), r.instanceId || "-"],
      [t("fldLastAttempt"), r.lastAttempt ? new Date(r.lastAttempt).toLocaleString() : "-"],
      [t("fldError"), r.error ? '<span class="status-red">' + r.error + "</span>" : t("stNone")],
    ]},
    { title: t("cardBinding"), items: [
      [t("fldStatus"), '<span class="' + statusClass(b.paired) + '">' + (b.paired ? t("stPaired") : t("stUnpaired")) + "</span>"],
      [t("fldInstanceId"), b.instanceId || "-"],
      [t("fldScheduler"), b.schedulerBaseUrl || "-"],
      [t("fldPairingCode"), b.pairingCode || "-"],
    ]},
    { title: t("cardSessions"), items: [
      [t("fldActiveSessions"), '<span class="' + ((data.sessions?.activeCount ?? 0) > 0 ? "status-green" : "status-muted") + '">' + (data.sessions?.activeCount ?? 0) + "</span>"],
      [t("fldPersistedSessions"), (data.sessions?.persistedCount ?? 0)],
      [t("fldManagedProcesses"), data.processes?.managed ?? 0],
      [t("fldOrphanProcesses"), '<span class="' + ((data.processes?.orphan ?? 0) > 0 ? "status-orange" : "status-muted") + '">' + (data.processes?.orphan ?? 0) + "</span>"],
      [t("fldRegistryEntries"), data.processes?.registryCount ?? 0],
    ]},
  ];

  document.getElementById("statusCards").innerHTML = cards.map(c => {
    const rows = c.items.map(([k, v]) =>
      '<div class="stat-row"><span class="stat-label">' + k + '</span><span class="stat-value">' + v + '</span></div>'
    ).join("");
    return '<div class="card"><div class="card-title">' + c.title + '</div>' + rows + '</div>';
  }).join("");

  // Metrics
  const m = data.metrics;
  const metricsGrid = document.getElementById("metricsGrid");
  if (m && m.ok) {
    const cpuPct = m.cpu?.usagePercent ?? 0;
    const memPct = m.memory?.usagePercent ?? 0;
    const storagePct = m.storage?.usagePercent ?? 0;
    metricsGrid.innerHTML = [
      { title: "CPU", usage: cpuPct, lines: [
        [t("fldUsage"), fmtPercent(cpuPct)],
        [t("fldCores"), m.cpu?.coreCount ?? "-"],
        [t("fldModel"), (m.cpu?.model || "-").substring(0, 40)],
      ]},
      { title: "Memory", usage: memPct, lines: [
        [t("fldUsage"), fmtPercent(memPct)],
        [t("fldUsed"), fmtBytes(m.memory?.usedBytes)],
        [t("fldFree"), fmtBytes(m.memory?.freeBytes)],
        [t("fldTotal"), fmtBytes(m.memory?.totalBytes)],
      ]},
      { title: "Storage (" + (m.storage?.path || "") + ")", usage: storagePct, lines: [
        [t("fldUsage"), fmtPercent(storagePct)],
        [t("fldFree"), m.storage ? m.storage.freeGiB + " GiB" : "-"],
        [t("fldTotal"), m.storage ? m.storage.totalGiB + " GiB" : "-"],
      ]},
      { title: "Network", usage: null, lines: [
        [t("fldDownload"), fmtBytes(m.network?.downloadBytesPerSecond) + "/s"],
        [t("fldUpload"), fmtBytes(m.network?.uploadBytesPerSecond) + "/s"],
        [t("fldTotalDown"), fmtBytes(m.network?.downloadTotalBytes)],
        [t("fldTotalUp"), fmtBytes(m.network?.uploadTotalBytes)],
      ]},
    ].map(c => {
      const barHtml = c.usage != null
        ? '<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>'
        : "";
      const rows = c.lines.map(([k, v]) =>
        '<div class="stat-row"><span class="stat-label">' + k + '</span><span class="stat-value">' + v + '</span></div>'
      ).join("");
      return '<div class="card"><div class="card-title">' + c.title + '</div>' + barHtml + rows + '</div>';
    }).join("");
  } else {
    metricsGrid.innerHTML = '<div class="card"><div class="empty">' + t("metricsUnavailable") + '</div></div>';
  }

  // Build process lookup maps from process list
  const plist = data.processes?.list || [];
  const pidByAgentId = {};
  const managedProcs = [];
  const orphanProcs = [];
  const unknownProcs = [];
  for (const p of plist) {
    if (p.agentId) pidByAgentId[p.agentId] = p.pid;
    if (p.managementStatus === 'managed') managedProcs.push(p);
    else if (p.managementStatus === 'orphan') orphanProcs.push(p);
    else unknownProcs.push(p);
  }

  // Summary badges for process categories
  document.getElementById("summaryManaged").textContent = managedProcs.length + " " + t("managed");
  document.getElementById("summaryOrphan").textContent = orphanProcs.length + " " + t("orphan");
  document.getElementById("summaryUnknown").textContent = unknownProcs.length + " unknown";

  // Active sessions (primary table, with PID from process cross-reference)
  const sessions = data.sessions?.active || [];
  document.getElementById("sessionCount").textContent = "(" + sessions.length + ")";
  if (sessions.length === 0) {
    document.getElementById("sessionTable").innerHTML = '<div class="empty">' + t("noSessions") + '</div>';
  } else {
    document.getElementById("sessionTable").innerHTML =
      '<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>' +
      sessions.map(s => {
        const pid = pidByAgentId[s.agentId];
        const st = s.runtimeStatus || "running";
        return '<tr><td>' + s.agentId + '</td><td>' + (pid ? '<code>' + pid + '</code>' : '<span class="status-muted">-</span>') + '</td>'
          + '<td><span class="' + statusClass(st) + '">' + (t("st" + st.charAt(0).toUpperCase() + st.slice(1)) || st) + '</span></td>'
          + '<td>' + (s.runtimeActionLabel || '-') + '</td>'
          + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.split(/[/\\\\]/).pop() : "-") + '</td>'
          + '<td>' + (s.providerId || "-") + '</td></tr>';
      }).join("") +
      '</tbody></table>';
  }

  // Orphan processes (shown only when orphans exist)
  const orphanContainer = document.getElementById("orphanContainer");
  if (orphanProcs.length > 0) {
    orphanContainer.style.display = '';
    document.getElementById("orphanCount").textContent = "(" + orphanProcs.length + ")";
    document.getElementById("orphanTable").innerHTML =
      '<table><thead><tr><th>' + t("thAgentId") + '</th><th>' + t("thPid") + '</th></tr></thead><tbody>' +
      orphanProcs.map(p =>
        '<tr><td>' + (p.agentId || '<span class="status-muted">-</span>') + '</td><td><code>' + p.pid + '</code></td></tr>'
      ).join("") +
      '</tbody></table>';
  } else {
    orphanContainer.style.display = 'none';
  }

  // Unknown processes (collapsible, hidden by default)
  document.getElementById("unknownCount").textContent = "(" + unknownProcs.length + ")";
  const unknownTable = document.getElementById("unknownTable");
  if (unknownProcs.length === 0) {
    unknownTable.innerHTML = '<div class="empty">' + t("noProcesses") + '</div>';
  } else {
    unknownTable.innerHTML =
      '<table><thead><tr><th>' + t("thPid") + '</th></tr></thead><tbody>' +
      unknownProcs.map(p =>
        '<tr><td><code>' + p.pid + '</code></td></tr>'
      ).join("") +
      '</tbody></table>';
  }

  // Persisted sessions
  const persisted = data.sessions?.persisted || [];
  document.getElementById("persistedCount").textContent = "(" + persisted.length + ")";
  if (persisted.length === 0) {
    document.getElementById("persistedTable").innerHTML = '<div class="empty">' + t("noPersisted") + '</div>';
  } else {
    document.getElementById("persistedTable").innerHTML =
      '<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>' +
      persisted.map(s =>
        '<tr><td>' + s.agentId + '</td><td>' + (s.status || "stopped") + '</td>'
        + '<td title="' + s.workspacePath + '">' + (s.workspacePath ? s.workspacePath.split(/[/\\\\]/).pop() : "-") + '</td>'
        + '<td>' + (s.command || "-") + '</td>'
        + '<td>' + (s.startedAt ? new Date(s.startedAt).toLocaleString() : "-") + '</td></tr>'
      ).join("") +
      '</tbody></table>';
  }
}

// ── Toggle unknown processes ────────────────────────────
function toggleUnknown() {
  const table = document.getElementById("unknownTable");
  const icon = document.getElementById("unknownIcon");
  const isHidden = table.style.display === "none";
  table.style.display = isHidden ? "" : "none";
  icon.classList.toggle("open", isHidden);
}

// ── 请求与轮询 ──────────────────────────────────────────
async function refresh() {
  try {
    const resp = await fetch("/runtime/dashboard");
    if (!resp.ok) throw new Error("HTTP " + resp.status);
    const data = await resp.json();
    render(data);
  } catch (err) {
    document.getElementById("refreshTime").textContent = "Error: " + err.message;
  }
}

// 每 10 秒自动刷新
setInterval(refresh, 10000);
refresh();

// ── 更新检查与安装 ──────────────────────────────────────
const UPDATE_LABELS = {
  zh: {
    label_available: "→ {v}",
    label_installing: "安装中...",
    label_restart: "重启",
    label_failed: "更新失败",
  },
  en: {
    label_available: "→ {v}",
    label_installing: "installing...",
    label_restart: "restart",
    label_failed: "failed",
  },
};

function ul(key) {
  const m = UPDATE_LABELS[lang] || UPDATE_LABELS.zh;
  return m[key] || UPDATE_LABELS.zh[key] || key;
}

let updateState = {
  installing: false,
  operationId: null,
};

// 检查更新
async function checkUpdate() {
  try {
    const resp = await fetch("/runtime/check-update");
    if (!resp.ok) throw new Error("HTTP " + resp.status);
    const data = await resp.json();
    if (data.ok && data.hasUpdate) {
      showUpdateAvailable(data);
    }
  } catch (err) {
    console.warn("Update check failed:", err.message);
  }
}

function showUpdateAvailable(data) {
  if (updateState.installing) return;
  const indicator = document.getElementById("updateIndicator");
  indicator.className = "update-indicator available";
  indicator.textContent = ul("label_available").replace("{v}", data.latestVersion);
  indicator.style.display = "";
  indicator.onclick = doInstallUpdate;
}

async function doInstallUpdate() {
  const indicator = document.getElementById("updateIndicator");
  updateState.installing = true;
  indicator.className = "update-indicator installing";
  indicator.textContent = ul("label_installing");
  indicator.onclick = null;

  try {
    const resp = await fetch("/runtime/update-bridge", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ requestedByUserId: "dashboard-ui" }),
    });
    const data = await resp.json();
    if (!data.ok && data.error) throw new Error(data.error);
    updateState.operationId = data.operationId;
  } catch (err) {
    indicator.className = "update-indicator failed";
    indicator.textContent = ul("label_failed");
    indicator.onclick = checkUpdate;
    updateState.installing = false;
  }
}

async function doRestartBridge() {
  const indicator = document.getElementById("updateIndicator");
  indicator.textContent = ul("label_restart") + "...";
  indicator.onclick = null;

  try {
    await fetch("/runtime/restart-bridge", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ preserveSessions: true, requestedByUserId: "dashboard-ui" }),
    });
  } catch (err) {
    // Bridge may shut down before responding — ignore
  }
}

// Override render to track lifecycle operation status
const __origRender = render;
render = function(data) {
  __origRender(data);

  const op = data.lifecycleOperation;
  if (!op || !updateState.installing) return;

  const indicator = document.getElementById("updateIndicator");

  if (op.operationType === "update") {
    if (op.status === "succeeded") {
      indicator.className = "update-indicator success";
      indicator.textContent = ul("label_restart");
      indicator.onclick = doRestartBridge;
      updateState.installing = false;
    } else if (op.status === "failed") {
      indicator.className = "update-indicator failed";
      indicator.textContent = ul("label_failed");
      indicator.onclick = checkUpdate;
      updateState.installing = false;
    }
  }
};

// Check for updates on page load
checkUpdate();
// Also check periodically (every 5 minutes)
setInterval(checkUpdate, 300000);
</script>
</body>
</html>`;
// ======================================================================
// LOGIN HTML — 登录页面
// ======================================================================
const LOGIN_HTML = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bridge Dashboard - Login</title>
<style>
  :root {
    --bg: #0f1117;
    --surface: #1a1d2e;
    --surface2: #232738;
    --border: #2d3248;
    --text: #e1e4f0;
    --text2: #8b90a8;
    --accent: #5b8def;
    --accent-hover: #4a7de0;
    --red: #f05454;
    --radius: 10px;
  }
  * { margin:0; padding:0; box-sizing:border-box; }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .login-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 40px 36px;
    width: 360px;
    max-width: 90vw;
  }
  .login-card h1 {
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 4px;
    text-align: center;
  }
  .login-card p.subtitle {
    font-size: 13px;
    color: var(--text2);
    text-align: center;
    margin-bottom: 28px;
  }
  .form-group {
    margin-bottom: 18px;
  }
  .form-group label {
    display: block;
    font-size: 13px;
    font-weight: 500;
    margin-bottom: 6px;
    color: var(--text2);
  }
  .form-group input {
    width: 100%;
    padding: 10px 12px;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-size: 14px;
    outline: none;
    transition: border-color .15s;
  }
  .form-group input:focus {
    border-color: var(--accent);
  }
  .form-group input::placeholder {
    color: var(--text2);
    opacity: 0.6;
  }
  button {
    width: 100%;
    padding: 10px;
    background: var(--accent);
    border: none;
    border-radius: 6px;
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: background .15s;
    margin-top: 4px;
  }
  button:hover { background: var(--accent-hover); }
  button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
  .error-msg {
    background: rgba(240,84,84,0.12);
    color: var(--red);
    padding: 8px 12px;
    border-radius: 6px;
    font-size: 13px;
    margin-bottom: 16px;
    display: none;
  }
  .error-msg.visible { display: block; }
</style>
</head>
<body>
<div class="login-card">
  <h1>Bridge Dashboard</h1>
  <p class="subtitle">Please log in to continue</p>
  <div class="error-msg" id="errorMsg"></div>
  <form id="loginForm" onsubmit="return doLogin(event)">
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text" id="username" name="username" placeholder="Username" autocomplete="username" autofocus>
    </div>
    <div class="form-group">
      <label for="password">Password</label>
      <input type="password" id="password" name="password" placeholder="Password" autocomplete="current-password">
    </div>
    <button type="submit" id="loginBtn">Log In</button>
  </form>
</div>
<script>
async function doLogin(e) {
  e.preventDefault();
  const btn = document.getElementById("loginBtn");
  const errorEl = document.getElementById("errorMsg");
  errorEl.classList.remove("visible");
  btn.disabled = true;
  btn.textContent = "Logging in...";

  try {
    const resp = await fetch("/runtime/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        username: document.getElementById("username").value,
        password: document.getElementById("password").value,
      }),
    });

    if (resp.ok) {
      window.location.href = "/";
      return;
    }

    const data = await resp.json();
    errorEl.textContent = data.error === "invalid credentials" ? "Invalid username or password" : "Login failed";
    errorEl.classList.add("visible");
  } catch (err) {
    errorEl.textContent = "Network error, please try again";
    errorEl.classList.add("visible");
  } finally {
    btn.disabled = false;
    btn.textContent = "Log In";
  }
}

// 回车键自动提交
document.getElementById("loginForm").addEventListener("keydown", function(e) {
  if (e.key === "Enter") doLogin(e);
});
</script>
</body>
</html>`;
