Closed bug report

Notification for a PR is stuck unread and can't be trashed after its target wiki page was deleted

barrett-land · 2mo ago ·edited ·closed by Colby Knox

A notification in the Notifications tab for an open PR is stuck as unread and cannot be trashed/dismissed because the wiki page the PR targeted was deleted. Deleting the page orphaned the notification, and the trash/dismiss action no longer clears it — the unread count is permanently inflated with no path to inbox-zero.

Steps to reproduce

  1. Open a PR against a wiki page.
  2. A notification for that PR appears in the Notifications tab (unread).
  3. Delete the wiki page the PR was opened for.
  4. In the Notifications tab, try to trash / mark-read / dismiss that PR notification.

Expected

The notification can be marked read and trashed like any other, regardless of whether the underlying page/PR still exists.

Actual

The notification stays stuck as unread, and the trash action is a no-op. There is no way to clear it once its target page is gone.

Impact

An un-clearable notification permanently inflates the unread badge — you can't reach inbox-zero.

Likely cause

The trash/dismiss handler probably resolves the notification's target (page → PR) and fails or no-ops when the page has been soft/hard-deleted, leaving the notification orphaned with no dismiss path. Deleting a page likely doesn't cascade to the notifications/PRs that referenced it.

Suggested fix

  • Make dismissing/trashing a notification independent of whether its target still resolves (tombstone the row rather than dereferencing a dead target).
  • When a page is deleted, cascade: auto-close/orphan its open PRs and mark their notifications dismissible.

Reporter: barrett-land ([email protected]) — can provide the specific PR ID / deleted-page slug on request.

Filed by agent BUG via the symphony mesh.

1 Reply

Colby Knox · 2mo ago

Fixed and live on prod. Thanks for the clear repro, though the cause turned out to be somewhere other than where the report pointed, and the bug is worse than filed.

The page deletion was a red herring. I chased that first. Deleting a page cascades both notification tables (notification_events and pr_notification_events each declare ON DELETE CASCADE on page_id, and the delete path purges pull requests explicitly before dropping the page row). I reproduced a full delete in a test harness and confirmed zero rows survive, so there was no orphan to get stuck on.

The actual cause is an id type mismatch in the inbox JS. Notification ids are opaque strings. Issue events are "42"; PR events are "pr-42", because the server uses that prefix to route the id to the right table. The client ran parseInt() on data-id:

parseInt("pr-42", 10)  // NaN

NaN is falsy, so the single-row mark-read, mark-unread and delete handlers all hit if (id) and returned without calling anything. The bulk handlers were worse: they mapped to NaN, which JSON.stringify turns into null, so the request was sent, matched no rows, and reported success. That is why it presented as a silent no-op rather than an error.

So this affects every PR notification, not only ones whose page was deleted. Your notification was stuck before the page was ever removed. Any PR notification anyone has is un-readable and un-trashable, which is probably worth knowing if you have others sitting in your inbox.

The fix: ids are passed through verbatim now, since the server already accepts both forms. Six call sites, all coercion removed. Regression test pins the working string ids, pins the exact NaN and null values the broken client was sending so the failure mode itself is covered, and adds a source guard so a future refactor cannot quietly reintroduce the coercion.

Your notification should clear now. If it does not, say so and I will look again, since that would mean there is a second cause behind the first.

One thing I did not do: I initially "fixed" this by making the unread counts join the same tables the list does, on the theory that an orphaned row was being counted but not rendered. That was a plausible-sounding fix for a problem that does not exist, and it would have added a join to the bell-badge query that runs on every page load. Reverted it once the reproduction showed the cascade already handles deletion.

Log in to reply.