// ── Brief reading view ────────────────────────────────────────────── // The morning desk note. Hero brief with a left rail of "Today's // front page" — companies that produced material updates today. /** When false, earnings release labels and earnings-based roster ordering are off. */ const BRIEF_SHOW_EARNINGS_RELEASE_INFO = false; function _parseWindowParts(iso) { if (!iso) return { weekday: "—", monShort: "—", day: "—", time: "—" }; const zone = _tzIana(); const d = new Date(iso); return { weekday: d.toLocaleDateString("en-US", { weekday: "short", timeZone: zone }), monShort: d.toLocaleDateString("en-US", { month: "short", timeZone: zone }), day: parseInt(d.toLocaleDateString("en-US", { day: "numeric", timeZone: zone }), 10), time: d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZone: zone, hour12: true }), }; } function _fmtDur(start, end) { if (!start || !end) return "—"; const ms = new Date(end) - new Date(start); if (ms < 0) return "—"; const mins = Math.round(ms / 60000); const h = Math.floor(mins / 60); const m = mins % 60; if (h < 1) return `${m}m`; return m > 0 ? `${h}h ${m}m` : `${h}h`; } function BriefWindowBand({ start, end }) { const s = _parseWindowParts(start); const e = _parseWindowParts(end); return (
Timespan
{_tzLong()}
{s.weekday}, {s.monShort} {s.day} {s.time}
{_fmtDur(start, end)}
{e.weekday}, {e.monShort} {e.day} {e.time}
); } function BriefView({ density, showDiscarded, dropcap, setShowDiscarded, setView, view, briefLayout, setBriefLayout, appPortfolioIds, appLandingSummaries, appEvents }) { const initialBrief = window.DATA.todaysBrief; const initialDates = window.DATA.availableDates || []; const initialDate = initialBrief?.windowEnd?.slice(0, 10) || initialDates[initialDates.length - 1] || null; const [currentBrief, setCurrentBrief] = React.useState(null); // mode derived from view: "overview" → brief, "overview-audit" → audit, "overview-archive" → archive const mode = view === "overview-audit" ? "audit" : view === "overview-archive" ? "archive" : "brief"; const setMode = (m) => setView(m === "audit" ? "overview-audit" : m === "archive" ? "overview-archive" : "overview"); const [currentPulse, setCurrentPulse] = React.useState(window.DATA.pulse); const [availableDates, setAvailableDates] = React.useState(initialDates); const [selectedDate, setSelectedDate] = React.useState(initialDate); const [companySummaries, setCompanySummaries] = React.useState(window.DATA.companySummaries || {}); const landingSummaries = appLandingSummaries || window.DATA.companySummaries || {}; const _summariesDateRef = React.useRef(initialDate); // tracks which date companySummaries currently reflects const [loading, setLoading] = React.useState(false); const [activeBulletId, setActiveBulletId] = React.useState(null); const [filterTheme, setFilterTheme] = React.useState(null); const [relatedBriefs, setRelatedBriefs] = React.useState([]); const [companySearch, setCompanySearch] = React.useState(""); const [entitySignals, setEntitySignals] = React.useState(null); const [signalsLoading, setSignalsLoading] = React.useState(false); const [signalMode, setSignalMode] = React.useState("zscore"); // "zscore" | "raw" const portfolioIds = appPortfolioIds; // null = loading, Set = loaded const _loadingEntityRef = React.useRef(null); // prevents auto-load from racing with explicit loadEntity calls // Reset to landing when The Brief nav is clicked React.useEffect(() => { if (view === "brief") { setCurrentBrief(null); setCurrentPulse(window.DATA.pulse || []); } }, [view]); const brief = currentBrief; React.useEffect(() => { const entityId = brief?.entityId; const date = selectedDate || (brief?.windowEnd && brief.windowEnd.slice(0, 10)) || null; if (!entityId || !date) { setRelatedBriefs([]); return; } let cancelled = false; fetch( `/api/frontend/brief/related?entity_id=${encodeURIComponent(entityId)}&date=${encodeURIComponent(date)}` ) .then(r => r.json()) .then(data => { if (cancelled) return; const list = Array.isArray(data.related) ? data.related : []; const shuffled = [...list]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const t = shuffled[i]; shuffled[i] = shuffled[j]; shuffled[j] = t; } setRelatedBriefs(shuffled.slice(0, 3)); }) .catch(() => { if (!cancelled) setRelatedBriefs([]); }); return () => { cancelled = true; }; }, [brief?.entityId, brief?.windowEnd, selectedDate]); const _allCompanies = Array.isArray(window.DATA?.companies) ? window.DATA.companies : []; const allCompanies = portfolioIds === null ? [] : portfolioIds.size === 0 ? _allCompanies : _allCompanies.filter(c => portfolioIds.has(c.id)); // Auto-load top company when navigating to any overview* view with no brief loaded. // Skips if loadEntity is already in flight (race-condition guard via _loadingEntityRef). React.useEffect(() => { const isOverview = view === "overview" || view === "overview-audit" || view === "overview-archive"; if (!isOverview || currentBrief || !portfolioIds || _loadingEntityRef.current) return; const top = [...allCompanies] .filter(c => companySummaries[c.id]?.bulletsSaved > 0) .sort((a, b) => (companySummaries[b.id]?.bulletsSaved ?? 0) - (companySummaries[a.id]?.bulletsSaved ?? 0))[0]; if (top) loadEntity(top.id, selectedDate, view); }, [view, portfolioIds]); const _searchLower = companySearch.toLowerCase(); const _filterCompany = (c) => !_searchLower || c.name.toLowerCase().includes(_searchLower) || (c.ticker || "").toLowerCase().includes(_searchLower); // Landing picker: sorted by bullet count descending const landingCompanies = React.useMemo(() => [...allCompanies].sort((a, b) => (landingSummaries[b.id]?.bulletsSaved ?? 0) - (landingSummaries[a.id]?.bulletsSaved ?? 0) ), [landingSummaries, allCompanies]); // Overview left rail: filtered to companies that ran on selectedDate, sorted by bullet count const companiesForFrontPage = React.useMemo(() => { const bid = brief?.entityId; const list = allCompanies.filter(c => { if (c.id === bid) return true; const s = companySummaries[c.id]; if (!s) return false; if (s.hasRunOnDate === true) return true; if (s.hasRunOnDate === false) return false; return (s.bulletsSaved ?? 0) > 0; }); return [...list].sort((a, b) => (companySummaries[b.id]?.bulletsSaved ?? 0) - (companySummaries[a.id]?.bulletsSaved ?? 0) ); }, [companySummaries, brief?.entityId, allCompanies]); function refreshSidebar(date) { const url = `/api/frontend/companies/summaries` + (date ? `?date=${date}` : ""); fetch(url) .then(r => r.json()) .then(data => { if (data.summaries) { setCompanySummaries(data.summaries); _summariesDateRef.current = date; } }) .catch(console.error); } React.useEffect(() => { if (selectedDate && selectedDate !== _summariesDateRef.current) refreshSidebar(selectedDate); }, [selectedDate]); React.useEffect(() => { const entityId = brief?.entityId; if (!entityId) { setEntitySignals(null); return; } let cancelled = false; setSignalsLoading(true); const endParam = selectedDate ? `&end_date=${encodeURIComponent(selectedDate)}` : ""; fetch(`/api/frontend/entity/${encodeURIComponent(entityId)}/signals?days=30${endParam}`) .then(r => r.json()) .then(data => { if (!cancelled) setEntitySignals(data); }) .catch(console.error) .finally(() => { if (!cancelled) setSignalsLoading(false); }); return () => { cancelled = true; }; }, [brief?.entityId, selectedDate]); function loadEntity(entityId, date, targetView = "overview") { const targetDate = typeof date === "string" && /^\d{4}-\d{2}-\d{2}/.test(date) ? date.slice(0, 10) : null; _loadingEntityRef.current = entityId; setView(targetView); setLoading(true); setFilterTheme(null); setActiveBulletId(null); const url = `/api/frontend/entity/${entityId}/brief` + (targetDate ? `?date=${encodeURIComponent(targetDate)}` : ""); fetch(url) .then(r => r.json()) .then(data => { if (data.availableDates) setAvailableDates(data.availableDates); if (data.brief) { setCurrentBrief(data.brief); setCurrentPulse(data.pulse || []); // Keep sidebar counts in sync: brief already aggregates all day's runs setCompanySummaries(prev => ({ ...prev, [entityId]: { ...(prev[entityId] || {}), bulletsSaved: data.brief.bulletsSaved, bulletsDiscarded: data.brief.bulletsDiscarded, lastRunDate: data.brief.coverageEnd || data.brief.windowEnd, }, })); const asked = targetDate && /^\d{4}-\d{2}-\d{2}$/.test(targetDate) ? targetDate.slice(0, 10) : null; if (asked) { setSelectedDate(asked); } else { const rd = (data.selectedDate || data.brief.windowEnd || "").toString().slice(0, 10); if (/^\d{4}-\d{2}-\d{2}$/.test(rd)) setSelectedDate(rd); } } }) .catch(console.error) .finally(() => { setLoading(false); _loadingEntityRef.current = null; }); } function navigateDate(direction) { const key = (selectedDate || brief?.windowEnd || "").toString().slice(0, 10); const idx = availableDates.indexOf(key); const nextIdx = idx + direction; if (nextIdx < 0 || nextIdx >= availableDates.length) return; const newDate = availableDates[nextIdx]; setSelectedDate(newDate); loadEntity(brief?.entityId, newDate, view); } const navDateKey = (selectedDate || brief?.windowEnd || "").toString().slice(0, 10); const dateIdx = availableDates.indexOf(navDateKey); const canPrev = dateIdx > 0; const canNext = dateIdx >= 0 && dateIdx < availableDates.length - 1; if (view === "brief") { return ; } // view === "overview" if (!brief || !Array.isArray(brief.bullets)) { return (
{loading ? "Loading…" : "No company data available."}
); } const allBullets = brief.bullets; const themesList = Array.isArray(brief.themes) ? brief.themes : []; const discardedList = Array.isArray(brief.discarded) ? brief.discarded : []; // Assign maximally-separated hues using golden angle so sibling themes never clash const _dark = document.documentElement.dataset.theme === "dark"; const _themeColorList = themesList.map((_, i) => `hsl(${(i * 137.508 + 30) % 360}, 70%, ${_dark ? 62 : 38}%)` ); const themeColors = {}; themesList.forEach((t, i) => { themeColors[t.name] = _themeColorList[i]; }); const bullets = filterTheme ? allBullets.filter(b => b.theme === filterTheme) : allBullets; const novelCount = allBullets.filter(b => b.novelty === "novel").length; const rewrittenCount = allBullets.filter(b => b.novelty === "rewritten").length; return (
{/* ── Left rail: today's front page ── */} {/* ── Main column ── */}
{/* Date navigation — shown in brief, audit and archive (buttons hidden in archive) */} {(mode === "brief" || mode === "audit" || mode === "archive") && (
{selectedDate ? (() => { const [y, m, d] = selectedDate.split("-").map(Number); return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString("en-US", { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "UTC" }); })() : "—"} {loading && loading…}
)} {mode === "archive" && ( { setMode("brief"); loadEntity(brief.entityId, d); }} /> )} {mode === "audit" && ( <>

{brief?.entityName} — Audit

Every bullet, kept or cut for {brief?.ticker || brief?.entityName}

)} {mode === "brief" && (<> {/* Hero */}

{brief?.entityName} {brief?.ticker ? `What's new on ` : `What's new at `}{brief?.ticker || brief?.entityName} this morning.

{loading ?

Loading…

: brief?.noRunForWindow ?

No pipeline run for this calendar day. Use prev/next to move along the coverage range, or run the scanner for this window.

: brief?.bulletsSaved === 0 ?

No material developments for this period.

: brief?.narrative ?

{brief.narrative}

:

Narrative not available.

}
{brief.bulletsSaved}
Material
developments
{brief.sourcesScanned}
Available
sources
{brief.chunksReviewed}
Excerpts
reviewed
{brief.bulletsDiscarded}
Filtered
out
{Math.round(brief.durationSec / 60)}m
Pipeline
runtime
{!brief.noRunForWindow && (brief.coverageStart || brief.windowStart) && ( )}

{/* Theme filter row */}
Sections {themesList.map(t => ( ))}
{/* Bullets, grouped by theme when no filter */}
{bullets.map((b, i) => ( setActiveBulletId(activeBulletId === b.id ? null : b.id)} themeColor={themeColors[b.theme]} /> ))}
)}
{/* ── Right rail: meta ── */}
); } // ── Single bullet ─────────────────────────────────────────────────── function BulletItem({ bullet, index, isFirst, active, onActivate, themeColor }) { const [noteOpen, setNoteOpen] = React.useState(false); const [expandedSources, setExpandedSources] = React.useState(new Set()); React.useEffect(() => { if (!active) setExpandedSources(new Set()); }, [active]); const toggleSource = (src) => { setExpandedSources(prev => { const next = new Set(prev); if (next.has(src)) next.delete(src); else next.add(src); return next; }); }; const groupedCitations = _groupCitations(bullet.citations); return (
{String(index + 1).padStart(2, "0")} {bullet.theme}

