// Dashboard — full Claude Design report viewer, wired to real job data

// Preview fallback helper — reads from window.FOSSYL_DEMO (set by graph.jsx).
// Hardcoded fallback matches graph.jsx RISK_CLUSTERS sums so both files stay in sync.
function D(key) {
  return (window.FOSSYL_DEMO || { total:145, ttw:125, diverged:8, synced:12, ttwPct:86, edges:481, elapsed:52, deathKb:187 })[key];
}

const DASH_API     = 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod';
const DASH_COG     = '7nffjheavp985k7b4bgug45jf4';

function dashToken() {
  const last = localStorage.getItem(`CognitoIdentityServiceProvider.${DASH_COG}.LastAuthUser`);
  if (last) { const t = localStorage.getItem(`CognitoIdentityServiceProvider.${DASH_COG}.${last}.idToken`); if (t) return t; }
  return window.getIdToken ? window.getIdToken() : '';
}
async function dashApi(path) {
  const res = await fetch(`${DASH_API}${path}`, { headers: { Authorization: `Bearer ${dashToken()}` } });
  const d = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(d.error || `HTTP ${res.status}`);
  return d;
}

function getActiveJobId() { return localStorage.getItem('fossyl_active_job') || ''; }

function Dashboard() {
  const { go }    = useNav();
  const { toast } = useToast();
  const [section,     setSection]     = useState('divergence');
  const [job,         setJob]         = useState(null);
  const [downloading, setDownloading] = useState(false);

  const jobId = getActiveJobId();

  // If no active job, send to archive
  useEffect(() => { if (!jobId) go('reports'); }, []);

  useEffect(() => {
    if (!jobId) return;
    dashApi(`/status/${jobId}`)
      .then(data => {
        setJob(data);
        if (window.validateRealStats) window.validateRealStats({
          ttw:      data.ttw_count      || 0,
          diverged: data.diverged_count || 0,
          synced:   data.synced_count   || 0,
          artifacts:data.artifact_count || 0,
        });
      })
      .catch(() => {});
  }, [jobId]);

  const handleDownload = async () => {
    setDownloading(true);
    try {
      const d = await dashApi(`/download/${jobId}`);
      window.location.href = d.download_url;
    } catch (e) { toast(e.message || 'Download failed', { tone: 'error' }); }
    finally { setDownloading(false); }
  };

  const handleShare = async () => {
    try {
      const d = await dashApi(`/download/${jobId}?view=1`);
      navigator.clipboard?.writeText(d.download_url).catch(() => {});
      toast('Link copied — valid 1 hour', { tone: 'success' });
    } catch (e) { toast(e.message || 'Failed', { tone: 'error' }); }
  };

  // Real stats from DynamoDB job record
  const realStats = job ? {
    job_id:        job.job_id || jobId,
    ttw:           job.ttw_count || 0,
    artifacts:     job.artifact_count || 0,
    edges:         job.edge_count || 0,
    nodes:         job.node_count || 0,
    diverged:      job.diverged_count || 0,
    synced:        job.synced_count || 0,
    elapsed:       job.elapsed_s || 0,
    size_kb:       job.report_size_kb || 0,
    completed_at:  job.completed_at || '',
  } : null;

  const scanLabel  = realStats ? realStats.job_id.slice(0, 8) : 'F-8a3f';
  const ttwPct     = realStats
    ? Math.min(100, Math.round(((realStats.ttw || 0) / Math.max((realStats.ttw || 0) + (realStats.diverged || 0) + (realStats.synced || 0), 1)) * 100))
    : D('ttwPct');
  // totalZodb = all ZODB objects (TTW-only + diverged + synchronized).
  // Distinct from realStats.artifacts (filesystem artifacts in the dep graph).
  const totalZodb  = realStats
    ? (realStats.ttw || 0) + (realStats.diverged || 0) + (realStats.synced || 0)
    : D('ttw') + D('diverged') + D('synced');
  const scanDate   = realStats?.completed_at ? new Date(realStats.completed_at).toISOString().slice(0, 10) : '—';

  return (
    <div style={{ minHeight: '100vh', display: 'grid', gridTemplateColumns: '240px 1fr', background: 'var(--bg)' }}>
      <DashSidebar section={section} setSection={setSection} go={go}
        scanLabel={scanLabel} ttwPct={ttwPct} zodbCount={totalZodb}/>
      <div style={{ overflow: 'auto' }}>
        <DashTopBar
          scanLabel={scanLabel}
          go={go}
          toast={toast}
          onDownload={handleDownload}
          onShare={handleShare}
          downloading={downloading}
          hasJob={!!jobId}
        />
        <DashContent section={section} realStats={realStats} go={go} toast={toast} jobId={jobId}/>
      </div>
    </div>
  );
}

