Skip to content

fix(notifications): restore macOS banners + Debug dropdown to verify - #32

Merged
AzimovS merged 3 commits into
mainfrom
feat/notification-debug-dropdown
Apr 20, 2026
Merged

fix(notifications): restore macOS banners + Debug dropdown to verify#32
AzimovS merged 3 commits into
mainfrom
feat/notification-debug-dropdown

Conversation

@AzimovS

@AzimovS AzimovS commented Apr 20, 2026

Copy link
Copy Markdown
Owner

Summary

Two changes that are best shipped together because the first proves the second is real:

  • feat: Debug Notifications dropdown in About (ae62910). A per-type "fire this notification now" dropdown next to "Debug Updater". Uses the real production code path so consent toggles are exercised, not bypassed. Single repro surface for every NotificationType.
  • fix: macOS banners via UNUserNotificationCenter (584dc5b). The fix the dropdown immediately uncovered: notifications reported success and landed in Notification Center, but banners never appeared on macOS 26.

The macOS bug

tauri-plugin-notificationnotify-rustmac-notification-sys uses [NSUserNotificationCenter deliverNotification:]. NSUserNotification was deprecated in macOS 10.14 (2018) and its banner path has been broken on modern macOS for years. On macOS 26, every API contract returns success but no banner ever fires. Upstream fix is stalled: mac-notification-sys PR #51 (draft since Nov 2025), tauri plugins-workspace RFC #2134 (no PR).

What changed

  • On macOS, SystemNotificationHandler::show_notification dispatches to a new macos_un module that uses UNUserNotificationCenter directly via objc2-user-notifications. A UNUserNotificationCenterDelegate whose willPresent returns .banner|.list|.sound is installed once (retained in OnceCell) so foreground-app notifications produce banners — the real fix.
  • On Windows and Linux, tauri-plugin-notification is unchanged.
  • Authorization flows through the real requestAuthorizationWithOptions — which means first launch of this build re-prompts for notification permission. NS and UN grants live in separate slots.
  • NotificationPriority::Critical maps to UNNotificationInterruptionLevel::TimeSensitive, not Critical. Real Critical requires Apple's Critical Alerts entitlement we don't have.

Deps added (macOS-only): objc2, objc2-foundation, objc2-user-notifications, block2.

Testing

Manually verified on macOS 26.4.1 from an ad-hoc-signed debug bundle. All 8 DebugNotificationKind variants produce visible top-right banners within ~1s, each with the Meetily icon and an entry in Notification Center.

  • Recording started → banner
  • Recording stopped → banner
  • Recording paused → banner
  • Recording resumed → banner
  • Transcription complete → banner
  • Meeting reminder → banner
  • System error → banner
  • Generic test → banner
  • Logs: Installed UNUserNotificationCenterDelegate (willPresent → Banner|List|Sound) once, UN present: id=... title=... level=... per send
  • cargo check clean (same preexisting warnings, no new ones)
  • Windows build (cfg-gated, paths untouched, smoke-test in CI if configured)
  • Linux build (same)

Docs updated

  • docs/solutions/build-errors/macos-dev-build-notifications-and-signing.md: new Phase 2: NS → UN migration section covering root cause, solution, re-prompt behavior, and gotchas specific to the new path (cb753b9)
  • frontend/src-tauri/NOTIFICATION_TESTING.md: reworked troubleshooting around UN authorization state, Alert Style, Focus / Scheduled Summary / Deliver Quietly, and the killall usernoted kick (cb753b9)

Caveats for reviewers

  1. First-launch re-prompt. Existing users will see a fresh "Would meetily like to send notifications?" prompt on the first run of this build because the NS-era grant does not transfer. Document in the release notes.
  2. tauri dev still can't deliver bannersUNUserNotificationCenter requires a valid CFBundleIdentifier, which tauri dev doesn't provide. Test from a built bundle (see solution doc).
  3. No fallback to the plugin on UN errors. If addNotificationRequest fails, we surface the error to the caller (same contract as before) rather than silently trying the broken NS path. Logs include the underlying NSError localized description.

