5214794f95
Co-authored-by: Cursor <cursoragent@cursor.com>
170 lines
4.9 KiB
JavaScript
170 lines
4.9 KiB
JavaScript
/**
|
|
* 系统日志:三所 + 中控 PM2 stdout/stderr.
|
|
*/
|
|
(function () {
|
|
const page = document.getElementById("page-logs");
|
|
if (!page) return;
|
|
|
|
const tabsEl = document.getElementById("hub-logs-tabs");
|
|
const statusEl = document.getElementById("hub-logs-status");
|
|
const outEl = document.getElementById("hub-logs-out");
|
|
const errEl = document.getElementById("hub-logs-err");
|
|
const btnRefresh = document.getElementById("hub-logs-btn-refresh");
|
|
const btnPause = document.getElementById("hub-logs-btn-pause");
|
|
|
|
const POLL_MS = 4000;
|
|
let activeKey = "binance";
|
|
let tabsMeta = [];
|
|
let pollTimer = null;
|
|
let paused = false;
|
|
let loading = false;
|
|
let bound = false;
|
|
|
|
async function apiFetch(url) {
|
|
const r = await fetch(url, { credentials: "same-origin" });
|
|
const ct = (r.headers.get("content-type") || "").toLowerCase();
|
|
if (ct.includes("application/json")) {
|
|
const data = await r.json();
|
|
if (!r.ok) throw new Error((data && (data.msg || data.detail)) || r.statusText || "请求失败");
|
|
return data;
|
|
}
|
|
if (!r.ok) throw new Error(r.statusText || "请求失败");
|
|
return r;
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function wasScrolledToBottom(el) {
|
|
if (!el) return true;
|
|
return el.scrollHeight - el.scrollTop - el.clientHeight < 24;
|
|
}
|
|
|
|
function setPreText(el, text, stickBottom) {
|
|
if (!el) return;
|
|
const atBottom = stickBottom || wasScrolledToBottom(el);
|
|
el.textContent = text || "(暂无日志)";
|
|
if (atBottom) el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
function setStatus(text, isErr) {
|
|
if (!statusEl) return;
|
|
statusEl.textContent = text || "";
|
|
statusEl.classList.toggle("is-err", !!isErr);
|
|
}
|
|
|
|
function renderTabs() {
|
|
if (!tabsEl) return;
|
|
tabsEl.innerHTML = tabsMeta
|
|
.map(
|
|
(t) =>
|
|
`<button type="button" class="hub-logs-tab${t.key === activeKey ? " is-active" : ""}" role="tab" aria-selected="${t.key === activeKey}" data-key="${esc(t.key)}">${esc(t.label)}</button>`
|
|
)
|
|
.join("");
|
|
tabsEl.querySelectorAll(".hub-logs-tab").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const key = btn.getAttribute("data-key");
|
|
if (!key || key === activeKey) return;
|
|
activeKey = key;
|
|
renderTabs();
|
|
void loadLogs(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loadMeta() {
|
|
const meta = await apiFetch("/api/system-logs/meta");
|
|
tabsMeta = Array.isArray(meta.targets) ? meta.targets : [];
|
|
if (tabsMeta.length && !tabsMeta.some((t) => t.key === activeKey)) {
|
|
activeKey = tabsMeta[0].key;
|
|
}
|
|
renderTabs();
|
|
}
|
|
|
|
async function loadLogs(force) {
|
|
if (loading && !force) return;
|
|
loading = true;
|
|
try {
|
|
const data = await apiFetch(`/api/system-logs/${encodeURIComponent(activeKey)}?lines=200`);
|
|
setPreText(outEl, data.out || "", true);
|
|
setPreText(errEl, data.err || "", true);
|
|
const ts = data.updated_at ? new Date(data.updated_at * 1000) : new Date();
|
|
const hh = String(ts.getHours()).padStart(2, "0");
|
|
const mm = String(ts.getMinutes()).padStart(2, "0");
|
|
const ss = String(ts.getSeconds()).padStart(2, "0");
|
|
const missing = [];
|
|
if (!data.out_exists) missing.push("实时");
|
|
if (!data.err_exists) missing.push("报错");
|
|
const hint = missing.length ? ` · ${missing.join("/")}日志文件暂无` : "";
|
|
setStatus(`已更新 ${hh}:${mm}:${ss}${hint}`, false);
|
|
} catch (e) {
|
|
setStatus(e.message || "加载失败", true);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function startPoll() {
|
|
stopPoll();
|
|
if (paused) return;
|
|
pollTimer = window.setInterval(() => {
|
|
void loadLogs(false);
|
|
}, POLL_MS);
|
|
}
|
|
|
|
function stopPoll() {
|
|
if (pollTimer) {
|
|
clearInterval(pollTimer);
|
|
pollTimer = null;
|
|
}
|
|
}
|
|
|
|
function bindControls() {
|
|
if (bound) return;
|
|
bound = true;
|
|
if (btnRefresh) {
|
|
btnRefresh.addEventListener("click", () => {
|
|
void loadLogs(true);
|
|
});
|
|
}
|
|
if (btnPause) {
|
|
btnPause.addEventListener("click", () => {
|
|
paused = !paused;
|
|
btnPause.textContent = paused ? "继续刷新" : "暂停刷新";
|
|
btnPause.classList.toggle("is-paused", paused);
|
|
if (paused) stopPoll();
|
|
else startPoll();
|
|
});
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
bindControls();
|
|
paused = false;
|
|
if (btnPause) {
|
|
btnPause.textContent = "暂停刷新";
|
|
btnPause.classList.remove("is-paused");
|
|
}
|
|
setStatus("加载中…", false);
|
|
try {
|
|
await loadMeta();
|
|
await loadLogs(true);
|
|
startPoll();
|
|
} catch (e) {
|
|
setStatus(e.message || "初始化失败", true);
|
|
}
|
|
}
|
|
|
|
function destroy() {
|
|
stopPoll();
|
|
setStatus("", false);
|
|
}
|
|
|
|
window.hubLogsPage = { init, destroy };
|
|
})();
|