v4.4.3 - Multisite-aware dashboard widgets
Highlights
Adds an explicit dashboard scope picker so admin dashboard widgets honour a real, visible "current site" selection — fixing the bug where widgets always rendered the role-based fallback site (super_admin → default site, others → first owned site) regardless of which site the user thought they were on.
Backwards compatible: the picker seeds itself using the same fallback the widgets already resolve, so first dashboard load after upgrade matches what users were seeing before — they just now have a visible scope control they can change.
What's new
DashboardSitePicker widget
New TallCms\Cms\Filament\Widgets\DashboardSitePicker registered at the top of the admin dashboard. Renders a select with the user's accessible sites (super_admins also see "All Sites"). On selection it writes session('multisite_admin_site_id') and dispatches the dashboard.site-changed Livewire event so other widgets refresh in place — no full page reload.
Server-side authorization
Every selection (mount + updatedSelected) is validated against the current user's access via resolveAuthorizedSiteValue():
__all_sites__is only valid for super_admins.- A numeric site_id must reference an active site that the user owns (super_admins can pick any active site).
- Tampered Livewire input is rejected without dispatching the event.
- Stale unauthorized session values (e.g.
__all_sites__left over from a previous super_admin login on a shared browser) are normalized on mount.
HasMultisiteWidgetContext trait
New shared trait at TallCms\Cms\Filament\Widgets\Concerns\HasMultisiteWidgetContext. Lifts the byte-for-byte duplicated getMultisiteSiteId() / getMultisiteName() helpers out of MenuOverviewWidget + ContentHealthWidget and adds isAllSitesSelected() for branching on the all-sites case. Both core and Pro widgets consume it.
Cms widgets refresh on scope change
MenuOverviewWidget and ContentHealthWidget now adopt the trait and add #[On('dashboard.site-changed')] listeners so they re-render when the picker fires.
Why a separate picker (not just wiring SiteSwitcher to write session)
SiteSwitcher::navigateToSite() (SiteSwitcher.php:47-51 comment) deliberately doesn't mutate session because authorization there flows through SitePolicy and content scopes through the Site resource's RelationManagers. Hijacking it would conflict with that design and ripple into every consumer of the same session key (ProSetting::resolveCurrentSiteId, PluginLicense::findForCurrentContext, redirect manager, etc.).
The picker keeps both concepts honest: SiteSwitcher = navigate; DashboardSitePicker = scope.
All Sites semantics
| Widget | All Sites behaviour |
|---|---|
| MenuOverviewWidget | Show global counts (sum across all sites) |
| ContentHealthWidget | Show global counts |
| AnalyticsOverviewWidget (Pro 1.10.0) | Empty-state message — analytics is per-site |
| LicenseStatusWidget (Pro 1.10.0) | Empty-state message — Pro licenses are per-site |
The asymmetry is intentional: count(*) is a meaningful aggregate for cms widgets, but per-site provider data (Plausible/Fathom) and per-site Pro licenses don't aggregate cleanly without provider-side fan-out.
Testing
- Trait unit suite: 11 assertions covering each branch of
getMultisiteSiteId(specific id, all-sites sentinel, super_admin default-site fallback, regular-user first-owned fallback, QueryException safety net),getMultisiteName, andisAllSitesSelected. - Picker behaviour + authorization: 11 Livewire tests (default-site seeding, regular-user single-owned-site seeding, explicit selection + event dispatch, can_view truthiness, regular user cannot select another user's site /
__all_sites__/ inactive site, mount normalizes stale foreign-user session value, mount rejects stale__all_sites__for non-super-admin). - Pro widget render tests in standalone (10 tests, 20 assertions): All Sites empty state, specific-site rendering,
setPeriod/refreshDatashort-circuiting on All Sites, event-driven data clearing, License heading and sentinel status.
Pull request
- #78 — Add DashboardSitePicker so widgets honour an explicit site scope
Pro plugin
tallcms/pro 1.10.0 (separately released) ships site-aware Analytics + License widgets that consume the new trait, listen for dashboard.site-changed, render an empty-state message under __all_sites__, and honour the picker as the source of truth. Pins compatibility.tallcms: >=4.4.3.
Upgrading
No code changes required. The dashboard will gain a "Dashboard scope" picker at the top; first load picks up the same role-based fallback site the widgets were already showing, so existing users see exactly what they saw before — until they pick a different site.
Note on direction
This release makes the existing session-driven model honest, so super_admins can see and change the dashboard's site scope. It is not a final multisite analytics architecture — provider keys/secrets are still configured globally on the Pro Settings page. A follow-up will move per-site analytics configuration into the Site resource's edit page (alongside Branding, Embed Code, etc.) so analytics provider, site key, and secret are explicitly per-site.