Files
sub2api-cn-relay-manager/deploy/tksea-portal/admin-batch-import.html
Hermes Agent 56474264d6
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled
refactor(portal): dedup inline scripts in accounts + batch-import + providers
All three admin pages had two parallel inline <script> blocks (a modern S1
that used adminRuntime + a legacy S2 that was self-contained). Both had
a nested <script> text inside S1 that the browser tolerated only because
the second script re-ran any state-affecting calls. Merge into a single
inline script per page; fix the nested <script> comment.

- providers.html: 100371 -> 62761 chars (-37610, -37%)
- accounts.html:  54878 -> 33098 chars  (-21780, -40%)
- batch-import:   43861 -> 26570 chars  (-17291, -39%)

Also rename draftProviderIDInput -> providerIDInput in providers.html
(the old draft-provider-id input was removed during the earlier workflow
merge, leaving the script with a null addEventListener on draft id).

All scripts pass node --check. Both test_tksea_portal_assets.sh and
verify_frontend_smoke.sh PASS.
2026-06-03 13:14:31 +08:00

661 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Batch Import Admin · 管理台</title>
<link rel="stylesheet" href="/portal/portal.css">
<link rel="stylesheet" href="/portal/admin-common.css">
<style>
/* Page-specific layout only. Tokens, cards, buttons come from portal.css + admin-common.css. */
.grid { display: grid; gap: var(--s-5); }
.grid-2 { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
.field-grid { display: grid; gap: 12px; }
.field-grid.two { grid-template-columns: 1fr 1fr; }
.field-grid.thwo { grid-template-columns: 2fr 1fr 1fr; }
.empty { color: var(--text-muted); font-size: 13px; line-height: 1.6; }
.panel-desc { color: var(--text-muted); font-size: 13px; line-height: 1.6; margin: 8px 0 0; }
.inline-code { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); word-break: break-word; }
.table-wrap { margin-top: 12px; }
.run-summary { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
.run-summary > div { padding: 8px 12px; border-radius: var(--r-md); background: var(--bg-elev-3); border: 1px solid var(--border-subtle); }
pre { margin: 0; padding: 16px; border-radius: var(--r-md); border: 1px solid var(--border-subtle); background: rgba(2,6,23,0.6); color: var(--text-default); font-size: 12px; line-height: 1.65; overflow: auto; white-space: pre-wrap; word-break: break-word; }
[data-theme="light"] pre { background: var(--slate-900); color: var(--slate-100); }
@media (max-width: 1100px) { .grid-2 { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<main class="shell fade-in">
<nav class="topnav" aria-label="Admin Navigation" data-admin-nav data-admin-current="batch-import"></nav>
<section class="page-hero">
<div>
<span class="page-hero__eyebrow">Batch Import</span>
<h1>live batch-import拉取 run 与 item 级别的 account_resolution</h1>
<p>
这页继续负责 live batch-import创建 run、拉取 run summary、查看 item 级别的
<code>matched_account_state</code><code>account_resolution</code>。批量导入第三方 key验证
<code>reused / created / reactivated / replaced</code> 状态语义。
</p>
<ul style="display:flex;flex-wrap:wrap;gap:10px;margin:18px 0 0;padding:0;list-style:none;">
<li style="padding:8px 12px;border-radius:999px;border:1px solid var(--border-subtle);background:var(--bg-elev-1);font-size:13px;font-weight:700;color:var(--text-muted);">默认 API Base<code>/portal-admin-api</code></li>
<li style="padding:8px 12px;border-radius:999px;border:1px solid var(--border-subtle);background:var(--bg-elev-1);font-size:13px;font-weight:700;color:var(--text-muted);">Runs 列表:<code>/api/batch-import/runs</code></li>
<li style="padding:8px 12px;border-radius:999px;border:1px solid var(--border-subtle);background:var(--bg-elev-1);font-size:13px;font-weight:700;color:var(--text-muted);">Run 详情:<code>/api/batch-import/runs/{run_id}/items</code></li>
</ul>
</div>
<div class="stack" style="gap:var(--s-3);">
<div class="stat-card">
<div class="stat-icon stat-icon-primary" id="bimp-m1"></div>
<div class="min-w-0">
<p class="stat-label">API Root</p>
<p class="stat-value" id="metric-api-root">-</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon stat-icon-info" id="bimp-m2"></div>
<div class="min-w-0">
<p class="stat-label">当前 Run ID</p>
<p class="stat-value" id="metric-run-id">-</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon stat-icon-success" id="bimp-m3"></div>
<div class="min-w-0">
<p class="stat-label">Run State</p>
<p class="stat-value" id="metric-run-state">-</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon stat-icon-warning" id="bimp-m4"></div>
<div class="min-w-0">
<p class="stat-label">Run Items</p>
<p class="stat-value" id="metric-run-items">-</p>
</div>
</div>
</div>
</section>
<section class="grid">
<article class="panel">
<h2>发起导入</h2>
<p class="panel-desc">
优先使用管理员登录会话调用当前控制面的 batch-import API必要时也可以回退到 Bearer token。
`entries` 每行一个供应商帐号:`base_url|api_key|model_a,model_b`。
</p>
<div class="field-grid two">
<label>API Base
<input id="api-base" type="text" placeholder="https://sub.tksea.top/portal-admin-api">
</label>
<label>Host ID
<input id="host-id" type="text" placeholder="remote43-current-host" list="preset-host-id">
<datalist id="preset-host-id">
<option value="remote43-current-host"></option>
<option value="remote43"></option>
<option value="localhost"></option>
<option value="dev-host"></option>
</datalist>
<span class="hint">目标子机的 host_id。要从「现有 host 列表」里选一个;若列表为空则先在 providers.html 加载 pack 目录。</span>
</label>
</div>
<div class="field-grid two">
<label>Admin Token可选
<input id="admin-token" type="password" placeholder="secret-token">
<span class="hint">优先使用下方管理员登录;这里只保留给脚本联调或紧急兜底。</span>
</label>
<label>Mode
<select id="mode">
<option value="strict">strict</option>
<option value="partial">partial</option>
</select>
</label>
</div>
<div class="field-grid two">
<label>管理员用户名
<input id="admin-username" type="text" placeholder="admin">
</label>
<label>管理员密码
<input id="admin-password" type="password" placeholder="请输入管理员密码">
</label>
</div>
<div class="toolbar">
<button id="admin-login-btn" type="button">管理员登录</button>
<button id="admin-logout-btn" type="button" class="ghost">退出登录</button>
<span id="admin-session-status" class="statusbar">尚未检查管理员会话。</span>
</div>
<div class="field-grid two">
<label>Access Mode
<select id="access-mode">
<option value="self_service">self_service</option>
<option value="subscription">subscription</option>
</select>
</label>
<label>Confirm Wait Timeout Sec
<input id="confirm-timeout" type="number" min="1" value="10">
</label>
</div>
<div class="field-grid two" id="self-service-fields">
<label>Probe API Key
<input id="probe-api-key" type="text" placeholder="sk-probe">
</label>
<div class="muted-block">
`self_service` 会直接用这把 key 执行 gateway completion 验证。
</div>
</div>
<div class="field-grid two" id="subscription-fields" hidden>
<label>Subscription Users
<input id="subscription-users" type="text" placeholder="user-1,user-2">
<span class="hint">逗号分隔,至少 1 个用户。</span>
</label>
<label>Subscription Days
<input id="subscription-days" type="number" min="1" value="30">
</label>
</div>
<label style="margin-top: 12px;">Entries
<textarea id="entries" rows="6" placeholder="https://api.example.com/v1|sk-example-1|kimi-k2.6
https://api.other.com/v1|sk-example-2|deepseek-chat,gpt-5.4"></textarea>
<span class="hint">
格式:<code>base_url|api_key|requested_model_1,requested_model_2</code><br>
· 每行一条供应商帐号;多个 key 走多行即可(批量导入)。<br>
· 第三段 <code>requested_model</code> 可省略CRM 会从 host 已发布 provider 自动推断。<br>
· 同 base_url 可合并多行;不同 base_url 走独立 row 即可。
</span>
</label>
<div class="actions">
<button class="primary" id="create-run-btn">创建 Run</button>
<button class="ghost" id="save-config-btn">保存本地配置</button>
<button class="ghost" id="load-sample-btn">恢复示例</button>
</div>
<div class="statusbar" id="statusbar">等待操作。</div>
</article>
<article class="panel">
<h2>Run 结果</h2>
<p class="panel-desc">
创建完成后会自动查询 run 摘要和 item 列表。也可以手动输入 run id 重新拉取。
</p>
<div class="field-grid two">
<label>Run ID
<input id="run-id" type="text" placeholder="run_1779848658025955399">
</label>
<div class="actions" style="align-items: end;">
<button class="secondary" id="refresh-run-btn">刷新 Run</button>
<button class="ghost" id="clear-items-btn">清空结果</button>
</div>
</div>
<div class="run-meta" id="run-meta"></div>
<div class="summary-cards">
<div class="summary-card"><span class="subtle">总条目</span><strong id="sum-total">0</strong></div>
<div class="summary-card"><span class="subtle">完成</span><strong id="sum-completed">0</strong></div>
<div class="summary-card"><span class="subtle">Active</span><strong id="sum-active">0</strong></div>
<div class="summary-card"><span class="subtle">Degraded</span><strong id="sum-degraded">0</strong></div>
<div class="summary-card"><span class="subtle">Broken</span><strong id="sum-broken">0</strong></div>
</div>
<div class="toolbar">
<label>搜索
<input id="filter-query" type="text" placeholder="provider_id / base_url">
</label>
<label>Matched Account State
<select id="filter-matched-state">
<option value="">全部</option>
<option value="active">active</option>
<option value="disabled">disabled</option>
<option value="deprecated">deprecated</option>
<option value="broken">broken</option>
</select>
</label>
<label>Account Resolution
<select id="filter-account-resolution">
<option value="">全部</option>
<option value="created">created</option>
<option value="reused">reused</option>
<option value="reactivated">reactivated</option>
<option value="replaced">replaced</option>
</select>
</label>
<button class="ghost" id="apply-filter-btn">应用过滤</button>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Provider</th>
<th>Base URL</th>
<th>Smoke Model</th>
<th>Matched / Resolution</th>
<th>Access</th>
<th>Badges</th>
<th>Advisory</th>
</tr>
</thead>
<tbody id="items-tbody">
<tr><td colspan="7" class="subtle">还没有结果。</td></tr>
</tbody>
</table>
</div>
</article>
</section>
</main>
<script src="/portal/admin-common.js"></script>
<script src="/portal/portal.js"></script>
<script>
const AdminCommon = window.Sub2ApiAdminCommon;
window.Sub2ApiPortal.renderModernAdminNav(document.querySelector("[data-admin-nav]"), "batch-import");
(function injectIcons(){
const M = (id, n) => { const el = document.getElementById(id); if (el) el.innerHTML = window.Sub2ApiPortal.svg(n, 22); };
M("bimp-m1", "shield");
M("bimp-m2", "import");
M("bimp-m3", "activity");
M("bimp-m4", "package");
})();
// <script>
AdminCommon.renderAdminNav(document.querySelector("[data-admin-nav]"), "batch-import");
const storageKey = "sub2api-crm-batch-import-admin-v1";
const state = {
currentRunID: "",
currentItems: [],
currentRun: null,
};
const statusbar = document.getElementById("statusbar");
const apiBaseInput = document.getElementById("api-base");
const hostIDInput = document.getElementById("host-id");
const adminTokenInput = document.getElementById("admin-token");
const adminUsernameInput = document.getElementById("admin-username");
const adminPasswordInput = document.getElementById("admin-password");
const adminLoginButton = document.getElementById("admin-login-btn");
const adminLogoutButton = document.getElementById("admin-logout-btn");
const adminSessionStatus = document.getElementById("admin-session-status");
const modeInput = document.getElementById("mode");
const accessModeInput = document.getElementById("access-mode");
const confirmTimeoutInput = document.getElementById("confirm-timeout");
const probeAPIKeyInput = document.getElementById("probe-api-key");
const subscriptionUsersInput = document.getElementById("subscription-users");
const subscriptionDaysInput = document.getElementById("subscription-days");
const entriesInput = document.getElementById("entries");
const runIDInput = document.getElementById("run-id");
const selfServiceFields = document.getElementById("self-service-fields");
const subscriptionFields = document.getElementById("subscription-fields");
const metricApiRoot = document.getElementById("metric-api-root");
const metricRunID = document.getElementById("metric-run-id");
const metricRunState = document.getElementById("metric-run-state");
const runMeta = document.getElementById("run-meta");
const itemsTbody = document.getElementById("items-tbody");
const filterQueryInput = document.getElementById("filter-query");
const filterMatchedStateInput = document.getElementById("filter-matched-state");
const filterAccountResolutionInput = document.getElementById("filter-account-resolution");
const summaryTargets = {
total: document.getElementById("sum-total"),
completed: document.getElementById("sum-completed"),
active: document.getElementById("sum-active"),
degraded: document.getElementById("sum-degraded"),
broken: document.getElementById("sum-broken"),
};
const adminRuntime = AdminCommon.createAdminPageRuntime({
apiBaseInput,
adminTokenInput,
adminUsernameInput,
adminPasswordInput,
adminSessionStatus,
onSessionPersist: saveConfig,
});
function setStatus(message, tone = "note") {
AdminCommon.setStatus(statusbar, message, tone);
}
function defaultApiBase() {
return adminRuntime.defaultApiBase();
}
function saveConfig() {
const payload = {
apiBase: apiBaseInput.value.trim(),
hostID: hostIDInput.value.trim(),
adminToken: adminTokenInput.value,
adminUsername: adminUsernameInput.value.trim(),
mode: modeInput.value,
accessMode: accessModeInput.value,
confirmTimeoutSec: confirmTimeoutInput.value,
probeAPIKey: probeAPIKeyInput.value.trim(),
subscriptionUsers: subscriptionUsersInput.value.trim(),
subscriptionDays: subscriptionDaysInput.value,
entries: entriesInput.value,
};
AdminCommon.writeStoredConfig(storageKey, payload);
setStatus("本地配置已保存。", "success");
syncHeaderMetrics();
}
function restoreConfig() {
const payload = AdminCommon.readStoredConfig(storageKey);
if (!Object.keys(payload).length) {
apiBaseInput.value = defaultApiBase();
hostIDInput.value = "";
confirmTimeoutInput.value = "10";
subscriptionDaysInput.value = "30";
return;
}
apiBaseInput.value = payload.apiBase || defaultApiBase();
hostIDInput.value = payload.hostID || "";
adminTokenInput.value = payload.adminToken || "";
adminUsernameInput.value = payload.adminUsername || "";
modeInput.value = payload.mode || "strict";
accessModeInput.value = payload.accessMode || "self_service";
confirmTimeoutInput.value = payload.confirmTimeoutSec || "10";
probeAPIKeyInput.value = payload.probeAPIKey || "";
subscriptionUsersInput.value = payload.subscriptionUsers || "";
subscriptionDaysInput.value = payload.subscriptionDays || "30";
entriesInput.value = payload.entries || entriesInput.value;
}
function loadSampleEntries() {
entriesInput.value = [
"https://api.example.com/v1|sk-example-1|kimi-k2.6",
"https://api.other.com/v1|sk-example-2|deepseek-chat,gpt-5.4",
].join("\\n");
setStatus("示例 entries 已恢复。");
}
function updateAccessModeFields() {
const accessMode = accessModeInput.value;
const subscriptionMode = accessMode === "subscription";
selfServiceFields.hidden = subscriptionMode;
subscriptionFields.hidden = !subscriptionMode;
}
function normalizeApiBase() {
return adminRuntime.normalizeApiBase();
}
function authHeaders() {
return {
"Content-Type": "application/json",
...adminRuntime.authHeaders(),
};
}
function parseEntries() {
return entriesInput.value
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line, index) => {
const [baseURL = "", apiKey = "", models = ""] = line.split("|").map((part) => part.trim());
if (!baseURL || !apiKey) {
throw new Error(`${index + 1} 行格式不完整,需要 base_url|api_key|models`);
}
return {
base_url: baseURL,
api_key: apiKey,
requested_models: models ? models.split(",").map((item) => item.trim()).filter(Boolean) : [],
};
});
}
function buildCreatePayload() {
const payload = {
host_id: hostIDInput.value.trim(),
mode: modeInput.value,
access_mode: accessModeInput.value,
confirm_wait_timeout_sec: Number(confirmTimeoutInput.value || 10),
entries: parseEntries(),
};
if (!payload.host_id) {
throw new Error("host_id 不能为空");
}
if (payload.access_mode === "self_service") {
payload.probe_api_key = probeAPIKeyInput.value.trim();
if (!payload.probe_api_key) {
throw new Error("self_service 模式下 probe_api_key 不能为空");
}
} else {
payload.subscription_users = subscriptionUsersInput.value
.split(",")
.map((value) => value.trim())
.filter(Boolean);
payload.subscription_days = Number(subscriptionDaysInput.value || 30);
if (!payload.subscription_users.length) {
throw new Error("subscription 模式下 subscription_users 不能为空");
}
if (!payload.subscription_days) {
throw new Error("subscription 模式下 subscription_days 不能为空");
}
}
return payload;
}
async function requestJSON(path, options = {}) {
return adminRuntime.requestJSON(path, options);
}
async function refreshAdminSession() {
return adminRuntime.refreshAdminSession();
}
async function loginAdminSession() {
return adminRuntime.loginAdminSession();
}
async function logoutAdminSession() {
return adminRuntime.logoutAdminSession();
}
function syncHeaderMetrics() {
metricApiRoot.textContent = normalizeApiBase();
metricRunID.textContent = state.currentRunID || "-";
metricRunState.textContent = state.currentRun?.state || "-";
}
function renderRunMeta(run) {
runMeta.innerHTML = "";
const meta = [
`run_id=${run.run_id}`,
`state=${run.state}`,
`mode=${run.mode}`,
`access_mode=${run.access_mode}`,
];
meta.forEach((entry) => {
const code = document.createElement("code");
code.textContent = entry;
runMeta.appendChild(code);
});
}
function toneClass(tone) {
if (!tone) return "tone-gray";
return `tone-${tone}`;
}
function renderBadges(badges) {
if (!Array.isArray(badges) || !badges.length) {
return '<span class="subtle">-</span>';
}
return `<div class="badge-row">${badges.map((badge) => `<span class="badge ${toneClass(badge.tone)}">${escapeHTML(badge.label)}</span>`).join("")}</div>`;
}
function renderItems(items) {
if (!items.length) {
itemsTbody.innerHTML = '<tr><td colspan="7" class="subtle">当前过滤条件下没有条目。</td></tr>';
return;
}
itemsTbody.innerHTML = items.map((item) => {
const advisory = Array.isArray(item.advisory_messages) && item.advisory_messages.length
? item.advisory_messages.map((message) => `<div>${escapeHTML(message)}</div>`).join("")
: '<span class="subtle">-</span>';
return `
<tr>
<td>
<strong>${escapeHTML(item.provider_id)}</strong><br>
<code>${escapeHTML(item.api_key_fingerprint || "-")}</code>
</td>
<td><code>${escapeHTML(item.base_url)}</code></td>
<td>
<div>${escapeHTML(item.resolved_smoke_model || "-")}</div>
<div class="subtle">${escapeHTML((item.canonical_model_families || []).join(", ") || "-")}</div>
</td>
<td>
<div><strong>${escapeHTML(item.matched_account_state || "-")}</strong></div>
<div class="subtle">${escapeHTML(item.account_resolution || "-")}</div>
</td>
<td>
<div>${escapeHTML(item.access_status || "-")}</div>
<div class="subtle">${escapeHTML(item.confirmation_status || "-")} / ${escapeHTML(item.current_stage || "-")}</div>
</td>
<td>${renderBadges(item.badges)}</td>
<td>${advisory}</td>
</tr>
`;
}).join("");
}
function renderRunSummary(run) {
state.currentRun = run;
summaryTargets.total.textContent = String(run.total_items || 0);
summaryTargets.completed.textContent = String(run.completed_items || 0);
summaryTargets.active.textContent = String(run.active_items || 0);
summaryTargets.degraded.textContent = String(run.degraded_items || 0);
summaryTargets.broken.textContent = String(run.broken_items || 0);
renderRunMeta(run);
syncHeaderMetrics();
}
function clearResults() {
state.currentRun = null;
state.currentRunID = "";
state.currentItems = [];
runIDInput.value = "";
renderRunSummary({
run_id: "-",
state: "-",
mode: "-",
access_mode: "-",
total_items: 0,
completed_items: 0,
active_items: 0,
degraded_items: 0,
broken_items: 0,
});
runMeta.innerHTML = "";
itemsTbody.innerHTML = '<tr><td colspan="7" class="subtle">还没有结果。</td></tr>';
setStatus("结果已清空。");
}
async function createRun() {
const button = document.getElementById("create-run-btn");
button.disabled = true;
try {
saveConfig();
setStatus("正在创建 batch import run ...");
const payload = buildCreatePayload();
const created = await requestJSON("/api/batch-import/runs", {
method: "POST",
headers: authHeaders(),
body: JSON.stringify(payload),
});
state.currentRunID = created.run_id;
runIDInput.value = created.run_id;
syncHeaderMetrics();
setStatus(`run 已创建:${created.run_id},正在拉取详情。`, "success");
await refreshRun();
} catch (error) {
setStatus(`创建失败:${error.message}`, "danger");
} finally {
button.disabled = false;
}
}
function buildItemsQuery() {
const params = new URLSearchParams();
if (filterQueryInput.value.trim()) params.set("q", filterQueryInput.value.trim());
if (filterMatchedStateInput.value) params.set("matched_account_state", filterMatchedStateInput.value);
if (filterAccountResolutionInput.value) params.set("account_resolution", filterAccountResolutionInput.value);
return params.toString();
}
async function refreshRun() {
const runID = runIDInput.value.trim();
if (!runID) {
setStatus("请先输入 run_id。", "warning");
return;
}
const button = document.getElementById("refresh-run-btn");
button.disabled = true;
try {
state.currentRunID = runID;
syncHeaderMetrics();
setStatus(`正在刷新 ${runID} ...`);
const runPayload = await requestJSON(`/api/batch-import/runs/${encodeURIComponent(runID)}`, {
headers: authHeaders(),
});
renderRunSummary(runPayload.run);
const query = buildItemsQuery();
const itemsPayload = await requestJSON(`/api/batch-import/runs/${encodeURIComponent(runID)}/items${query ? `?${query}` : ""}`, {
headers: authHeaders(),
});
state.currentItems = itemsPayload.items || [];
renderItems(state.currentItems);
setStatus(`run ${runID} 已刷新,当前显示 ${state.currentItems.length} 条 item。`, "success");
} catch (error) {
setStatus(`刷新失败:${error.message}`, "danger");
} finally {
button.disabled = false;
}
}
function escapeHTML(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
document.getElementById("create-run-btn").addEventListener("click", createRun);
document.getElementById("refresh-run-btn").addEventListener("click", refreshRun);
document.getElementById("save-config-btn").addEventListener("click", saveConfig);
document.getElementById("load-sample-btn").addEventListener("click", loadSampleEntries);
document.getElementById("clear-items-btn").addEventListener("click", clearResults);
document.getElementById("apply-filter-btn").addEventListener("click", refreshRun);
adminLoginButton.addEventListener("click", async () => {
try {
await loginAdminSession();
} catch (error) {}
});
adminLogoutButton.addEventListener("click", async () => {
try {
await logoutAdminSession();
await refreshAdminSession();
} catch (error) {}
});
accessModeInput.addEventListener("change", updateAccessModeFields);
restoreConfig();
updateAccessModeFields();
syncHeaderMetrics();
refreshAdminSession().catch(() => {});
</script>
</body>
</html>