The notifications page rebuilt on the shared table, an account and auth consistency pass, and a state-persistence bug that made read/dismiss quietly do nothing.
Notifications page, rebuilt
/notifications/all was 1,821 lines of hand-rolled table, filters, selection and realtime reconciliation. It is now ~390 lines on the same Datatable the other eight list pages use — one component to fix, one set of behaviours to learn.
What changed for the reader:
- One sort. Column headers and the toolbar used to run two different sorts — the headers reordered the current page in the browser while the toolbar sorted the whole set on the server, and they could silently disagree. Sorting is now server-side behind a whitelist, over the full set.
- One filter. A six-field grid (scope, read, dismissed, type, sort, per-page) becomes a single Unread · All · Dismissed control. Each label means what it says; dismissed rows show a badge in All rather than being quietly hidden from it.
- Row actions behind ⋯ with real words — Mark read, Dismiss, Delete — gated on the reader's actual permissions, so a view-only account gets no dead menu.
- Relative dates ("2 hours ago") with the exact stamp on hover, matching the sessions table. CSV export takes the exact stamp.
- Admin create and edit merge into one sheet from the notifications index; the two standalone pages (and their routes) are gone — they were the same form, drifting apart.
System banner
Every active notice used to render its own full-width band, each pushing the header, sidebar and content further down — two notices already cost 172px, and nothing capped the stack. One shows at a time, loudest severity first, with a "· N more" count; dismissing reveals the next. The banner also re-measures on resize (it used to go stale on rewrap and cover the header) and now honours the same mute preferences as the feed — muting the system scope used to still hand you a fixed bar on every page.
Severity muting removed from preferences
Muting by severity cut across every topic at once: silencing one noisy source meant going deaf to the quiet ones, and errors had to be carved out to stop it being dangerous. Per-item dismiss answers the one-off case, a scope mute the recurring one. Stored muted_types keys are ignored — nothing to migrate.
Account, auth and forms pass
The account tabs, auth pages and shared form controls aligned on the established patterns. Session device labels now come from one FormatsUserAgent trait, so "OS X" finally reads "macOS" everywhere at once.
Defects fixed along the way
Marking read and dismissing never persisted. Every state write goes through updateOrCreate(), whose values pass through fill() — and the four state columns were missing from AppNotificationRead::$fillable, so the row was created and the timestamps silently dropped. The page looked right only because it patched rows optimistically and reverted on the next load. Same class of bug as v3.3.0's User::$fillable find; if a model write "doesn't stick", check $fillable first.
Pagination had never rendered. The page checked Array.isArray(links) but the payload's links is an object, so the pager was always empty and nobody could get past the first 25 notifications.
Datatable's sort headers were mouse-only. <th @click> with no button and no aria-sort — keyboard and screen-reader users couldn't sort any table in the app. Headers are real buttons now, announced, on every page at once.
A dangling selector in base.css (.main-heading, with no block) swallowed the @layer after it, so the button cursor rules never reached the stylesheet.
Upgrading
No migrations. Two notes:
- The admin notification create/edit routes are gone — the sheet on the notifications index replaces both pages. Update any bookmarks or links.
- Row actions require
edit-notifications/delete-notifications. Accounts created before v3.3.0's seeder run may lack them:
php artisan db:seed --class=PermissionRoleSeederFull Changelog: v3.3.0...v3.4.0