// 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 (
Pipeline healthy All sources synced · outputs up to date · last run {s.ranNow ? 'just now' : 'today 12:05 AM'}
); } const bits = []; if (pendingCount > 0) bits.push(`${pendingCount} field ${pendingCount === 1 ? 'change' : 'changes'} pending review`); if (staleCount > 0) bits.push(`${staleCount} source not processed`); return (
Needs attention {bits.join(' · ')}
{pendingCount > 0 && ( )}
); } /* ===== Shared: agent card (steps + run log) ===== */ function LxAgentCard({ s, pendingCount, defaultLogOpen = false }) { const [logOpen, setLogOpen] = useStateLX(defaultLogOpen); const steps = lxSteps(s, pendingCount); const runs = s.ranNow ? [{ d: 'Just now · manual run', result: 'ok', note: 'Monthly R1 processed · 2 fields updated' }].concat(LX_RUNS) : LX_RUNS; const headSub = s.running ? 'Running now…' : s.ranNow ? 'Last run just now · manual' : 'Last run today 12:05 AM · nightly schedule'; const chip = s.running ? { cls: 'run', label: 'Running' } : s.ranNow ? { cls: 'ok', label: 'Success' } : { cls: 'warn', label: 'Partial' }; return (
AI Agent — Source sync
{headSub}
{chip.label}
    {steps.map((st, i) => (
  1. {st.t}
    {st.d}
    {st.time}
  2. ))}
{logOpen && ( )}
); } /* ===== Shared: source / used-by rows ===== */ function LxSourceRow({ src, status, onReview }) { return (
  • {src.t}
    {status.label}
    {status.sub &&
    {status.sub}
    }
    {status.review ? ( ) : ( )}
  • ); } function LxUsedRow({ u }) { return (
  • {u.t}
    {u.note}
  • ); } /* ===== Shared: record node with output statuses ===== */ function LxRecordNode({ s, ctx, filterKey }) { const outputs = [ { key: 'dashboard', label: 'Dashboard', state: 'ok', note: s.ranNow ? 'Updated just now' : 'Updated today 12:05 AM' }, { key: 'report', label: 'Report', state: 'ok', note: s.ranNow ? 'Updated just now' : 'Updated today 12:05 AM' }, { key: 'slide', label: 'Slide', state: s.ranNow ? 'ok' : 'stale', note: s.ranNow ? 'Refreshed just now' : 'Not refreshed since Mar 28 · pending changes affect 1 stat' }, ].filter((o) => !filterKey || o.key === filterKey); return (
    Cedar Narrows WST — Project Overview
    This record · {filterKey ? '1 output shown' : '3 outputs'}
    ); } function LxConnector() { return ( ); } /* ========================================================================= VARIANT A — Pipeline (vertical flow) ========================================================================= */ function LxVariantPipeline({ s, ctx, filterKey, visibleSources, visibleUsedBy, pendingBySrc, pendingCount, openReview }) { return (
    Sources {visibleSources.length} files · via Egnyte sync
    Used by {visibleUsedBy.length} downstream records
      {visibleUsedBy.map((u) => )}
    ); } /* ========================================================================= MAIN PANEL ========================================================================= */ function RecordLineagePanel({ onClose }) { const ctx = useOutput(); const su = useSourceUpdates(); const s = useLxStore(); const output = (ctx && ctx.output) || 'dashboard'; const isEmpty = ctx && ctx.isEmpty; const [scope, setScope] = useStateLX(labelFromKey(output)); if (isEmpty) { return (
    No sources connected yet
    As soon as the agent extracts data from your sources, this panel becomes the control surface for the whole pipeline — sources, agent runs, and downstream records.
    ); } // Live pending counts from the Source Updates flow, grouped per file. const updates = (su && su.updates) || []; const pendingBySrc = {}; updates.forEach((u) => { if (u.status === 'pending') pendingBySrc[u.sourceFile] = (pendingBySrc[u.sourceFile] || 0) + 1; }); const pendingCount = (su && su.pendingCount) || 0; const staleCount = s.ranNow ? 0 : 1; const filterKey = keyFromLabel(scope); const visibleSources = filterKey ? LX_SOURCES.filter((x) => x.outputs.includes(filterKey)) : LX_SOURCES; const visibleUsedBy = filterKey ? LX_USED_BY.filter((x) => x.outputs.includes(filterKey)) : LX_USED_BY; const openReview = (file) => { onClose(); if (su) su.openPanel(typeof file === 'string' ? file : undefined); }; const editInstructions = () => { onClose(); if (ctx && ctx.onOpenAgentChat) ctx.onOpenAgentChat(); }; const toolbar = ( ); const footer = (
    Runs nightly at 12:00 AM e.preventDefault()}>Edit schedule
    ); const variantProps = { s, ctx, filterKey, visibleSources, visibleUsedBy, pendingBySrc, pendingCount, staleCount, openReview }; return (
    ); } /* ========================================================================= PROBLEM BANNER — shown above the record only when the pipeline needs attention (pending review / unprocessed sources). Quiet otherwise. ========================================================================= */ function LineageProblemBanner() { const ctx = useOutput(); const su = useSourceUpdates(); const s = useLxStore(); if (!ctx || !su) return null; // Only meaningful on the demo record that actually has tracked updates. const isDemo = (su.allCount || 0) > 0; const pendingCount = su.pendingCount || 0; const staleCount = isDemo && !s.ranNow ? 1 : 0; if (s.bannerDismissed || (pendingCount === 0 && staleCount === 0)) return null; const bits = []; if (pendingCount > 0) bits.push(`${pendingCount} field ${pendingCount === 1 ? 'change' : 'changes'} pending review`); if (staleCount > 0) bits.push('1 source not processed'); return (
    Data pipeline needs attention {bits.join(' · ')} Egnyte → AI Agent → This record · last run {s.ranNow ? 'just now' : 'today 12:05 AM'}
    {pendingCount > 0 && ( )}
    ); } Object.assign(window, { RecordLineagePanel, // overrides the legacy panel from record_panels.jsx LineageProblemBanner, });