"use strict"; /* ========================================================================== B · 值班台账 Ledger Desk —— 表格本身就是主舞台,每条真实事件一行。 响应/汇入/处理/发布/审计五列只填该行真实具备的阶段,没有发生的阶段留空, 不做跨行编造关联。底部调用带只滚真实 recent_calls。 ========================================================================== */ (function () { const C = window.Core, S = window.HubShared; const { $, esc, timeShort } = C; let root = null; let unsubs = []; let seenRowKeys = new Set(); let mounted = false; let pendingTimers = []; function safeTimeout(fn, ms) { const id = setTimeout(() => { if (mounted) fn(); }, ms); pendingTimers.push(id); return id; } const SKELETON = `
来源健康 SOURCES
调度任务 JOBS
事件台账 EVENT LEDGER 每条事件依次经过 响应→汇入→处理→发布→审计
时间事件来源 响应
RESP
汇入
INGEST
处理
PROC
发布
PUB
审计
AUDIT
调用带 CALL TAPE
盘后发布 RELEASE
审计 AUDIT
`; function renderSources() { const v = S.sourcesView(); const host = $("lg-srcList"); if (!v) { host.innerHTML = `
正在加载…
`; return; } host.innerHTML = v.live.map((it) => { const health = it.health || {}; const link = it._link || C.linkState(health); return `
${esc(C.FLOW_LABEL[it.provider] || it.provider)} ${C.chipHtml(link, health.state || link)} ${health.latency_ms == null ? "-" : health.latency_ms + "ms"}
`; }).join(""); S.bindProbeButtons(host, () => renderSources()); } function renderJobs() { const v = S.jobsView(); const host = $("lg-jobList"); if (!v) { host.innerHTML = `
正在加载…
`; return; } host.innerHTML = v.jobs.map((job) => { const latest = v.latestByJob.get(job.id); return `
${C.chipHtml(latest ? latest.state : "idle")} ${esc(job.id)} ${esc(job.at)}
`; }).join(""); S.bindRunButtons(host, () => renderJobs()); } function renderRelease() { const v = S.releaseView(); const dateInput = $("lg-rel-date"); if (!dateInput.value) dateInput.value = C.state.releaseDate; if (!v) { $("lg-pubTable").innerHTML = `
正在加载…
`; return; } $("lg-pubTable").innerHTML = C.table(["数据集", "批次", "状态", "时间", ""], v.pubs.map((p) => [ `${esc(p.dataset)}`, `${esc(p.active_batch)}`, C.chipHtml(p.state), `${esc(p.published_at)}`, p.prev_batch ? `` : "-", ])); S.bindRollbackButtons($("lg-pubTable"), () => renderRelease()); } function renderAuditList() { const v = S.auditView(); const host = $("lg-auditList"); if (!v) { host.innerHTML = `
正在加载…
`; return; } host.innerHTML = v.items.slice(0, 10).map((a) => `
${esc(timeShort(a.created_at))} ${esc(a.actor)} ${esc(a.action)}
${esc(a.target)}${a.detail ? " · " + esc(a.detail) : ""}
`).join(""); } /* ---------------------------------------------------------------- 台账行合成 每行只填该事件类型真实具备的阶段;不同类型互不编造对方的字段。 */ function cell(text, kind) { if (text == null || text === "") return ``; return `${esc(text)}`; } function buildLedgerRows() { const ov = S.overviewView(), jv = S.jobsView(), rv = S.releaseView(), av = S.auditView(); const rows = []; if (ov) { for (const c of ov.recentCalls) { rows.push({ key: `call:${c.id}`, t: c.created_at, name: c.endpoint, sub: "来源响应", source: c.provider, resp: cell(timeShort(c.created_at), c.ok ? "ok" : "err"), ingest: c.ok ? cell(timeShort(c.created_at), "ok") : cell(), proc: cell(), pub: cell(), audit: cell(), err: !c.ok, }); } } if (jv) { for (const r of jv.runs.slice(0, 30)) { const job = jv.jobs.find((j) => j.id === r.job_id); rows.push({ key: `run:${r.id}`, t: r.started_at, name: (job && job.title) || r.job_id, sub: r.job_id, source: "system", resp: cell(), ingest: cell(timeShort(r.started_at), "ok"), proc: r.state === "running" ? cell("running", "warn") : r.state === "failed" ? cell(r.error || "failed", "err") : cell(timeShort(r.finished_at), "ok"), pub: cell(), audit: cell(), err: r.state === "failed", }); } } if (rv) { for (const p of rv.pubs) { if (!p.published_at || p.published_at === "-") continue; rows.push({ key: `pub:${p.dataset}:${p.active_batch}`, t: p.published_at, name: `批次发布 · ${p.dataset}`, sub: p.active_batch, source: "system", resp: cell(), ingest: cell(), proc: cell(), pub: cell(timeShort(p.published_at), p.state === "published" ? "ok" : "warn"), audit: cell(), err: p.state === "missing" || p.state === "error", }); } } if (av) { for (const a of av.items.slice(0, 30)) { const isRollback = String(a.action).includes("rollback"); const isProbe = a.action === "probe"; rows.push({ key: `audit:${a.id}`, t: a.created_at, name: a.action, sub: a.target, source: a.actor, resp: isProbe ? cell(timeShort(a.created_at), String(a.detail).includes("失败") ? "err" : "ok") : cell(), ingest: cell(), proc: cell(), pub: isRollback ? cell(timeShort(a.created_at), "warn") : cell(), audit: cell(timeShort(a.created_at), isRollback ? "warn" : "ok"), err: isRollback, }); } } rows.sort((a, b) => String(b.t).localeCompare(String(a.t))); return rows.slice(0, 60); } function renderLedger() { const rows = buildLedgerRows(); const body = $("lg-ledgerBody"); body.innerHTML = rows.map((r) => ` ${esc(timeShort(r.t))}
${esc(r.name)}
${esc(r.sub || "")}
${esc(r.source)} ${r.resp} ${r.ingest} ${r.proc} ${r.pub} ${r.audit} `).join(""); rows.forEach((r) => seenRowKeys.add(r.key)); if (!C.state.reduced) { body.querySelectorAll("tr.flashnew").forEach((tr) => { setTimeout(() => tr.classList.remove("flashnew"), 1000); }); } else { body.querySelectorAll("tr.flashnew").forEach((tr) => tr.classList.remove("flashnew")); } } /* ---------------------------------------------------------------- 调用带:只滚真实 recent_calls */ function renderTape() { const ov = S.overviewView(); const inner = $("lg-tape-inner"); if (!ov || !ov.recentCalls.length) { inner.innerHTML = `暂无真实调用`; inner.style.animation = "none"; return; } const items = ov.recentCalls.slice(0, 16).map((c) => `${esc(timeShort(c.created_at))} ${esc(c.provider)} ${esc(c.endpoint)} ${c.ok ? "✓" : "× " + esc(c.error)}`).join(""); inner.innerHTML = items + items; // 首尾拼接形成无缝循环,内容仍全部来自真实调用 if (C.state.reduced) { inner.style.animation = "none"; return; } const width = inner.scrollWidth / 2; inner.style.animation = "none"; void inner.offsetWidth; inner.style.setProperty("--tape-w", `-${width}px`); inner.style.animation = `lg-tape-scroll ${Math.max(12, width / 40)}s linear infinite`; } if (!document.getElementById("lg-tape-keyframes")) { const style = document.createElement("style"); style.id = "lg-tape-keyframes"; style.textContent = `@keyframes lg-tape-scroll { from { transform: translateX(0); } to { transform: translateX(var(--tape-w, -800px)); } }`; document.head.appendChild(style); } function updateTapePauseState() { const el = $("lg-tape"); if (!el) return; el.classList.toggle("paused", !C.Poller.runtimeAvailable() || C.state.reduced); } function refresh() { if (!root) return; renderSources(); renderJobs(); renderRelease(); renderAuditList(); renderLedger(); renderTape(); updateTapePauseState(); } function onEvent(evt) { if (!root || !mounted) return; if (C.FLOW_PROVIDERS.includes(evt.channel)) { const row = root.querySelector(`.lg-src-row[data-provider="${evt.channel}"] [data-act]`); if (row) { row.classList.remove("on", "err"); row.classList.add("on"); if (evt.kind === "error") row.classList.add("err"); setTimeout(() => row.classList.remove("on", "err"), 800); } } renderLedger(); renderTape(); } function mount(el) { root = el; mounted = true; seenRowKeys = new Set(); root.innerHTML = SKELETON; refresh(); S.bindReleaseDateReload(root, "#lg-rel-date", "#lg-rel-load", () => renderRelease()); S.bindBackfillButton(root, "#lg-rel-backfill", () => renderRelease()); unsubs.push(C.Bus.on("data", refresh)); unsubs.push(C.Bus.on("event", onEvent)); unsubs.push(C.Bus.on("runtime", updateTapePauseState)); unsubs.push(C.Bus.on("reduced", () => { renderTape(); updateTapePauseState(); })); } function unmount() { mounted = false; unsubs.forEach((fn) => fn()); unsubs = []; pendingTimers.forEach(clearTimeout); pendingTimers = []; root = null; } window.HUB_LAYOUTS = window.HUB_LAYOUTS || {}; window.HUB_LAYOUTS.ledger = { mount, unmount }; })();