diff --git a/xiaobai-datahub/admin/app.js b/xiaobai-datahub/admin/app.js
index afaf599..ef37e34 100644
--- a/xiaobai-datahub/admin/app.js
+++ b/xiaobai-datahub/admin/app.js
@@ -1,12 +1,25 @@
-const state = { csrf: "", page: "overview" };
+'use strict';
+/* =====================================================================
+ 小白复盘 · 数据中枢 admin/app.js — HEL-529 第十二版
+ 三页真实数据中枢:运行总览 / 数据源配置 / 数据血缘。
+ 视觉系统逐行照抄已通过打样 hub-kimi.html + SPEC-V11.md(v12 血缘压缩增补),
+ 但本文件不含任何随机/模拟数据——全部状态、数字、事件均来自真实接口:
+ 既有 /admin/api/{overview,sources,jobs,batches,datasets,audit,...}
+ 与 HEL-543 新增只读旁路 /admin/api/{providers/status,source-catalog,
+ lineage,lineage/affected}。
+ ===================================================================== */
+/* ================= 基础:DOM helper / API / CSRF ================= */
function $(id) { return document.getElementById(id); }
+function esc(v) { return String(v ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
+
+const state = { csrf: '', page: 'overview', calm: false, username: '' };
async function api(path, options = {}) {
- const headers = Object.assign({ "Content-Type": "application/json" }, options.headers || {});
- if (state.csrf && (options.method || "GET") !== "GET") headers["X-CSRF-Token"] = state.csrf;
- const res = await fetch(path, Object.assign({}, options, { headers, credentials: "same-origin" }));
- const body = await res.json();
+ const headers = Object.assign({ 'Content-Type': 'application/json' }, options.headers || {});
+ if (state.csrf && (options.method || 'GET') !== 'GET') headers['X-CSRF-Token'] = state.csrf;
+ const res = await fetch(path, Object.assign({}, options, { headers, credentials: 'same-origin' }));
+ const body = await res.json().catch(() => ({}));
if (!res.ok) {
const msg = (body.error && body.error.message) || body.error || res.statusText;
throw new Error(msg);
@@ -14,286 +27,1071 @@ async function api(path, options = {}) {
return body;
}
-function show(id) {
- ["login-view", "change-view", "shell"].forEach((key) => { $(key).hidden = key !== id; });
-}
-
-function esc(value) {
- return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[ch]));
-}
-
-function table(headers, rows) {
- const thead = headers.map((h) => `
-
-
-
+const OFFICIAL_DATASET_IDS = ['daily', 'valuation', 'moneyflow', 'auction', 'index_daily', 'limit_events', 'popularity', 'dragon_tiger', 'sector_daily', 'stocks'];
+const DATASET_NAME = {
+ daily: '日K', valuation: '估值', moneyflow: '资金流', auction: '竞价', index_daily: '指数日K',
+ limit_events: '涨停池', popularity: '人气榜', dragon_tiger: '龙虎榜', sector_daily: '题材板块', stocks: '股票主档',
+};
+
+/* =====================================================================
+ 总览页
+ ===================================================================== */
+function buildOverviewVM() {
+ const ov = RAW.overview || {};
+ const eod = ov.eod_status || {};
+ const rev = ov.revision_status || {};
+ const pubsByDs = {};
+ (ov.publications || []).forEach((p) => { pubsByDs[p.dataset] = p; });
+ const batchByDs = {};
+ ((RAW.batches && RAW.batches.batches) || []).forEach((b) => { if (!batchByDs[b.dataset] || b.started_at > batchByDs[b.dataset].started_at) batchByDs[b.dataset] = b; });
+ const hidx = healthIndex();
+ const totalOfficial = OFFICIAL_DATASET_IDS.length;
+ const publishedCount = OFFICIAL_DATASET_IDS.filter((id) => pubsByDs[id] && pubsByDs[id].state === 'published').length;
+ const anomalyCount = (ov.anomalies || []).length;
+ const recentCalls = ov.recent_calls || [];
+ const successRate = recentCalls.length ? (recentCalls.filter((c) => c.ok).length / recentCalls.length * 100) : null;
+
+ // 主站回退页面:主源异常且备源健康 → 判定为“回退中”的 provisional 数据集
+ let fallbackPages = 0;
+ (RAW.lineage && RAW.lineage.items || []).forEach((it) => {
+ if (it.tier !== 'provisional' || !it.backup_source) return;
+ const rows = it.live_provider_health || [];
+ const primaryProv = (it.providers || [])[0];
+ const backupProv = (it.providers || [])[1];
+ const primaryState = worstBucket(rows.filter((r) => r.provider === primaryProv).map((r) => bucketOf(r.state)));
+ const backupState = worstBucket(rows.filter((r) => r.provider === backupProv).map((r) => bucketOf(r.state)));
+ if ((primaryState === 'fail' || primaryState === 'degrade' || primaryState === 'slow') && backupState === 'ok') fallbackPages += 1;
+ });
+
+ const datasetCards = OFFICIAL_DATASET_IDS.map((id) => {
+ const pub = pubsByDs[id];
+ const batch = batchByDs[id];
+ let bucket = 'plan', note = '';
+ if (pub) {
+ bucket = pub.state === 'published' ? 'ok' : (pub.state === 'degraded' ? 'degrade' : 'fail');
+ note = pub.published_at ? hm(pub.published_at) : '';
+ } else if (batch) {
+ bucket = batch.state === 'failed' ? 'fail' : (batch.state === 'staged' ? 'review' : 'plan');
+ note = batch.error ? esc(batch.error) : '';
+ }
+ const rows = (batch && batch.rows_out) || (pub && pub.rows_out) || null;
+ return { id, name: DATASET_NAME[id] || id, bucket, time: note && !batch?.error ? note : '', rows, err: batch && batch.error ? batch.error : '' };
+ });
+
+ // 实时观察层:eastmoney / tencent 的真实 provider_health 行
+ const catalogByProv = {};
+ (RAW.catalog && RAW.catalog.items || []).forEach((it) => { catalogByProv[it.provider] = it; });
+ const observers = [];
+ ['eastmoney', 'tencent'].forEach((prov) => {
+ const rows = hidx[prov] || [];
+ const cat = catalogByProv[prov];
+ rows.forEach((row) => {
+ const iface = (cat && cat.interfaces.find((i) => i.interface === row.interface)) || {};
+ observers.push({
+ name: iface.capability || row.interface,
+ provider: prov,
+ interface: row.interface,
+ lastOk: row.last_ok_at,
+ lat: row.last_latency_ms,
+ bucket: bucketOf(row.state),
+ note: row.last_error || row.last_fallback_reason || '',
+ });
+ });
+ });
+
+ // 主站影响:lineage() 条目按 known_consumers 展开
+ const siteImpact = [];
+ (RAW.lineage && RAW.lineage.items || []).forEach((it) => {
+ const rows = it.live_provider_health || [];
+ const bucket = it.tier === 'official'
+ ? (it.publication ? (it.publication.state === 'published' ? 'ok' : (it.publication.state === 'degraded' ? 'degrade' : 'fail')) : (rows.length ? worstBucket(rows.map((r) => bucketOf(r.state))) : 'plan'))
+ : (rows.length ? worstBucket(rows.map((r) => bucketOf(r.state))) : 'off');
+ const note = rows.find((r) => r.last_error)?.last_error || rows.find((r) => r.last_fallback_reason)?.last_fallback_reason || '';
+ (it.known_consumers || []).forEach((c) => {
+ siteImpact.push({ page: c, dataset: it.dataset, bucket, note });
+ });
+ });
+ const okPages = siteImpact.filter((s) => s.bucket === 'ok').length;
+
+ // 今晚时间线:真实 jobs() 定义 + job_runs
+ const jobDefs = (RAW.jobs && RAW.jobs.jobs || []).filter((j) => j.at !== 'manual');
+ const runsByJob = {};
+ ((RAW.jobs && RAW.jobs.runs) || []).forEach((r) => { (runsByJob[r.job_id] = runsByJob[r.job_id] || []).push(r); });
+ const timeline = jobDefs.map((j) => {
+ const runs = (runsByJob[j.id] || []).slice().sort((a, b) => (a.id < b.id ? 1 : -1));
+ const latest = runs[0];
+ let st = 'wait';
+ if (latest) st = latest.state === 'ok' ? 'done' : (latest.state === 'failed' ? 'fail' : 'wait');
+ return { t: (j.at.split('-')[0] || j.at).slice(0, 5), label: j.title, state: st, jobId: j.id };
+ });
+
+ // 待处理异常:批次失败/停滞 + 主源连续失败 + 未配置授权源
+ const incidents = [];
+ (ov.anomalies || []).forEach((b) => {
+ incidents.push({ sev: 'fail', tag: b.state, title: `${DATASET_NAME[b.dataset] || b.dataset} ${b.state === 'failed' ? '失败' : '停滞'}`, meta: `${b.error ? esc(b.error) : '无错误详情'} · 批次 ${b.batch_id}`, dataset: b.dataset });
+ });
+ if (eod.state === 'waiting_upstream' || eod.state === 'cutoff_failed') {
+ incidents.push({ sev: eod.state === 'cutoff_failed' ? 'fail' : 'slow', tag: eod.state, title: eod.state === 'cutoff_failed' ? '盘后批已截止失败' : '等待上游重试', meta: `已试 ${eod.attempts || 0} 次${(eod.missing_datasets || []).length ? ' · 缺 ' + eod.missing_datasets.join(',') : ''}`, dataset: (eod.missing_datasets || [])[0] || '' });
+ }
+ Object.keys(hidx).forEach((prov) => {
+ const bad = (hidx[prov] || []).filter((r) => (r.consec_failures || 0) > 0 && bucketOf(r.state) === 'fail');
+ if (bad.length) incidents.push({ sev: 'fail', tag: prov, title: `${prov} 部分接口连续失败`, meta: bad.map((r) => `${r.interface}×${r.consec_failures}`).join(' · '), provider: prov });
+ });
+ const ifindCat = catalogByProv.ifind;
+ if (ifindCat && !(ifindCat.credential && ifindCat.credential.configured)) {
+ incidents.push({ sev: 'off', tag: '未配置', title: 'iFinD · 授权源未启用', meta: '凭证未配置 · 功能隐藏 · 配置后启用', provider: 'ifind' });
+ }
+
+ return {
+ trade_date: ov.trade_date, session_phase: ov.session_phase, eod, rev,
+ publishedCount, totalOfficial, anomalyCount, successRate,
+ fallbackPages, datasetCards, observers, siteImpact, okPages, timeline, incidents,
+ };
+}
+
+function heroHtml(vm) {
+ const anomalyTxt = vm.anomalyCount ? `
${vm.anomalyCount} 项异常` : `
无异常`;
+ const tags = [];
+ if (vm.eod.state === 'waiting_upstream') tags.push(tagHtml('amb', `
等待上游 · 已试 ${vm.eod.attempts || 0} 次`));
+ if (vm.rev.state === 'waiting_review') tags.push(tagHtml('cy', '复核 · 待发布'));
+ const subParts = [];
+ if (vm.eod.attempts) subParts.push(`已试
${vm.eod.attempts} 次`);
+ if (vm.eod.next_retry_at) subParts.push(`下次重试
${hm(vm.eod.next_retry_at)}`);
+ if ((vm.eod.missing_datasets || []).length) subParts.push(`缺
${esc(vm.eod.missing_datasets.join(','))}`);
+ if (vm.rev.window) subParts.push(`复核窗口
${esc(vm.rev.window)}`);
+ return `
` +
+ `
` +
+ `
${vm.anomalyCount ? '部分异常' : '整体可用'}·${anomalyTxt}
` +
+ `
${tags.join('')}
` +
+ (subParts.length ? `
${subParts.join('|')}
` : '') +
+ `
` +
+ `
` +
+ kpiHtml('已发布数据集', `${vm.publishedCount}/${vm.totalOfficial}`, null) +
+ kpiHtml('今日调用成功率', vm.successRate == null ? '—' : `${vm.successRate.toFixed(1)}%`, null, 'mint', vm.successRate != null) +
+ kpiHtml('主站回退页面', `${vm.fallbackPages} 页`, null, vm.fallbackPages ? 'amb' : 'txt') +
+ kpiHtml('EOD 尝试', `${vm.eod.attempts || 0} 次`, '上限 5 · 23:30 止', vm.eod.attempts ? 'rd' : 'txt') +
+ `
`;
+}
+function datasetStripHtml(vm) {
+ return `
${vm.datasetCards.map((d) => {
+ const ring = d.bucket === 'fail' ? 'fail' : d.bucket === 'degrade' ? 'slow' : d.bucket === 'review' ? 'review' : '';
+ let note = '';
+ if (d.time) note += `
${esc(d.time)} · `;
+ if (d.rows) note += `
${Number(d.rows).toLocaleString('en-US')} 行`;
+ if (d.err) note += `
${esc(d.err)}`;
+ if (!d.time && !d.rows && !d.err) note = '
今日尚未发布';
+ return `
` +
+ `
${esc(d.name)}${pillHtml(d.bucket)}
` +
+ `
${note}
` +
+ `
`;
+ }).join('')}
`;
+}
+function observatoryHtml(vm) {
+ const legendByProv = { eastmoney: 'EM 东财', tencent: 'TX 腾讯' };
+ const right = `
${Object.keys(legendByProv).map((p) => `${ledHtml('ok')}${legendByProv[p]}`).join('')}`;
+ const rows = vm.observers.length ? vm.observers.map((o) => `
` +
+ `| ${esc(o.name)} | ` +
+ `${o.provider === 'eastmoney' ? '东方财富' : '腾讯行情'} | ` +
+ `${ageHtml(o.lastOk)} | ` +
+ `${latHtml(o.lat, o.bucket !== 'ok')} | ` +
+ `${pillHtml(o.bucket)}${o.note ? `${esc(o.note)}` : ''} | ` +
+ `
`).join('') : `
| 尚无真实观测记录(等待首次探测/调用) |
`;
+ const body = `
| 观察项 | 当前来源 | 最近成功 | 延迟 | 状态 · 备注 |
${rows}
`;
+ return panelHtml('实时观察层', right, body, false);
+}
+function siteImpactHtml(vm) {
+ const right = `
${vm.okPages} 项正常 · ${vm.siteImpact.length - vm.okPages} 项待关注`;
+ const rows = vm.siteImpact.length ? vm.siteImpact.map((s) => `
` +
+ `| ${esc(s.page)} | ` +
+ `${pillHtml(s.bucket)} | ` +
+ `${s.note ? esc(s.note) : '—'} | ` +
+ ` | ` +
+ `
`).join('') : `
| 尚无血缘记录 |
`;
+ return panelHtml('主站影响', right, `
`, false);
+}
+function timelineHtml(vm) {
+ const nodes = vm.timeline;
+ if (!nodes.length) return panelHtml('今晚时间线', '', '
暂无可展示的调度任务
', true);
+ const W = 560, H = 150, x0 = 34, x1 = W - 30, y = 75;
+ const step = (x1 - x0) / Math.max(1, nodes.length - 1);
+ const X = (i) => x0 + i * step;
+ const now = new Date();
+ const nowMinutes = now.getHours() * 60 + now.getMinutes();
+ const toMin = (t) => { const [h, m] = t.split(':').map(Number); return h * 60 + (m || 0); };
+ let nowX = X(0);
+ for (let i = 0; i < nodes.length - 1; i++) {
+ const a = toMin(nodes[i].t), b = toMin(nodes[i + 1].t);
+ if (nowMinutes >= a && nowMinutes <= b && b > a) { nowX = X(i) + (X(i + 1) - X(i)) * ((nowMinutes - a) / (b - a)); break; }
+ if (nowMinutes < toMin(nodes[0].t)) { nowX = X(0); break; }
+ if (nowMinutes > toMin(nodes[nodes.length - 1].t)) { nowX = X(nodes.length - 1); }
+ }
+ let s = `
`;
+ const right = `
现在 ${p2(now.getHours())}:${p2(now.getMinutes())} · SCHEDULER.PY`;
+ return panelHtml('今晚时间线', right, s, false);
+}
+function incidentsHtml(vm) {
+ if (!vm.incidents.length) return panelHtml('待处理异常', tagHtml('mint', '0 OPEN'), '
当前无待处理异常
', true);
+ const cards = vm.incidents.map((it) => {
+ const tag = it.sev === 'fail' ? tagHtml('rd', esc(it.tag)) : it.sev === 'slow' ? tagHtml('amb', esc(it.tag)) : tagHtml('dashed', esc(it.tag));
+ return `
${tag}` +
+ `
` +
+ `
${esc(it.title)}
` +
+ `
${esc(it.meta)}
` +
+ `
` +
+ `
` +
+ `
`;
+ }).join('');
+ return panelHtml('待处理异常', tagHtml('rd', vm.incidents.length + ' OPEN'), `
${cards}
`, true);
+}
+function overviewHtml() {
+ const vm = buildOverviewVM();
+ return `
${heroHtml(vm)}${datasetStripHtml(vm)}` +
+ `
${observatoryHtml(vm)}${siteImpactHtml(vm)}
` +
+ `
${timelineHtml(vm)}${incidentsHtml(vm)}
`;
+}
+
+/* =====================================================================
+ 数据源配置页
+ ===================================================================== */
+const srcUiState = {};
+function buildSourcesVM() {
+ const catalog = (RAW.catalog && RAW.catalog.items) || [];
+ const legacy = {};
+ (RAW.sources && RAW.sources.items || []).forEach((it) => { legacy[it.provider] = it; });
+ const hidx = healthIndex();
+ const cidx = callsIndex();
+ const active = catalog.filter((c) => c.role !== 'reserved');
+ const reserved = catalog.filter((c) => c.role === 'reserved');
+ const totalIfaces = active.reduce((n, c) => n + c.interfaces.length, 0);
+ const usedIfaces = active.reduce((n, c) => n + c.interfaces.filter((i) => (hidx[c.provider] || []).some((h) => h.interface === i.interface)).length, 0);
+ const cards = active.map((c) => {
+ const leg = legacy[c.provider] || {};
+ const legState = (leg.health && (leg.health.state || leg.health.status)) || null;
+ const rows = hidx[c.provider] || [];
+ const worst = rows.length ? worstBucket(rows.map((r) => bucketOf(r.state))) : (legState ? bucketOf(legState) : 'off');
+ const cred = c.credential || {};
+ const lastCall = (cidx[c.provider] || [])[0];
+ if (!(c.provider in srcUiState)) srcUiState[c.provider] = { open: true, probing: false, probeResult: null };
+ const ifaceRows = c.interfaces.map((i) => {
+ const h = rows.find((r) => r.interface === i.interface);
+ return {
+ api: i.interface, use: i.capability,
+ ok: h ? (h.last_ok_at ? hm(h.last_ok_at) : '—') : '—',
+ lat: h ? h.last_latency_ms : null,
+ st: h ? bucketOf(h.state) : 'off',
+ err: h && h.state !== 'ok' ? (h.last_error || h.last_fallback_reason || '') : '',
+ };
+ });
+ return {
+ id: c.provider, name: c.label, role: c.role, tag: c.credential_key ? 'licensed' : 'free',
+ bucket: worst, credential: cred, usedCount: ifaceRows.filter((r) => r.st !== 'off').length, totalCount: ifaceRows.length,
+ lastProbe: lastCall ? `${hm(lastCall.created_at)} · ${lastCall.ok ? `✓ ${lastCall.latency_ms}ms` : `× ${lastCall.error || 'ERROR'}`}` : '—',
+ latency: rows.length ? (rows.map((r) => r.last_latency_ms).filter((v) => v != null)[0] || null) : null,
+ error: rows.find((r) => r.last_error)?.last_error || '',
+ ifaceRows,
+ };
+ });
+ return { cards, reserved, totalIfaces, usedIfaces };
+}
+function sourceCardHtml(card) {
+ const ui = srcUiState[card.id];
+ const ringStyle = card.bucket === 'fail' ? 'border-color:rgba(248,113,113,.35)' :
+ card.bucket === 'degrade' ? 'border-color:rgba(251,191,36,.25)' :
+ card.bucket === 'slow' ? 'border-color:rgba(251,191,36,.3)' :
+ card.bucket === 'off' ? 'border-style:dashed;border-color:#243152' : '';
+ const ledSt = HEALTH[card.bucket].led;
+ // source_catalog.py:credential_key 为空的来源(eastmoney/tencent 等公开
+ // 接口)真实语义是"无需凭证"而非"已配置",两者都体现为 configured:true,
+ // 靠 note 字段区分,这里必须原样展示 note,不能一律折叠成"已配置"。
+ const credBadge = card.credential.note ? card.credential.note :
+ card.credential.configured ? `已配置${card.credential.last4 ? ' · ' + card.credential.last4 : ''}` : '未配置';
+ const badges = [credBadge, card.role];
+ let h = `
`;
+ h += `` +
+ ledHtml(ledSt, card.bucket === 'fail') +
+ `${esc(card.name)}` +
+ `${esc(card.tag)}` +
+ pillHtml(card.bucket) +
+ `${badges.map((b) => tagHtml(card.bucket === 'off' ? 'dashed' : 'dim', esc(b))).join('')}` +
+ `` +
+ `` +
+ `` +
+ ``;
+ h += `` +
+ `接口 ${card.usedCount}/${card.totalCount} 在用` +
+ `|最后探测 ${esc(card.lastProbe)}` +
+ `|延迟 ${latHtml(card.latency)}` +
+ (card.error ? `|最近错误 ${esc(card.error)}` : `|无最近错误`) +
+ `
`;
+ if (ui.open && card.ifaceRows.length) {
+ h += `` +
+ `
| 接口 | 用途 | 最近成功 | 延迟 | 状态 |
` +
+ card.ifaceRows.map((it) => `` +
+ `| ${esc(it.api)} | ` +
+ `${esc(it.use)} | ` +
+ `${esc(it.ok)} | ` +
+ `${latHtml(it.lat)} | ` +
+ `${pillHtml(it.st)}${it.err ? `${esc(it.err)}` : ''} | ` +
+ `
`).join('') + `
`;
+ } else if (!card.ifaceRows.length) {
+ h += ``;
+ }
+ return h + ``;
+}
+function sourcesHtml() {
+ const vm = buildSourcesVM();
+ const alerts = [];
+ vm.cards.forEach((c) => {
+ if (c.bucket === 'fail') alerts.push(`
${ledHtml('fail', true)}${esc(c.name)} 故障${c.error ? ' · ' + esc(c.error) : ''}`);
+ else if (c.bucket === 'degrade' || c.bucket === 'slow') alerts.push(`
${ledHtml('slow')}${esc(c.name)} 降级/延迟`);
+ });
+ return `
` +
+ `
` +
+ `数据源 ${vm.cards.length} 路 · 接口 ${vm.usedIfaces}/${vm.totalIfaces} 在用` +
+ alerts.join('') +
+ `` +
+ `凭证安全存储 · 探测只读 · 不影响盘中链路` +
+ `
` +
+ `
${vm.cards.map(sourceCardHtml).join('')}
` +
+ `
${vm.reserved.map((r) => `
${ledHtml('off')}${esc(r.label)}${tagHtml('dashed', '预留位 · 未接入')}
`).join('')}
` +
+ `
`;
+}
+function renderSourceCards() {
+ const el = $('srcCards');
+ if (!el) return;
+ const vm = buildSourcesVM();
+ el.innerHTML = vm.cards.map(sourceCardHtml).join('');
+}
+
+/* =====================================================================
+ 数据血缘页
+ ===================================================================== */
+function pageNameOf(consumer) {
+ const idx = consumer.indexOf('(');
+ return (idx > 0 ? consumer.slice(0, idx) : consumer).trim();
+}
+const linFilter = { onlyBad: false, src: '全部', q: '' };
+let linHover = null;
+let GW = 1500, GH = 286;
+const gpos = {};
+const adj = { sd: new Map(), ds: new Map(), dp: new Map(), pd: new Map() };
+function adjAdd(m, k, v) { if (!m.has(k)) m.set(k, new Set()); m.get(k).add(v); }
+
+function buildLineageVM() {
+ const items = (RAW.lineage && RAW.lineage.items) || [];
+ const PROV_LABEL = { tushare: 'Tushare', eastmoney: '东方财富', tencent: '腾讯行情', ifind: 'iFinD' };
+ const srcState = {};
+ const dsState = {};
+ const pageState = {};
+ const pageDatasets = {};
+ const edgesSD = [];
+ const edgesDP = [];
+ const rows = [];
+ items.forEach((it) => {
+ const rowsHealth = it.live_provider_health || [];
+ let bucket;
+ if (it.tier === 'official') {
+ bucket = it.publication ? (it.publication.state === 'published' ? 'ok' : (it.publication.state === 'degraded' ? 'degrade' : 'fail')) : (rowsHealth.length ? worstBucket(rowsHealth.map((r) => bucketOf(r.state))) : 'plan');
+ } else {
+ bucket = rowsHealth.length ? worstBucket(rowsHealth.map((r) => bucketOf(r.state))) : 'off';
+ }
+ dsState[it.dataset] = bucket;
+ (it.providers || []).forEach((p) => {
+ srcState[p] = worstBucket([srcState[p] || 'ok', ...rowsHealth.filter((r) => r.provider === p).map((r) => bucketOf(r.state))]);
+ edgesSD.push([p, it.dataset]);
+ });
+ const consumers = it.known_consumers || [];
+ const pages = [...new Set(consumers.map(pageNameOf))];
+ pages.forEach((pg) => {
+ pageState[pg] = worstBucket([pageState[pg] || 'ok', bucket]);
+ edgesDP.push([it.dataset, pg]);
+ (pageDatasets[pg] = pageDatasets[pg] || []).push(it);
+ });
+ const lat = rowsHealth.map((r) => r.last_latency_ms).find((v) => v != null);
+ const ok = it.tier === 'official' && it.publication ? it.publication.published_at : (rowsHealth.find((r) => r.last_ok_at) || {}).last_ok_at;
+ rows.push({
+ page: pages.join(' / ') || '—', item: consumers.join(' / '), dataset: it.dataset,
+ via: `${it.primary_source}${it.backup_source ? ' → ' + it.backup_source : ''}`,
+ role: it.backup_source ? '主/备' : (it.tier === 'licensed' ? '主(授权)' : '主(无备)'),
+ ok: ok ? hm(ok) : '—', lat, st: bucket,
+ note: rowsHealth.find((r) => r.last_error)?.last_error || rowsHealth.find((r) => r.last_fallback_reason)?.last_fallback_reason || (it.publication && it.publication.state !== 'published' ? '未正式发布' : ''),
+ });
+ });
+ const sources = Object.keys(srcState).map((id) => ({ id, name: PROV_LABEL[id] || id, st: srcState[id] }));
+ const datasets = Object.keys(dsState).map((id) => ({ id, st: dsState[id] }));
+ const pages = Object.keys(pageState).map((id) => ({ id, name: id, st: pageState[id] }));
+ return { sources, datasets, pages, edgesSD, edgesDP, rows };
+}
+
+function spread(n, y0, y1) { return Array.from({ length: n }, (_, i) => (n === 1 ? (y0 + y1) / 2 : y0 + (i / (n - 1)) * (y1 - y0))); }
+function layoutGraph(vm) {
+ const wide = (window.innerWidth || 1440) > 1100;
+ GW = wide ? 1500 : 1000; GH = wide ? 286 : 560;
+ vm.sources.forEach((s, i) => { gpos['s:' + s.id] = { x: wide ? 80 : 60, y: spread(vm.sources.length, wide ? 60 : 90, wide ? 232 : 500)[i] }; });
+ vm.datasets.forEach((d, i) => { gpos['d:' + d.id] = { x: wide ? 700 : 480, y: spread(vm.datasets.length, wide ? 30 : 45, wide ? 264 : 525)[i] }; });
+ vm.pages.forEach((p, i) => { gpos['p:' + p.id] = { x: wide ? 1400 : 930, y: spread(vm.pages.length, wide ? 26 : 30, wide ? 276 : 540)[i] }; });
+}
+function linkPath(p1, p2) { const mx = (p1.x + p2.x) / 2; return `M ${p1.x} ${p1.y} C ${mx} ${p1.y}, ${mx} ${p2.y}, ${p2.x} ${p2.y}`; }
+const stColorOf = (b) => (HEALTH[b] || HEALTH.off).color;
+
+function lineageGraphHtml(vm) {
+ layoutGraph(vm);
+ adj.sd.clear(); adj.ds.clear(); adj.dp.clear(); adj.pd.clear();
+ vm.edgesSD.forEach(([s, d]) => { adjAdd(adj.sd, s, d); adjAdd(adj.ds, d, s); });
+ vm.edgesDP.forEach(([d, p]) => { adjAdd(adj.dp, d, p); adjAdd(adj.pd, p, d); });
+ let h = `
悬停节点 · 追踪上下游链路
`;
+ return h;
+}
+function activeSetCalc(hover, vm) {
+ if (!hover) return null;
+ const set = new Set([hover]);
+ const isSrc = vm.sources.some((s) => s.id === hover);
+ const isDs = vm.datasets.some((d) => d.id === hover);
+ if (isSrc) { (adj.sd.get(hover) || new Set()).forEach((d) => { set.add(d); (adj.dp.get(d) || new Set()).forEach((p) => set.add(p)); }); }
+ else if (isDs) { (adj.ds.get(hover) || new Set()).forEach((s) => set.add(s)); (adj.dp.get(hover) || new Set()).forEach((p) => set.add(p)); }
+ else { (adj.pd.get(hover) || new Set()).forEach((d) => { set.add(d); (adj.ds.get(d) || new Set()).forEach((s) => set.add(s)); }); }
+ return set;
+}
+function applyLinHover(vm) {
+ const act = activeSetCalc(linHover, vm);
+ const isSrcHov = vm.sources.some((s) => s.id === linHover);
+ document.querySelectorAll('#linSvg path[data-link]').forEach((p) => {
+ const a = p.dataset.a, b = p.dataset.b;
+ const shown = !act || (act.has(a) && act.has(b));
+ let hot = false;
+ if (act && act.has(a) && act.has(b)) hot = isSrcHov ? (a === linHover ? true : (adj.sd.get(linHover) || new Set()).has(a)) : true;
+ p.setAttribute('stroke-width', hot ? 1.8 : 1);
+ p.setAttribute('stroke-opacity', shown ? (act ? 0.85 : (p.dataset.kind === 'sd' ? 0.28 : 0.22)) : 0.05);
+ });
+ document.querySelectorAll('#linSvg g[data-nid]').forEach((g) => {
+ const id = g.dataset.nid, kind = g.dataset.gkind;
+ const dimmed = act && !act.has(id);
+ g.setAttribute('opacity', dimmed ? (kind === 'src' ? 0.3 : 0.25) : 1);
+ if (kind !== 'src') { const t = g.querySelector('text'); if (t) t.setAttribute('fill', act && act.has(id) ? '#e8f1ff' : '#8b9bb4'); }
+ });
+ const hint = $('linHint');
+ if (hint) hint.textContent = linHover ? `FOCUS: ${linHover} · 高亮上下游链路` : '悬停节点 · 追踪上下游链路';
+}
+function setLinHover(id, vm) { linHover = id; applyLinHover(vm); }
+function linRowsData(vm) {
+ return vm.rows.filter((r) => {
+ if (linFilter.onlyBad && !['fail', 'slow', 'degrade', 'off', 'review', 'plan'].includes(r.st)) return false;
+ if (linFilter.src !== '全部' && !r.via.includes({ Tushare: 'tushare', 东方财富: 'eastmoney', 腾讯: 'tencent', iFinD: 'ifind' }[linFilter.src] || '')) return false;
+ if (linFilter.q && !(r.page + r.item + r.dataset + r.via).toLowerCase().includes(linFilter.q.toLowerCase())) return false;
+ return true;
+ });
+}
+function renderLinRows(vm) {
+ const body = $('linBody'), cnt = $('linCount'), bodyN = $('linBodyN');
+ if (!body) return;
+ const rows = linRowsData(vm);
+ if (cnt) cnt.textContent = `${rows.length}/${vm.rows.length} 行`;
+ const cls = (st) => st === 'fail' ? 'row-fail' : st === 'degrade' || st === 'slow' ? 'row-slow' : st === 'off' ? 'row-off' : '';
+ body.innerHTML = rows.map((r) => `
` +
+ `| ${esc(r.page)} | ` +
+ `${esc(r.item)} | ` +
+ `${esc(r.dataset)} | ` +
+ `${esc(r.via)} | ` +
+ `${esc(r.role)} | ` +
+ `${esc(r.ok)} | ` +
+ `${latHtml(r.lat)} | ` +
+ `${pillHtml(r.st)}${r.note ? `${esc(r.note)}` : ''} | ` +
+ `
`).join('') || `
| 无匹配记录 · 调整筛选条件 |
`;
+ if (bodyN) bodyN.innerHTML = rows.map((r) => `
` +
+ `| ${esc(r.page)}${esc(r.item)} | ` +
+ `${esc(r.dataset)} | ` +
+ `${esc(r.via)} | ` +
+ `${esc(r.ok)}${r.lat != null ? r.lat + 'ms' : '—'} | ` +
+ `${pillHtml(r.st)}${r.note ? `${esc(r.note)}` : ''} | ` +
+ `
`).join('') || `
| 无匹配记录 · 调整筛选条件 |
`;
+}
+function lineageHtml() {
+ const vm = buildLineageVM();
+ const anomalyPages = new Set();
+ vm.rows.forEach((r) => { if (r.st === 'fail') (r.page || '').split(' / ').forEach((p) => anomalyPages.add(p)); });
+ const html = `
` +
+ `
` +
+ `血缘地图` +
+ `${vm.sources.length} 源 → ${vm.datasets.length} 数据集 → ${vm.pages.length} 页面` +
+ `${ledHtml('ok')}健康${ledHtml('slow')}慢/降级${ledHtml('fail')}失败${ledHtml('plan')}计划${ledHtml('off')}未配置` +
+ `` +
+ (anomalyPages.size ? tagHtml('rd', `${[...anomalyPages][0]} 断链影响 ${anomalyPages.size} 页`) : tagHtml('mint', '暂无断链影响')) +
+ `
` +
+ `
` +
+ `
` +
+ `
`;
+ window.__linVM = vm;
+ return html;
+}
+
+/* =====================================================================
+ 页面路由 / 顶栏 / EVENT TAPE / 抽屉 / 危险操作弹层
+ ===================================================================== */
+const mainEl = $('mainEl');
+function pageFromHash() {
+ const h = location.hash.replace('#', '');
+ return ['overview', 'sources', 'lineage'].includes(h) ? h : 'overview';
+}
+function renderPage(p) {
+ state.page = p;
+ mainEl.dataset.page = p;
+ document.querySelectorAll('[data-nav]').forEach((b) => b.classList.toggle('act', b.dataset.nav === p));
+ if (p === 'overview') mainEl.innerHTML = overviewHtml();
+ else if (p === 'sources') mainEl.innerHTML = sourcesHtml();
+ else {
+ mainEl.innerHTML = lineageHtml();
+ renderLinRows(window.__linVM);
+ applyLinHover(window.__linVM);
+ const svg = $('linSvg');
+ if (svg) svg.addEventListener('mouseleave', () => setLinHover(null, window.__linVM));
+ fitLineageTable();
+ }
+ updatePhaseTag();
+}
+window.addEventListener('hashchange', () => renderPage(pageFromHash()));
+document.querySelectorAll('[data-nav]').forEach((b) => b.addEventListener('click', () => { location.hash = b.dataset.nav; }));
+let resizeT = null;
+window.addEventListener('resize', () => {
+ clearTimeout(resizeT);
+ resizeT = setTimeout(() => { if (state.page === 'lineage') renderPage('lineage'); }, 180);
+});
+
+/* HEL-529: 血缘详情表随真实数据集数量(当前 17 条,非打样假设的 13 条)
+ 动态撑高;宽档(>1100px,覆盖 1920×920/1440×900 验收位)给表格容器一个
+ 基于剩余可视高度计算的 max-height + 内部滚动,保证整页 scrollHeight
+ 始终 <= clientHeight(硬性验收项),同时全部行仍可在容器内滚动查看。
+ 窄档(<=1100px)保持原有整页纵向滚动,不设上限。 */
+function fitLineageTable() {
+ const wrap = document.querySelector('.lin-scroll');
+ const tape = document.querySelector('.tape');
+ if (!wrap || !tape) return;
+ wrap.style.maxHeight = '';
+ if (window.innerWidth <= 1100) return;
+ const tapeTop = tape.getBoundingClientRect().top;
+ const wrapTop = wrap.getBoundingClientRect().top;
+ const mainPadBottom = parseFloat(getComputedStyle(mainEl).paddingBottom) || 0;
+ const avail = tapeTop - wrapTop - mainPadBottom - 4;
+ if (avail > 0) wrap.style.maxHeight = Math.floor(avail) + 'px';
+}
+
+function updatePhaseTag() {
+ const ov = RAW.overview;
+ if (!ov) return;
+ $('phaseTag').innerHTML = `${ledHtml('rev', true)} ${esc(ov.session_phase || '')}`;
+}
+
+mainEl.addEventListener('click', (e) => {
+ const pb = e.target.closest('[data-probe]');
+ if (pb) { doProbe(pb.dataset.probe); return; }
+ const tg = e.target.closest('[data-toggle]');
+ if (tg) { const ui = srcUiState[tg.dataset.toggle]; if (ui) { ui.open = !ui.open; renderSourceCards(); } return; }
+ const goto = e.target.closest('[data-goto-lineage]');
+ if (goto) { linFilter.q = ''; location.hash = 'lineage'; return; }
+ const inc = e.target.closest('[data-incident-action]');
+ if (inc) { openOpsDrawer('release', { dataset: inc.dataset.dataset, provider: inc.dataset.provider }); return; }
+ if (e.target.closest('#linBad')) {
+ linFilter.onlyBad = !linFilter.onlyBad;
+ renderLinRows(window.__linVM);
+ const btn = $('linBad');
+ btn.classList.toggle('on', linFilter.onlyBad);
+ btn.innerHTML = `${ledHtml(linFilter.onlyBad ? 'fail' : 'plan')} 只看异常`;
+ return;
+ }
+});
+mainEl.addEventListener('change', (e) => { if (e.target.id === 'linSel') { linFilter.src = e.target.value.split('· ')[1] || '全部'; renderLinRows(window.__linVM); } });
+mainEl.addEventListener('input', (e) => { if (e.target.id === 'linQ') { linFilter.q = e.target.value; renderLinRows(window.__linVM); } });
+mainEl.addEventListener('mouseover', (e) => { const g = e.target.closest('g[data-nid]'); if (g) setLinHover(g.dataset.nid, window.__linVM); });
+
+async function doProbe(id) {
+ const ui = srcUiState[id];
+ if (!ui || ui.probing) return;
+ ui.probing = true;
+ renderSourceCards();
+ try { await api(`/admin/api/sources/${id}/probe`, { method: 'POST', body: '{}' }); } catch (err) { toast(err.message, 'err'); }
+ ui.probing = false;
+ await refreshQuiet();
+ renderSourceCards();
+}
+
+/* ---------- EVENT TAPE:真实事件差量渲染 ---------- */
+const tapeSeen = { call: 0, provCall: 0, audit: 0, job: 0, first: true };
+function tapeEventsFromRaw() {
+ const evs = [];
+ ((RAW.overview && RAW.overview.recent_calls) || []).forEach((c) => { if (c.id > tapeSeen.call) evs.push({ ts: c.created_at, src: 'tushare', msg: c.endpoint, ok: c.ok, ms: c.latency_ms, sortId: 'c' + c.id }); });
+ ((RAW.providers && RAW.providers.recent_calls) || []).forEach((c) => { if (c.id > tapeSeen.provCall) evs.push({ ts: c.fetched_at, src: c.provider, msg: c.interface, ok: c.status === 'ok', ms: c.latency_ms, sortId: 'p' + c.id }); });
+ ((RAW.audit && RAW.audit.items) || []).forEach((a) => { if (a.id > tapeSeen.audit) evs.push({ ts: a.created_at, src: 'hub_admin', msg: `${a.action} ${a.target || ''}`.trim(), ok: true, sortId: 'a' + a.id }); });
+ ((RAW.jobs && RAW.jobs.runs) || []).forEach((r) => { if (r.id > tapeSeen.job) evs.push({ ts: r.finished_at || r.started_at, src: 'scheduler', msg: `${r.job_id} ${r.state}`, ok: r.state !== 'failed', sortId: 'j' + r.id }); });
+ const maxOf = (arr, key) => arr.reduce((m, x) => Math.max(m, x[key] || 0), 0);
+ tapeSeen.call = Math.max(tapeSeen.call, maxOf((RAW.overview && RAW.overview.recent_calls) || [], 'id'));
+ tapeSeen.provCall = Math.max(tapeSeen.provCall, maxOf((RAW.providers && RAW.providers.recent_calls) || [], 'id'));
+ tapeSeen.audit = Math.max(tapeSeen.audit, maxOf((RAW.audit && RAW.audit.items) || [], 'id'));
+ tapeSeen.job = Math.max(tapeSeen.job, maxOf((RAW.jobs && RAW.jobs.runs) || [], 'id'));
+ return evs;
+}
+let tapeBuf = [];
+function updateTape() {
+ const evs = tapeEventsFromRaw();
+ if (tapeSeen.first) { tapeSeen.first = false; return; } // 首次加载只建基线,不补播历史事件
+ if (!evs.length) return;
+ tapeBuf = tapeBuf.concat(evs).slice(-16);
+ const html = tapeBuf.map((e) => `
${esc(hm(e.ts))}${esc(e.src)}${esc(e.msg)}` +
+ (e.ok ? `✓${e.ms ? ` ${e.ms}ms` : ''}` : '× 失败') +
+ `///`).join('');
+ $('tapeA').innerHTML = html; $('tapeB').innerHTML = html;
+}
+
+/* ---------- 抽屉:调度任务 / 盘后发布 / 审计 ---------- */
+let drawerTab = 'jobs';
+function openOpsDrawer(tab, ctx) {
+ drawerTab = tab || 'jobs';
+ drawerCtx = ctx || {};
+ $('drawerMask').classList.add('open');
+ renderDrawer();
+}
+function closeOpsDrawer() { $('drawerMask').classList.remove('open'); }
+let drawerCtx = {};
+$('drawerClose').addEventListener('click', closeOpsDrawer);
+$('drawerMask').addEventListener('click', (e) => { if (e.target.id === 'drawerMask') closeOpsDrawer(); });
+$('opsBtn').addEventListener('click', () => openOpsDrawer('jobs', {}));
+$('drawerTabs').addEventListener('click', (e) => {
+ const b = e.target.closest('[data-dtab]');
+ if (!b) return;
+ drawerTab = b.dataset.dtab;
+ renderDrawer();
+});
+function renderDrawer() {
+ $('drawerTabs').querySelectorAll('[data-dtab]').forEach((b) => b.classList.toggle('act', b.dataset.dtab === drawerTab));
+ $('drawerSub').textContent = { jobs: '既有调度手动触发', release: '盘后发布 / 回滚 / 补数', audit: '操作审计日志' }[drawerTab] || '';
+ if (drawerTab === 'jobs') renderDrawerJobs();
+ else if (drawerTab === 'release') renderDrawerRelease();
+ else renderDrawerAudit();
+}
+function renderDrawerJobs() {
+ const jobs = (RAW.jobs && RAW.jobs.jobs) || [];
+ const runs = (RAW.jobs && RAW.jobs.runs) || [];
+ const runsByJob = {};
+ runs.forEach((r) => { (runsByJob[r.job_id] = runsByJob[r.job_id] || []).push(r); });
+ $('drawerBody').innerHTML = `
+
-
当前映射
- ${table(["数据集", "活跃批次", "上一批次", "状态", "发布时间", "操作"], data.publications.map((row) => [
- esc(row.dataset), esc(row.active_batch), esc(row.prev_batch), esc(row.state), esc(row.published_at),
- row.prev_batch ? `
` : "-",
- ]))}
-
批次
- ${table(["batch_id", "数据集", "状态", "行数", "错误"], data.batches.map((row) => [
- esc(row.batch_id), esc(row.dataset), esc(row.state), row.rows_out ?? "", esc(row.error),
- ]))}
- `;
+
+
最近运行(最新 20 条)
+
| ID | 任务 | 状态 | 开始 | 结束 | 错误 |
+ ${runs.slice(0, 20).map((r) => `| ${r.id} | ${esc(r.job_id)} | ${pillHtml(bucketOf(r.state))} | ${esc(r.started_at || '')} | ${esc(r.finished_at || '')} | ${esc(r.error || '')} |
`).join('') || '| 暂无运行记录 |
'}
+
+
`;
+ $('drawerBody').querySelectorAll('[data-run]').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const dateInput = $('drawerBody').querySelector(`[data-run-date="${btn.dataset.run}"]`);
+ try {
+ await api(`/admin/api/jobs/${btn.dataset.run}/run`, { method: 'POST', body: JSON.stringify({ trade_date: (dateInput && dateInput.value) || '' }) });
+ toast('已触发:' + btn.dataset.run, 'ok');
+ await refreshQuiet(); renderDrawer();
+ } catch (err) { toast(err.message, 'err'); }
+ });
+ });
+}
+function renderDrawerRelease() {
+ const date = drawerCtx.date || today();
+ $('drawerBody').innerHTML = `
+
+
+
+
`;
+ $('relLoad').addEventListener('click', async () => { drawerCtx.date = $('relDate').value; await loadRelease(); });
+ $('relBackfill').addEventListener('click', () => openDangerModal('backfill', { dataset: drawerCtx.dataset || '', date: drawerCtx.date || date }));
+ loadRelease();
+}
+async function loadRelease() {
+ const date = drawerCtx.date || today();
+ let data;
+ try { data = await api(`/admin/api/batches?date=${encodeURIComponent(date)}`); } catch (err) { toast(err.message, 'err'); return; }
+ $('relBody').innerHTML = `
+
发布映射
+
| 数据集 | 活跃批次 | 上一批次 | 状态 | 发布时间 | 操作 |
+ ${data.publications.map((row) => `| ${esc(row.dataset)} | ${esc(row.active_batch)} | ${esc(row.prev_batch || '')} | ${pillHtml(bucketOf(row.state))} | ${esc(row.published_at || '')} |
+ ${row.prev_batch ? `` : '—'} |
`).join('') || '| 当日暂无发布记录 |
'}
+
+
批次
+
| batch_id | 数据集 | 状态 | 行数 | 错误 |
+ ${data.batches.map((row) => `| ${esc(row.batch_id)} | ${esc(row.dataset)} | ${pillHtml(bucketOf(row.state))} | ${row.rows_out ?? ''} | ${esc(row.error || '')} |
`).join('') || '| 当日暂无批次 |
'}
+
`;
+ $('relBody').querySelectorAll('[data-rollback]').forEach((btn) => btn.addEventListener('click', () => openDangerModal('rollback', { dataset: btn.dataset.rollback, date })));
+}
+function renderDrawerAudit() {
+ const items = (RAW.audit && RAW.audit.items) || [];
+ $('drawerBody').innerHTML = `
| 时间 | 操作者 | 动作 | 对象 | 详情 |
+ ${items.map((row) => `| ${esc(row.created_at)} | ${esc(row.actor)} | ${esc(row.action)} | ${esc(row.target || '')} | ${esc(row.detail || '')} |
`).join('') || '| 暂无审计记录 |
'}
+
`;
}
-async function dangerous(kind, dataset) {
- const date = ($("rel-date") && $("rel-date").value) || "";
- const ds = dataset || prompt("数据集(daily/valuation/moneyflow/auction/stocks→A组整批;index_daily→B组;或 reference)", "daily");
- if (!ds) return;
- const password = prompt("二次确认:输入管理密码");
- if (!password) return;
- const confirmWord = `${ds}:${date}`;
- const typed = prompt(`请输入确认词:${confirmWord}`);
- const path = kind === "rollback" ? "/admin/api/rollback" : "/admin/api/backfill";
- await api(path, {
- method: "POST",
- body: JSON.stringify({ dataset: ds, trade_date: date, password, confirm: typed }),
- });
- render();
+/* ---------- 危险操作确认弹层(回滚 / 补数) ---------- */
+let dangerCtx = null;
+function openDangerModal(kind, ctx) {
+ dangerCtx = { kind, ...ctx };
+ const word = `${ctx.dataset}:${ctx.date}`;
+ $('modalTitle').textContent = kind === 'rollback' ? '确认回滚' : '确认补数';
+ $('modalDesc').textContent = kind === 'rollback'
+ ? `将把 ${ctx.dataset} 在 ${ctx.date} 的发布回退到上一批次,需管理密码与确认词。`
+ : `将对 ${ctx.dataset || '(请先在发布列表选择数据集)'} 于 ${ctx.date} 重新补数并发布,需管理密码与确认词。`;
+ $('modalConfirmWord').textContent = word;
+ $('modalPassword').value = ''; $('modalConfirm').value = ''; $('modalErr').textContent = '';
+ $('modalMask').classList.add('open');
+}
+$('modalCancel').addEventListener('click', () => $('modalMask').classList.remove('open'));
+$('modalMask').addEventListener('click', (e) => { if (e.target.id === 'modalMask') $('modalMask').classList.remove('open'); });
+$('modalOk').addEventListener('click', async () => {
+ if (!dangerCtx) return;
+ const password = $('modalPassword').value;
+ const confirm = $('modalConfirm').value;
+ const path = dangerCtx.kind === 'rollback' ? '/admin/api/rollback' : '/admin/api/backfill';
+ try {
+ await api(path, { method: 'POST', body: JSON.stringify({ dataset: dangerCtx.dataset, trade_date: dangerCtx.date, password, confirm }) });
+ $('modalMask').classList.remove('open');
+ toast('操作成功', 'ok');
+ await refreshQuiet();
+ if (state.page === 'overview') renderPage('overview');
+ if (drawerTab === 'release') loadRelease();
+ } catch (err) {
+ $('modalErr').textContent = err.message;
+ }
+});
+
+/* ---------- toast ---------- */
+let toastT = null;
+function toast(msg, kind) {
+ const el = $('toastEl');
+ el.textContent = msg;
+ el.className = 'toast show' + (kind ? ' ' + kind : '');
+ clearTimeout(toastT);
+ toastT = setTimeout(() => { el.classList.remove('show'); }, 3200);
+}
+
+/* ================= 顶栏:时钟 / 减少动态 / 主题 ================= */
+function updateClock() {
+ const now = new Date();
+ $('ckD').textContent = `${now.getFullYear()}-${p2(now.getMonth() + 1)}-${p2(now.getDate())}`;
+ $('ckH').textContent = p2(now.getHours());
+ $('ckM').textContent = p2(now.getMinutes());
+ $('ckS').textContent = p2(now.getSeconds());
+}
+function applyCalm(on) {
+ state.calm = on;
+ $('appRoot').classList.toggle('reduce-motion', on);
+ $('calmBtn').textContent = on ? '恢复动态' : '减少动态';
+ $('calmBtn').classList.toggle('on', on);
+}
+$('calmBtn').addEventListener('click', () => applyCalm(!state.calm));
+if (window.matchMedia) {
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
+ const syncMq = () => { if (mq.matches && !state.calm) applyCalm(true); };
+ syncMq();
+ mq.addEventListener ? mq.addEventListener('change', syncMq) : mq.addListener(syncMq);
+}
+
+/* ================= 轮询:拉取真实数据、驱动渲染、可见性/网络门禁 ================= */
+const Poller = (() => {
+ let timer = null;
+ let running = false;
+ const INTERVAL = 4000;
+ async function tick() {
+ if (!running) return;
+ try {
+ await fetchAll();
+ updateTape();
+ if (state.page === 'overview') mainEl.innerHTML = overviewHtml();
+ else if (state.page === 'sources') renderSourceCards();
+ else if (state.page === 'lineage') { /* 血缘图交互态复杂,仅在用户操作或下次轮询后整体重绘一次 */ mainEl.innerHTML = lineageHtml(); renderLinRows(window.__linVM); applyLinHover(window.__linVM); fitLineageTable(); }
+ updatePhaseTag();
+ setNetOk(true);
+ } catch (err) {
+ setNetOk(false);
+ }
+ }
+ function setNetOk(ok) {
+ $('liveSpan').style.opacity = ok ? '1' : '.35';
+ }
+ function scheduleNext() { timer = setTimeout(async () => { await tick(); scheduleNext(); }, INTERVAL); }
+ return {
+ async start() {
+ if (running) return;
+ running = true;
+ await tick();
+ scheduleNext();
+ },
+ stop() { running = false; if (timer) clearTimeout(timer); timer = null; },
+ isRunning() { return running; },
+ };
+})();
+
+function resetTapeBaselines() {
+ // 恢复轮询时静默重建基线,不补播暂停期间错过的事件。
+ tapeSeen.call = tapeSeen.provCall = tapeSeen.audit = tapeSeen.job = Number.MAX_SAFE_INTEGER;
+ tapeSeen.first = true;
+}
+document.addEventListener('visibilitychange', () => {
+ if (document.hidden) { Poller.stop(); }
+ else if (!$('appRoot').hidden) { resetTapeBaselines(); Poller.start(); }
+});
+window.addEventListener('offline', () => { Poller.stop(); $('liveSpan').style.opacity = '.35'; });
+window.addEventListener('online', () => { if (!$('appRoot').hidden && !document.hidden) { resetTapeBaselines(); Poller.start(); } });
+
+async function refreshQuiet() {
+ try { await fetchAll(); } catch {}
+}
+
+/* ================= 启动 ================= */
+async function startApp() {
+ setInterval(updateClock, 1000);
+ updateClock();
+ await fetchAll();
+ tapeSeen.first = true;
+ renderPage(pageFromHash());
+ await Poller.start();
}
boot();
diff --git a/xiaobai-datahub/admin/index.html b/xiaobai-datahub/admin/index.html
index b7eb893..a9493c2 100644
--- a/xiaobai-datahub/admin/index.html
+++ b/xiaobai-datahub/admin/index.html
@@ -1,16 +1,18 @@
-
-
-
xiaobai-datahub 管理后台
-
+
+
+
小白复盘 · 数据中枢
+
-
-
+
+
+
+
数据中枢
- 内网管理后台,用于查看源状态、调度和盘后发布批次。
+ 内网管理后台 · 运行总览 / 数据源配置 / 数据血缘。
+
-
+
+
修改初始密码
+ 首次登录需先设置新密码才能进入。
-
-
-
- xiaobai-datahub
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
危险操作确认
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xiaobai-datahub/admin/styles.css b/xiaobai-datahub/admin/styles.css
index 1631ac1..df7ab2d 100644
--- a/xiaobai-datahub/admin/styles.css
+++ b/xiaobai-datahub/admin/styles.css
@@ -1,51 +1,527 @@
+/* JetBrains Mono (variable, latin subset) — 内嵌,零外部依赖 */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 100 800;
+ font-display: swap;
+ src: url(data:font/woff2;base64,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) format('woff2');
+}
+
+/* ================= index.css 逐行照抄(去掉 tailwind 指令) ================= */
:root {
- color-scheme: light;
- --bg: #f4f5f7;
- --surface: #ffffff;
- --text: #1f2329;
- --muted: #646a73;
- --line: #dee0e3;
- --action: #3370ff;
- --danger: #e04536;
- --ok: #16a34a;
- --warn: #b45309;
- --radius: 8px;
- --pad: 16px;
- font-family: "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
+ --ink: #050810;
+ --panel: #0c1220;
+ --panel2: #111a2b;
+ --line: #1a2540;
+ --line2: #243152;
+ --cy: #22d3ee;
+ --mint: #34d399;
+ --amb: #fbbf24;
+ --rd: #f87171;
+ --txt: #d7e1f0;
+ --mut: #8b9bb4;
+ --dim: #54637e;
}
-:root[data-theme="night"] {
- color-scheme: dark;
- --bg: #111318;
- --surface: #1b1e24;
- --text: #e8eaed;
- --muted: #9aa0a6;
- --line: #2a2f38;
- --action: #5b8cff;
+
+html, body, #root { height: 100%; }
+html, body { margin: 0; }
+
+/* HEL-529 生产实现补充:确保原生 [hidden] 属性不被下方任何 display 规则
+ (如 .auth-wrap/.app 等 flex/relative 布局)覆盖——两者选择器优先级相同时
+ 源码顺序在后的规则会赢,必须显式声明 !important 兜底。 */
+[hidden] { display: none !important; }
+
+body {
+ background: var(--ink);
+ color: var(--txt);
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
+ "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", monospace;
+ font-size: 13px;
+ -webkit-font-smoothing: antialiased;
+ overflow-x: hidden;
+}
+
+/* ---------- ambient background: radial depth + grid + scanlines ---------- */
+.bg-ambient {
+ background:
+ radial-gradient(1200px 500px at 70% -10%, rgba(34, 211, 238, 0.055), transparent 60%),
+ radial-gradient(900px 420px at 10% 110%, rgba(52, 211, 153, 0.04), transparent 60%),
+ radial-gradient(700px 500px at 100% 100%, rgba(248, 113, 113, 0.03), transparent 60%),
+ var(--ink);
+}
+.bg-gridlines {
+ background-image:
+ linear-gradient(rgba(26, 37, 64, 0.35) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(26, 37, 64, 0.35) 1px, transparent 1px);
+ background-size: 48px 48px;
+ mask-image: radial-gradient(120% 90% at 50% 0%, black 30%, transparent 90%);
+ -webkit-mask-image: radial-gradient(120% 90% at 50% 0%, black 30%, transparent 90%);
+}
+.scanlines {
+ background: repeating-linear-gradient(
+ to bottom,
+ rgba(255, 255, 255, 0.022) 0px,
+ rgba(255, 255, 255, 0.022) 1px,
+ transparent 1px,
+ transparent 3px
+ );
+}
+
+/* ---------- typography helpers ---------- */
+.lab {
+ font-size: 10px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--dim);
+ font-weight: 500;
+}
+.num { font-variant-numeric: tabular-nums; }
+
+/* ---------- panels ---------- */
+.panel {
+ background: linear-gradient(180deg, rgba(17, 26, 43, 0.6), rgba(12, 18, 32, 0.9));
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ position: relative;
+}
+.panel-hd {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--line);
+ font-size: 11px;
+ letter-spacing: 0.14em;
+ color: var(--mut);
+}
+.panel-hd b { color: var(--txt); font-weight: 600; }
+
+/* ---------- glow status LED ---------- */
+.led {
+ width: 7px; height: 7px; border-radius: 9999px;
+ display: inline-block; flex: none;
+ position: relative;
+}
+.led.ok { background: var(--mint); box-shadow: 0 0 6px 1px rgba(52,211,153,.7); }
+.led.slow { background: var(--amb); box-shadow: 0 0 6px 1px rgba(251,191,36,.7); }
+.led.fail { background: var(--rd); box-shadow: 0 0 6px 1px rgba(248,113,113,.8); }
+.led.rev { background: var(--cy); box-shadow: 0 0 6px 1px rgba(34,211,238,.7); }
+.led.plan { background: #475569; box-shadow: none; }
+.led.off { background: transparent; border: 1px dashed #475569; box-shadow: none; }
+.led.pulse::after {
+ content: ""; position: absolute; inset: -4px; border-radius: 9999px;
+ border: 1px solid currentColor; opacity: .6;
+ animation: ledPing 1.6s ease-out infinite;
+}
+.led.ok.pulse { color: var(--mint); }
+.led.fail.pulse { color: var(--rd); }
+.led.rev.pulse { color: var(--cy); }
+.led.slow.pulse { color: var(--amb); }
+@keyframes ledPing {
+ 0% { transform: scale(.4); opacity: .7; }
+ 80%, 100% { transform: scale(1.4); opacity: 0; }
+}
+
+/* ---------- text glow ---------- */
+.glow-cy { color: var(--cy); text-shadow: 0 0 12px rgba(34,211,238,.45); }
+.glow-mint { color: var(--mint); text-shadow: 0 0 12px rgba(52,211,153,.45); }
+.glow-amb { color: var(--amb); text-shadow: 0 0 12px rgba(251,191,36,.45); }
+.glow-rd { color: var(--rd); text-shadow: 0 0 12px rgba(248,113,113,.5); }
+
+/* ---------- blinking / pulsing ---------- */
+.blink { animation: blink 1.1s steps(2, start) infinite; }
+@keyframes blink { 50% { opacity: 0; } }
+.pulse-dot { animation: pulseDot 1.5s ease-in-out infinite; }
+@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
+
+/* ---------- flash on value change ---------- */
+.flash { animation: flash .45s ease-out; }
+@keyframes flash {
+ 0% { background: rgba(34, 211, 238, 0.22); color: #e8fbff; }
+ 100% { background: transparent; }
+}
+
+/* ---------- ticker marquee ---------- */
+.marquee-track {
+ display: inline-flex;
+ white-space: nowrap;
+ animation: marquee 46s linear infinite;
+ will-change: transform;
+}
+.marquee-track:hover { animation-play-state: paused; }
+@keyframes marquee {
+ from { transform: translateX(0); }
+ to { transform: translateX(-50%); }
+}
+
+/* ---------- flow lines (lineage / timeline) ---------- */
+.flowline {
+ stroke-dasharray: 4 6;
+ animation: flowDash 1.4s linear infinite;
+}
+@keyframes flowDash { to { stroke-dashoffset: -10; } }
+
+/* ---------- radar sweep ---------- */
+.radar-sweep {
+ background: conic-gradient(from 0deg, rgba(34,211,238,.55), transparent 22%, transparent);
+ animation: sweep 4.2s linear infinite;
+ border-radius: 9999px;
+}
+@keyframes sweep { to { transform: rotate(360deg); } }
+
+/* ---------- buttons ---------- */
+.tbtn {
+ font-size: 11px; letter-spacing: .1em;
+ border: 1px solid var(--line2);
+ color: var(--mut);
+ padding: 4px 12px; border-radius: 4px;
+ background: rgba(17,26,43,.5);
+ transition: all .15s ease;
+ cursor: pointer;
+ font-family: inherit;
+}
+.tbtn:hover { color: var(--cy); border-color: rgba(34,211,238,.5); box-shadow: 0 0 10px rgba(34,211,238,.15); }
+.tbtn:active { transform: translateY(1px); }
+.tbtn:disabled { opacity: .45; cursor: default; }
+.tbtn.on { color: var(--cy); border-color: rgba(34,211,238,.6); background: rgba(34,211,238,.08); }
+
+/* ---------- table ---------- */
+.dtable { width: 100%; border-collapse: collapse; }
+.dtable th {
+ font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
+ color: var(--dim); font-weight: 500; text-align: left;
+ padding: 7px 10px; border-bottom: 1px solid var(--line);
+}
+.dtable td { padding: 7px 10px; border-bottom: 1px solid rgba(26,37,64,.55); vertical-align: middle; }
+.dtable tr:last-child td { border-bottom: none; }
+.dtable tbody tr { transition: background .12s ease; }
+.dtable tbody tr:hover { background: rgba(34,211,238,.04); }
+
+/* ---------- scrollbar ---------- */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--ink); }
+::-webkit-scrollbar-thumb { background: var(--line2); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: #31406b; }
+* { scrollbar-width: thin; scrollbar-color: var(--line2) var(--ink); }
+
+/* ---------- reduced motion ---------- */
+.reduce-motion *, .reduce-motion *::before, .reduce-motion *::after {
+ animation-duration: .001s !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: .001s !important;
+}
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: .001s !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: .001s !important;
+ }
+}
+
+/* ================= 结构样式(对应 App.tsx / chrome.tsx 的 tailwind 类) ================= */
+#root { min-height: 100%; }
+.app { min-height: 100%; position: relative; }
+.bg-layer { position: fixed; inset: 0; pointer-events: none; }
+.z0 { z-index: 0; } .z50 { z-index: 50; }
+.scanlines.z50 { opacity: .7; }
+
+/* ---------- 顶栏 ---------- */
+.hdr { position: sticky; top: 0; z-index: 30; border-bottom: 1px solid #1a2540; background: rgba(5,8,16,.88); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
+.hdr-in { display: flex; align-items: center; gap: 12px; padding: 0 16px; height: 48px; }
+.hdr-in > * { flex: none; }
+.hdr-in > .flex1 { flex: 1; }
+.brand { display: flex; flex-direction: column; line-height: 1; flex: none; white-space: nowrap; }
+.brand .b1 { font-size: 13px; font-weight: 600; color: #e8f1ff; letter-spacing: .06em; }
+.brand .b2 { font-size: 9px; color: #54637e; letter-spacing: .26em; margin-top: 4px; }
+.vsep { width: 1px; height: 16px; background: #1a2540; margin: 0 4px; display: inline-block; flex: none; }
+.nav { display: flex; align-items: center; gap: 4px; margin-left: 8px; flex: none; }
+.navbtn { position: relative; padding: 6px 14px; font-size: 12px; letter-spacing: .08em; border-radius: 4px; transition: all .15s; background: none; border: none; cursor: pointer; color: #8b9bb4; font-family: inherit; }
+.navbtn:hover { color: #d7e1f0; background: rgba(34,211,238,.04); }
+.navbtn.act { color: #22d3ee; background: rgba(34,211,238,.08); box-shadow: inset 0 0 0 1px rgba(34,211,238,.35), 0 0 14px rgba(34,211,238,.12); }
+.navbtn .tri { margin-right: 4px; font-size: 9px; display: none; }
+.navbtn.act .tri { display: inline; }
+.navbtn .en { margin-left: 6px; font-size: 9px; letter-spacing: .14em; color: #3d4c66; }
+.navbtn.act .en { color: rgba(34,211,238,.6); }
+.flex1 { flex: 1; }
+.mdtag { display: none; align-items: center; gap: 8px; border-radius: 3px; border: 1px solid rgba(34,211,238,.4); background: rgba(34,211,238,.07); padding: 4px 8px; font-size: 10px; letter-spacing: .14em; color: #22d3ee; }
+@media (min-width: 768px) { .mdtag { display: flex; } }
+@media (max-width: 1150px) { .mdtag { display: none; } }
+.livespan { display: none; align-items: center; gap: 6px; font-size: 10px; letter-spacing: .18em; color: #34d399; }
+@media (min-width: 640px) { .livespan { display: flex; } }
+.livedot { width: 6px; height: 6px; border-radius: 9999px; background: #34d399; box-shadow: 0 0 6px 1px rgba(52,211,153,.7); }
+.clock { font-size: 12px; color: #8b9bb4; letter-spacing: .05em; white-space: nowrap; }
+.clock .csep { margin: 0 6px; color: #243152; }
+.clock .ct { color: #d7e1f0; }
+.clock .cc { color: #22d3ee; }
+.demotag { display: inline-flex; align-items: center; gap: 4px; border-radius: 3px; border: 1px solid rgba(251,191,36,.45); background: rgba(251,191,36,.06); color: #fbbf24; padding: 2px 6px; font-size: 10px; letter-spacing: .08em; white-space: nowrap; }
+
+/* ---------- 雷达 logo ---------- */
+.radar { position: relative; flex: none; border-radius: 9999px; border: 1px solid rgba(34,211,238,.5); overflow: hidden; box-shadow: 0 0 12px rgba(34,211,238,.25), inset 0 0 8px rgba(34,211,238,.15); }
+.radar .sweep { position: absolute; inset: 0; }
+.radar .ring1 { position: absolute; border-radius: 9999px; border: 1px solid rgba(34,211,238,.3); }
+.radar .ring2 { position: absolute; border-radius: 9999px; border: 1px solid rgba(34,211,238,.2); }
+.radar .center { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); width: 3px; height: 3px; border-radius: 9999px; background: #22d3ee; box-shadow: 0 0 6px 2px rgba(34,211,238,.8); }
+.radar .blip { position: absolute; width: 2.5px; height: 2.5px; border-radius: 9999px; }
+
+/* ---------- EVENT TAPE ---------- */
+.tape { position: fixed; bottom: 0; left: 0; right: 0; height: 32px; z-index: 40; display: flex; align-items: center; border-top: 1px solid #1a2540; background: rgba(5,8,16,.92); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); overflow: hidden; }
+.tape-label { flex: none; display: flex; align-items: center; gap: 6px; padding: 0 12px; height: 100%; border-right: 1px solid #1a2540; font-size: 10px; letter-spacing: .18em; color: #54637e; }
+.tape-view { flex: 1; overflow: hidden; position: relative; }
+.tape-item { display: inline-flex; align-items: center; gap: 8px; margin: 0 20px; font-size: 10.5px; letter-spacing: .04em; }
+.tape-item .tt { color: #54637e; }
+.tape-item .ts { color: #22d3ee; }
+.tape-item .tm { color: #8b9bb4; }
+.tape-item .tok { color: #34d399; }
+.tape-item .tbad { color: #f87171; }
+.tape-item .tsep { color: #1a2540; margin-left: 12px; }
+
+/* ---------- 主区 ---------- */
+.main { position: relative; z-index: 10; padding: 12px 16px 48px; max-width: 1720px; margin: 0 auto; }
+.col { display: flex; flex-direction: column; gap: 12px; }
+
+/* ---------- tag / pill ---------- */
+.tag { display: inline-flex; align-items: center; gap: 4px; border-radius: 3px; border: 1px solid #243152; padding: 2px 6px; font-size: 10px; letter-spacing: .08em; line-height: 1.5; color: #8b9bb4; white-space: nowrap; }
+.tag.cy { border-color: rgba(34,211,238,.45); color: #22d3ee; background: rgba(34,211,238,.06); }
+.tag.amb { border-color: rgba(251,191,36,.45); color: #fbbf24; background: rgba(251,191,36,.06); }
+.tag.rd { border-color: rgba(248,113,113,.45); color: #f87171; background: rgba(248,113,113,.07); }
+.tag.mint { border-color: rgba(52,211,153,.45); color: #34d399; background: rgba(52,211,153,.06); }
+.tag.dashed { border-style: dashed; border-color: #243152; color: #54637e; }
+.minidot { width: 6px; height: 6px; border-radius: 9999px; display: inline-block; }
+.spill { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; white-space: nowrap; }
+
+/* ---------- 总览 Hero ---------- */
+.hero { padding: 16px 20px; display: flex; flex-wrap: wrap; align-items: center; gap: 16px 32px; }
+.hero-l { display: flex; flex-direction: column; gap: 8px; min-width: 300px; }
+.hero-title { font-size: 22px; font-weight: 600; letter-spacing: .025em; }
+.hero-title .dot { color: #54637e; margin: 0 8px; }
+.hero-tags { display: flex; align-items: center; gap: 8px; }
+.hero-sub { font-size: 11px; color: #54637e; }
+.hero-sub .sep { margin: 0 8px; color: #243152; }
+.kpis { display: flex; align-items: flex-start; gap: 32px; padding-right: 8px; }
+.kpi { display: flex; flex-direction: column; gap: 4px; }
+.kpi .kv { font-size: 26px; line-height: 1; font-weight: 600; letter-spacing: -.025em; color: #e8f1ff; }
+.kpi .ks { font-size: 10px; color: #54637e; }
+
+/* ---------- 数据集卡 ---------- */
+.ds-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
+@media (min-width: 640px) { .ds-grid { grid-template-columns: repeat(5, 1fr); } }
+@media (min-width: 1280px) { .ds-grid { grid-template-columns: repeat(10, 1fr); } }
+.dsc { border-radius: 5px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; transition: all .15s; cursor: default; border: 1px solid #1a2540; }
+.dsc:hover { transform: translateY(-1px); border-color: #243152; }
+.dsc.fail { border-color: rgba(248,113,113,.55); box-shadow: 0 0 18px rgba(248,113,113,.14); }
+.dsc.fail:hover { border-color: rgba(248,113,113,.55); }
+.dsc.slow { border-color: rgba(251,191,36,.4); }
+.dsc.slow:hover { border-color: rgba(251,191,36,.4); }
+.dsc.review { border-color: rgba(34,211,238,.45); box-shadow: 0 0 14px rgba(34,211,238,.1); }
+.dsc.review:hover { border-color: rgba(34,211,238,.45); }
+.dsc-top { display: flex; align-items: center; justify-content: space-between; }
+.dsc-name { font-size: 12px; font-weight: 500; color: #e8f1ff; }
+.dsc-note { font-size: 10px; color: #54637e; line-height: 1.6; min-height: 28px; }
+
+/* ---------- 总览下半网格 ---------- */
+.ov-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
+@media (min-width: 1280px) { .ov-grid { grid-template-columns: repeat(12, 1fr); } .ov-l { grid-column: span 7; } .ov-r { grid-column: span 5; } }
+.panel-b { padding: 12px; }
+.legend { display: flex; align-items: center; gap: 12px; font-size: 10px; letter-spacing: .1em; }
+.legend span { display: flex; align-items: center; gap: 4px; }
+.jit { padding: 0 4px; border-radius: 3px; }
+.obs-note { font-size: 10px; color: #fbbf24; }
+.site-foot { padding: 10px 12px; border-top: 1px solid #1a2540; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; font-size: 10px; color: #54637e; }
+.tbtn.mini { padding: 3px 8px; }
+
+/* ---------- 时间线 ---------- */
+@keyframes nowPulse { 0%,100% { opacity: 1; } 50% { opacity: .5; } }
+.now-dot { animation: nowPulse 1.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
+@keyframes nowR { 0%,100% { r: 3.6px; } 50% { r: 5.2px; } }
+.now-r { animation: nowR 1.6s ease-in-out infinite; }
+
+/* ---------- 异常卡 ---------- */
+.inc { display: flex; align-items: center; gap: 12px; border-radius: 4px; border: 1px solid #1a2540; border-left-width: 3px; padding: 10px 12px; }
+.inc.fail { border-left-color: #f87171; background: rgba(248,113,113,.05); }
+.inc.slow { border-left-color: #fbbf24; background: rgba(251,191,36,.04); }
+.inc.off { border-left-color: #334155; background: transparent; border-style: dashed; border-color: #243152; border-left-width: 3px; border-left-style: solid; border-left-color: #334155; }
+.inc-title { font-size: 12px; color: #e8f1ff; }
+.inc-meta { font-size: 10px; color: #54637e; margin-top: 2px; }
+
+/* ---------- Sources 页 ---------- */
+.strip { padding: 12px 20px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 24px; }
+.src-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 12px 16px; border-bottom: 1px solid #1a2540; }
+.src-name { font-size: 15px; font-weight: 600; color: #e8f1ff; letter-spacing: .025em; }
+.src-role { font-size: 10px; color: #54637e; letter-spacing: .1em; }
+.src-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; padding: 8px 16px; border-bottom: 1px solid #1a2540; font-size: 11px; color: #8b9bb4; }
+.src-meta .msep { color: #243152; }
+.mx-grid { display: grid; grid-template-columns: 1fr; gap: 16px 24px; padding: 16px; }
+@media (min-width: 768px) { .mx-grid { grid-template-columns: repeat(2, 1fr); } }
+@media (min-width: 1280px) { .mx-grid { grid-template-columns: repeat(3, 1fr); } }
+.mx-grid > div { min-width: 0; }
+.mx-grid table { table-layout: auto; max-width: 100%; }
+.offbox { border-radius: 4px; border: 1px dashed #243152; padding: 24px 16px; text-align: center; font-size: 11px; color: #54637e; }
+.rsv-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
+@media (min-width: 768px) { .rsv-grid { grid-template-columns: repeat(3, 1fr); } }
+.rsv { border-radius: 6px; border: 1px dashed #243152; padding: 16px; display: flex; align-items: center; gap: 12px; color: #54637e; }
+@keyframes spin { to { transform: rotate(360deg); } }
+.animate-spin { display: inline-block; animation: spin 1s linear infinite; }
+
+/* ---------- Lineage 页 ---------- */
+.lin-cols { display: flex; justify-content: space-between; padding: 4px 24px 0; font-size: 10px; letter-spacing: .2em; color: #54637e; }
+.lin-hint { position: absolute; left: 24px; bottom: 4px; font-size: 10px; color: #54637e; letter-spacing: .1em; }
+@keyframes gnodePulse { 0%,100% { opacity: 1; } 50% { opacity: .55; } }
+.gnode-pulse { animation: gnodePulse 1.8s ease-in-out infinite; }
+.lin-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid #1a2540; }
+.lin-sel, .lin-q { background: #0c1220; border: 1px solid #243152; border-radius: 4px; font-size: 11px; color: #8b9bb4; padding: 6px 8px; outline: none; font-family: inherit; }
+.lin-q { color: #d7e1f0; padding: 6px 10px; width: 220px; }
+.lin-q::placeholder { color: #3d4c66; }
+.lin-sel:focus, .lin-q:focus { border-color: rgba(34,211,238,.5); }
+.lin-scroll { overflow-x: auto; }
+/* HEL-529:宽档下由 app.js 的 fitLineageTable() 按剩余可视高度写入
+ max-height(真实数据集 17 条 > 打样假设的 13 条),此处只声明纵向滚动本身;
+ 未设置 max-height(窄档 <=1100px)时 overflow-y:auto 不产生任何视觉影响。 */
+.lin-scroll { overflow-y: auto; }
+.lin-table { min-width: 1080px; }
+.lin-table-n { display: none; }
+.lin-table-n .sub { display: block; font-size: 10px; color: #54637e; margin-top: 2px; }
+/* ---------- Lineage 页压缩(仅血缘页,其他页不受影响) ---------- */
+#mainEl[data-page="lineage"] .col { gap: 8px; }
+#mainEl[data-page="lineage"] { padding-bottom: 36px; }
+#mainEl[data-page="lineage"] .strip { padding: 7px 16px; gap: 6px 20px; }
+#mainEl[data-page="lineage"] .lin-filter { padding: 6px 12px; }
+#mainEl[data-page="lineage"] .dtable th { padding: 5px 10px; }
+#mainEl[data-page="lineage"] .dtable td { padding: 4px 10px; }
+@media (max-width: 1100px) {
+ .hdr-in { flex-wrap: wrap; height: auto; padding-top: 6px; padding-bottom: 6px; row-gap: 6px; }
+ .hdr-in > .flex1 { flex: 1 1 100%; height: 0; }
+ .mdtag { display: flex; }
+ .lin-table { display: none; }
+ .lin-table-n { display: table; width: 100%; }
+}
+.row-fail { background: rgba(248,113,113,.045); }
+.row-slow { background: rgba(251,191,36,.03); }
+.row-off { opacity: .55; }
+
+/* ---------- sparkline ---------- */
+@keyframes sparkBlink { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
+.spark-end { animation: sparkBlink 1.4s ease-in-out infinite; }
+
+/* =================================================================
+ 以下为 HEL-529 第十二版生产实现补充样式(非 Kimi 打样内容):
+ 登录/改密视图、抽屉(调度任务/盘后发布/审计合并入口)、危险操作弹层。
+ 全部复用上方 token(--ink/--panel/--line/--cy/--mint/--amb/--rd 等),
+ 不引入新色值、不新增动效缓动函数。
+ ================================================================= */
+
+/* ---------- 登录 / 改密(保留原有表单结构,套用新 token) ---------- */
+.auth-wrap {
+ min-height: 100%; display: flex; align-items: center; justify-content: center;
+ padding: 24px;
+}
+.auth-panel {
+ width: 100%; max-width: 380px; padding: 28px 26px 24px;
+}
+.auth-panel h1 { font-size: 16px; font-weight: 600; color: #e8f1ff; margin: 0 0 6px; letter-spacing: .02em; }
+.auth-panel .muted { font-size: 11px; color: var(--dim); line-height: 1.6; margin: 0 0 18px; }
+.auth-panel form { display: flex; flex-direction: column; gap: 12px; }
+.auth-panel label { display: flex; flex-direction: column; gap: 6px; font-size: 11px; color: var(--mut); letter-spacing: .04em; }
+.auth-panel input {
+ background: #0c1220; border: 1px solid var(--line2); border-radius: 4px;
+ color: var(--txt); font-family: inherit; font-size: 13px; padding: 8px 10px; outline: none;
+}
+.auth-panel input:focus { border-color: rgba(34,211,238,.55); box-shadow: 0 0 0 2px rgba(34,211,238,.12); }
+.auth-panel button[type="submit"] {
+ margin-top: 4px; font-size: 12px; letter-spacing: .08em; border: 1px solid rgba(34,211,238,.5);
+ color: #0a1420; background: var(--cy); padding: 9px 12px; border-radius: 4px; cursor: pointer;
+ font-family: inherit; font-weight: 600; transition: filter .15s ease;
+}
+.auth-panel button[type="submit"]:hover { filter: brightness(1.08); }
+.auth-panel .error { color: var(--rd); font-size: 11px; margin: 2px 0 0; }
+
+/* ---------- 抽屉(调度任务 / 盘后发布 / 审计) ---------- */
+.drawer-mask {
+ position: fixed; inset: 0; background: rgba(2,4,10,.55); z-index: 60;
+ opacity: 0; pointer-events: none; transition: opacity .2s ease-out;
+}
+.drawer-mask.open { opacity: 1; pointer-events: auto; }
+.drawer {
+ position: fixed; top: 0; right: 0; bottom: 0; width: min(720px, 92vw);
+ background: linear-gradient(180deg, rgba(17,26,43,.98), rgba(9,13,23,.99));
+ border-left: 1px solid var(--line); z-index: 61; display: flex; flex-direction: column;
+ transform: translateX(100%); transition: transform .28s cubic-bezier(.16,1,.3,1);
+ box-shadow: -18px 0 40px rgba(0,0,0,.4);
+}
+.drawer-mask.open .drawer { transform: translateX(0); }
+.drawer-hd {
+ display: flex; align-items: center; gap: 10px; padding: 14px 18px;
+ border-bottom: 1px solid var(--line); flex: none;
+}
+.drawer-hd .ttl { font-size: 13px; font-weight: 600; color: #e8f1ff; letter-spacing: .04em; }
+.drawer-hd .sub { font-size: 10px; color: var(--dim); }
+.drawer-tabs { display: flex; gap: 4px; padding: 10px 18px 0; flex: none; }
+.drawer-tab {
+ font-size: 11px; letter-spacing: .06em; color: var(--mut); background: none; border: none;
+ padding: 6px 12px; border-radius: 4px 4px 0 0; cursor: pointer; font-family: inherit;
+ border-bottom: 2px solid transparent;
+}
+.drawer-tab.act { color: var(--cy); border-bottom-color: var(--cy); background: rgba(34,211,238,.06); }
+.drawer-body { flex: 1; overflow-y: auto; padding: 14px 18px 24px; }
+.drawer-close {
+ margin-left: auto; background: none; border: 1px solid var(--line2); color: var(--mut);
+ width: 26px; height: 26px; border-radius: 4px; cursor: pointer; font-size: 13px; line-height: 1;
+ font-family: inherit;
+}
+.drawer-close:hover { color: var(--cy); border-color: rgba(34,211,238,.5); }
+.drawer-section { margin-bottom: 18px; }
+.drawer-section .lab { margin-bottom: 8px; }
+.dtoolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
+.dtoolbar input {
+ background: #0c1220; border: 1px solid var(--line2); border-radius: 4px; color: var(--txt);
+ font-family: inherit; font-size: 11px; padding: 5px 8px; outline: none;
+}
+
+/* ---------- 危险操作确认弹层 ---------- */
+.modal-mask {
+ position: fixed; inset: 0; background: rgba(2,4,10,.6); z-index: 70;
+ display: none; align-items: center; justify-content: center; padding: 20px;
+}
+.modal-mask.open { display: flex; }
+.modal-box {
+ width: 100%; max-width: 380px; background: var(--panel2); border: 1px solid rgba(248,113,113,.4);
+ border-radius: 6px; padding: 18px 20px; box-shadow: 0 0 28px rgba(248,113,113,.12);
+}
+.modal-box h3 { margin: 0 0 4px; font-size: 13px; color: var(--rd); letter-spacing: .04em; }
+.modal-box p { margin: 0 0 12px; font-size: 11px; color: var(--mut); line-height: 1.7; }
+.modal-box label { display: flex; flex-direction: column; gap: 5px; font-size: 11px; color: var(--mut); margin-bottom: 10px; }
+.modal-box input {
+ background: #0c1220; border: 1px solid var(--line2); border-radius: 4px; color: var(--txt);
+ font-family: inherit; font-size: 12px; padding: 7px 9px; outline: none;
+}
+.modal-box input:focus { border-color: rgba(248,113,113,.55); }
+.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
+.modal-actions .tbtn.danger { color: var(--rd); border-color: rgba(248,113,113,.5); }
+.modal-actions .tbtn.danger:hover { color: #fff; background: rgba(248,113,113,.18); box-shadow: 0 0 10px rgba(248,113,113,.2); }
+.modal-err { color: var(--rd); font-size: 11px; margin: 6px 0 0; min-height: 14px; }
+
+/* ---------- 小工具 ---------- */
+.opbtn {
+ font-size: 10px; letter-spacing: .06em; border: 1px solid var(--line2); color: var(--mut);
+ background: rgba(17,26,43,.5); padding: 3px 9px; border-radius: 3px; cursor: pointer; font-family: inherit;
+}
+.opbtn:hover { color: var(--cy); border-color: rgba(34,211,238,.5); }
+.opbtn.danger { color: var(--rd); border-color: rgba(248,113,113,.4); }
+.opbtn.danger:hover { color: #fff; background: rgba(248,113,113,.14); }
+.empty-hint { text-align: center; padding: 22px 0; color: var(--dim); font-size: 11px; }
+.toast {
+ position: fixed; left: 50%; bottom: 44px; transform: translate(-50%, 8px); z-index: 80;
+ background: var(--panel2); border: 1px solid var(--line2); border-radius: 5px; padding: 9px 16px;
+ font-size: 12px; color: var(--txt); opacity: 0; pointer-events: none; transition: opacity .18s ease-out, transform .18s ease-out;
+ box-shadow: 0 8px 24px rgba(0,0,0,.35);
+}
+.toast.show { opacity: 1; transform: translate(-50%, 0); }
+.toast.err { border-color: rgba(248,113,113,.5); color: #fbd2d2; }
+.toast.ok { border-color: rgba(52,211,153,.5); color: #cdf5e4; }
+
+/* 减少动态:抽屉/弹层的滑入过渡也一并归零,保持与其余过渡同一门禁 */
+.reduce-motion .drawer,
+.reduce-motion .drawer-mask,
+.reduce-motion .toast { transition-duration: .001s !important; }
+@media (prefers-reduced-motion: reduce) {
+ .drawer, .drawer-mask, .toast { transition-duration: .001s !important; }
}
-* { box-sizing: border-box; }
-body { margin: 0; background: var(--bg); color: var(--text); }
-.panel, header.top, nav, main { background: var(--surface); }
-.auth-panel { max-width: 420px; margin: 12vh auto; padding: 28px; border-radius: var(--radius); border: 1px solid var(--line); }
-label { display: block; margin: 12px 0; }
-input, select { width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 4px; background: var(--bg); color: var(--text); }
-button { background: var(--action); color: #fff; border: 0; border-radius: 4px; padding: 8px 14px; cursor: pointer; }
-button.ghost { background: transparent; color: var(--text); border: 1px solid var(--line); }
-button.danger { background: var(--danger); }
-.muted { color: var(--muted); }
-.error { color: var(--danger); }
-.top { display: flex; gap: 12px; align-items: center; padding: 10px var(--pad); border-bottom: 1px solid var(--line); }
-nav { display: flex; gap: 4px; padding: 8px var(--pad); border-bottom: 1px solid var(--line); }
-nav button { background: transparent; color: var(--muted); }
-nav button.active { color: var(--action); background: transparent; font-weight: 600; }
-main { padding: var(--pad); min-height: calc(100vh - 96px); }
-.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 16px; }
-.card { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px; }
-table { width: 100%; border-collapse: collapse; font-size: 13px; }
-th, td { text-align: left; padding: 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
-.pill { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
-.ok { color: var(--ok); }
-.warn { color: var(--warn); }
-.fail { color: var(--danger); }
-.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; align-items: end; }
-.toolbar label { margin: 0; }
-dialog { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: var(--text); padding: 20px; }