Description
src/components/treasuryOverviewPage/useWidgetLayout.ts's read path is carefully defensive:
const loadLayout = useCallback((): WidgetLayout => {
const defaultLayout = generateDefaultLayout(metrics);
try {
const data = localStorage.getItem(storageKey);
...
} catch (e) {
console.warn("Failed to load layout from localStorage, falling back to default.", e);
return defaultLayout;
}
}, [metrics, storageKey, generateDefaultLayout]);
but its write path is not:
const saveLayout = useCallback((newLayout: WidgetLayout) => {
setLayout(newLayout);
localStorage.setItem(storageKey, JSON.stringify(newLayout));
}, [storageKey]);
localStorage.setItem throws (e.g. QuotaExceededError when storage is full, or a SecurityError in some private-browsing configurations) exactly like getItem can fail — but saveLayout has no try/catch around it. Since reorderWidgets, updateWidgetSize, updateWidgetVisibility, and resetLayout all funnel through saveLayout, any drag-and-drop reorder, resize, visibility toggle, or "Reset Layout" click (WidgetTray.tsx's "Reset Layout" button) performed while storage is full or unavailable will throw an uncaught exception from inside a React event handler, breaking the interaction the user just performed even though setLayout(newLayout) already optimistically updated the in-memory UI state one line earlier.
Requirements
saveLayout must not throw uncaught when localStorage.setItem fails; the in-memory layout state update should still succeed so the UI remains responsive even if persistence silently fails.
- Failures should be logged (consistent with
loadLayout's console.warn pattern) rather than swallowed silently or left to crash.
Suggested execution
- Wrap the
localStorage.setItem call in saveLayout in a try/catch, logging a console.warn on failure (mirroring loadLayout's existing pattern) without re-throwing.
- Consider surfacing a subtle non-blocking indicator (or reuse
ToastProvider) if persistence repeatedly fails, so users understand their widget layout preference isn't being saved — optional, but worth a TODO if out of scope for this fix.
- Add a test that mocks
localStorage.setItem to throw and asserts reorderWidgets/updateWidgetSize still update in-memory layout state without throwing.
Acceptance criteria
Security notes
None; resilience/correctness fix.
Guidelines
- Minimum 95% test coverage
- Timeframe: 96 hours
Description
src/components/treasuryOverviewPage/useWidgetLayout.ts's read path is carefully defensive:but its write path is not:
localStorage.setItemthrows (e.g.QuotaExceededErrorwhen storage is full, or aSecurityErrorin some private-browsing configurations) exactly likegetItemcan fail — butsaveLayouthas notry/catcharound it. SincereorderWidgets,updateWidgetSize,updateWidgetVisibility, andresetLayoutall funnel throughsaveLayout, any drag-and-drop reorder, resize, visibility toggle, or "Reset Layout" click (WidgetTray.tsx's "Reset Layout" button) performed while storage is full or unavailable will throw an uncaught exception from inside a React event handler, breaking the interaction the user just performed even thoughsetLayout(newLayout)already optimistically updated the in-memory UI state one line earlier.Requirements
saveLayoutmust not throw uncaught whenlocalStorage.setItemfails; the in-memorylayoutstate update should still succeed so the UI remains responsive even if persistence silently fails.loadLayout'sconsole.warnpattern) rather than swallowed silently or left to crash.Suggested execution
localStorage.setItemcall insaveLayoutin atry/catch, logging aconsole.warnon failure (mirroringloadLayout's existing pattern) without re-throwing.ToastProvider) if persistence repeatedly fails, so users understand their widget layout preference isn't being saved — optional, but worth a TODO if out of scope for this fix.localStorage.setItemto throw and assertsreorderWidgets/updateWidgetSizestill update in-memorylayoutstate without throwing.Acceptance criteria
saveLayoutno longer throws uncaught whenlocalStorage.setItemfails.Security notes
None; resilience/correctness fix.
Guidelines