"use strict"; /* ========================================================================== A · 装配线 Flowline —— 来源 → 加工 → 发布 → 审计 四工位水平因果链。 数据卡是线上工件,真实事件沿发丝导线从触发的那一工位接力到下一工位。 ========================================================================== */ (function () { const C = window.Core, S = window.HubShared; const { $, esc, timeShort } = C; let root = null; let unsubs = []; let stationFlashTimers = {}; let mounted = false; let pendingTimers = []; /* 布局随时可能被切走:任何跨阶段延时回调都必须先确认还挂载着, 否则 unmount 之后残留的 setTimeout 会在 root=null 时报错。 */ function safeTimeout(fn, ms) { const id = setTimeout(() => { if (mounted) fn(); }, ms); pendingTimers.push(id); return id; } const SKELETON = `
来源 SOURCES 加工 PROCESS 发布 PUBLISH 审计 AUDIT
中枢处理-
发布闸口GATE
审计末端
最近调用 RECENT CALLS
调度任务 JOBS
最近运行 RUNS
盘后发布 RELEASE
当前映射 MAPPING
`; function srcCardHtml(item) { const health = item.health || {}; const link = item._link || C.linkState(health); const cred = item.credential || {}; const credText = cred.configured ? `已配置 · ${esc(cred.last4 || "****")}` : "未配置"; return `
${esc(C.FLOW_LABEL[item.provider] || item.provider)} ${esc(C.FLOW_ROLE[item.provider] || item.role)} ${C.chipHtml(link, health.state || link)}
LINK ACT ${health.latency_ms == null ? "-" : health.latency_ms + "ms"}
${credText}
`; } function renderSources() { const v = S.sourcesView(); const host = $("fl-srcCards"); if (!v) { host.innerHTML = `
正在加载…
`; return; } host.innerHTML = v.live.map(srcCardHtml).join("") + (v.reserved.length ? `
预留源 ${v.reserved.length} 个 · 未接入 · 无真实调用
` : ""); S.bindProbeButtons(host); } function renderProcess() { const jv = S.jobsView(); const body = $("fl-processBody"); const pctEl = root.querySelector('[data-pct]'); if (!jv || !jv.latest) { body.innerHTML = `
正在加载…
`; return; } const latest = jv.latest; const running = jv.runs.filter((r) => r.state === "running").length; const pct = latest.state === "running" ? 62 : latest.state === "success" ? 100 : latest.state === "failed" ? 100 : 0; pctEl.textContent = latest.state === "running" ? `${pct}%` : "空闲"; body.innerHTML = `
${esc(latest.job_id)} · ${esc((jv.jobs.find((j) => j.id === latest.job_id) || {}).title || "")}
队列 ${running} · 最近 20 次失败 ${jv.failedRecent}
${jv.runs.slice(0, 3).map((r) => `
${r.id}${esc(r.state)}${esc(r.error || (r.finished_at !== "-" && r.finished_at ? timeShort(r.finished_at) : "—"))}
`).join("")}
`; } function renderPublish() { const rv = S.releaseView(); const body = $("fl-publishBody"); if (!rv) { body.innerHTML = `
正在加载…
`; return; } const valuation = rv.pubs.find((p) => p.dataset === "valuation"); const latestPub = rv.pubs.slice().sort((a, b) => String(a.published_at).localeCompare(String(b.published_at))).pop(); body.innerHTML = `
${rv.pubs.length}今日发布
最新批次 ${esc(latestPub ? `${latestPub.active_batch} · ${latestPub.published_at}` : "暂无")}
${valuation ? `
valuation${C.chipHtml(valuation.state)}
` : ""}`; } function renderAudit() { const av = S.auditView(); const body = $("fl-auditBody"); if (!av || !av.items.length) { body.innerHTML = `
正在加载…
`; return; } const a0 = av.items[0]; body.innerHTML = `
${esc(timeShort(a0.created_at))} ${esc(a0.actor)} · ${esc(a0.action)}
${esc(a0.target)}${a0.detail ? " · " + esc(a0.detail) : ""}
${av.items.slice(1, 3).map((a) => `
${esc(timeShort(a.created_at))} ${esc(a.actor)} · ${esc(a.action)}
`).join("")}`; } function renderCalls() { const ov = S.overviewView(); const sv = S.sourcesView(); if (!ov) { $("fl-callsTable").innerHTML = `
正在加载…
`; return; } $("fl-callsTable").innerHTML = C.table(["时间", "源", "端点", "结果", "耗时"], ov.recentCalls.map((row) => [ `${esc(timeShort(row.created_at))}`, `${esc(row.provider)}`, `${esc(row.endpoint)}`, row.ok ? '成功' : `${esc(row.error)}`, `${row.latency_ms ?? "-"} ms`, ])); if (sv) { $("fl-callsFoot").textContent = "今日调用 · " + sv.live.map((s) => `${s.provider} ${s.calls_today ?? 0}`).join(" · "); } } function renderJobsCol() { const jv = S.jobsView(); const jobRows = $("fl-jobRows"); if (!jv) { jobRows.innerHTML = `
正在加载…
`; return; } jobRows.innerHTML = jv.jobs.map((job) => `
${esc(job.id)}${esc(job.title)} ${esc(job.at)}${C.chipHtml(jv.latestByJob.get(job.id) ? jv.latestByJob.get(job.id).state : "idle")}
`).join(""); S.bindRunButtons(jobRows); $("fl-runsTable").innerHTML = C.table(["ID", "任务", "状态", "开始", "结束", "错误"], jv.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 || "")}`, ])); } function renderReleaseCol() { const rv = S.releaseView(); const dateInput = $("fl-rel-date"); if (!dateInput.value) dateInput.value = C.state.releaseDate; if (!rv) { $("fl-pubTable").innerHTML = `
正在加载…
`; return; } $("fl-pubTable").innerHTML = C.table(["数据集", "活跃批次", "上一批次", "状态", "发布时间", "操作"], rv.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 ? `` : "-", ])); S.bindRollbackButtons($("fl-pubTable"), () => renderReleaseCol()); const failed = rv.batches.filter((b) => b.state === "failed" || b.state === "error"); $("fl-anom").innerHTML = failed.length ? failed.map((b) => `${esc(b.batch_id)} · ${esc(b.dataset)} · ${esc(b.error || "异常")}`).join(" ") : `批次全部正常`; } function refresh() { if (!root) return; renderSources(); renderProcess(); renderPublish(); renderAudit(); renderCalls(); renderJobsCol(); renderReleaseCol(); } /* ---------------------------------------------------------------- 真实事件动效 水平接力:source ACT 闪 → 沿导线光梭滑到 process → process 高亮 → 光梭滑到 publish → publish 高亮 → 光梭滑到 audit → audit 高亮。reduced 时只做状态高亮, 不做位移。 */ function flashSource(provider, kind) { const card = root.querySelector(`.fl-src-card[data-provider="${provider}"]`); if (!card) return; const act = card.querySelector("[data-act]"); act.classList.remove("on", "err"); act.classList.add("on"); if (kind === "error" || kind === "rollback") act.classList.add("err"); const edge = card.querySelector(".edge"); edge.classList.toggle("err", kind === "error"); edge.style.opacity = "1"; clearTimeout(card._flashTimer); card._flashTimer = setTimeout(() => { act.classList.remove("on", "err"); edge.style.opacity = "0"; }, 820); return card; } function pulseStation(key, ms) { const el = root.querySelector(`.fl-station[data-station="${key}"]`); if (!el) return; el.classList.add("pulse"); clearTimeout(stationFlashTimers[key]); stationFlashTimers[key] = setTimeout(() => el.classList.remove("pulse"), ms || 640); } function travelPacket(fromEl, toEl, color) { if (!fromEl || !toEl || C.state.reduced) return; const pk = $("fl-packet"); const band = $("fl-band").getBoundingClientRect(); const a = fromEl.getBoundingClientRect(), b = toEl.getBoundingClientRect(); const x0 = a.left + a.width / 2 - band.left, y0 = a.top + a.height / 2 - band.top; const x1 = b.left + b.width / 2 - band.left, y1 = b.top + b.height / 2 - band.top; pk.style.background = color || "var(--packet)"; pk.style.top = y0 - 1.5 + "px"; pk.style.left = x0 - 17 + "px"; pk.style.opacity = "0"; if (pk._anim) pk._anim.cancel(); pk._anim = pk.animate([ { transform: "translate(0,0)", opacity: 0 }, { transform: "translate(0,0)", opacity: 1, offset: .08 }, { transform: `translate(${x1 - x0}px, ${y1 - y0}px)`, opacity: 1, offset: .92 }, { transform: `translate(${x1 - x0}px, ${y1 - y0}px)`, opacity: 0 }, ], { duration: 640, easing: "linear" }); pk._anim.onfinish = () => { pk.style.opacity = "0"; }; } function onEvent(evt) { if (!root || !mounted) return; const process = root.querySelector('.fl-station[data-station="process"]'); const publish = root.querySelector('.fl-station[data-station="publish"]'); const audit = root.querySelector('.fl-station[data-station="audit"]'); if (C.FLOW_PROVIDERS.includes(evt.channel)) { const card = flashSource(evt.channel, evt.kind); safeTimeout(() => { travelPacket(card, process, evt.kind === "error" ? "var(--error)" : "var(--packet)"); pulseStation("process", 700); }, 260); renderCalls(); } else if (evt.channel === "junction") { pulseStation("process", 700); safeTimeout(() => { travelPacket(process, publish, "var(--packet)"); pulseStation("publish", 700); renderProcess(); }, 260); } else if (evt.channel === "tx") { pulseStation("publish", 700); safeTimeout(() => { travelPacket(publish, audit, evt.kind === "rollback" ? "var(--error)" : "var(--packet)"); pulseStation("audit", 700); renderPublish(); renderReleaseCol(); }, 260); } else if (evt.channel === "audit") { pulseStation("audit", 700); renderAudit(); } } function mount(el) { root = el; mounted = true; root.innerHTML = SKELETON; refresh(); S.bindReleaseDateReload(root, "#fl-rel-date", "#fl-rel-load", () => renderReleaseCol()); S.bindBackfillButton(root, "#fl-rel-backfill", () => renderReleaseCol()); unsubs.push(C.Bus.on("data", refresh)); unsubs.push(C.Bus.on("event", onEvent)); } function unmount() { mounted = false; unsubs.forEach((fn) => fn()); unsubs = []; Object.values(stationFlashTimers).forEach(clearTimeout); stationFlashTimers = {}; pendingTimers.forEach(clearTimeout); pendingTimers = []; root = null; } window.HUB_LAYOUTS = window.HUB_LAYOUTS || {}; window.HUB_LAYOUTS.flowline = { mount, unmount }; })();