"use strict";
/* ==========================================================================
C · 地层剖面 Strata —— 来源层 → 加工层 → 发布层 → 审计沉积层 四条全宽横带。
真实事件自上而下贯穿(脊线上的光点从上一层滑到下一层);点击任一层的项目
进入"选中"态,用同一颜色在其它层高亮可关联的真实行,解决跨层追踪费眼问题。
========================================================================== */
(function () {
const C = window.Core, S = window.HubShared;
const { $, esc, timeShort } = C;
let root = null;
let mounted = false;
let pendingTimers = [];
function safeTimeout(fn, ms) {
const id = setTimeout(() => { if (mounted) fn(); }, ms);
pendingTimers.push(id);
return id;
}
let unsubs = [];
let selection = null; // { type: 'provider'|'job'|'dataset', value }
const SKELETON = `
`;
function applySelectionClasses() {
root.querySelectorAll("[data-sel-provider]").forEach((el) => el.classList.toggle("sel", selection && selection.type === "provider" && el.dataset.selProvider === selection.value));
root.querySelectorAll("[data-sel-job]").forEach((el) => el.classList.toggle("sel", selection && selection.type === "job" && el.dataset.selJob === selection.value));
root.querySelectorAll("[data-rel-job]").forEach((el) => el.classList.toggle("hi", selection && selection.type === "job" && el.dataset.relJob === selection.value));
root.querySelectorAll("[data-rel-dataset]").forEach((el) => el.classList.toggle("hi", selection && selection.type === "dataset" && el.dataset.relDataset === selection.value));
}
function setSelection(type, value) {
selection = (selection && selection.type === type && selection.value === value) ? null : { type, value };
applySelectionClasses();
}
function renderSources() {
const v = S.sourcesView();
const host = $("st-sources");
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)}
${esc(C.FLOW_ROLE[it.provider] || it.role)} · 配置 ${(it.credential || {}).configured ? "已配置" : "未配置"} · ${it.calls_today ?? 0} 次
${health.latency_ms == null ? "-" : health.latency_ms + "ms"}
`;
}).join("") + (v.reserved.length ? `预留 ${v.reserved.length} 个
未接入
` : "");
S.bindProbeButtons(host, () => renderSources());
host.querySelectorAll(".st-src").forEach((el) => el.addEventListener("click", (e) => {
if (e.target.closest("button")) return;
setSelection("provider", el.dataset.provider);
}));
applySelectionClasses();
}
function renderProcess() {
const v = S.jobsView();
const jobHost = $("st-jobList");
if (!v) { jobHost.innerHTML = `正在加载…
`; return; }
jobHost.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(jobHost, () => renderProcess());
jobHost.querySelectorAll(".st-job").forEach((el) => el.addEventListener("click", (e) => {
if (e.target.closest("button")) return;
setSelection("job", el.dataset.job);
}));
$("st-runsTable").innerHTML = C.table(["ID", "任务", "状态", "开始", "结束", "错误"], v.runs.map((r) => [
`${r.id}`,
`${esc(r.job_id)}`,
`${esc(r.state)}`,
`${esc(timeShort(r.started_at))}`, `${esc(timeShort(r.finished_at))}`,
`${esc(r.error || "")}`,
]));
root.querySelectorAll("#st-runsTable tr").forEach((tr, i) => {
if (i === 0) return;
const run = v.runs[i - 1]; if (run) tr.dataset.relJob = run.job_id;
});
applySelectionClasses();
}
function renderPublish() {
const v = S.releaseView();
const dateInput = $("st-rel-date");
if (!dateInput.value) dateInput.value = C.state.releaseDate;
if (!v) { $("st-pubnum").textContent = "-"; $("st-datasetsTable").innerHTML = `正在加载…
`; return; }
$("st-pubnum").textContent = v.pubs.length;
const eod = (S.overviewView() || {}).eod || {};
$("st-pubwin").textContent = eod.state === "waiting_upstream" ? `等待上游 · 已试 ${eod.attempts ?? "-"} 次` : "";
$("st-datasetsTable").innerHTML = C.table(["数据集", "活跃批次", "上一批次", "状态", "发布时间", "操作"], v.pubs.map((p) => [
`${esc(p.dataset)}`,
`${esc(p.active_batch)}`, `${esc(p.prev_batch || "-")}`,
C.chipHtml(p.state), `${esc(p.published_at)}`,
p.prev_batch ? `` : "-",
]));
root.querySelectorAll("#st-datasetsTable tr").forEach((tr, i) => {
if (i === 0) return;
const p = v.pubs[i - 1]; if (p) tr.dataset.relDataset = p.dataset;
});
S.bindRollbackButtons($("st-datasetsTable"), () => renderPublish());
applySelectionClasses();
}
function renderAudit() {
const v = S.auditView();
const host = $("st-audit");
if (!v) { host.innerHTML = `正在加载…
`; return; }
host.innerHTML = v.items.slice(0, 8).map((a) => `
${esc(timeShort(a.created_at))} ${esc(a.actor)} ${esc(a.action)} · ${esc(a.target)}${a.detail ? " · " + esc(a.detail) : ""}
`).join("");
}
function refresh() { if (!root) return; renderSources(); renderProcess(); renderPublish(); renderAudit(); }
/* ---------------------------------------------------------------- 纵向贯穿动效 */
const BAND_ORDER = ["sources", "process", "publish", "audit"];
function pulseBand(key, err) {
if (!root) return;
const el = root.querySelector(`.st-band[data-band="${key}"]`);
if (!el) return;
el.classList.add("pulse"); el.classList.toggle("err", !!err);
clearTimeout(el._t);
el._t = setTimeout(() => el.classList.remove("pulse", "err"), 700);
}
function travelSpine(fromKey, toKey) {
if (C.state.reduced || !root) return;
const dot = $("st-dot");
const a = root.querySelector(`.st-band[data-band="${fromKey}"]`);
const b = root.querySelector(`.st-band[data-band="${toKey}"]`);
if (!a || !b) return;
const rootRect = $("st-root").getBoundingClientRect();
const ar = a.getBoundingClientRect(), br = b.getBoundingClientRect();
const y0 = ar.top - rootRect.top + 20, y1 = br.top - rootRect.top + 20;
dot.style.top = y0 + "px"; dot.style.opacity = "0";
if (dot._anim) dot._anim.cancel();
dot._anim = dot.animate([
{ transform: "translateY(0)", opacity: 0 },
{ transform: "translateY(0)", opacity: 1, offset: .1 },
{ transform: `translateY(${y1 - y0}px)`, opacity: 1, offset: .9 },
{ transform: `translateY(${y1 - y0}px)`, opacity: 0 },
], { duration: 620, easing: "linear" });
}
function onEvent(evt) {
if (!root || !mounted) return;
if (C.FLOW_PROVIDERS.includes(evt.channel)) {
const act = root.querySelector(`.st-src[data-provider="${evt.channel}"] [data-act]`);
if (act) {
act.classList.remove("on", "err"); act.classList.add("on"); if (evt.kind === "error") act.classList.add("err");
setTimeout(() => act.classList.remove("on", "err"), 800);
}
pulseBand("sources", evt.kind === "error");
safeTimeout(() => { travelSpine("sources", "process"); pulseBand("process"); renderSources(); }, 200);
} else if (evt.channel === "junction") {
pulseBand("process", evt.kind === "error");
safeTimeout(() => { travelSpine("process", "publish"); pulseBand("publish"); renderProcess(); }, 200);
} else if (evt.channel === "tx") {
pulseBand("publish", evt.kind === "rollback");
safeTimeout(() => { travelSpine("publish", "audit"); pulseBand("audit", evt.kind === "rollback"); renderPublish(); }, 200);
} else if (evt.channel === "audit") {
pulseBand("audit", evt.kind === "rollback");
renderAudit();
}
}
function mount(el) {
root = el;
mounted = true;
selection = null;
root.innerHTML = SKELETON;
refresh();
S.bindReleaseDateReload(root, "#st-rel-date", "#st-rel-load", () => renderPublish());
S.bindBackfillButton(root, "#st-rel-backfill", () => renderPublish());
unsubs.push(C.Bus.on("data", refresh));
unsubs.push(C.Bus.on("event", onEvent));
}
function unmount() {
mounted = false;
unsubs.forEach((fn) => fn()); unsubs = [];
pendingTimers.forEach(clearTimeout); pendingTimers = [];
root = null;
}
window.HUB_LAYOUTS = window.HUB_LAYOUTS || {};
window.HUB_LAYOUTS.strata = { mount, unmount };
})();