{bullet.text} {bullet.citations.map((c, i) => )}

{bullet.novelty === "rewritten" && (
{noteOpen && {bullet.rewriteReason}}
)}
{groupedCitations.map((sg, gi) => { const isOpen = active || expandedSources.has(sg.source); const excerptCount = sg.headlineGroups.reduce((s, hg) => s + hg.excerpts.length, 0); return ( ); })}
{(active || expandedSources.size > 0) && (
{groupedCitations .filter(sg => active || expandedSources.has(sg.source)) .map((sg, si) => (
{sg.source}{sg.date ? · {sg.date} : null}
{sg.headlineGroups.map((hg, hi) => (
{hg.headline}
{hg.excerpts.length === 1 ?

"{hg.excerpts[0]}"

: hg.excerpts.map((ex, xi) => (
Text {xi + 1}:

"{ex}"

)) }
))}
))}
)}
); } // ── Discarded list ────────────────────────────────────────────────── function DiscardedList({ items }) { const grouped = items.reduce((acc, item) => { if (!acc[item.stage]) acc[item.stage] = []; acc[item.stage].push(item); return acc; }, {}); const stageLabels = { relevance_score: "Relevance score", grounding: "Grounding", novelty_embedding: "Novelty (embedding)", novelty_search: "Novelty (search)", }; return (

