close
Skip to content

feat: deep-link a notification by ts on the notifications page (#2018) - #2518

Merged
iamwhatever merged 1 commit into
mainfrom
fix/notification-deep-link-2018
Aug 10, 2026
Merged

feat: deep-link a notification by ts on the notifications page (#2018)#2518
iamwhatever merged 1 commit into
mainfrom
fix/notification-deep-link-2018

Conversation

@bolichen97

Copy link
Copy Markdown
Collaborator

Summary

Push notifications relayed off the gateway (e.g. an ntfy bridge watching the WS notification stream with a Click URL) could open /notifications but not the specific notification the push was about, leaving the user to hunt a busy feed.

The notifications page now honours a deep-link query param:

/notifications?note=<url-encoded ts>
  • Same code path as a tap. The resolved id routes through the page's existing row-select handler, so auto-ack-on-select, read state, and the mobile full-width detail behave identically to a real tap.
  • Async-safe. The id is captured off the URL into component state and resolved once the feed delivers the matching note, so a slow initial fetch cannot drop the deep link.
  • User intent wins. An explicit tap disarms a still-pending deep link — a late-arriving target can no longer steal the selection or ack a note the user never opened.
  • Param consumed with a history REPLACE, so reload/back cannot re-select and re-ack (pinned by a back-navigation test).
  • Desktop reveal is feed-owned. The feed expands a collapsed group_key stack hiding the target and scrolls the row into view via its own container ref (never a document-scoped query, which could hit the bell popover's rows).
  • Quiet degrade. An unknown/expired/cleared id renders the plain page: nothing selected, no ack, no error toast.

No new backend surface: ts already is the store id — every mutating notification API keys on it, and both the push response and the WS envelope carry it, so any pusher already holds the value it needs. Param documented for external pushers in docs/system-specs/features/app-notifications.md (including the percent-encoding requirement — ISO ts contains +).

Closes #2018

Screenshots

Captured against a live gateway serving this build (seeded feed, real deep-link navigation; post-navigation URL verified param-free).

Desktop: plain page Desktop: deep link selects + scrolls + opens detail
plain deep link
Desktop: unknown id degrades quietly Mobile: deep link opens full-width detail
unknown mobile

Testing

  • website: npx tsc -b clean; npx vitest run full suite 11,801 passed (877 files); jscpd 0 clones.
  • New src/test/NotificationsPage.deepLink.test.tsx (6 tests): valid id selects + acks + scrolls; param consumed with REPLACE (back-nav probe asserts the entry is gone and nothing re-acks); deep link survives a slow feed load; explicit tap disarms a pending deep link; collapsed group_key stack expands to reveal the target; unknown id degrades quietly (bystander seeded unacked so the no-ack assertion is falsifiable).
  • Backend: no .py changes (docs only). isort / flake8 / mypy green; full pytest 40,268 passed with 76 failures all pre-existing host-environment classes (this box's /tmp is owned by uid 65534, breaking AppArmor-ownership and unix-socket assertions; KIROCREW_PORT env leak from the live gateway) — verified unrelated by re-running samples with sanitized env.
  • scripts/docs-lint.sh and scripts/check_brand_name.py green.

Pre-push review fleet

Two model-pinned READ-ONLY reviewers ran before push (GPT 5.6 Sol — codex charter; Opus 5 — claude charter + BASE-ref AUTOSDE rules; no fallback models needed).

  • Opus: all six blocking AUTOSDE rules pass. 4 advisories — 3 fixed (deep-link-outranks-user-tap disarm, vacuous ack assertion, replace-vs-push unpinned), 1 resolved structurally (document-scoped query + comment accuracy, superseded by the feed-owned reveal).
  • GPT: Low (collapsed stack hides the deep-linked row) — fixed via the feed-owned reveal. Medium (overlapping fetchNotifications responses can overwrite an optimistic ack) — pre-existing on main: the wholesale fetchNotifications.fulfilled items replacement races the fire-and-forget ack for a plain tapped row too; this PR adds no new fetch. Filed as a follow-up issue rather than expanding this PR's scope.

Push notifications relayed off the gateway (e.g. an ntfy bridge with a
Click URL) could open /notifications but not the specific note the push
was about. The page now honours /notifications?note=<url-encoded ts>:
the id is captured off the URL, the param is consumed with a history
replace (reload/back cannot re-select and re-ack), and the id resolves
against the asynchronously loaded feed through the page's existing
row-select path — so auto-ack-on-select, mobile full-width detail, and
desktop behavior stay identical to a real tap. Desktop additionally
scrolls the matching feed row into view. An unknown/expired id degrades
to the plain page with nothing selected and no error surface.

No new backend surface: ts already is the store id (every mutating
notification API keys on it, and both the push response and the WS
envelope carry it), so any pusher already holds the value it needs.

Closes #2018
@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of eb491b16f098229f3a0486f599a01dd448c2236e — updated in place on each push; does not block merge.

Design-Verdict: PASS

Deep link rides the existing select path with disarm/replace semantics thought through — a proportionate, contract-documented solution to a real push-relay gap.

Watch

  • temp-screenshots/notification-deep-link/ commits ~324KB of PR-illustration PNGs into permanent git history, and the PR body hotlinks them at this SHA — deleting the "temp" directory later breaks the PR's images, keeping it bloats the tree. Host PR screenshots on the PR itself (drag-drop attachment) and drop the directory before merge.

[DESIGN-REVIEWED] eb491b1

@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed eb491b16f098229f3a0486f599a01dd448c2236e — this comment is updated in place on each push.

Review details

I've analyzed the diff thoroughly. Let me verify the resolve/disarm effect interaction once more against the feed's stacking logic — that's the only place with real state-machine complexity, and I've traced it: capture → pending state → resolve-via-handleSelect (which nulls pending, so a user tap disarms), reveal scoped to listRef. All paths are sound. No correctness or lifecycle defect survives falsification.

The only thing outside the feature itself: four binary PNGs committed under a temp-screenshots/ scratch directory, which the PR body itself renders as [image removed] (i.e. unused).

No blocking findings.

FINDING — temp-screenshots/notification-deep-link/*.png — four binary screenshots committed under a scratch temp- dir, unreferenced (PR body shows them as [image removed]) → permanent repo bloat from accidental artifacts → Fix: drop the four PNGs from the PR.

[OPUS-REVIEWED] eb491b1

Verdict parsed from the review's SHA-scoped output markers for commit eb491b16f098229f3a0486f599a01dd448c2236e.

False positive or not applicable? A repository writer can comment:
/ai-review override fable eb491b16f098229f3a0486f599a01dd448c2236e: <one-sentence reason>

@github-actions

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — 🟡 CONCERNS

Advisory UX-level review of eb491b16f098229f3a0486f599a01dd448c2236e — updated in place on each push; does not block merge.

UX-Verdict: CONCERNS

The PR's own desktop screenshot shows the deep-linked note still Unread after selection — the promised auto-ack visibly doesn't stick.

Watch

  • Read state contradicts the pixels. PR claims "auto-ack-on-select … identical to a real tap," but 02-desktop-deep-link-selected.png shows the selected row keeping its unread dot, the detail badged Unread, and the UNREAD stat still 2 (it drops to 1 only in the later 03 capture). The deep-link ack fires during initial feed load — exactly when the acknowledged pre-existing fetchNotifications overwrite race can revert an optimistic ack — so this failure mode is likely routine on this path, not a one-off. User sees a note they just opened still counted unread and must hit "Mark read" manually. Frequency: most deep links; impact: wrong state + extra step; persistence: recurring. Fix: re-verify the live capture, and if the race is the cause, treat the "follow-up issue" as a prerequisite for this flow rather than deferred.
  • Silent degrade gives zero feedback. An expired/cleared id renders "the plain page: nothing selected, no ack, no error" (03 + the degrade test). The user tapped a push about a specific notification; landing on an unexplained generic page reads as "the tap did nothing" and returns them to hunting the feed — the exact problem this PR exists to fix. Rare (only expired ids) but total orientation loss each time. Smallest fix: a dismissible inline line above the feed ("That notification is no longer available") once the initial fetch settles without a match.

[UX-REVIEWED] eb491b1

@github-actions

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of eb491b16f098229f3a0486f599a01dd448c2236e and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] eb491b1

False positive or not applicable? A repository writer can comment:
/ai-review override gpt eb491b16f098229f3a0486f599a01dd448c2236e: <one-sentence reason>

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 10, 2026
@iamwhatever
iamwhatever merged commit 51c23b6 into main Aug 10, 2026
51 checks passed
@iamwhatever
iamwhatever deleted the fix/notification-deep-link-2018 branch August 10, 2026 15:42
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 10, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…otdev#2018) (kirodotdev#2518)

Push notifications relayed off the gateway (e.g. an ntfy bridge with a
Click URL) could open /notifications but not the specific note the push
was about. The page now honours /notifications?note=<url-encoded ts>:
the id is captured off the URL, the param is consumed with a history
replace (reload/back cannot re-select and re-ack), and the id resolves
against the asynchronously loaded feed through the page's existing
row-select path — so auto-ack-on-select, mobile full-width detail, and
desktop behavior stay identical to a real tap. Desktop additionally
scrolls the matching feed row into view. An unknown/expired id degrades
to the plain page with nothing selected and no error surface.

No new backend surface: ts already is the store id (every mutating
notification API keys on it, and both the push response and the WS
envelope carry it), so any pusher already holds the value it needs.

Closes kirodotdev#2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Push notifications cannot deep-link to the notification they are about

2 participants