Add hub system logs page with PM2 stdout/stderr tabs for all exchanges.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 系统日志:三所 + 中控 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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user