The WidgetTimeProvider plumbed a server timestamp and the container's
timezone down to useWidgetNow so clock widgets would paint a real time in
the SSR HTML instead of "--:--". That traded a cosmetic first-paint win for
a wrong clock: the seeded timezone is the server's, so anyone whose timezone
differs from the container's saw an incorrect time for the whole hydration
window. Fixing that properly needs a client-set timezone cookie read during
RSC render, which is far more plumbing than the problem warrants.
Revert the clock to release/v2 behaviour ("--:--" until hydration) and drop
the provider, the @homarr/widgets/time export and their tests. The e2e clock
assertions now wait on hydration explicitly instead of the seeded paint.
Also restore the widget.beszel.getSystemStats persistence exclusion. #6289
added it deliberately to keep those large time-series payloads out of the
persisted cache, and this branch had removed it with no stated rationale.
Query-cache persistence and the background-refresh indicator are unchanged.
Review follow-ups on the session query cache:
- Cache the per-payload size verdict and reuse a single TextEncoder. The
persist subscription re-dehydrates the whole cache on every cache event,
so every widget payload was being superjson-serialized several times per
refresh cycle just to measure it.
- Skip the size measurement on the restore path. It runs before any query is
allowed to fetch and the payload was already budgeted when it was written.
- Reject stored queries whose dataUpdatedAt is not a positive finite number;
`undefined <= 0` is false, so those were hydrating with a NaN timestamp.
- Add `stop()` and tear the persister down before `queryClient.clear()`, so
the clear cannot overwrite the saved snapshot with an empty cache.
- Remove every other scope's cache when a persister is created: signing out
navigates away before the scope guard can react, which left the previous
account's dashboard data in the tab.
- Remove the entry instead of storing an empty cache.
Refresh indicator:
- Make it decorative instead of an aria-live region; every widget refreshes
on a timer, so it made assistive technology repeat "loading" forever.
Also broadens the e2e hydration-error matcher to production React's minified
error codes, hoists the single WidgetTimeProvider in ClientBoard, memoizes the
resolved local time zone, and drops the exports this branch made dead.
Rebuild the integration marquee on Mantine's Marquee, SimpleGrid and
ThemeIcon instead of the hand-rolled grid, track and keyframes, and
reduce the stylesheet to the mask and reduced-motion rule.
Drop the useMounted guards from the login form and let the buttons rely
on the pending state alone, and use Group for the restore flow's file
summary.
Replace the stacked install modals (browse -> details -> import review)
with page-based flows so the primary action is always reachable:
- /manage/custom-widgets/workshop browses submissions as a card grid
with large previews, sitting in a tab next to the installed widgets
- /manage/custom-widgets/workshop/[id] merges the listing, security
review and source setup onto one page with a sticky install footer
- /manage/custom-widgets/publish/[id] replaces the publish modal
- /manage/workshop redirects; its navbar entry is gone
- the Custom CSS picker collapses from three modals to one
Extract useCustomWidgetImport and ImportReviewContent so the Workshop
page and the file/clipboard dialog share one import pipeline.
Votes now show the viewer's own choice filled Reddit-style, backed by a
new user-votes query, and screenshots get a zoomable lightbox.
Rework the installed list onto ManageCollectionItem: show the widget
description, replace the cryptic "JSX - 2" badge with plain request and
origin facts, and swap the toggle and bare pencil for a status badge,
a labelled Edit button and a menu entry for enable/disable.
Fixes found while reviewing:
- importing could run twice and duplicate a widget; guard on pending and
succeeded, and return to the installed list instead of the heavy edit
form
- a background refetch of the submission (window focus, or the
invalidation a vote triggers) reset the source setup and silently wiped
part-typed credentials; key the reset on the sources, not the object
- breadcrumbs raised MISSING_MESSAGE for unmapped dynamic segments; fall
back to the raw segment via t.has
- publishing had no in-flight guard and could post duplicate listings
- tooltips on disabled vote and report controls never fired, making the
sign-in hint unreachable