The notification feed grows a way to turn itself down. It subscribed to every model event the socket delivered and showed all of them — right for a change log, wrong for a person.
Two bugs behind one symptom
I'm getting a lot of notifications at once.
A burst of events raised a burst of toasts.
const collapsed = collapse([event])[0]; // an array of exactly ONE eventcollapse([event]) can never merge with anything — a no-op wrapper. Every arriving event popped its own toast and its own desktop notification, while the bell list collapsed correctly. The same sweep produced one tidy "42 resources updated" row and 42 stacked toasts beside it.
Events do not arrive one at a time: a status evaluator walks every resource and writes the ones that changed; a discovery poll touches last_seen on every guest. Popups now buffer for burstMs (900ms, reset on each arrival so a continuous stream reports once when it settles) and go through the same collapse() the list uses, capped at maxToastsPerBurst. burstMs: 0 restores the old behaviour.
And there was no filtering at all.
Muting
| key | meaning |
|---|---|
'Resource:update' |
one model, one action |
'Resource' |
one model, every action |
'*:update' |
one action, every model — the "hide updates" button |
A mute is a view, not a filter on what is recorded. Events keep arriving and keep being retained, so unmuting shows the history you had been ignoring rather than a gap — which is what makes muting safe to experiment with. A muted kind does not count toward unread and does not toast, because muting something that still nags has not really been muted.
Preferences persist per-app in localStorage; a corrupt or unavailable store degrades to "no mutes" rather than breaking the feed.
The filter UI
Rendered into #notify-filters when the shell provides it, and built from the models actually present in the feed rather than a fixed list: you filter the noise you can see, a model nobody emits never clutters the panel, and a muted model stays listed so the mute can be undone. Without that element, muting still works through configure({mutes}) and the mute/unmute/toggleMute/isMuted/mutes API.
config.titles
model -> (record) => string, for naming an event whose primary key is a UUID.
readableTarget() suppresses UUIDs — right, since a UUID in a sentence is noise — which left a UUID-keyed model rendering as a bare "access request added": no who, no what. Exactly the models where the pk is the least interesting thing about the record. Titles are escaped like any other text, and a throwing title function cannot take the feed down.
Upgrading
No breaking changes. burstMs defaults to 900ms, so toasts are now summarised rather than one-per-event; set burstMs: 0 to keep the old behaviour. Add <div id="notify-filters"></div> to your bell dropdown to get the filter UI.
118/118 (17 new). Full changelog: https://github.com/simpleworkjs/frontend/blob/v0.5.0/CHANGELOG.md