Post-Deploy Monitoring & Validation

This is a local desktop app with no production backend — no dashboards, no log queries. Validation is user-driven.

  • Healthy signal (user-reported). Banner fires from About → Debug Notifications within ~1s on macOS 13+.
  • Failure signal (user-reported). "I clicked Debug Notifications and nothing appears." First triage step: await window.__TAURI_INTERNALS__.invoke('get_notification_stats') in DevTools — system_permission_granted must be true. If not, follow the re-prompt procedure in NOTIFICATION_TESTING.md.
  • Rollback trigger. If multiple macOS users report zero banners after a fresh install and they've clicked Allow on the prompt. Rollback is a one-commit revert of 584dc5b — the non-macOS path is unchanged and isolated by cfg.
  • Validation window. Watch the first week of releases; Sherkhan as owner.
  • Cross-platform. Windows/Linux users: no behavior change expected. A regression there means a cfg-gate mistake — inspect frontend/src-tauri/src/notifications/system.rs.

🤖 Generated with Claude Code

AzimovS and others added 3 commits April 18, 2026 16:42
Adds a "Debug Notifications" dropdown next to the existing "Debug
Updater" in the About section. The dropdown exposes every OS
notification type Meetily emits (recording started/stopped/paused/
resumed, transcription complete, meeting reminder, system error,
generic test) and fires each one through the real production code
path so developers and support engineers can validate the notification
channel end-to-end without starting a real meeting.

Backend (Rust):
- New serde-tagged DebugNotificationKind enum (snake_case) and a
  single debug_show_notification(kind) Tauri command that dispatches
  to the existing internal helpers in notifications/commands.rs.
- New show_meeting_reminder_notification helper matching the shape of
  show_recording_paused_notification (no helper previously existed for
  the meeting reminder flow; the method lived only on NotificationManager).
- Command registered in the notification commands group of lib.rs.

Frontend (React):
- About.tsx adds a DropdownMenu styled to match the adjacent Debug
  Updater button (variant=ghost, size=sm, text-xs ml-2) with Bell and
  ChevronDown affordances.
- A DEBUG_NOTIFICATION_ITEMS table maps each kind to a label and to the
  notification_preferences flag on the Rust side. The handler pre-reads
  is_notification_system_ready and get_notification_settings before
  invoking, so it can surface a toast.info explaining otherwise-silent
  suppression when consent is off or the per-type preference is disabled.
  Production suppression behaviour is unchanged.

Docs:
- NOTIFICATION_TESTING.md points at the new UI dropdown as the primary
  per-type test path; the dev-console invoke recipes remain for
  auto-consent / bypass scenarios.

Brainstorm: docs/brainstorms/2026-04-18-notification-debug-dropdown-brainstorm.md
…ionCenter

tauri-plugin-notification -> notify-rust -> mac-notification-sys uses the deprecated
NSUserNotification API whose banner delivery is broken on modern macOS. Notifications
land in Notification Center but no banner ever appears top-right.

Route macOS notifications through UNUserNotificationCenter directly (via objc2-user-
notifications). Install a UNUserNotificationCenterDelegate whose willPresent handler
returns .banner|.list|.sound so foreground-app notifications show banners. Keep the
existing tauri-plugin-notification path for Windows and Linux.

First launch of the new build will re-prompt for notification permission (NS and UN
grants live in separate slots).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Phase 2 section to the macOS notifications solution doc covering the
NSUserNotification→UNUserNotificationCenter migration: new symptoms, root cause, the
UNUserNotificationCenterDelegate willPresent handler, first-launch re-prompt behavior,
and the Critical-Alerts-entitlement gotcha (map Critical→TimeSensitive).

Rewrite NOTIFICATION_TESTING.md troubleshooting around UN authorization state,
Alert Style verification, Focus/Scheduled-Summary/Deliver-Quietly pitfalls, and the
killall usernoted kick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AzimovS
AzimovS merged commit 5f95dd7 into main Apr 20, 2026
2 checks passed
AzimovS added a commit that referenced this pull request Apr 20, 2026
… docs)

