"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 = `
事件台账 EVENT LEDGER
每条事件依次经过 响应→汇入→处理→发布→审计
| 时间 | 事件 | 来源 |
响应 RESP |
汇入 INGEST |
处理 PROC |
发布 PUB |
审计 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 };
})();