Items the pipeline considered and rejected, with the reason. Helps audit what the brief did not tell you.

{Object.entries(grouped).map(([stage, list]) => (
{stageLabels[stage] || stage} {list.length} item{list.length > 1 ? "s" : ""}
    {list.map(item => (
  • {item.text} — {item.reason}
  • ))}
))}
); } window.BriefView = BriefView; // ── Archive bullet item — bullet text + collapsible source chips ──── function ArchiveBulletItem({ bullet, index }) { const [expandedSources, setExpandedSources] = React.useState(new Set()); const groupedCitations = _groupCitations(bullet.citations || []); const toggleSource = (src) => setExpandedSources(prev => { const next = new Set(prev); if (next.has(src)) next.delete(src); else next.add(src); return next; }); return (
{bullet.theme && (  {bullet.theme} )}

{bullet.text}

{groupedCitations.length > 0 && (
{groupedCitations.map((sg, gi) => { const isOpen = expandedSources.has(sg.source); const excerptCount = sg.headlineGroups.reduce((s, hg) => s + hg.excerpts.length, 0); return ( ); })}
)} {expandedSources.size > 0 && (
{groupedCitations.filter(sg => expandedSources.has(sg.source)).map((sg, si) => (
{sg.source}{sg.date ? · {sg.date} : null}
{sg.headlineGroups.map((hg, hi) => (
{hg.headline}
{hg.excerpts.length === 1 ?

"{hg.excerpts[0]}"

: hg.excerpts.map((ex, xi) => (
Text {xi + 1}:

"{ex}"

)) }
))}
))}
)}
); } // ── Brief landing ───────────────────────────────────────── // Two-column split: Portfolio Brief narrative on the left, company picker on the right. function BriefLanding({ loading, companies, allCompanies, summaries, onPick, companySearch, setCompanySearch, selectedDate, briefLayout, setBriefLayout, upcomingEvents, eventsLoading }) { const [pbView, setPbView] = React.useState("bullets"); // "bullets" | "summary" // Stats are always computed from the full (unfiltered) company list for the selected date const _statsBase = (allCompanies || companies).filter(c => summaries[c.id]?.hasRunOnDate === true); const ranToday = _statsBase; const totalSaved = ranToday.reduce((s, c) => s + (summaries[c.id]?.bulletsSaved || 0), 0); const totalDiscarded = ranToday.reduce((s, c) => s + (summaries[c.id]?.bulletsDiscarded || 0), 0); const moversAll = ranToday .map(c => ({ ...c, saved: summaries[c.id]?.bulletsSaved || 0, discarded: summaries[c.id]?.bulletsDiscarded || 0 })) .filter(c => c.saved > 0) .sort((a, b) => b.saved - a.saved); const movers = moversAll.slice(0, 5); // top 5 for display list const activeCount = moversAll.length; // actual count of active companies // Portfolio brief state const [portfolioBrief, setPortfolioBrief] = React.useState(null); const [briefLoading, setBriefLoading] = React.useState(false); const [narrativeMode, setNarrativeMode] = React.useState("thematic"); // "thematic" | "lead" const dateLabel = React.useMemo(() => { const iso = portfolioBrief?.date; if (!iso) return null; const [y, m, d] = iso.split("-").map(Number); return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric", timeZone: "UTC", }); }, [portfolioBrief?.date]); // Fetch portfolio brief — always uses most recent date (no date param) React.useEffect(() => { setBriefLoading(true); fetch("/api/frontend/portfolio-brief?top_n=5") .then(r => r.json()) .then(data => setPortfolioBrief(data)) .catch(() => setPortfolioBrief(null)) .finally(() => setBriefLoading(false)); }, []); function _fmtEventDateTime(iso) { if (!iso) return { date: "—", time: "" }; const d = new Date(iso); const zone = _tzIana(); const date = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: zone }); const time = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, timeZone: zone }); return { date, time }; } const [showExtraCols, setShowExtraCols] = React.useState(false); const leftRef = React.useRef(null); const pickListRef = React.useRef(null); const _layout = briefLayout || "below"; // Cap pick-list height to match left panel in both layouts React.useLayoutEffect(() => { const list = pickListRef.current; const left = leftRef.current; if (!list || !left) return; const leftH = left.getBoundingClientRect().height; const header = list.previousElementSibling; const headerH = header ? header.getBoundingClientRect().height : 0; const available = Math.max(leftH - headerH, 220); list.style.maxHeight = available + "px"; list.style.overflowY = "auto"; }, [_layout, companies.length, portfolioBrief?.date, briefLoading, upcomingEvents]); const narrativeText = narrativeMode === "lead" && portfolioBrief?.narrative_b ? portfolioBrief.narrative_b : portfolioBrief?.narrative; const companiesCount = ranToday.length; const events = upcomingEvents?.events || []; // Group events by day for calendar strip const eventsByDay = React.useMemo(() => { const zone = _tzIana(); const days = {}; events.forEach(ev => { const d = new Date(ev.event_datetime); const key = d.toLocaleDateString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", timeZone: zone }); if (!days[key]) days[key] = { date: d, events: [] }; days[key].events.push(ev); }); return Object.values(days).sort((a, b) => a.date - b.date); }, [events]); const eventsInPanelBlock = ( <>
Next closest events
{eventsLoading ? (

Loading events…

) : events.length === 0 ? (

No upcoming events found.

) : ( )} ); const eventsBelowBlock = eventsByDay.length > 0 && (
Next closest events
{eventsByDay.map((day, di) => { const zone = _tzIana(); const dow = day.date.toLocaleDateString("en-US", { weekday: "short", timeZone: zone }); const num = day.date.toLocaleDateString("en-US", { day: "numeric", timeZone: zone }); const mon = day.date.toLocaleDateString("en-US", { month: "short", timeZone: zone }); return (
{dow} {num} {mon}
{day.events.map((ev, ei) => { const time = new Date(ev.event_datetime).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, timeZone: zone, }); return (
{time} {DISPLAY_TZ}
{ev.entity_name} {ev.category === "conference-call" ? "Conference" : "Earnings Call"} {(ev.fiscal_period || ev.fiscal_year) && ( {[ev.fiscal_period, ev.fiscal_year].filter(Boolean).join(" ")} )}
); })}
); })}
); return (
{/* LEFT: Company picker */}

{loading ? "Loading…" : companies.length === 0 ? "Add companies in My Portfolio to see briefs here." : "Choose a company to read its brief."}

setCompanySearch(e.target.value)} style={{ marginTop: 12 }} />
Ticker Company Items
{companies.map(c => { const s = summaries[c.id] || {}; const saved = s.bulletsSaved != null ? s.bulletsSaved : "—"; return ( ); })}
{/* RIGHT: Portfolio Brief */}
Portfolio Brief{dateLabel ? ` — ${dateLabel}` : ""}