function DashSidebar({ section, setSection, go, scanLabel, ttwPct, zodbCount }) {
  const sections = [
    { id: 'overview',  num: '§0', label: 'Overview' },
    { id: 'divergence',num: '§1', label: 'Divergence', badge: `${ttwPct}%` },
    { id: 'graph',     num: '§2', label: 'Dependency graph' },
    { id: 'coupling',  num: '§3', label: 'Coupling / risk' },
    { id: 'versions',  num: '§4', label: 'Version groups' },
    { id: 'dead',      num: '§5', label: 'Dead code' },
    { id: 'ai',        num: '§6', label: 'Field explanations' },
    { id: 'source',    num: '§7', label: 'Source browser' },
    { id: 'schema',    num: '§8', label: 'Schema map' },
  ];
  return (
    <aside style={{ borderRight: '1px solid var(--line)', background: 'var(--bg-1)', padding: '18px 0', display: 'flex', flexDirection: 'column', gap: 16, position: 'sticky', top: 0, height: '100vh', overflowY: 'auto' }}>
      <div style={{ padding: '0 20px 16px', borderBottom: '1px solid var(--line)' }}>
        <a onClick={() => go('landing')} style={{ cursor: 'pointer' }}><Logo size={20}/></a>
      </div>
      <div style={{ padding: '0 20px' }}>
        <span className="caps" style={{ display: 'block', marginBottom: 10 }}>Current dig</span>
        <div style={{ border: '1px solid var(--line)', padding: '10px 12px', borderRadius: 2, background: 'var(--bg)' }}>
          <div className="mono" style={{ fontSize: 10.5, color: 'var(--dim)' }}>scan_id {scanLabel}</div>
          <div className="serif" style={{ fontSize: 15, marginTop: 2, letterSpacing: '-0.01em' }}>Excavation {scanLabel}</div>
          <div className="mono" style={{ fontSize: 10.5, color: 'var(--muted)', marginTop: 4 }}>{zodbCount} ZODB objects · {ttwPct}% untracked</div>
        </div>
      </div>
      <nav style={{ padding: '0 8px', display: 'flex', flexDirection: 'column', gap: 1, flex: 1 }}>
        <span className="caps" style={{ padding: '12px 12px 6px' }}>Report</span>
        {sections.map((s) => (
          <button key={s.id} onClick={() => setSection(s.id)} style={{
            background: section === s.id ? 'var(--bg-2)' : 'transparent',
            border: 0, textAlign: 'left', padding: '8px 12px',
            color: section === s.id ? 'var(--ink)' : 'var(--ink-dim)',
            fontFamily: 'var(--sans)', fontSize: 13,
            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
            cursor: 'pointer',
            borderLeft: section === s.id ? '1px solid var(--bone)' : '1px solid transparent',
            letterSpacing: '-0.005em',
          }}>
            <span><span className="mono" style={{ color: 'var(--dim)', fontSize: 11, marginRight: 10 }}>{s.num}</span>{s.label}</span>
            {s.badge && <span className="badge risk" style={{ fontSize: 9.5 }}>{s.badge}</span>}
          </button>
        ))}
        <div style={{ borderTop: '1px solid var(--line)', margin: '12px 8px 8px' }}/>
        <button onClick={() => go('reports')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
          <span className="mono" style={{ color: 'var(--dim)', fontSize: 11, marginRight: 10 }}>←</span>All excavations
        </button>
        <button onClick={() => go('upload')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
          <span className="mono" style={{ color: 'var(--dim)', fontSize: 11, marginRight: 10 }}>§X</span>New excavation
        </button>
        <button onClick={() => go('billing')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
          <span className="mono" style={{ color: 'var(--dim)', fontSize: 11, marginRight: 10 }}>§B</span>Billing
        </button>
        <button onClick={() => go('landing')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
          <span className="mono" style={{ color: 'var(--dim)', fontSize: 11, marginRight: 10 }}>↩</span>Back to landing
        </button>
      </nav>
      <div style={{ padding: '16px 20px', borderTop: '1px solid var(--line)' }}>
        <div className="row g-8 ai-c" style={{ cursor: 'pointer' }} onClick={() => go('account')}>
          <div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--bg-2)', border: '1px solid var(--line-2)', display: 'grid', placeItems: 'center', fontFamily: 'var(--serif)', fontSize: 13, color: 'var(--bone)' }}>EK</div>
          <div className="col" style={{ flex: 1, minWidth: 0 }}>
            <span style={{ fontSize: 12, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>E. Kirsten</span>
            <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>Owner · settings →</span>
          </div>
        </div>
      </div>
    </aside>
  );
}

function DashTopBar({ scanLabel, go, toast, onDownload, onShare, downloading, hasJob }) {
  const { signOut } = useAuth();
  return (
    <div style={{ borderBottom: '1px solid var(--line)', padding: '12px 32px', background: 'var(--bg)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', position: 'sticky', top: 0, zIndex: 10 }}>
      <div className="row g-8 ai-c" style={{ fontSize: 12, color: 'var(--muted)', fontFamily: 'var(--mono)' }}>
        <span style={{ cursor: 'pointer', color: 'var(--ink-dim)' }} onClick={() => go('reports')}>excavations</span>
        <span className="dim">/</span>
        <span className="ink">{scanLabel}</span>
      </div>
      <div className="row g-8 ai-c">
        {hasJob && (
          <>
            <button className="btn btn-sm" onClick={onShare}>Copy link</button>
            <button className="btn btn-sm" onClick={() => toast('PDF: download the HTML then File → Print → Save as PDF', { duration: 4000 })}>Export PDF</button>
            <button className="btn btn-sm btn-primary" onClick={onDownload} disabled={downloading}>
              {downloading ? 'Generating…' : 'Download HTML →'}
            </button>
            <span className="dim" style={{ margin: '0 4px' }}>·</span>
          </>
        )}
        <button className="btn btn-sm" onClick={() => go('upload')}>New scan</button>
        <span className="dim" style={{ margin: '0 4px' }}>·</span>
        <a className="mono" style={{ fontSize: 11, color: 'var(--ink-dim)', cursor: 'pointer' }} onClick={() => go('account')}>Account</a>
        <a className="mono" style={{ fontSize: 11, color: 'var(--ink-dim)', cursor: 'pointer' }} onClick={() => { signOut(); go('landing'); }}>Sign out</a>
      </div>
    </div>
  );
}

function DashContent({ section, realStats, go, toast, jobId }) {
  return (
    <div style={{ padding: '32px' }}>
      {section === 'overview'   && <Overview   realStats={realStats}/>}
      {section === 'divergence' && <Divergence realStats={realStats}/>}
      {section === 'graph'      && <GraphSection realStats={realStats}/>}
      {section === 'coupling'   && <Coupling/>}
      {section === 'versions'   && <Versions/>}
      {section === 'dead'       && <DeadCode/>}
      {section === 'ai'         && <AIExplanations jobId={jobId}/>}
      {section === 'source'     && <SourceBrowser jobId={jobId} go={go} toast={toast}/>}
      {section === 'schema'     && <SchemaSection realStats={realStats}/>}
      <DemoDataNote/>
    </div>
  );
}

function DemoDataNote() {
  return (
    <FieldNote index={null}>
      <span style={{ color: 'var(--dim)', fontSize: 11 }}>
        Detailed section data (coupling tables, version groups, source listing) shown as design reference.
        The full interactive report with all real data is available via <span style={{ color: 'var(--bone)' }}>Download HTML →</span>
      </span>
    </FieldNote>
  );
}

// ── §0 Overview ───────────────────────────────────────────────────────────────
function Overview({ realStats: r }) {
  const ttw        = r?.ttw       || D('ttw');
  const fsArtifacts= r?.artifacts || D('total');
  const edges      = r?.edges     || D('edges');
  const diverged   = r?.diverged  || D('diverged');
  const synced     = r?.synced    || D('synced');
  const elapsed    = r?.elapsed   || D('elapsed');
  const scanLabel  = r ? r.job_id.slice(0, 8) : 'F-8a3f';
  // ZODB objects = all objects in the live database (TTW-only + diverged + synchronized).
  // Distinct from fsArtifacts (scripts found in the filesystem/ZIP).
  const totalZodb  = ttw + diverged + synced;

  return (
    <div>
      <SectionHeader
        number="00"
        eyebrow="Scan overview"
        title={`Excavation ${scanLabel}.`}
        kicker={`${elapsed}s end-to-end. ${fsArtifacts} FS artifacts in dependency graph. ${totalZodb} ZODB objects reconciled — ${ttw} (${Math.round(ttw/Math.max(totalZodb,1)*100)}%) have no version-control equivalent.`}
      />
      <div className="row" style={{ margin: '20px 0', border: '1px solid var(--line)' }}>
        <Stat label="ZODB objects"  value={totalZodb.toLocaleString()}/>
        <Stat label="TTW-only"      value={ttw.toLocaleString()}        tone="rust"/>
        <Stat label="Diverged"      value={diverged.toLocaleString()}/>
        <Stat label="Synchronized"  value={synced.toLocaleString()}     tone="lichen"/>
        <Stat label="FS artifacts"  value={fsArtifacts.toLocaleString()}/>
        <Stat label="Scan time"     value={elapsed}                     unit="s"/>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: 24 }}>
        <div className="panel">
          <div className="panel-h"><span className="caps">Fig. 0.1 — Section index</span></div>
          <div className="panel-b" style={{ padding: 0 }}>
            {[
              ['§1', 'Divergence',       `${ttw} of ${totalZodb} ZODB objects untracked`, 'rust'],
              ['§2', 'Dependency graph', `${edges} edges · ${fsArtifacts} FS artifacts`, 'slate'],
              ['§3', 'Coupling / risk',  'High-in-degree artifacts', 'bone'],
              ['§4', 'Version groups',   'Duplicate script clusters', 'slate'],
              ['§5', 'Dead code',        'Zero-caller candidates', 'lichen'],
              ['§6', 'AI explanations',  'Plain-English field notes', 'slate'],
              ['§7', 'Source browser',   'Full artifact index', 'slate'],
              ['§8', 'Schema map',       'MySQL cross-reference', 'slate'],
            ].map((row) => (
              <div key={row[0]} style={{ padding: '14px 20px', borderTop: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 16 }}>
                <span className="mono" style={{ color: 'var(--dim)', fontSize: 11, minWidth: 28 }}>{row[0]}</span>
                <span className="serif" style={{ fontSize: 17, flex: 1 }}>{row[1]}</span>
                <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{row[2]}</span>
                <span className={`dot ${row[3]}`}/>
              </div>
            ))}
          </div>
        </div>
        <div className="col g-24">
          <div className="panel">
            <div className="panel-h"><span className="caps">Fig. 0.2 — Scan pipeline</span></div>
            <div className="panel-b">
              {[
                ['Ingest ZIP',         null],
                ['Parse ZODB',         null],
                ['Resolve traversal',  null],
                ['Build dep graph',    null],
                ['Coupling analysis',  null],
                ['AI field notes',     null],
                ['Emit HTML',          null],
              ].map((row, i) => (
                <div key={i} className="row jc-b" style={{ padding: '6px 0', borderBottom: i < 6 ? '1px solid var(--line)' : 'none' }}>
                  <span className="mono" style={{ fontSize: 11.5, color: 'var(--ink-dim)' }}>{i + 1}. {row[0]}</span>
                  <span className="mono" style={{ fontSize: 11.5, color: 'var(--lichen)' }}>✓</span>
                </div>
              ))}
            </div>
          </div>
          <FieldNote index={1}>
            {ttw} scripts run in live production but exist only in the ZODB — not in version control. In event of hardware loss, these artifacts are permanently unrecoverable.
          </FieldNote>
        </div>
      </div>
    </div>
  );
}

// §01 TTW table — exposed as window.FOSSYL_TTW_TABLE for validateDemoFixture inDeg cross-check.
// Columns: [id, path, type, lastEdit, callers, risk]
const TTW_TABLE = [
  ['F-0418', '/portal_skins/custom/admitPatient',    'Python Script',   '2014-11-22', 41, 100],
  ['F-0371', '/portal_skins/custom/claim_to_ledger', 'Python Script',   '2012-02-17', 38,  89],
  ['F-0329', '/portal_skins/custom/hl7_in',          'Python Script',   '2013-12-01', 27,  64],
  ['F-0427', '/portal_skins/custom/sendReferralFax', 'Python Script',   '2011-03-08', 23,  52],
  ['F-0312', '/portal_skins/custom/discharge_pt',    'Page Template',   '2012-08-19', 18,  44],
  ['F-0392', '/portal_skins/custom/ward_roster_pt',  'Page Template',   '2013-06-04', 14,  35],
  ['F-0355', '/portal_skins/custom/printLabel',      'External Method', '2010-09-30', 12,  28],
  ['F-0341', '/portal_skins/custom/mergePatient',    'Python Script',   '2015-04-12',  9,  23],
];
window.FOSSYL_TTW_TABLE = TTW_TABLE;

// ── §1 Divergence ─────────────────────────────────────────────────────────────
function Divergence({ realStats: r }) {
  const ttw      = r?.ttw      || D('ttw');
  const diverged = r?.diverged || D('diverged');
  const synced   = r?.synced   || D('synced');
  const ttwPct   = Math.min(100, Math.round((ttw / Math.max(ttw + diverged + synced, 1)) * 100)) || D('ttwPct');
  return (
    <div>
      <SectionHeader
        number="01"
        eyebrow="Filesystem ↔ live database"
        title={`${ttwPct}% of what runs is untracked.`}
        kicker="Every ZODB object reconciled against the filesystem. Items below exist in the live server with no corresponding file in version control. In event of hardware loss these artifacts are unrecoverable."
        right={
          <div className="col g-8 ai-e">
            <div className="row g-8">
              <button className="btn btn-sm">Filter</button>
              <button className="btn btn-sm">Export CSV</button>
            </div>
          </div>
        }
      />

      <div className="row" style={{ margin: '20px 0 32px', border: '1px solid var(--line)' }}>
        <Stat label="TTW-only"          value={ttw.toLocaleString()}      tone="rust"/>
        <Stat label="Diverged"          value={diverged.toLocaleString()}/>
        <Stat label="Synchronized"      value={synced.toLocaleString()}   tone="lichen"/>
        <Stat label="Total ZODB objects" value={(ttw+diverged+synced).toLocaleString()}/>
      </div>

      <div className="panel">
        <div className="panel-h">
          <span className="caps">Fig. 1.1 — Stratigraphic reconciliation</span>
          <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>per-folder breakdown · top 8 shown · demo data</span>
        </div>
        <div className="panel-b" style={{ padding: 0 }}>
          <div style={{ padding: '12px 16px' }}><Ticks count={50} height={10}/></div>
          {[
            { p: '/Plone/portal_skins/custom', fs: 122, zodb: 487, ttw: 412 },
            { p: '/Plone/patient_portal',      fs: 304, zodb: 288, ttw: 41 },
            { p: '/Plone/admission',           fs: 88,  zodb: 124, ttw: 68 },
            { p: '/Plone/billing',             fs: 140, zodb: 206, ttw: 91 },
            { p: '/Plone/lab_results',         fs: 67,  zodb: 77,  ttw: 22 },
            { p: '/Plone/pharmacy',            fs: 54,  zodb: 89,  ttw: 38 },
            { p: '/Plone/portal_workflow',     fs: 44,  zodb: 51,  ttw: 9 },
            { p: '/Plone/acl_users',           fs: 28,  zodb: 34,  ttw: 12 },
          ].map((row) => <DivRow key={row.p} {...row}/>)}
        </div>
      </div>

      <div className="panel" style={{ marginTop: 24 }}>
        <div className="panel-h">
          <span className="caps">Fig. 1.2 — TTW-only artifacts, highest coupling first</span>
          <div className="row g-8 ai-c">
            <span className="badge risk"><span className="dot risk"/>TTW · no VCS backup</span>
            <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>showing 8 of {ttw} · demo data</span>
          </div>
        </div>
        <table className="table">
          <thead><tr><th style={{ width: 60 }}>ID</th><th>Path</th><th>Type</th><th>Last edit</th><th>Callers</th><th>Risk</th><th style={{ width: 60 }}/></tr></thead>
          <tbody>
            {TTW_TABLE.map((r) => (
              <tr key={r[0]}>
                <td className="mono" style={{ color: 'var(--dim)' }}>{r[0]}</td>
                <td className="mono" style={{ color: 'var(--ink)' }}>{r[1]}</td>
                <td>{r[2]}</td>
                <td style={{ color: 'var(--muted)' }}>{r[3]}</td>
                <td style={{ color: 'var(--ink-dim)', textAlign: 'right' }}>{r[4]}</td>
                <td><RiskBar value={r[5]}/></td>
                <td><a className="bone mono" style={{ fontSize: 11, cursor: 'pointer' }}>open →</a></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function DivRow({ p, fs, zodb, ttw }) {
  const matched = Math.min(fs, zodb - ttw);
  const orphanFs = Math.max(0, fs - matched);
  const total2 = matched + orphanFs + ttw;
  const pct = (n) => (n / total2) * 100;
  return (
    <div style={{ padding: '10px 16px', borderTop: '1px solid var(--line)', display: 'grid', gridTemplateColumns: '280px 1fr 160px', gap: 16, alignItems: 'center' }}>
      <div className="mono" style={{ fontSize: 12, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p}</div>
      <div className="strata" style={{ height: 10 }}>
        <span style={{ width: pct(matched) + '%', background: 'var(--lichen)' }}/>
        <span style={{ width: pct(orphanFs) + '%', background: 'var(--slate-dim)' }}/>
        <span style={{ width: pct(ttw) + '%', background: 'var(--rust)' }}/>
      </div>
      <div className="row g-12 jc-e mono" style={{ fontSize: 11, color: 'var(--muted)' }}>
        <span><span style={{ color: 'var(--lichen)' }}>●</span> {matched}</span>
        <span><span style={{ color: 'var(--slate)' }}>●</span> {orphanFs}</span>
        <span><span style={{ color: 'var(--rust)' }}>●</span> {ttw}</span>
      </div>
    </div>
  );
}

// ── §2 Dependency graph ───────────────────────────────────────────────────────
function GraphSection({ realStats: r }) {
  const fsArtifacts   = r?.artifacts || D('total');
  const ttw           = r?.ttw       || D('ttw');
  const edges         = r?.edges     || D('edges');
  const nodes         = r?.nodes     || D('total');
  const diverged      = r?.diverged  || D('diverged');
  const synced        = r?.synced    || D('synced');
  const totalZodb     = ttw + diverged + synced;
  const highlyCoupled = (window.COUPLING_TOP || []).filter(c => c.inDeg >= 5).length || 15;
  return (
    <div>
      <SectionHeader
        number="02"
        eyebrow="Dependency map"
        title="What's at risk, and what breaks if you touch it."
        kicker={`Two views designed for non-technical decision-makers. (A) every artifact cluster scored by how much of it lives only in the database. (B) the ${window.COUPLING_TOP?.length || 15} artifacts that the rest of the codebase leans on hardest.`}
      />
      <div className="row" style={{ margin: '20px 0', border: '1px solid var(--line)' }}>
        <Stat label="ZODB objects"   value={totalZodb.toLocaleString()}/>
        <Stat label="Edges"          value={edges.toLocaleString()} unit="ZODB graph"/>
        <Stat label="TTW-only"       value={`${ttw} (${Math.round(ttw/Math.max(totalZodb,1)*100)}%)`} tone="rust"/>
        <Stat label="FS artifacts"   value={fsArtifacts.toLocaleString()}/>
        <Stat label="Highly-coupled" value={highlyCoupled} unit="in-deg ≥ 5"/>
      </div>
      <RiskStratigraphy/>
      <details style={{ marginTop: 24, border: '1px solid var(--line)', background: 'var(--bg-1)' }}>
        <summary style={{ listStyle: 'none', cursor: 'pointer', padding: '14px 20px', borderBottom: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <div className="row g-10 ai-c">
            <span className="mono" style={{ fontSize: 11, color: 'var(--dim)' }}>▸</span>
            <span className="caps">Fig. 2.2 — Raw dependency graph (technical detail)</span>
          </div>
          <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>{nodes} nodes · {edges} edges · click to expand</span>
        </summary>
        <div style={{ padding: '14px 20px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>Illustrative graph — download the HTML report for the real interactive graph.</span>
          <div className="row g-12 ai-c">
            <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>layout</span>
            <div className="row g-0" style={{ border: '1px solid var(--line-2)', borderRadius: 2, overflow: 'hidden' }}>
              <GraphToggle val="force">Force</GraphToggle>
              <GraphToggle val="radial">Radial</GraphToggle>
              <GraphToggle val="matrix">Matrix</GraphToggle>
            </div>
          </div>
        </div>
        <div style={{ padding: 12, background: 'var(--bg-1)' }}>
          <GraphSurface/>
        </div>
      </details>
    </div>
  );
}

function GraphToggle({ val, children }) {
  const [current, setCurrent] = useState(window.__graph || 'force');
  useEffect(() => {
    const onChange = () => setCurrent(window.__graph || 'force');
    window.addEventListener('graph-change', onChange);
    return () => window.removeEventListener('graph-change', onChange);
  }, []);
  const active = current === val;
  return (
    <button onClick={() => { window.__graph = val; window.dispatchEvent(new Event('graph-change')); }} style={{
      padding: '5px 12px', background: active ? 'var(--bg-2)' : 'transparent',
      border: 0, borderRight: '1px solid var(--line-2)',
      color: active ? 'var(--ink)' : 'var(--muted)',
      fontFamily: 'var(--mono)', fontSize: 11, cursor: 'pointer',
    }}>{children}</button>
  );
}

function GraphSurface() {
  const [current, setCurrent] = useState(window.__graph || 'force');
  useEffect(() => {
    const onChange = () => setCurrent(window.__graph || 'force');
    window.addEventListener('graph-change', onChange);
    return () => window.removeEventListener('graph-change', onChange);
  }, []);
  return <DependencyGraph style={current} width={1100} height={560}/>;
}

// ── §3 Coupling ───────────────────────────────────────────────────────────────
// risk = W.in·in + W.ch·churn + W.ttw·ttw (ttw=1 for through-the-web only), normalized 0–100.
// Rows sorted descending by raw score. Weights exposed for validateDemoFixture cross-check.
(function buildCouplingRows() {
  const W = { in: 0.6, ch: 0.2, ttw: 0.2 };
  window.FOSSYL_RISK_WEIGHTS = W;
  // Formula label generated from W so validator can assert label === constants
  window.FOSSYL_RISK_FORMULA = `risk = ${W.in}·in + ${W.ch}·churn + ${W.ttw}·ttw, norm. to 100`;
  const raw = [
    { id: 'F-0418', n: 'admitPatient',    in: 41, out: 14, ch: 18, ttw: 1 },
    { id: 'F-0371', n: 'claim_to_ledger', in: 38, out: 22, ch: 12, ttw: 1 },
    { id: 'F-0285', n: 'triage_rules',    in: 29, out: 11, ch: 14, ttw: 0 },
    { id: 'F-0329', n: 'hl7_in',          in: 27, out: 16, ch:  9, ttw: 1 },
    { id: 'F-0427', n: 'sendReferralFax', in: 23, out:  8, ch:  4, ttw: 1 },
    { id: 'F-0312', n: 'discharge_pt',    in: 18, out:  9, ch:  8, ttw: 1 },
    { id: 'F-0392', n: 'ward_roster_pt',  in: 14, out:  7, ch:  6, ttw: 1 },
    { id: 'F-0341', n: 'mergePatient',    in:  9, out: 13, ch:  5, ttw: 1 },
  ];
  const score = r => W.in*r.in + W.ch*r.ch + W.ttw*r.ttw;
  const maxRaw = Math.max(...raw.map(score));
  const rows = raw
    .map(r => ({ ...r, risk: Math.round(score(r) / maxRaw * 100) }))
    .sort((a, b) => score(b) - score(a));
  window.FOSSYL_COUPLING_ROWS = rows;
})();

function Coupling() {
  const files = window.FOSSYL_COUPLING_ROWS || [];
  const maxIn = files.length ? files.reduce((m, f) => Math.max(m, f.in), 0) : 1;
  return (
    <div>
      <SectionHeader number="03" eyebrow="Coupling analysis" title="Touch these and everything moves."
        kicker="Files ranked by weighted in-degree × churn. Items above the rust line are unsafe to modify without a full regression pass."/>
      <div className="panel" style={{ marginTop: 24 }}>
        <div className="panel-h">
          <span className="caps">Fig. 3.1 — Top coupling risk · demo data</span>
          <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{window.FOSSYL_RISK_FORMULA || 'risk = 0.6·in + 0.2·churn + 0.2·ttw, norm. to 100'}</span>
        </div>
        <table className="table">
          <thead><tr><th style={{ width: 60 }}>ID</th><th>Artifact</th><th>In-deg</th><th>Churn</th><th>TTW</th><th>Risk</th><th style={{ width: 160 }} title="bar proportional to in-degree relative to highest-coupled artifact">In-deg bar</th></tr></thead>
          <tbody>{files.map((f) => (
            <tr key={f.id}>
              <td className="mono" style={{ color: 'var(--dim)' }}>{f.id}</td>
              <td className="mono" style={{ color: 'var(--ink)' }}>{f.n}</td>
              <td className="text-right">{f.in}</td>
              <td className="text-right">{f.ch}</td>
              <td className="text-right mono" style={{ color: f.ttw ? 'var(--rust)' : 'var(--dim)' }}>{f.ttw ? '✓' : '—'}</td>
              <td><RiskBar value={f.risk}/></td>
              <td><div style={{ height: 6, background: 'var(--bg-2)', borderRadius: 1, overflow: 'hidden' }}><div style={{ width: (f.in / maxIn) * 100 + '%', height: '100%', background: 'var(--rust)' }}/></div></td>
            </tr>
          ))}</tbody>
        </table>
      </div>
    </div>
  );
}

// ── §4 Versions ───────────────────────────────────────────────────────────────
// sims: per-copy similarity, all in (0.86, 0.999] (threshold ≥ 0.86, never ≥ 1.0).
// Header range derived from actual min/max of each group's sims.
// Exposed as window.FOSSYL_VERSION_GROUPS for validateDemoFixture.
const VERSION_GROUPS = [
  { id: 'V-01', n: 'send_mail',      copies: 5,
    paths: ['portal_skins/custom/sendMail', 'billing/sendMail_v2', 'admission/sendMail', 'pharmacy/sendMail_legacy', 'shared/mailer_patch'],
    sims:  [0.97, 0.94, 0.91, 0.88, 0.86] },
  { id: 'V-02', n: 'patient_lookup', copies: 4,
    paths: ['patient_portal/lookup', 'admission/lookup_v2', 'lab_results/lookup', 'pharmacy/lookup'],
    sims:  [0.96, 0.92, 0.89, 0.87] },
  { id: 'V-03', n: 'format_date',    copies: 7,
    paths: ['utils/format_date', 'patient_portal/fmt', 'billing/fmt_date', 'lab_results/dateFmt', 'admission/dateFmt', 'discharge/dateFmt', 'archive/fmt_date'],
    sims:  [0.99, 0.96, 0.93, 0.91, 0.89, 0.87, 0.86] },
];
window.FOSSYL_VERSION_GROUPS = VERSION_GROUPS;

function Versions() {
  return (
    <div>
      <SectionHeader number="04" eyebrow="Version-group detection" title="Clusters of duplicate logic."
        kicker="Each cluster contains 2+ files whose source is substantially identical (shingle similarity ≥ 0.86). Demo data — full list in downloaded HTML."/>
      <div className="col g-16" style={{ marginTop: 24 }}>
        {VERSION_GROUPS.map((g) => {
          const minSim = Math.min(...g.sims).toFixed(2);
          const maxSim = Math.max(...g.sims).toFixed(2);
          return (
            <div key={g.id} className="panel">
              <div className="panel-h">
                <div className="row g-12 ai-c">
                  <span className="mono" style={{ color: 'var(--dim)', fontSize: 11 }}>{g.id}</span>
                  <span className="serif" style={{ fontSize: 17 }}>{g.n}</span>
                  <span className="badge warn">{g.copies} copies</span>
                </div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>similarity {minSim}–{maxSim}</span>
              </div>
              <div className="panel-b" style={{ padding: 0 }}>
                {g.paths.map((p, i) => (
                  <div key={i} className="row jc-b" style={{ padding: '10px 16px', borderTop: i > 0 ? '1px solid var(--line)' : 'none' }}>
                    <div className="row g-12 ai-c">
                      <span className="mono" style={{ fontSize: 11, color: 'var(--dim)' }}>copy_{i + 1}</span>
                      <span className="mono" style={{ fontSize: 12, color: 'var(--ink)' }}>/{p}</span>
                    </div>
                    <div className="row g-8 ai-c">
                      <div className="strata" style={{ width: 120 }}><span style={{ width: (g.sims[i] * 100).toFixed(0) + '%', background: 'var(--bone)' }}/></div>
                      <span className="mono" style={{ fontSize: 11, color: 'var(--muted)', minWidth: 44, textAlign: 'right' }}>{g.sims[i].toFixed(2)}</span>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ── §5 Dead code ──────────────────────────────────────────────────────────────
const DEAD_CANDIDATES = [
  { id: 'F-1201', path: '/portal_skins/custom/old_admit_v1',  type: 'Python Script',   last: '2009-04-18', kb: 12.4 },
  { id: 'F-1177', path: '/portal_skins/custom/print_v1',      type: 'Python Script',   last: '2010-02-04', kb:  8.1 },
  { id: 'F-1102', path: '/portal_skins/custom/legacy_fax',    type: 'External Method', last: '2011-08-22', kb:  5.7 },
  { id: 'F-1044', path: '/portal_skins/custom/test_harness',  type: 'Python Script',   last: '2012-01-11', kb: 11.2 },
  { id: 'F-0988', path: '/portal_skins/custom/old_ward_view', type: 'Page Template',   last: '2012-06-17', kb:  9.8 },
];

function DeadCode() {
  const totalKb  = D('deathKb');      // ~187 KB estimated for all 23 candidates
  const totalMb  = (totalKb / 1024).toFixed(2);
  const candCount = 23;
  return (
    <div>
      <SectionHeader number="05" eyebrow="Dead-code candidates" title="Artifacts with zero callers."
        kicker="These artifacts are referenced by no other object in the graph. Safe-to-delete candidates, pending manual review. Demo data — full list in downloaded HTML."/>
      <div className="row" style={{ margin: '20px 0', border: '1px solid var(--line)' }}>
        <Stat label="Candidates"       value={candCount}/>
        <Stat label="Zero callers"     value={candCount}/>
        <Stat label="Estimated reclaim" value={`~${totalKb}`} unit="KB · est. all candidates"/>
      </div>
      <div className="panel">
        <div className="panel-h"><span className="caps">Fig. 5.1 — Candidates · demo data</span><span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>showing {DEAD_CANDIDATES.length} of {candCount}</span></div>
        <table className="table">
          <thead><tr><th>ID</th><th>Path</th><th>Type</th><th>Last edit</th><th style={{ textAlign: 'right' }}>Size</th><th>Callers</th></tr></thead>
          <tbody>
            {DEAD_CANDIDATES.map((r) => (
              <tr key={r.id}>
                <td className="mono" style={{ color: 'var(--dim)' }}>{r.id}</td>
                <td className="mono" style={{ color: 'var(--ink)' }}>{r.path}</td>
                <td>{r.type}</td>
                <td style={{ color: 'var(--muted)' }}>{r.last}</td>
                <td className="mono text-right" style={{ color: 'var(--muted)' }}>{r.kb} KB</td>
                <td className="text-right lichen">0</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ── §6 AI explanations ────────────────────────────────────────────────────────
function AIExplanations({ jobId }) {
  const { toast } = useToast();
  const [items,   setItems]   = useState(null);  // null = loading, [] = no data
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!jobId) { setLoading(false); setItems([]); return; }
    dashApi(`/download/${jobId}?ai=1`)
      .then(d => {
        // Guard: Lambda returns {"ai_url":...} for ?ai=1. If ai_url is absent
        // (e.g. Lambda returned HTML download_url instead), fetch(undefined) would
        // silently resolve to fetch("undefined") → getfossyl.dev/undefined 404.
        if (!d.ai_url) throw new Error('No AI URL returned — scan may not have AI enabled');
        return fetch(d.ai_url);
      })
      .then(r => {
        if (!r.ok) throw new Error(`AI JSON fetch failed: ${r.status}`);
        return r.json();
      })
      .then(data => { setItems(data.explanations || []); setLoading(false); })
      .catch(() => { setItems([]); setLoading(false); });
  }, [jobId]);

  return (
    <div>
      {/* FOSSYL_AI_KICKER_COPY — payload description comes from window.FOSSYL_SIGNATURES_PAYLOAD_DESC (app.jsx).
           Copy-drift validator in graph.jsx asserts both strings reference the same field-type tokens. */}
      {(window.FOSSYL_AI_KICKER_COPY = "Signatures only — " + (window.FOSSYL_SIGNATURES_PAYLOAD_DESC||"") + " — are sent to Anthropic's external API at api.anthropic.com (claude-sonnet-4-6). Implementation bodies are never transmitted. Fossyl retains nothing after report generation. Tone is deliberately dry; verify before acting.") && null}
      <SectionHeader number="06" eyebrow="AI field explanations" title="Plain-English notes on critical artifacts."
        kicker={window.FOSSYL_AI_KICKER_COPY}/>

      {loading && (
        <div style={{ marginTop: 32, padding: 32, textAlign: 'center', border: '1px solid var(--line)', background: 'var(--bg-1)' }}>
          <div className="mono" style={{ fontSize: 12, color: 'var(--muted)' }}>Loading field notes…</div>
        </div>
      )}

      {!loading && items && items.length > 0 && (
        <div className="col g-16" style={{ marginTop: 24 }}>
          {items.map((item, i) => (
            <div key={i} className="panel">
              <div className="panel-h">
                <div className="row g-12 ai-c">
                  {item.path && <span className="mono" style={{ color: 'var(--dim)', fontSize: 11 }}>{item.path.split('/').pop()}</span>}
                  <span className="serif" style={{ fontSize: 16 }}>
                    {item.name || '—'}{item.artifact_type ? ` — ${item.artifact_type}` : ''}
                  </span>
                </div>
                {item.risk > 0 && <span className={`badge ${item.risk > 70 ? 'risk' : item.risk > 40 ? 'warn' : 'safe'}`}>risk {item.risk}</span>}
              </div>
              <div className="panel-b col g-12" style={{ fontSize: 13.5, lineHeight: 1.65, color: 'var(--ink-dim)' }}>
                {!item.summary && !item.explanation && !item.business_role && !item.data_flow && !item.change_risk && !item.plone_migration && (
                  <p style={{ margin: 0, color: 'var(--muted)', fontStyle: 'italic' }}>
                    No analysis returned — artifact may be a built-in Plone component or had insufficient source context for the model to analyse.
                  </p>
                )}
                {item.summary && <p style={{ margin: 0, fontStyle: 'italic', color: 'var(--ink)', fontWeight: 500 }}>{item.summary}</p>}
                {item.explanation && !item.summary && <p style={{ margin: 0 }}>{item.explanation}</p>}

                {item.business_role && (
                  <div>
                    <div className="caps" style={{ marginBottom: 6, fontSize: 10.5 }}>Business role</div>
                    <p style={{ margin: 0 }}>{item.business_role}</p>
                  </div>
                )}
                {item.data_flow && (
                  <div>
                    <div className="caps" style={{ marginBottom: 6, fontSize: 10.5 }}>Data flow</div>
                    <p style={{ margin: 0 }}>{item.data_flow}</p>
                  </div>
                )}
                {item.change_risk && (
                  <div style={{ padding: '12px 14px', background: 'rgba(186,90,50,0.06)', borderLeft: '2px solid var(--rust)' }}>
                    <div className="caps" style={{ marginBottom: 6, fontSize: 10.5, color: 'var(--rust)' }}>Change risk</div>
                    <p style={{ margin: 0, color: 'var(--rust-dim)' }}>{item.change_risk}</p>
                  </div>
                )}
                {item.warning && !item.change_risk && (
                  <p style={{ margin: 0 }}><span className="bone">Warning.</span> {item.warning}</p>
                )}
                {item.plone_migration && (
                  <div>
                    <div className="caps" style={{ marginBottom: 6, fontSize: 10.5 }}>Plone 6 migration</div>
                    <p style={{ margin: 0 }}>{item.plone_migration}</p>
                  </div>
                )}
              </div>
            </div>
          ))}
        </div>
      )}

      {!loading && (!items || items.length === 0) && (
        <div style={{ marginTop: 32, padding: '40px 32px', textAlign: 'center', border: '1px solid var(--line)', background: 'var(--bg-1)' }}>
          <div className="caps" style={{ marginBottom: 12 }}>No AI explanations for this scan</div>
          <p style={{ fontSize: 14, color: 'var(--ink-dim)', margin: '0 0 16px', maxWidth: 480, marginLeft: 'auto', marginRight: 'auto' }}>
            This excavation was run with AI explanations disabled, or was completed before this feature was available.
            Re-run with <span className="bone">AI plain-English explanations</span> enabled to generate field notes.
          </p>
        </div>
      )}
    </div>
  );
}

// ── §7 Source browser ─────────────────────────────────────────────────────────
function SourceBrowser({ jobId, go, toast }) {
  const sample = `## Python Script: sendReferralFax
## Parameters: patient_id, referral_type, recipient_fax

from Products.CMFCore.utils import getToolByName
patient = context.patient_portal.get(patient_id)
if patient is None:
    raise KeyError(patient_id)

pdf = context.pdf_template.render(patient, referral_type)
gateway = context.portal_properties.fax_gateway
gateway.dispatch(recipient_fax, pdf)

ledger = context.billing.claim_to_ledger
ledger(patient_id=patient_id, event='referral_fax',
       amount=context.portal_properties.fax_fee,
       ref=recipient_fax)

return {'ok': True, 'timestamp': DateTime()}`;

  return (
    <div>
      <SectionHeader number="07" eyebrow="Source browser" title="Every artifact, searchable."
        kicker="Full-text search over source, titles, and metadata. Demo data shown — the downloaded HTML report contains your full artifact index."/>
      <div style={{ marginTop: 24, display: 'grid', gridTemplateColumns: '320px 1fr', gap: 0, border: '1px solid var(--line)', minHeight: 400 }}>
        <div style={{ borderRight: '1px solid var(--line)', background: 'var(--bg-1)' }}>
          <div style={{ padding: 12, borderBottom: '1px solid var(--line)' }}>
            <input className="input mono" placeholder="search artifacts…" style={{ fontSize: 12, padding: '7px 10px' }}/>
          </div>
          {[['F-0427', 'sendReferralFax', 'Python Script', 52], ['F-0418', 'admitPatient', 'Python Script', 100], ['F-0392', 'ward_roster_pt', 'Page Template', 35]].map((f, i) => (
            <div key={f[0]} style={{ padding: '10px 14px', borderBottom: '1px solid var(--line)', background: i === 0 ? 'var(--bg-2)' : 'transparent', borderLeft: i === 0 ? '1px solid var(--bone)' : '1px solid transparent', cursor: 'pointer' }}>
              <div className="row jc-b"><span className="mono" style={{ fontSize: 12, color: i === 0 ? 'var(--ink)' : 'var(--ink-dim)' }}>{f[1]}</span><span className="badge risk" style={{ fontSize: 9.5 }}>TTW</span></div>
              <div className="row jc-b" style={{ marginTop: 4 }}><span className="mono" style={{ fontSize: 10.5, color: 'var(--dim)' }}>{f[0]} · {f[2]}</span><RiskBar value={f[3]}/></div>
            </div>
          ))}
        </div>
        <div style={{ background: 'var(--bg)' }}>
          <div style={{ padding: '12px 20px', borderBottom: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <div className="row g-12 ai-c"><span className="mono" style={{ color: 'var(--dim)', fontSize: 11 }}>F-0427</span><span className="serif" style={{ fontSize: 16 }}>sendReferralFax</span><span className="badge risk">TTW · risk 52</span></div>
          </div>
          <pre className="mono" style={{ margin: 0, padding: '20px 24px', fontSize: 12.5, lineHeight: 1.65, color: 'var(--ink-dim)', background: 'var(--bg)', whiteSpace: 'pre-wrap' }}>{sample}</pre>
        </div>
      </div>
    </div>
  );
}

// ── §8 Schema map ─────────────────────────────────────────────────────────────
function SchemaSection({ realStats }) {
  // Only show real data if schema was attached to this scan.
  // We detect this by checking if the job has schema stats — for now we
  // always show the "no schema" state since schema is not yet wired to DynamoDB.
  // When a SQL file IS attached, docker_worker.py will write schema_table_count > 0.
  const hasSchema = realStats?.schema_table_count > 0;

  return (
    <div>
      <SectionHeader number="08" eyebrow="Schema mapping · optional" title="MySQL schema cross-referenced to Zope types."
        kicker="Attach a phpMyAdmin SQL export during excavation. Each table gets cross-linked to every Zope object that reads or writes it."/>

      {!hasSchema && (
        <div style={{ marginTop: 32, padding: '40px 32px', textAlign: 'center', border: '1px solid var(--line)', background: 'var(--bg-1)' }}>
          <div className="caps" style={{ marginBottom: 12 }}>No schema attached to this scan</div>
          <p style={{ fontSize: 14, color: 'var(--ink-dim)', margin: '0 0 16px', maxWidth: 480, marginLeft: 'auto', marginRight: 'auto' }}>
            Attach a phpMyAdmin structure-only SQL export on the upload page to generate the schema cross-reference map.
            This enables §8 and adds cross-links between Zope artifacts and database tables.
          </p>
          <button className="btn btn-sm btn-primary" onClick={() => window.dispatchEvent(new CustomEvent('fossyl-nav', { detail: 'upload' }))}>
            Run new excavation with schema →
          </button>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { Dashboard });

// Run after dashboard.jsx is fully initialized — all window globals (FOSSYL_RISK_WEIGHTS,
// FOSSYL_COUPLING_ROWS, FOSSYL_TTW_TABLE, FOSSYL_VERSION_GROUPS) are set by this point.
if (window.validateDemoFixture) window.validateDemoFixture();