diff --git a/src/app/(dashboard)/reports/page.tsx b/src/app/(dashboard)/reports/page.tsx index 2cb3d00..d3856f1 100644 --- a/src/app/(dashboard)/reports/page.tsx +++ b/src/app/(dashboard)/reports/page.tsx @@ -54,7 +54,7 @@ export default async function ReportsPage({ searchParams }: { searchParams: Prom ] return ( -
+

DataShield Security Report

diff --git a/src/components/reports/ReportCanvas.tsx b/src/components/reports/ReportCanvas.tsx index 8c576a4..b167b30 100644 --- a/src/components/reports/ReportCanvas.tsx +++ b/src/components/reports/ReportCanvas.tsx @@ -63,6 +63,8 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { const [hidden, setHidden] = useState([]) const [sectionsMenuOpen, setSectionsMenuOpen] = useState(false) const [minHeights, setMinHeights] = useState>({}) + // Bumped to force a re-measure even when widths are unchanged (e.g. Reset). + const [measureNonce, setMeasureNonce] = useState(0) const containerRef = useRef(null) const menuRef = useRef(null) const contentEls = useRef(new Map()) @@ -96,8 +98,12 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { // fitHeights effect. An unrounded value changes every render and feeds an // endless update loop (Maximum update depth exceeded). const ro = new ResizeObserver((entries) => { - const w = Math.round(entries[0].contentRect.width) - setContainerW((prev) => (prev === w ? prev : w)) + // Floor (never round up): the grid is rendered at width=containerW, and a + // value 1px wider than the container overflows horizontally, toggling a + // scrollbar that shrinks the container, which feeds an endless loop. + const w = Math.floor(entries[0].contentRect.width) + // Ignore sub-2px wobble so a 1px layout ping-pong cannot drive a loop. + setContainerW((prev) => (Math.abs(prev - w) < 2 ? prev : w)) }) ro.observe(el) return () => ro.disconnect() @@ -144,17 +150,16 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { }) }, []) + // Content height only changes with width (container queries reflow on the + // tile width). Re-measure when widths change, never via a continuous + // ResizeObserver: height is intentionally NOT a dependency, so fitHeights + // setting h cannot retrigger this effect. That makes the auto-fit + // loop-free by construction (previous versions oscillated forever). + const widthSig = layout.map((l) => `${l.i}:${l.w}`).join("|") + const hiddenSig = hidden.join(",") useEffect(() => { - const ro = new ResizeObserver(() => fitHeights()) - // Observe the intrinsic probe, not the cell-sized node: the cell height is - // driven by fitHeights, so observing it would feed an infinite loop. - contentEls.current.forEach((node) => { - const probe = node.querySelector("[data-measure]") - if (probe) ro.observe(probe) - }) fitHeights() - return () => ro.disconnect() - }, [fitHeights, hidden, containerW, sections.length]) + }, [fitHeights, widthSig, containerW, hiddenSig, sections.length, measureNonce]) useEffect(() => { if (!sectionsMenuOpen) return @@ -192,12 +197,10 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { // while still allowing manual grow. Width has no reliable intrinsic min // (content reflows), so it stays freely resizable down to MIN_W. const floorH = Math.max(MIN_H, minHeights[s.id] ?? MIN_H) - return { - ...base, - minH: floorH, - minW: MIN_W, - h: Math.max(base.h, floorH), - } + // h comes straight from layout state (fitHeights keeps it at the content + // floor); do not override it here, or the rendered h differs from stored h + // and RGL keeps echoing the difference back through onLayoutChange. + return { ...base, minH: floorH, minW: MIN_W } }) const onLayoutChange = (current: RglItem[]) => { @@ -234,6 +237,11 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { const l = buildDefaultLayout(sections) setLayout(l) setHidden([]) + // Drop stale content-fit floors (they may have been measured at a different + // zoom/width) and force a fresh measure, otherwise default heights can be + // wrong and tiles overlap. + setMinHeights({}) + setMeasureNonce((n) => n + 1) persist({ layout: l, hidden: [] }) } @@ -331,7 +339,7 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { if (node) contentEls.current.set(s.id, node) else contentEls.current.delete(s.id) }} - className="h-full overflow-auto" + className="h-full overflow-hidden" > {s.content}