"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 = `
01SOURCES
来源层
02PROCESS
加工层
调度任务 JOBS
最近运行 RUNS
03PUBLISH
发布层
盘后发布 RELEASE
-今日发布
数据集 DATASETS
04AUDIT
审计层
`; 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 }; })();