- Cfg-gate the `tauri-plugin-notification` fallback blocks in
  show_recording_started_notification and show_recording_stopped_notification to
  #[cfg(not(target_os = "macos"))]. On macOS those paths would route through the
  deprecated NSUserNotification API (which doesn't deliver banners) and could race
  with our UNUserNotificationCenterDelegate via last-writer-wins setDelegate:.
  On macOS, if the manager fails to initialize, log a warn and drop the
  notification rather than silently deliver via the broken path.

- Add per-unsafe-block SAFETY comments throughout macos_un.rs: the define_class!
  attributes (super = NSObject, NSObjectProtocol, UNUserNotificationCenterDelegate,
  method selector), the msg_send![super, init] in BannerDelegate::new, and the
  error_message helper (including a /// # Safety doc).

Closes todos/001, todos/002 from the review of PR #32.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AzimovS added a commit that referenced this pull request Apr 20, 2026
… docs) (#33)

- Cfg-gate the `tauri-plugin-notification` fallback blocks in
  show_recording_started_notification and show_recording_stopped_notification to
  #[cfg(not(target_os = "macos"))]. On macOS those paths would route through the
  deprecated NSUserNotification API (which doesn't deliver banners) and could race
  with our UNUserNotificationCenterDelegate via last-writer-wins setDelegate:.
  On macOS, if the manager fails to initialize, log a warn and drop the
  notification rather than silently deliver via the broken path.

- Add per-unsafe-block SAFETY comments throughout macos_un.rs: the define_class!
  attributes (super = NSObject, NSObjectProtocol, UNUserNotificationCenterDelegate,
  method selector), the msg_send![super, init] in BannerDelegate::new, and the
  error_message helper (including a /// # Safety doc).

Closes todos/001, todos/002 from the review of PR #32.
AzimovS added a commit that referenced this pull request Apr 21, 2026
`UNUserNotificationCenter` dereferences `NSBundle.mainBundle` and throws
`NSInternalInconsistencyException: bundleProxyForCurrentProcess is nil`
when the executable is launched directly (e.g. `pnpm run tauri dev`,
which runs `target/debug/meetily` rather than the bundled `.app`). Since
commits #32/#33 migrated macOS notifications from the legacy
`NSUserNotification` path to UN, every `tauri dev` run crashes at
startup.

Skip the UN calls when `current_exe()` has no `.app` ancestor.
`request_authorization` returns `Err` (not `Ok(false)`) so
`manager.rs` falls into its existing `Err` arm and does not persist
`system_permission_granted = false` — the dev run and the bundled `.app`
share `~/Library/Application Support/com.meetily.ai/`, so persisting
`false` would silently suppress every real notification until the user
re-granted consent. `show()` returns `Ok(())`; callers do not store
state on success.

Bundled `.app` behavior is unchanged.
@AzimovS AzimovS mentioned this pull request Apr 24, 2026
6 tasks
AzimovS added a commit that referenced this pull request Apr 24, 2026
Prepares a release containing the work merged since v0.1.15:
- feat(detection): mic-activity meeting auto-detection on macOS (#35)
- feat(transcription): retry with backoff + in-transcript failure
  placeholder (#39)
- feat(summary): TownHall template (#30), current template name in
  dropdown (#31), specificity prompt tweak (#37)
- feat(remote): test-connection button + model selection surface
  improvements (#28, #29)
- fix(notifications): migrate to UNUserNotificationCenter (#32),
  SAFETY + fallback race fixes (#33), unbundled-dev crash guard (#38),
  drop OS recording banners and flip auto_save default to false (#36)
- chore(about): drop Zackriya services CTA (#40)

Behavior change to call out in release notes: fresh installs (and
users with no stored recording_preferences.json) now default
auto_save to false — audio files are not written to disk unless the
user opts in via Recording Settings. Existing users with saved
preferences are unaffected.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant