// Data Lineage panel v2 — an orchestration surface, not just a link list. // Shows the full chain Sources → AI Agent (multi-step) → This record → // Outputs → Used by, with per-node status, an inline agent run log, // orchestration actions (re-run / schedule / edit instructions), and a // problem banner that surfaces at the top of the record when the pipeline // needs attention. // // Rendered as a vertical pipeline flow with explicit connectors. // // Loaded AFTER record_panels.jsx: overrides window.RecordLineagePanel. const { useState: useStateLX, useEffect: useEffectLX } = React; /* ===== Shared store (variant + run simulation), survives panel remounts ===== */ const lxStore = { state: { variant: 'pipeline', running: false, // re-run simulation in progress runStep: -1, // 0..2 active step while running ranNow: false, // a manual run completed this session bannerDismissed: false, }, listeners: new Set(), set(patch) { this.state = Object.assign({}, this.state, patch); this.listeners.forEach((fn) => fn(this.state)); }, }; function useLxStore() { const [s, setS] = useStateLX(lxStore.state); useEffectLX(() => { const fn = (st) => setS(st); lxStore.listeners.add(fn); return () => lxStore.listeners.delete(fn); }, []); return s; } /* ===== Run simulation ===== */ let lxRunTimers = []; function lxRunAgentNow() { if (lxStore.state.running) return; lxRunTimers.forEach(clearTimeout); lxRunTimers = []; lxStore.set({ running: true, runStep: 0 }); lxRunTimers.push(setTimeout(() => lxStore.set({ runStep: 1 }), 1400)); lxRunTimers.push(setTimeout(() => lxStore.set({ runStep: 2 }), 2900)); lxRunTimers.push(setTimeout(() => lxStore.set({ running: false, runStep: -1, ranNow: true }), 4200)); } /* ===== Demo data ===== */ const LX_SOURCES = [ { id: 's-safety', t: 'Safety-Stats-March-2026.xlsx', kind: 'sheet', outputs: ['dashboard', 'report', 'slide'], sync: 'Synced today 12:05 AM' }, { id: 's-pipe', t: 'AWST_Pipe_Install_Tracker.xlsx', kind: 'sheet', outputs: ['dashboard', 'report', 'slide'], sync: 'Synced today 9:32 AM' }, { id: 's-cost', t: 'Cost-Tracker-March-2026.xlsx', kind: 'sheet', outputs: ['dashboard', 'report', 'slide'], sync: 'Synced today 9:58 AM' }, { id: 's-comm', t: 'AWST_Commercial_Register.xlsx', kind: 'sheet', outputs: ['dashboard', 'report'], sync: 'Synced today 9:58 AM' }, { id: 's-monthly', t: '2026-03 Monthly Report-R0.pdf', kind: 'doc', outputs: ['dashboard', 'report', 'slide'], sync: 'Synced Apr 2', stale: { note: 'Revision R1 uploaded 3 hrs ago — not processed', queued: 'Queued for tonight’s 12:00 AM run' } }, { id: 's-w220', t: 'AWST_Weekly Report_220_29Mar2026.pdf', kind: 'doc', outputs: ['dashboard', 'report'], sync: 'Synced today 12:05 AM' }, { id: 's-daily', t: 'AWST Daily Shift Report 12010 — South.pdf', kind: 'doc', outputs: ['dashboard'], sync: 'Synced today 12:05 AM' }, ]; const LX_USED_BY = [ { t: 'MV-Monthly-Submission-Mar2026.pdf', kind: 'doc', outputs: ['dashboard', 'report'], state: 'ok', note: 'Refreshed from this record 2 hrs ago' }, { t: 'AWST-Q1-2026-Executive-Summary.html', kind: 'doc', outputs: ['report', 'slide'], state: 'stale', note: 'Not refreshed since this record last changed' }, { t: 'TAGP-GP-Meeting-14-Deck.pptx', kind: 'doc', outputs: ['slide'], state: 'ok', note: 'Refreshed Mar 30' }, ]; const LX_RUNS = [ { d: 'Today · 12:05 AM', result: 'partial', note: '11 fields applied · 3 held for review' }, { d: 'Apr 20 · 12:04 AM', result: 'ok', note: '9 fields updated' }, { d: 'Apr 19 · 12:03 AM', result: 'ok', note: 'No changes in sources' }, { d: 'Apr 18 · 12:05 AM', result: 'fail', note: 'Egnyte timeout — retried 6:00 AM ✓' }, { d: 'Apr 17 · 12:04 AM', result: 'ok', note: '12 fields updated' }, ]; /* Agent steps for the three display modes */ function lxSteps(s, pendingCount) { if (s.running) { const labels = [ { t: 'Check & fetch sources', d: 'Checking 7 files on Egnyte…' }, { t: 'Extract & compare data', d: 'Reading new versions…' }, { t: 'Update record', d: 'Applying changes…' }, ]; return labels.map((l, i) => ({ t: l.t, d: i === s.runStep ? l.d : (i < s.runStep ? 'Done' : 'Waiting…'), state: i < s.runStep ? 'ok' : (i === s.runStep ? 'run' : 'idle'), time: i <= s.runStep ? 'Now' : '', })); } if (s.ranNow) { return [ { t: 'Check & fetch sources', d: '7 files checked · 1 new version (Monthly R1)', state: 'ok', time: 'Just now' }, { t: 'Extract & compare data', d: '2 fields extracted from Monthly R1', state: 'ok', time: 'Just now' }, { t: 'Update record', d: pendingCount > 0 ? `2 fields applied · ${pendingCount} earlier changes still awaiting review` : '2 fields applied · no exceptions', state: pendingCount > 0 ? 'warn' : 'ok', time: 'Just now' }, ]; } return [ { t: 'Check & fetch sources', d: '7 files checked · 2 new versions found', state: 'ok', time: '12:02 AM' }, { t: 'Extract & compare data', d: '14 fields extracted & compared with record', state: 'ok', time: '12:04 AM' }, { t: 'Update record', d: '11 fields applied · 3 held for your review', state: 'warn', time: '12:05 AM' }, ]; } /* ===== Small bits ===== */ function LxDot({ state }) { return ; } function LxFileIcon({ kind }) { if (kind === 'sheet') { return ( ); } return ( ); } function LxStepMark({ state }) { if (state === 'ok') { return ( ); } if (state === 'warn') { return ( ); } if (state === 'run') { return ; } return ; } /* Per-source resolved status */ function lxSourceStatus(src, pendingBySrc, s) { const pend = pendingBySrc[src.t] || 0; if (pend > 0) return { state: 'conflict', label: `${pend} field ${pend === 1 ? 'change' : 'changes'} pending review`, review: pend }; if (src.stale && !s.ranNow) return { state: 'stale', label: src.stale.note, sub: src.stale.queued }; if (src.stale && s.ranNow) return { state: 'ok', label: 'Synced just now · R1 processed' }; return { state: 'ok', label: src.sync }; } /* ===== Shared: health strip ===== */ function LxHealth({ pendingCount, staleCount, s, onReview }) { const ok = pendingCount === 0 && staleCount === 0; if (ok) { return (