Where the news moved.

The most active names in your portfolio today — short reads, one per company.

{companiesCount} companies {totalSaved} material developments {activeCount} active names
{briefLoading ? null : pbView === "summary" ? (narrativeText ? (() => { const _companyById = {}; (portfolioBrief?.companies || []).forEach(c => { _companyById[c.name] = c.entityId; }); return narrativeText.split("\n\n").map((section, i) => { const nl = section.indexOf("\n"); const company = nl === -1 ? section : section.slice(0, nl); const body = nl === -1 ? "" : section.slice(nl + 1); const entityId = _companyById[company]; const _go = entityId ? () => onPick(entityId, null) : null; return (
{company}
{body &&

{body}

}
); }); })() : No portfolio brief available yet — will be generated after the next run. ) : ((portfolioBrief?.bullets_by_company || []).length > 0 ? (portfolioBrief.bullets_by_company).map((c, i) => { const _go = c.entityId ? () => onPick(c.entityId, null) : null; return (
{c.name}
    {c.bullets.map((b, j) =>
  • {b}
  • )}
); }) : No bullet points available yet. ) }
{_layout === "in-panel" && eventsInPanelBlock}
{_layout === "below" && eventsBelowBlock}
); } // ── Inline Archive for the selected entity (Change 4) ─────────────── function BriefEntityArchive({ entityId, entityName, ticker, onOpenDate }) { const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [expandedRunId, setExpandedRunId] = React.useState(null); React.useEffect(() => { setLoading(true); fetch(`/api/frontend/entity/${entityId}/history`) .then(r => r.json()) .then(d => setData(d)) .catch(console.error) .finally(() => setLoading(false)); }, [entityId]); if (loading || !data) { return

