/** * 系统日志:三所 + 中控 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, """); } 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) => `` ) .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 }; })();