feat(FN-3786): add reports dashboard view and comparison tooling

Adds a Reports dashboard view (`ReportsView`) to the reports plugin with a full supporting stack of components (`ReportComparisonDrawer`, `ReportDetailPanel`, `ReportFiltersBar`, `ReportListItem`, `ReportEmptyState`), hooks (`useReports`, `useReportPreview`, `useReportSectionDiff`, `useViewportMode`

Fusion-Task-Id: FN-3786
This commit is contained in:
Fusion
2026-05-10 12:27:36 -07:00
committed by gsxdsm
parent 2fa4ba940a
commit 4535db5419
33 changed files with 753 additions and 6 deletions

View File

@@ -0,0 +1,21 @@
import { useMemo } from "react";
import type { ReportRecord } from "../types.js";
import { useReportPreview } from "../useReportPreview.js";
import { useReportSectionDiff } from "../useReportSectionDiff.js";
export function ReportComparisonDrawer({ reports, leftId, rightId, onPick, onClose, projectId }: { reports: ReportRecord[]; leftId?: string; rightId?: string; onPick: (slot: "a" | "b", id: string) => void; onClose: () => void; projectId?: string }) {
const left = useMemo(() => reports.find((r) => r.id === leftId), [reports, leftId]);
const right = useMemo(() => reports.find((r) => r.id === rightId), [reports, rightId]);
const leftPreview = useReportPreview(leftId, projectId);
const rightPreview = useReportPreview(rightId, projectId);
const diff = useReportSectionDiff(left, right);
return <div className="modal-overlay open reports-compare-overlay" role="dialog" aria-modal="true" aria-label="Compare reports">
<div className="modal modal-lg reports-compare">
<div className="modal-header reports-compare-header"><h3>Compare reports</h3><button className="btn btn-sm" onClick={onClose}>Close</button></div>
<div className="reports-compare-pickers"><select className="select" value={leftId ?? ""} onChange={(e) => onPick("a", e.target.value)}>{reports.map((r) => <option key={r.id} value={r.id}>{r.title}</option>)}</select>
<select className="select" value={rightId ?? ""} onChange={(e) => onPick("b", e.target.value)}>{reports.map((r) => <option key={r.id} value={r.id}>{r.title}</option>)}</select></div>
<div className="reports-compare-frames"><iframe sandbox="allow-same-origin" srcDoc={leftPreview.html} title="Report A" /><iframe sandbox="allow-same-origin" srcDoc={rightPreview.html} title="Report B" /></div>
<div>Changed: {diff.changed.map((s) => s.id).join(", ")}</div>
</div>
</div>;
}

View File

@@ -0,0 +1,23 @@
import { useMemo, useRef } from "react";
import { getReportExportUrl } from "../api.js";
import { useReportPreview } from "../useReportPreview.js";
import type { ReportRecord } from "../types.js";
const SECTION_IDS = ["summary", "system-wins", "system-highlights", "system-lowlights", "system-proposals", "system-deep-dives", "agent-card", "data-coverage", "review-panel"];
export function ReportDetailPanel({ report, projectId }: { report?: ReportRecord; projectId?: string }) {
const frameRef = useRef<HTMLIFrameElement | null>(null);
const { html, loading, error } = useReportPreview(report?.id, projectId);
const sections = useMemo(() => SECTION_IDS, []);
if (!report) return <div className="reports-detail card">Select a report.</div>;
return <div className="reports-detail card">
<div className="reports-detail-header"><h3>{report.title}</h3><a className="btn btn-sm" href={getReportExportUrl(report.id, projectId)} download>Download standalone HTML</a></div>
<div className="reports-detail-meta">{report.cadence} {report.status} {report.periodStart} {report.periodEnd}</div>
<div className="reports-detail-body">
<nav className="reports-detail-sections">{sections.map((section) => <button key={section} className="btn btn-sm" onClick={() => frameRef.current?.contentWindow?.document.querySelector(`[data-section="${section}"]`)?.scrollIntoView()}>{section}</button>)}</nav>
{loading ? <div>Loading preview...</div> : null}
{error ? <div>{error}</div> : null}
<iframe ref={frameRef} sandbox="allow-same-origin" srcDoc={html} title="Report preview" />
</div>
</div>;
}

View File

@@ -0,0 +1,3 @@
export function ReportEmptyState() {
return <div className="reports-empty card"><h3>No reports found</h3><p>Adjust filters or enable schedules in settings.</p></div>;
}

View File

@@ -0,0 +1,19 @@
import { useEffect, useState } from "react";
import type { ReportFilters } from "../types.js";
export function ReportFiltersBar({ filters, onChange, agents }: { filters: ReportFilters; onChange: (next: ReportFilters) => void; agents: string[] }) {
const [query, setQuery] = useState(filters.q);
useEffect(() => {
const timeout = setTimeout(() => onChange({ ...filters, q: query }), 250);
return () => clearTimeout(timeout);
}, [query]);
return <div className="reports-filters">
<select className="select" value={filters.cadence} onChange={(e) => onChange({ ...filters, cadence: e.target.value as ReportFilters["cadence"] })}><option value="all">All cadence</option><option value="daily">Daily</option><option value="weekly">Weekly</option></select>
<select className="select" value={filters.status} onChange={(e) => onChange({ ...filters, status: e.target.value as ReportFilters["status"] })}><option value="all">All status</option><option value="generating">Generating</option><option value="review_pending">Review pending</option><option value="review_in_progress">Review in progress</option><option value="review_complete">Review complete</option><option value="approved">Approved</option><option value="published">Published</option><option value="failed">Failed</option></select>
<input className="input" type="date" value={filters.from} onChange={(e) => onChange({ ...filters, from: e.target.value })} />
<input className="input" type="date" value={filters.to} onChange={(e) => onChange({ ...filters, to: e.target.value })} />
<input className="input" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search title" />
<select className="select" value={filters.agentId} onChange={(e) => onChange({ ...filters, agentId: e.target.value })}><option value="">All agents</option>{agents.map((agent) => <option key={agent} value={agent}>{agent}</option>)}</select>
</div>;
}

View File

@@ -0,0 +1,8 @@
import type { ReportListItemVm } from "../types.js";
export function ReportListItem({ report, selected, onSelect }: { report: ReportListItemVm; selected: boolean; onSelect: (id: string) => void }) {
return <button className="card reports-list-item" data-selected={selected ? "true" : "false"} onClick={() => onSelect(report.id)}>
<div className="card-header"><span className="card-title">{report.title}</span></div>
<div className="card-meta"><span>{report.cadence}</span><span>{report.status}</span><span>{report.periodStart} {report.periodEnd}</span></div>
</button>;
}