mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards. Co-authored-by: Cursor <cursoragent@cursor.com>
668 lines
24 KiB
JavaScript
668 lines
24 KiB
JavaScript
import { t, tf, el, tdCell, apiGet, resolveAdminApiUrl, getStoredAuthToken, SESSION_MONITOR_ROLE_FILTER_KEY, formatAuditActor, renderPageShell, renderSectionCard, enableTableColumnResize } from "../core.js";
|
|
|
|
async function renderAdminAudit() {
|
|
const action = el("input", { class: "input", placeholder: t("adminAudit.action") });
|
|
const actor = el("input", { class: "input", placeholder: t("adminAudit.actor") });
|
|
const status = el("input", { class: "input", placeholder: t("adminAudit.status") });
|
|
const tbody = el("tbody");
|
|
const pager = el("div", { class: "row", style: "gap:8px;align-items:center;flex-wrap:wrap;margin-top:8px;" });
|
|
const pageInfo = el("span", { class: "muted", text: tf("sessionMonitor.pageInfo", { page: 1, totalPages: 1 }) });
|
|
const totalInfo = el("span", { class: "muted", text: tf("adminAudit.total", { total: 0 }) });
|
|
const pageInput = el("input", {
|
|
class: "input",
|
|
type: "number",
|
|
min: "1",
|
|
step: "1",
|
|
placeholder: t("adminAudit.jumpPlaceholder"),
|
|
style: "width:90px;",
|
|
});
|
|
let page = 1;
|
|
const pageSize = 50;
|
|
let total = 0;
|
|
const totalPages = () => Math.max(1, Math.ceil((Number(total) || 0) / pageSize));
|
|
const setPager = () => {
|
|
const tp = totalPages();
|
|
pageInfo.textContent = tf("sessionMonitor.pageInfo", { page, totalPages: tp });
|
|
totalInfo.textContent = tf("adminAudit.total", { total });
|
|
pageInput.value = String(page);
|
|
btnPrev.disabled = page <= 1;
|
|
btnNext.disabled = page >= tp;
|
|
};
|
|
const load = async () => {
|
|
const p = new URLSearchParams();
|
|
p.set("limit", String(pageSize));
|
|
p.set("offset", String((Math.max(1, page) - 1) * pageSize));
|
|
if (action.value.trim()) p.set("action", action.value.trim());
|
|
if (actor.value.trim()) p.set("actor_user_id", actor.value.trim());
|
|
if (status.value.trim()) p.set("status", status.value.trim());
|
|
const resp = await apiGet("/admin/api/admin-audit?" + p.toString());
|
|
total = Math.max(0, Number(resp.total || 0) || 0);
|
|
const rows = Array.isArray(resp.items) ? resp.items : [];
|
|
tbody.innerHTML = "";
|
|
if (!rows.length) {
|
|
tbody.appendChild(el("tr", {}, [el("td", { text: t("audit.empty"), colspan: "8" })]));
|
|
setPager();
|
|
return;
|
|
}
|
|
rows.forEach((r) => {
|
|
tbody.appendChild(el("tr", {}, [
|
|
tdCell(r.timestamp || "", 24),
|
|
tdCell(formatAuditActor(r), 28),
|
|
tdCell(r.action || "", 20),
|
|
tdCell(r.target_type || "", 16),
|
|
tdCell(r.target_id || "", 24),
|
|
tdCell(r.status || "", 12),
|
|
tdCell(r.actor_tenant_id || "", 24),
|
|
tdCell(JSON.stringify(r.detail || {}), 120),
|
|
]));
|
|
});
|
|
setPager();
|
|
};
|
|
const btn = el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("audit.query"),
|
|
onclick: async () => {
|
|
page = 1;
|
|
await load();
|
|
},
|
|
});
|
|
const btnPrev = el("button", {
|
|
class: "btn btn--small",
|
|
text: t("sessionMonitor.pagePrev"),
|
|
disabled: true,
|
|
onclick: async () => {
|
|
if (page <= 1) return;
|
|
page -= 1;
|
|
await load();
|
|
},
|
|
});
|
|
const btnNext = el("button", {
|
|
class: "btn btn--small",
|
|
text: t("sessionMonitor.pageNext"),
|
|
disabled: true,
|
|
onclick: async () => {
|
|
const tp = totalPages();
|
|
if (page >= tp) return;
|
|
page += 1;
|
|
await load();
|
|
},
|
|
});
|
|
const btnJump = el("button", {
|
|
class: "btn btn--small",
|
|
text: t("adminAudit.jump"),
|
|
onclick: async () => {
|
|
const tp = totalPages();
|
|
let target = parseInt(String(pageInput.value || "").trim(), 10);
|
|
if (!Number.isFinite(target)) target = page;
|
|
target = Math.max(1, Math.min(tp, target));
|
|
if (target === page) {
|
|
setPager();
|
|
return;
|
|
}
|
|
page = target;
|
|
await load();
|
|
},
|
|
});
|
|
pageInput.addEventListener("keydown", async (ev) => {
|
|
if (ev.key !== "Enter") return;
|
|
ev.preventDefault();
|
|
btnJump.click();
|
|
});
|
|
pager.appendChild(btnPrev);
|
|
pager.appendChild(btnNext);
|
|
pager.appendChild(pageInfo);
|
|
pager.appendChild(totalInfo);
|
|
pager.appendChild(pageInput);
|
|
pager.appendChild(btnJump);
|
|
await load();
|
|
return el("div", {}, [
|
|
el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: t("adminAudit.title") }),
|
|
el("div", { class: "row" }, [action, actor, status, btn]),
|
|
pager,
|
|
el("div", { class: "table-wrap" }, [el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [el("tr", {}, [
|
|
el("th", { text: t("table.timestamp") }),
|
|
el("th", { text: t("adminAudit.actor") }),
|
|
el("th", { text: t("adminAudit.action") }),
|
|
el("th", { text: "target_type" }),
|
|
el("th", { text: "target_id" }),
|
|
el("th", { text: t("adminAudit.status") }),
|
|
el("th", { text: "tenant_id" }),
|
|
el("th", { text: t("table.payload") }),
|
|
])]),
|
|
tbody,
|
|
])]),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
async function renderSessionMonitor() {
|
|
const userQ = el("input", { class: "input", placeholder: t("sessionMonitor.filterUser") });
|
|
const sessQ = el("input", { class: "input", placeholder: t("sessionMonitor.filterSession") });
|
|
const activeOnlyChk = el("input", { type: "checkbox" });
|
|
const selectedHint = el("div", { class: "muted", text: t("sessionMonitor.selectUserFirst") });
|
|
const userBody = el("tbody");
|
|
const sessBody = el("tbody");
|
|
const sessionPagerTop = el("div", { class: "row" });
|
|
const sessionPagerBottom = el("div", { class: "row" });
|
|
const detailBody = el("tbody");
|
|
const detailStatus = el("div", { class: "muted", text: t("sessionMonitor.noMessages") });
|
|
const detailPager = el("div", { class: "row" });
|
|
const detailRoleFilter = el("select", { class: "input" });
|
|
const detailModal = el("div", { class: "session-monitor-modal u-hidden" });
|
|
const detailModalCard = el("div", { class: "session-monitor-modal__card" });
|
|
const totalsWrap = el("div", { class: "row" });
|
|
let selectedUserId = "";
|
|
let selectedSessionId = "";
|
|
let sessionLimit = 20;
|
|
let sessionOffset = 0;
|
|
let sessionTotal = 0;
|
|
let detailMessages = [];
|
|
let detailPage = 1;
|
|
const detailPageSize = 20;
|
|
let sessionMenuEl = null;
|
|
|
|
const rebuildDetailRoleFilter = () => {
|
|
const saved = String(localStorage.getItem(SESSION_MONITOR_ROLE_FILTER_KEY) || "").trim();
|
|
const prev = String(detailRoleFilter.value || saved || "all");
|
|
detailRoleFilter.innerHTML = "";
|
|
detailRoleFilter.appendChild(el("option", { value: "all", text: t("sessionMonitor.roleAll") }));
|
|
detailRoleFilter.appendChild(el("option", { value: "user", text: t("sessionMonitor.roleUser") }));
|
|
detailRoleFilter.appendChild(el("option", { value: "assistant", text: t("sessionMonitor.roleAssistant") }));
|
|
detailRoleFilter.appendChild(el("option", { value: "tool", text: t("sessionMonitor.roleTool") }));
|
|
detailRoleFilter.value = ["all", "user", "assistant", "tool"].includes(prev) ? prev : "all";
|
|
};
|
|
|
|
const buildPageInfo = (page, totalPages) => tf("sessionMonitor.pageInfo", { page, totalPages });
|
|
|
|
const downloadSessionExport = async (sessionId, format) => {
|
|
const q = format === "json" ? "format=json" : "format=md";
|
|
const path = `/admin/api/chat/sessions/${encodeURIComponent(sessionId)}/export?${q}`;
|
|
const url = resolveAdminApiUrl(path);
|
|
const tok = getStoredAuthToken();
|
|
const res = await fetch(url, {
|
|
headers: tok ? { authorization: `Bearer ${tok}` } : {},
|
|
});
|
|
if (!res.ok) throw new Error(`export_failed_${res.status}`);
|
|
const blob = await res.blob();
|
|
const a = document.createElement("a");
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = `chat-${String(sessionId || "").slice(0, 8)}.${format === "json" ? "json" : "md"}`;
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
};
|
|
|
|
const closeSessionMenu = () => {
|
|
if (sessionMenuEl && sessionMenuEl.parentNode) {
|
|
sessionMenuEl.remove();
|
|
}
|
|
sessionMenuEl = null;
|
|
};
|
|
|
|
const closeDetailModal = () => {
|
|
detailModal.style.display = "none";
|
|
};
|
|
|
|
const renderTotals = (totals) => {
|
|
totalsWrap.innerHTML = "";
|
|
const cards = [
|
|
{ key: "total_tokens_est", label: t("sessionMonitor.totalTokensEst") },
|
|
{ key: "active_sessions_30m", label: t("sessionMonitor.activeSessions30m") },
|
|
{ key: "active_logins_30m", label: t("sessionMonitor.activeLogins30m") },
|
|
{ key: "users_count", label: t("sessionMonitor.usersCount") },
|
|
];
|
|
cards.forEach((c) => {
|
|
totalsWrap.appendChild(
|
|
el("div", { class: "kv", text: `${c.label}: ${String((totals && totals[c.key]) ?? 0)}` }),
|
|
);
|
|
});
|
|
};
|
|
|
|
const renderDetailPager = () => {
|
|
detailPager.innerHTML = "";
|
|
const role = String(detailRoleFilter.value || "all");
|
|
const filtered = Array.isArray(detailMessages)
|
|
? detailMessages.filter((m) => {
|
|
if (role === "all") return true;
|
|
const r = String(m.role || "").toLowerCase();
|
|
return role === "tool" ? r === "tool" || r === "function" : r === role;
|
|
})
|
|
: [];
|
|
const total = filtered.length;
|
|
const totalPages = Math.max(1, Math.ceil(total / detailPageSize));
|
|
const info = el("span", { class: "muted", text: buildPageInfo(detailPage, totalPages) });
|
|
const btnPrev = el("button", {
|
|
class: "btn btn--small",
|
|
text: t("sessionMonitor.pagePrev"),
|
|
disabled: detailPage <= 1,
|
|
onclick: () => {
|
|
if (detailPage <= 1) return;
|
|
detailPage -= 1;
|
|
renderDetailMessages();
|
|
},
|
|
});
|
|
const btnNext = el("button", {
|
|
class: "btn btn--small",
|
|
text: t("sessionMonitor.pageNext"),
|
|
disabled: detailPage >= totalPages,
|
|
onclick: () => {
|
|
if (detailPage >= totalPages) return;
|
|
detailPage += 1;
|
|
renderDetailMessages();
|
|
},
|
|
});
|
|
detailPager.appendChild(btnPrev);
|
|
detailPager.appendChild(btnNext);
|
|
detailPager.appendChild(info);
|
|
};
|
|
|
|
const renderDetailMessages = () => {
|
|
detailBody.innerHTML = "";
|
|
if (!selectedSessionId) {
|
|
detailStatus.textContent = t("sessionMonitor.selectUserFirst");
|
|
detailPager.innerHTML = "";
|
|
return;
|
|
}
|
|
const role = String(detailRoleFilter.value || "all");
|
|
const all = Array.isArray(detailMessages) ? detailMessages : [];
|
|
const filtered = all.filter((m) => {
|
|
if (role === "all") return true;
|
|
const r = String(m.role || "").toLowerCase();
|
|
return role === "tool" ? r === "tool" || r === "function" : r === role;
|
|
});
|
|
if (!filtered.length) {
|
|
detailStatus.textContent = t("sessionMonitor.noMessages");
|
|
detailPager.innerHTML = "";
|
|
return;
|
|
}
|
|
const totalPages = Math.max(1, Math.ceil(filtered.length / detailPageSize));
|
|
if (detailPage > totalPages) detailPage = totalPages;
|
|
const start = (detailPage - 1) * detailPageSize;
|
|
const rows = filtered.slice(start, start + detailPageSize);
|
|
detailStatus.textContent = `session_id: ${selectedSessionId} | ${filtered.length}/${all.length}`;
|
|
rows.forEach((m) => {
|
|
detailBody.appendChild(
|
|
el("tr", {}, [
|
|
tdCell(m.id || "", 8),
|
|
tdCell(m.role || "", 10),
|
|
tdCell(m.content || "", 120),
|
|
tdCell(m.timestamp || "", 24),
|
|
]),
|
|
);
|
|
});
|
|
renderDetailPager();
|
|
};
|
|
|
|
const loadSessionDetail = async (sessionId, opts = {}) => {
|
|
const openModal = Boolean(opts && opts.openModal);
|
|
selectedSessionId = String(sessionId || "");
|
|
detailPage = 1;
|
|
detailBody.innerHTML = "";
|
|
detailStatus.textContent = `${t("chat.loading")}...`;
|
|
detailPager.innerHTML = "";
|
|
if (!selectedSessionId) {
|
|
detailStatus.textContent = t("sessionMonitor.noMessages");
|
|
if (openModal) detailModal.style.display = "flex";
|
|
return;
|
|
}
|
|
try {
|
|
const resp = await apiGet(`/admin/api/chat/sessions/${encodeURIComponent(selectedSessionId)}/messages`);
|
|
detailMessages = Array.isArray(resp.messages) ? resp.messages : [];
|
|
renderDetailMessages();
|
|
} catch (err) {
|
|
detailMessages = [];
|
|
detailBody.innerHTML = "";
|
|
detailPager.innerHTML = "";
|
|
detailStatus.textContent = `${t("chat.error")}: ${String(err && err.message ? err.message : err)}`;
|
|
}
|
|
if (openModal) detailModal.style.display = "flex";
|
|
};
|
|
|
|
const renderSessionPager = () => {
|
|
const totalPages = Math.max(1, Math.ceil(Math.max(0, sessionTotal) / sessionLimit));
|
|
const current = Math.floor(sessionOffset / sessionLimit) + 1;
|
|
const row = el("div", { class: "row" }, [
|
|
el("button", {
|
|
class: "btn btn--small",
|
|
text: t("sessionMonitor.pagePrev"),
|
|
disabled: sessionOffset <= 0,
|
|
onclick: async () => {
|
|
if (sessionOffset <= 0) return;
|
|
sessionOffset = Math.max(0, sessionOffset - sessionLimit);
|
|
await loadSessions();
|
|
},
|
|
}),
|
|
el("button", {
|
|
class: "btn btn--small",
|
|
text: t("sessionMonitor.pageNext"),
|
|
disabled: sessionOffset + sessionLimit >= sessionTotal,
|
|
onclick: async () => {
|
|
if (sessionOffset + sessionLimit >= sessionTotal) return;
|
|
sessionOffset += sessionLimit;
|
|
await loadSessions();
|
|
},
|
|
}),
|
|
el("span", { class: "muted", text: buildPageInfo(current, totalPages) }),
|
|
]);
|
|
return row;
|
|
};
|
|
|
|
const updateSessionPagers = () => {
|
|
sessionPagerTop.innerHTML = "";
|
|
sessionPagerBottom.innerHTML = "";
|
|
sessionPagerTop.appendChild(renderSessionPager());
|
|
sessionPagerBottom.appendChild(renderSessionPager());
|
|
};
|
|
detailRoleFilter.addEventListener("change", () => {
|
|
try {
|
|
localStorage.setItem(SESSION_MONITOR_ROLE_FILTER_KEY, String(detailRoleFilter.value || "all"));
|
|
} catch (_) {}
|
|
detailPage = 1;
|
|
renderDetailMessages();
|
|
});
|
|
|
|
const loadSessions = async () => {
|
|
sessBody.innerHTML = "";
|
|
if (!selectedUserId) {
|
|
sessBody.appendChild(el("tr", {}, [el("td", { text: t("sessionMonitor.selectUserFirst"), colspan: "8" })]));
|
|
selectedHint.textContent = t("sessionMonitor.selectUserFirst");
|
|
sessionTotal = 0;
|
|
updateSessionPagers();
|
|
return;
|
|
}
|
|
const sp = new URLSearchParams();
|
|
sp.set("user_id", selectedUserId);
|
|
sp.set("limit", String(sessionLimit));
|
|
sp.set("offset", String(sessionOffset));
|
|
if (sessQ.value.trim()) sp.set("q", sessQ.value.trim());
|
|
if (activeOnlyChk.checked) sp.set("active_only", "1");
|
|
const resp = await apiGet("/admin/api/chat/admin/sessions?" + sp.toString());
|
|
const rows = Array.isArray(resp.sessions) ? resp.sessions : [];
|
|
sessionTotal = Number(resp.total || 0);
|
|
selectedHint.textContent = `${t("table.userId")}: ${selectedUserId}`;
|
|
if (!rows.length) {
|
|
sessBody.appendChild(el("tr", {}, [el("td", { text: t("audit.empty"), colspan: "8" })]));
|
|
if (selectedSessionId && !rows.some((x) => String(x.session_id || "") === selectedSessionId)) {
|
|
selectedSessionId = "";
|
|
detailMessages = [];
|
|
renderDetailMessages();
|
|
}
|
|
updateSessionPagers();
|
|
return;
|
|
}
|
|
rows.forEach((r) => {
|
|
const sid = String(r.session_id || "");
|
|
const btnMore = el("button", {
|
|
class: "chat-sess-more" + (sid === selectedSessionId ? " chat-sess-more--active" : ""),
|
|
text: "⋯",
|
|
title: t("chat.sessionMenu"),
|
|
onclick: (ev) => {
|
|
ev.stopPropagation();
|
|
closeSessionMenu();
|
|
const menu = el("div", { class: "chat-sess-menu-pop u-overlay-fixed" }, [
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("sessionMonitor.viewDetail"),
|
|
onclick: async () => {
|
|
closeSessionMenu();
|
|
await loadSessionDetail(sid, { openModal: true });
|
|
},
|
|
}),
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("sessionMonitor.viewAudit"),
|
|
onclick: () => {
|
|
closeSessionMenu();
|
|
location.hash = `#/audit?session_id=${encodeURIComponent(sid)}`;
|
|
},
|
|
}),
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("sessionMonitor.exportMd"),
|
|
onclick: async () => {
|
|
closeSessionMenu();
|
|
await downloadSessionExport(sid, "md");
|
|
},
|
|
}),
|
|
el("button", {
|
|
class: "chat-sess-menu-item",
|
|
text: t("sessionMonitor.exportJson"),
|
|
onclick: async () => {
|
|
closeSessionMenu();
|
|
await downloadSessionExport(sid, "json");
|
|
},
|
|
}),
|
|
]);
|
|
const rect = ev.currentTarget.getBoundingClientRect();
|
|
document.body.appendChild(menu);
|
|
// Clamp into viewport; flip above if near bottom.
|
|
const mrect = menu.getBoundingClientRect();
|
|
const pad = 8;
|
|
let left = rect.left - 120;
|
|
let top = rect.bottom + 4;
|
|
if (top + mrect.height > window.innerHeight - pad) {
|
|
top = rect.top - 4 - mrect.height;
|
|
}
|
|
left = Math.max(pad, Math.min(left, window.innerWidth - pad - mrect.width));
|
|
top = Math.max(pad, Math.min(top, window.innerHeight - pad - mrect.height));
|
|
menu.style.left = `${left}px`;
|
|
menu.style.top = `${top}px`;
|
|
sessionMenuEl = menu;
|
|
const close = (e) => {
|
|
if (!menu.contains(e.target)) {
|
|
closeSessionMenu();
|
|
document.removeEventListener("click", close);
|
|
}
|
|
};
|
|
setTimeout(() => document.addEventListener("click", close), 0);
|
|
},
|
|
});
|
|
sessBody.appendChild(
|
|
el(
|
|
"tr",
|
|
{
|
|
"data-session-id": sid,
|
|
class: sid === selectedSessionId ? "session-monitor-row--active" : "",
|
|
onclick: async () => {
|
|
selectedSessionId = sid;
|
|
closeSessionMenu();
|
|
const sessionRows = Array.from(sessBody.querySelectorAll("tr[data-session-id]"));
|
|
sessionRows.forEach((tr) => {
|
|
const curr = String(tr.getAttribute("data-session-id") || "");
|
|
tr.classList.toggle("session-monitor-row--active", curr === selectedSessionId);
|
|
});
|
|
},
|
|
},
|
|
[
|
|
tdCell(sid, 24),
|
|
tdCell(r.title || "", 30),
|
|
tdCell(r.username || "", 16),
|
|
tdCell(r.message_count || "", 10),
|
|
tdCell(r.last_message_at || "", 24),
|
|
tdCell(r.is_active_30m ? "yes" : "no", 8),
|
|
el("td", { class: "table__cell-actions" }, [btnMore]),
|
|
],
|
|
),
|
|
);
|
|
});
|
|
updateSessionPagers();
|
|
};
|
|
|
|
const loadUsers = async () => {
|
|
const p = new URLSearchParams();
|
|
p.set("limit", "200");
|
|
if (userQ.value.trim()) p.set("q", userQ.value.trim());
|
|
const resp = await apiGet("/admin/api/chat/admin/user-stats?" + p.toString());
|
|
renderTotals(resp.totals || {});
|
|
const rows = Array.isArray(resp.users) ? resp.users : [];
|
|
userBody.innerHTML = "";
|
|
if (!rows.length) {
|
|
userBody.appendChild(el("tr", {}, [el("td", { text: t("audit.empty"), colspan: "9" })]));
|
|
selectedUserId = "";
|
|
selectedSessionId = "";
|
|
detailMessages = [];
|
|
await loadSessions();
|
|
return;
|
|
}
|
|
if (!selectedUserId || !rows.some((x) => String(x.user_id || "") === selectedUserId)) {
|
|
selectedUserId = String(rows[0].user_id || "");
|
|
}
|
|
rows.forEach((r) => {
|
|
const uid = String(r.user_id || "");
|
|
const pickBtn = el("button", {
|
|
class: "btn btn--small",
|
|
"data-user-pick": "1",
|
|
"data-user-id": uid,
|
|
text: uid === selectedUserId ? "●" : "○",
|
|
onclick: async () => {
|
|
selectedUserId = uid;
|
|
sessionOffset = 0;
|
|
selectedSessionId = "";
|
|
detailMessages = [];
|
|
const pickBtns = Array.from(userBody.querySelectorAll("button[data-user-pick='1']"));
|
|
pickBtns.forEach((btnEl) => {
|
|
const id = String(btnEl.getAttribute("data-user-id") || "");
|
|
btnEl.textContent = id === selectedUserId ? "●" : "○";
|
|
});
|
|
await loadSessions();
|
|
},
|
|
});
|
|
userBody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", {}, [pickBtn]),
|
|
tdCell(r.username || "", 16),
|
|
tdCell(r.display_name || "", 16),
|
|
tdCell(r.role || "", 10),
|
|
tdCell(r.sessions_count || "", 10),
|
|
tdCell(r.active_sessions_30m || "", 10),
|
|
tdCell(r.active_login_30m || "", 10),
|
|
tdCell(r.total_tokens_est || "", 14),
|
|
tdCell(r.last_message_at || "", 24),
|
|
]),
|
|
);
|
|
});
|
|
await loadSessions();
|
|
};
|
|
|
|
const btnQueryUsers = el("button", { class: "btn btn--primary", text: t("audit.query"), onclick: loadUsers });
|
|
const btnQuerySessions = el("button", {
|
|
class: "btn",
|
|
text: t("audit.query"),
|
|
onclick: async () => {
|
|
sessionOffset = 0;
|
|
selectedSessionId = "";
|
|
detailMessages = [];
|
|
await loadSessions();
|
|
},
|
|
});
|
|
const activeOnlyLabel = el("label", { class: "row" }, [
|
|
activeOnlyChk,
|
|
el("span", { class: "muted", text: t("sessionMonitor.activeSessions30m") }),
|
|
]);
|
|
|
|
await loadUsers();
|
|
rebuildDetailRoleFilter();
|
|
const btnCloseDetail = el("button", {
|
|
class: "btn btn--small",
|
|
text: t("sessionMonitor.closeDetail"),
|
|
onclick: () => {
|
|
selectedSessionId = "";
|
|
detailMessages = [];
|
|
detailBody.innerHTML = "";
|
|
detailStatus.textContent = t("sessionMonitor.noMessages");
|
|
detailPager.innerHTML = "";
|
|
closeDetailModal();
|
|
loadSessions().catch(() => {});
|
|
},
|
|
});
|
|
const detailTable = el("table", { class: "table table--compact session-monitor-detail-table" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: "id" }),
|
|
el("th", { text: "role" }),
|
|
el("th", { text: "content" }),
|
|
el("th", { text: t("table.timestamp") }),
|
|
]),
|
|
]),
|
|
detailBody,
|
|
]);
|
|
detailModalCard.appendChild(
|
|
el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: t("sessionMonitor.messages") }),
|
|
el("div", { class: "row" }, [
|
|
el("span", { class: "muted", text: t("sessionMonitor.roleFilter") }),
|
|
detailRoleFilter,
|
|
btnCloseDetail,
|
|
]),
|
|
detailStatus,
|
|
detailPager,
|
|
el("div", { class: "table-wrap" }, [
|
|
detailTable,
|
|
]),
|
|
]),
|
|
);
|
|
enableTableColumnResize(detailTable, [2, 3]);
|
|
detailModal.appendChild(detailModalCard);
|
|
detailModal.addEventListener("click", (ev) => {
|
|
if (ev.target === detailModal) closeDetailModal();
|
|
});
|
|
return renderPageShell({
|
|
title: t("title.sessionMonitor"),
|
|
subtitle: "按用户、会话与消息详情进行巡检与导出",
|
|
sections: [
|
|
{ id: "sm-totals", label: "总览" },
|
|
{ id: "sm-users", label: "用户" },
|
|
{ id: "sm-sessions", label: "会话" },
|
|
],
|
|
}, [
|
|
renderSectionCard(t("sessionMonitor.totals"), "", [totalsWrap], { id: "sm-totals" }),
|
|
renderSectionCard(t("sessionMonitor.userStats"), "", [
|
|
el("div", { class: "row" }, [userQ, btnQueryUsers]),
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: "#" }),
|
|
el("th", { text: t("table.username") }),
|
|
el("th", { text: t("table.name") }),
|
|
el("th", { text: t("tenants.role") }),
|
|
el("th", { text: t("table.sessionsCount") }),
|
|
el("th", { text: t("table.activeSessions30m") }),
|
|
el("th", { text: t("table.activeLogins30m") }),
|
|
el("th", { text: t("table.totalTokensEst") }),
|
|
el("th", { text: t("table.timestamp") }),
|
|
]),
|
|
]),
|
|
userBody,
|
|
]),
|
|
]),
|
|
], { id: "sm-users" }),
|
|
renderSectionCard(t("sessionMonitor.sessions"), "", [
|
|
el("div", { class: "row" }, [sessQ, activeOnlyLabel, btnQuerySessions]),
|
|
selectedHint,
|
|
sessionPagerTop,
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact session-monitor-session-table" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: "session_id" }),
|
|
el("th", { text: t("table.name") }),
|
|
el("th", { text: t("table.username") }),
|
|
el("th", { text: "msg_count" }),
|
|
el("th", { text: t("table.timestamp") }),
|
|
el("th", { text: t("table.activeSessions30m") }),
|
|
el("th", { text: t("table.action") }),
|
|
]),
|
|
]),
|
|
sessBody,
|
|
]),
|
|
]),
|
|
sessionPagerBottom,
|
|
], { id: "sm-sessions" }),
|
|
detailModal,
|
|
]);
|
|
}
|
|
|
|
|
|
export { renderAdminAudit, renderSessionMonitor };
|