From a194b333169efb5e25bc05c94567600eec8bb823 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 2 Sep 2026 12:44:04 +0200 Subject: fix(hub): a dismissed notification stays dismissed across a restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a notification navigated to the group and the entry disappeared — the intended behaviour — and it was back on the next launch. Neither half was wrong on its own, which is why it survived. `markRead` drops the entry locally *and* marks it read on the hub, deliberately: "Reading it is the point of clicking it: it goes, here and in the count, rather than sitting there greyed out." The hub honoured that and persisted it. But the startup fetch asked for `/v1/notifications?limit=20` with no filter, and the endpoint returns read and unread alike, so every dismissed notification came straight back. It asks for `unread_only=true` now — a parameter the endpoint already had and already tested. The feed still carried the fossil of the older intent: `class="notif-item ${n.read ? '' : 'notif-unread'}"`, styling for a read entry rendered greyed out, from before clicking meant dismissing. Nothing read reaches the feed any more, so that branch was dead code describing behaviour the application had abandoned — and noticing it is what made the two halves' disagreement visible. Removed. `unread_count` is computed server-side over the whole table and is unaffected by the filter, so the bell is unchanged. test_notification_dismissal.py holds both halves: the API round trip that is the reported bug (list, read, list again), its mirror showing the unfiltered endpoint still returns it — so the fix cannot read as a coincidence — and a static check that the SPA asks for the filter, which is the only one of the three that catches the defect that actually happened. Verified by dropping the parameter again: that one fails, the API tests do not. Read notifications now accumulate unread in the table rather than being deleted. Purge removes them; the volume is small. Making dismissal a delete would suit `purge_notifications`' own docstring — "these are signals, not a record" — but it would leave `/read` a misnomer and `read-all` inconsistent, so it is a separate decision. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'packages/meshbay-hub/src') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 06b6c10..7cc7c83 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -342,7 +342,7 @@ function NotificationFeed({ notifications, onMarkRead, onPurge }) { ${notifications.map(n => html` -
{ // Reading it is the point of clicking it: it goes, here and in the // count, rather than sitting there greyed out. @@ -579,7 +579,13 @@ function App() { if (!user || notifDisabled) { setNotifications([]); setUnreadCount(0); return; } - hubFetch('/v1/notifications?limit=20', { token: user.token }) + // `unread_only`: clicking one is what dismisses it (see markRead), so a + // read notification is a dismissed notification and must not come back on + // the next launch. Without this the two halves disagreed — the click + // removed it here and marked it read on the hub, and the next startup + // asked for everything and put it straight back. `unread_count` is + // computed server-side and is unaffected by the filter. + hubFetch('/v1/notifications?limit=20&unread_only=true', { token: user.token }) .then(data => { setNotifications(data.notifications || []); setUnreadCount(data.unread_count || 0); -- cgit v1.2.3