Loading archive…

; } const history = data.history || []; return (

{entityName} — Archive

Every brief filed for {ticker || entityName}

{history.length} runs · {history.reduce((s, h) => s + h.saved, 0)} bullets saved · {history.reduce((s, h) => s + h.discarded, 0)} discarded

{history.map(entry => { const d = new Date(entry.date + "T00:00:00Z"); const day = d.getUTCDate(); const month3 = d.toLocaleDateString("en-US", { month: "short", timeZone: "UTC" }); const wd = d.toLocaleDateString("en-US", { weekday: "short", timeZone: "UTC" }); return (
{String(day).padStart(2, "0")}
{month3}
{wd}

onOpenDate(entry.date)}> {entry.narrative || entry.bullets?.[0]?.text || "No material developments"}

run-{entry.runId} · {entry.saved} saved · {entry.discarded} discarded
{entry.bullets?.length > 0 && ( )} {expandedRunId === entry.runId && entry.bullets?.length > 0 && (
{entry.bullets.map((b, i) => ( ))}
)}
); })} {history.length === 0 && (

No briefs found for this entity.

)}
); } // ── Inline Audit (forensic) for the selected entity (Change 4) ────── // ── Inline Audit — renders identical content to HistoryDetailsView (no sidebar) ── function BriefEntityAudit({ entityId, selectedDate }) { const [forensicsData, setForensicsData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [openRunId, setOpenRunId] = React.useState(null); const [expandedRejection, setExpandedRejection] = React.useState(null); const [expandedPubCitation, setExpandedPubCitation] = React.useState(null); React.useEffect(() => { if (!entityId) return; setLoading(true); fetch(`/api/frontend/entity/${entityId}/forensics`) .then(r => r.json()) .then(d => { setForensicsData(d); // Auto-open the run matching selectedDate, else open the first run if (d.days && d.days.length > 0) { const targetDay = selectedDate ? d.days.find(day => day.date === selectedDate) : d.days[0]; const day = targetDay || d.days[0]; if (day && day.runs && day.runs.length > 0) { setOpenRunId(day.runs[0].runId); } } }) .catch(console.error) .finally(() => setLoading(false)); }, [entityId, selectedDate]); if (loading) return (
Loading audit…
); const allDays = forensicsData?.days || []; // Show only the day matching the selected date const days = selectedDate ? allDays.filter(d => d.date === selectedDate) : allDays.slice(0, 1); if (!days.length) return (
No audit data for this date.
); return (
{days.map(d => { const isMulti = d.runs.length > 1; // Static header — no collapse, always expanded const RunHeader = ({ r }) => (
{r.published} published · {r.rejected} rejected · run-{r.runId} {r.windowStart && ( {_fmtWindow(r.windowStart, r.windowEnd)} )}
); if (!isMulti) { const r = d.runs[0]; return (
); } const totalPub = d.runs.reduce((s, r) => s + r.published, 0); const totalRej = d.runs.reduce((s, r) => s + r.rejected, 0); return (
{d.runs.length} runs · {totalPub} published · {totalRej} rejected
{d.runs.map(r => ( ))}
); })}
); }