Skip to content

feat(webview): native OS notifications from embedded webview apps (#714)#727

Open
M3gA-Mind wants to merge 22 commits intotinyhumansai:mainfrom
M3gA-Mind:feat/native-os-notification
Open

feat(webview): native OS notifications from embedded webview apps (#714)#727
M3gA-Mind wants to merge 22 commits intotinyhumansai:mainfrom
M3gA-Mind:feat/native-os-notification

Conversation

@M3gA-Mind
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind commented Apr 21, 2026

Summary

  • Wire the tauri-cef notification intercept into tauri-plugin-notification so embedded apps (Slack, Discord, Gmail, WhatsApp) produce real native OS toasts instead of silently dropping them
  • Prefix each toast with the provider label (e.g. [Slack] New message from Alice) for clear attribution; honour silent, icon, and tag from the Web Notification API
  • Record a NotificationRoute keyed by {provider}:{account_id}:{tag_or_uuid} to enable click-to-focus routing once platform delegates (UNUserNotificationCenter / notify-rust) are wired
  • Round-trip OS notification permission via two new invokes (webview_notification_permission_state / webview_notification_permission_request) so the frontend sees "granted" | "denied" | "default" on both CEF and wry runtimes

Problem

Embedded webview apps call window.Notification inside CEF but never produce native macOS/Windows toasts. The CEF runtime intercepts the web Notification API but the intercept was dropped on the floor — no bridge to tauri-plugin-notification, no click routing back to the originating account, no permission query/request pipeline.

Solution

Rust (app/src-tauri/src/webview_accounts/mod.rs)

  • NotificationRoute struct: provider, account_id, tag, created_at
  • notification_routes: Mutex<HashMap<String, NotificationRoute>> on WebviewAccountState
  • clear_notification_routes(account_id) — purged on close/purge
  • forward_native_notification(app, provider, account_id, payload) — prefixes title, respects silent/icon/tag, falls back to monotonic timestamp for untagged payloads
  • webview_notification_permission_state / webview_notification_permission_request — map 4-variant tauri::plugin::PermissionState onto 3-value web API shape; non-CEF stubs always return "default"
  • CEF registration in setup: tauri_runtime_cef::notification::register with handler calling forward_native_notification; unregister on account close

Frontend (app/src/services/webviewAccountService.ts)

  • ensureNotificationPermission(accountId) — invokes permission state, requests if "default", runs once per session; called after openWebviewAccount
  • handleNotificationClick + listen('notification:click', …) — dispatches setActiveAccount and invokes activate_main_window when the dormant platform click hook fires

Capabilities — added notification:allow-notify, notification:allow-request-permission, notification:allow-is-permission-granted

Submission Checklist

  • Unit tests — N/A for this intercept wiring; behavior requires a live CEF instance with a real embedded app firing notifications. Manual verification needed (see below).
  • E2E / integration — Manual: build .app, open Slack/Discord/Gmail in a webview account, trigger a notification, confirm provider-prefixed OS toast appears.
  • Doc comments — All new public functions have doc comments in webview_accounts/mod.rs
  • Inline comments — Route-keying strategy and permission shape mapping documented inline

Impact

  • Desktop only (macOS/Windows/Linux). CEF code path; wry path has no-op stubs.
  • No changes to mobile, core sidecar, JSON-RPC, or non-webview flows.
  • Dormant click listener registered frontend-side; Rust doesn't emit notification:click yet — follow-up work to wire UNUserNotificationCenter / notify-rust delegates.

Related

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Notifications center for viewing and managing notifications with read/unread status tracking.
    • Added notification settings with Do Not Disturb toggle.
    • Added per-provider notification routing configuration with importance threshold controls.

oxoxDev and others added 3 commits April 20, 2026 18:41
tinyhumansai#714)

Forward CEF notification intercept payloads to tauri-plugin-notification,
prefixing the title with the provider label so the source of each toast is
obvious at a glance. Honour `silent` (skip toast, still record route),
`icon` (passed through to the native builder), and `tag` (used as the
dedup key, with a monotonic timestamp fallback for untagged payloads).

Record a NotificationRoute keyed by `{provider}:{account_id}:{tag_or_uuid}`
so a future click hook (UNUserNotificationCenter / notify-rust on_response)
can route the OS click back to the source account. Entries are cleared on
webview_account_close / _purge to bound map growth.

Expose webview_notification_permission_state / _request commands mapping
tauri::plugin::PermissionState onto the web API triple. Non-cef stubs
return "default" so the frontend can call the same invoke names on both
runtimes. Wire notification:allow-* capabilities so the plugin can be
invoked from the webview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nsai#714)

Round-trip the OS notification permission once per session on first
account open via the new invoke pair. Attach a dormant notification:click
listener that dispatches setActiveAccount and brings the main window to
front when a platform click hook starts emitting the event — contract
matches the Rust NotificationRoute shape so the emit side is a one-liner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lockfile picked up the pending 0.52.26 version bump from Cargo.toml
while building the notification feature. No dependency graph change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

This PR implements native OS notifications from embedded webview applications by adding backend RPC handlers, persistence infrastructure, frontend notification UI, permission management, and webview notification API interception via Tauri integration and CEF injection.

Changes

Cohort / File(s) Summary
Tauri Backend Notification Infrastructure
src/openhuman/notifications/{rpc,store,types,schemas}.rs, src/openhuman/notifications/mod.rs
Implements notification domain: RPC handlers for ingest/list/mark-read/settings management, SQLite-backed persistence with filtering/pagination, status enums, request/response types, and schema registry with five controller endpoints. Triage pipeline spawned async on ingest, settings upsert with threshold clamping and default fallback.
Webview Notification Interception
app/src-tauri/src/webview_accounts/{mod.rs,ua_spoof.js}, app/src-tauri/src/cdp/session.rs
Injects notification API shims into CEF webviews: ua_spoof.js overrides navigator.permissions.query() and Notification.permission to return "granted", CDP session injects script to force requestPermission() to resolve "granted". Native logs title character count instead of full string.
Frontend Notification Service Layer
app/src/services/{notificationService.ts,webviewAccountService.ts}
Adds notification RPC wrappers (fetch/mark-read/ingest/settings), webview account service event listeners for notification clicks with account focus/window activation, recipe event handling to ingest notifications, permission ensure helper, and export focus/mute preference functions.
Frontend Notification UI
app/src/components/notifications/{NotificationCard,NotificationCenter}.tsx, app/src/pages/Notifications.tsx
NotificationCard renders individual notifications with status/importance/triage badges and relative timestamps. NotificationCenter loads/filters notifications by provider, displays unread badges, mark-all-read action, and empty states. Notifications page wraps center in full-height container.
Settings & Routing
app/src/components/settings/{SettingsHome.tsx,hooks/useSettingsNavigation.ts}, app/src/components/settings/panels/{NotificationsPanel,NotificationRoutingPanel}.tsx, app/src/pages/Settings.tsx, app/src/AppRoutes.tsx
Adds /notifications route and notification settings routes (/settings/notifications, /settings/notification-routing). NotificationsPanel manages global DND toggle; NotificationRoutingPanel manages per-provider settings (enablement, importance threshold, routing). SettingsRoute union expanded with new routes.
State Management
app/src/store/{notificationsSlice.ts,index.ts}, app/src/types/notifications.ts
Redux slice for notifications state (items, unread count, loading, error) with reducers for loading/error/set/mark-read/add actions. TypeScript types for NotificationStatus, IntegrationNotification, and NotificationSettings. Store integrated into root reducer.
Core Registration
src/openhuman/{mod.rs,...}, src/core/all.rs, src/openhuman/agent/triage/envelope.rs
Registers notifications module as public export; adds TriggerSource::WebviewIntegration variant for webview-originating triggers; controller/schema aggregation includes all notification endpoints in registry validation.
Configuration & Dependencies
app/src-tauri/{Cargo.toml,capabilities/default.json}, app/src-tauri/vendor/tauri-cef, app/src-tauri/src/cdp/emulation.rs
Adds Tauri webview-data-url feature; adds three notification permission capabilities (is-permission-granted, request-permission, notify). Updates vendored tauri-cef commit. Updates Chrome UA string version from 124 to 136 and macOS platform version 14.0→15.0.
Documentation & Testing
docs/SUMMARY.md, tests/json_rpc_e2e.rs, scripts/setup-dev-codesign.sh
Documents Issue #714 implementation outline with architecture, permission pipeline, routing semantics. Adds e2e test for settings roundtrip and disabled-ingest skip behavior. Fixes openssl pkcs12 flag ordering in codesign script.

Sequence Diagram

sequenceDiagram
    participant User
    participant WebviewApp as Webview (CEF)
    participant TauriRuntime as Tauri Runtime
    participant Backend as Backend RPC
    participant Store as SQLite Store
    participant Frontend as Frontend UI
    participant Triage as Triage Pipeline
    participant OS as OS Notification

    User->>WebviewApp: Slack message arrives
    WebviewApp->>WebviewApp: Calls window.Notification("Msg from `@john`")
    Note over WebviewApp: ua_spoof.js intercepts, extracts title/body

    WebviewApp->>TauriRuntime: recipe event: kind='notify'
    TauriRuntime->>Frontend: webview_recipe_event
    Frontend->>Backend: notification:ingest {provider,title,body,raw_payload}
    
    Backend->>Backend: Check provider enabled
    alt disabled
        Backend-->>Frontend: {skipped:true, reason:"..."}
    else enabled
        Backend->>Store: insert notification (status=unread)
        Backend-->>Frontend: {id, skipped:false}
        
        Backend->>Triage: spawn async run_triage()
        Triage->>Store: update_triage {importance_score, action}
        Triage->>Backend: apply_decision if meets threshold
    end

    Frontend->>Frontend: dispatch addNotification()
    Frontend->>OS: invoke tauri notification plugin
    OS->>User: Display native OS notification

    User->>OS: Click notification
    OS->>TauriRuntime: notification:click event
    TauriRuntime->>Frontend: listener receives {account_id, provider}
    Frontend->>Frontend: setActiveAccount(account_id)
    Frontend->>Frontend: setFocusedAccount(account_id)
    Frontend->>TauriRuntime: invoke activate_main_window
    TauriRuntime->>User: Bring OpenHuman to foreground, show Slack webview
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The changes span multiple heterogeneous domains (Rust backend RPC/store/triage, TypeScript/React frontend UI/state/services, CEF/webview interception, settings routing, type definitions) with significant logic density in the store module (SQL schema/queries), RPC handlers (async triage spawning, filtering, settings persistence), frontend Redux integration, and webview API interception. Multiple file categories require separate reasoning despite related functionality.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • senamakel
  • graycyrus

Poem

🐰 Hop hop, notifications now zoom in!
From webviews deep to OS—let them spin.
Tauri pipes the clicks, the permissions grant,
And users see the messages they want.
A feature worthy of the digital dance! 🔔

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(webview): native OS notifications from embedded webview apps (#714)' directly and specifically describes the main change: enabling native OS notifications for embedded webview applications, which is the core objective of the PR.
Linked Issues check ✅ Passed All major objectives from issue #714 are met: native notifications for Slack/Discord/Gmail/WhatsApp via CEF interception and tauri-plugin-notification, permission flow mapping to web shapes, click-to-focus routing by provider/account/tag, attribution via provider prefixing, background delivery via tauri notifications, and no regressions to existing webview functionality.
Out of Scope Changes check ✅ Passed All changes are in scope: notification capture and forwarding (webview_accounts/mod.rs, ua_spoof.js, cdp files), backend notification domain (store/rpc/types), frontend notification UI and services, permission handling, and routing infrastructure. Minor tangential changes (UA bump, codesign script arg reordering, trigger source enum) are necessary support updates and not out of scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src-tauri/src/webview_accounts/mod.rs`:
- Around line 265-275: The info-level log in log::info! is currently emitting
raw_title (user-facing notification text) which may contain PII; remove
raw_title from the info log call in webview_accounts::notify-cef code path (the
log::info! invocation) and either omit it entirely or replace it with a
non-sensitive placeholder/metadata (e.g., title length or a redacted token) and,
if you still need the full title for debugging, log the raw_title at debug or
trace level instead (move it to log::debug! or log::trace! referencing
raw_title). Ensure the format string and argument list for log::info! are
updated to match the removed/replaced field.
- Around line 256-307: The code currently inserts a notification_routes entry
for every notification (using notification_route_key and
WebviewAccountsState::notification_routes) before checking payload.silent or
whether builder.show() succeeds, causing unbounded growth; change the logic so
entries are only stored for notifications that will be routable later: either
move the insert to after the payload.silent check and after builder.show()
returns Ok(), or if you must insert earlier, ensure you remove the route on
payload.silent and on Err(e) from builder.show(). Use the existing symbols
notification_route_key, NotificationRoute, payload.silent, builder.show(), and
state.notification_routes.lock().unwrap().insert/remove to implement the fix.

In `@app/src/services/webviewAccountService.ts`:
- Around line 123-135: ensureNotificationPermission currently sets
permissionChecked = true before the async invokes, so a transient failure leaves
the session marked checked; move the mutation of permissionChecked into the
success path inside ensureNotificationPermission: only set permissionChecked =
true after the initial invoke('webview_notification_permission_state') completes
successfully (and likewise after the follow-up
invoke('webview_notification_permission_request') if performed), and keep the
try/catch so that on error permissionChecked remains false and future calls will
retry; references: ensureNotificationPermission, permissionChecked,
invoke('webview_notification_permission_state'),
invoke('webview_notification_permission_request').
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9119250-e554-4f60-8ade-e9024ddf561f

📥 Commits

Reviewing files that changed from the base of the PR and between 1792135 and e6f6018.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • app/src-tauri/capabilities/default.json
  • app/src-tauri/src/lib.rs
  • app/src-tauri/src/webview_accounts/mod.rs
  • app/src/services/webviewAccountService.ts

Comment thread app/src-tauri/src/webview_accounts/mod.rs Outdated
Comment thread app/src-tauri/src/webview_accounts/mod.rs
Comment thread app/src/services/webviewAccountService.ts
@M3gA-Mind M3gA-Mind marked this pull request as draft April 21, 2026 08:41
M3gA-Mind and others added 17 commits April 21, 2026 15:14
tinyhumansai#679)

- Add NotificationBypassPrefs (global DND, per-account mute, bypass-when-focused)
  to WebviewAccountsState with thread-safe AtomicBool window focus tracking
- Evaluate all three bypass conditions inside forward_native_notification before
  showing OS toast; each suppression path logs at debug with [notify-bypass] prefix
- Add four new Tauri commands: webview_notification_set_dnd,
  webview_notification_mute_account, webview_notification_get_bypass_prefs,
  webview_set_focused_account
- Wire window focus tracking in setup hook via on_window_event Focused handler
- Frontend: add setAccountMuted, setGlobalDnd, getBypassPrefs, setFocusedAccount
  helpers in webviewAccountService; sync focused account on open + click
- Add NotificationsPanel settings page with Global DND toggle
- Register NotificationsPanel at /settings/notifications

Closes tinyhumansai#679
- Added Notifications page and routing to AppRoutes.
- Introduced NotificationRoutingPanel in Settings for managing notification settings.
- Updated SettingsHome to include navigation for notification routing.
- Integrated notifications reducer into the store for state management.
- Enhanced Rust backend to support notification handling from embedded webviews.

This commit lays the groundwork for a comprehensive notification system within the application.
- Simplified JSX structure in NotificationCard for better readability.
- Consolidated fetchNotifications call in NotificationCenter for cleaner syntax.
- Improved formatting in NotificationRoutingPanel and notificationsSlice for consistency.
- Enhanced Rust code readability by streamlining function signatures and logic.

These changes enhance code maintainability and readability across the notifications feature.
…ction signature

- Removed unnecessary line breaks in the webview_notification_set_dnd function for improved readability.
…anagement

- Added `getNotificationSettings` and `setNotificationSettings` functions to manage notification settings for providers.
- Enhanced `NotificationRoutingPanel` to display and update settings for Gmail, Slack, Discord, and WhatsApp.
- Introduced new RPC endpoints for retrieving and updating notification settings.
- Updated database schema to store notification settings persistently.

This commit establishes a robust system for managing notification preferences, improving user control over notifications.
- Enhanced formatting in NotificationRoutingPanel for better clarity.
- Streamlined function signatures in notificationService and Rust backend.
- Improved readability of assertions in tests by adjusting line breaks.

These changes contribute to a more maintainable and comprehensible codebase for the notifications feature.
…nner

Updates the tauri-cef submodule to 55db2d6 which adds a
navigator.permissions.query shim in the CEF render process. Slack checks
this API (not just Notification.permission) to decide whether to show its
"needs your permission" banner — the shim returns "granted" for
notifications queries so the banner no longer appears.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches from context.eval() to a proper PermissionsQueryV8Handler so
the navigator.permissions.query fix actually runs in on_context_created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The V8 set_value_bykey approach in cef-helper's on_context_created does
not stick on CEF platform objects (Chromium's V8 binding layer silently
ignores property writes on native wrappers like Permissions). The init
script path via frame.execute_java_script runs in the fully-initialised
JS context where navigator.permissions IS writable, matching how
ua_spoof.js already overrides navigator.userAgent successfully.

Slack checks navigator.permissions.query({ name: 'notifications' })
before showing its "needs permission" banner — patching it here to
return "granted" removes the banner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ssions

Two-layer fix for the Slack "needs permission to enable notifications" banner:

1. cef-helper (submodule update to 99a2686): context.eval() in
   on_context_created installs Object.defineProperty(navigator, 'permissions',
   ...) before any page JS runs.

2. ua_spoof.js: same Object.defineProperty pattern as belt-and-suspenders
   for frames that reload or trigger permission checks after on_load_end.

Simple property assignment on Blink platform objects is silently ignored;
Object.defineProperty on the navigator wrapper itself (the same mechanism
already used for navigator.userAgent) works correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fication

# Conflicts:
#	Cargo.lock
#	app/src-tauri/Cargo.lock
#	app/src-tauri/src/lib.rs
#	app/src-tauri/src/webview_accounts/mod.rs
#	app/src-tauri/vendor/tauri-cef
#	src/core/all.rs
#	src/openhuman/mod.rs
…ai#727

- Move raw_title out of log::info! (PII risk) — log title_chars at info,
  raw_title at debug only
- Fix permissionChecked set before async invoke in ensureNotificationPermission
  so transient failures allow retry on next account open
The CEF backend uses a data: URL as the initial webview location so CDP
can attach before the real provider URL loads. Tauri's add_child rejects
data: URLs unless the webview-data-url feature is enabled.
@M3gA-Mind M3gA-Mind marked this pull request as ready for review April 22, 2026 09:59
@M3gA-Mind M3gA-Mind requested a review from a team April 22, 2026 09:59
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (1)
app/src/services/webviewAccountService.ts (1)

135-143: ⚠️ Potential issue | 🟠 Major

Only mark the session permission bootstrap complete after the request path succeeds.

permissionChecked flips immediately after the state probe. If webview_notification_permission_request throws once, later openWebviewAccount() calls will skip permission bootstrap for the rest of the session.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/webviewAccountService.ts` around lines 135 - 143, The bug is
that permissionChecked is set to true immediately after probing state so a
subsequent failing call to invoke('webview_notification_permission_request')
will leave the session marked complete; modify ensureNotificationPermission so
that permissionChecked is only set to true after the request path succeeds
(i.e., set permissionChecked = true after a successful
invoke('webview_notification_permission_request') or when state === 'granted'),
and ensure any thrown error from
invoke('webview_notification_permission_request') does not flip
permissionChecked (so leave permissionChecked false and rethrow or handle the
error); update references in ensureNotificationPermission and callers like
openWebviewAccount if needed to rely on the corrected permissionChecked
behavior.
🧹 Nitpick comments (5)
app/src/components/settings/panels/NotificationsPanel.tsx (1)

12-23: Consider adding error handling for resilience.

The async operations lack error handling:

  1. getBypassPrefs() rejection will leave the component in loading state forever (line 15 won't execute)
  2. If setGlobalDnd(next) fails, the UI shows the toggled state but the backend wasn't updated
♻️ Suggested improvement
   useEffect(() => {
-    getBypassPrefs().then(prefs => {
-      if (prefs) setDnd(prefs.global_dnd);
-      setLoading(false);
-    });
+    getBypassPrefs()
+      .then(prefs => {
+        if (prefs) setDnd(prefs.global_dnd);
+      })
+      .catch(err => {
+        console.warn('[NotificationsPanel] Failed to load preferences:', err);
+      })
+      .finally(() => setLoading(false));
   }, []);

   const handleDndToggle = async () => {
     const next = !dnd;
     setDnd(next);
-    await setGlobalDnd(next);
+    try {
+      await setGlobalDnd(next);
+    } catch (err) {
+      console.warn('[NotificationsPanel] Failed to update DND:', err);
+      setDnd(!next); // Revert optimistic update
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/NotificationsPanel.tsx` around lines 12 -
23, Add robust error handling around the async calls in useEffect and
handleDndToggle: wrap the getBypassPrefs() call inside a try/catch/finally in
the useEffect so any rejection is caught, ensure setLoading(false) runs in
finally, and log or surface the error; in handleDndToggle wrap
setGlobalDnd(next) in try/catch and on failure revert the optimistic UI change
by calling setDnd(prev) (or re-read state) and surface/log the error so the UI
and backend remain consistent. Ensure you update functions referenced
(useEffect, getBypassPrefs, setLoading, setDnd, handleDndToggle, setGlobalDnd)
only by adding try/catch/finally and revert logic, not by changing their
signatures.
app/src/services/notificationService.ts (1)

17-22: Prefer named interfaces for the RPC contracts in this file.

opts, the ingest payload, and the settings shapes are all anonymous object types right now. Extracting named interfaces will make the API surface easier to reuse and keep it in line with the repo's TS convention.

As per coding guidelines: **/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript.

Also applies to: 57-80, 92-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/notificationService.ts` around lines 17 - 22, Replace the
anonymous object types with named interfaces: define e.g. interface
NotificationFetchOptions { provider?: string; limit?: number; offset?: number;
min_score?: number } and use it in the fetchNotifications signature
(fetchNotifications(opts?: NotificationFetchOptions)), and similarly extract
interfaces for the ingest payload (e.g. NotificationIngestPayload) and settings
shape (e.g. NotificationSettings) used elsewhere in this file (including the
other function signatures referenced around the later blocks). Update function
parameters and return types to reference these interfaces so the RPC contract is
explicit and reusable, and export the interfaces if they need to be consumed
externally.
app/src/components/settings/panels/NotificationRoutingPanel.tsx (1)

19-24: Extract a shared settings interface instead of repeating the object shape.

The same settings structure is duplicated in the state type and the patch type. A named interface will keep this file aligned with the repo's TS conventions and give you one source of truth.

As per coding guidelines: **/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript.

Also applies to: 52-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/NotificationRoutingPanel.tsx` around lines
19 - 24, Extract the repeated object shape into a named interface (e.g.,
NotificationSetting) and replace the inline type annotations currently used in
the useState declaration for settings and the patch type referenced around the
patch-handling code (the inline Record<string, { enabled: boolean;
importance_threshold: number; route_to_orchestrator: boolean }> and the
identical shape used later at lines ~52-56); update the useState generic to
Record<string, NotificationSetting> and update the patch type to use
NotificationSetting so there is a single source of truth for the settings shape
and it follows the project's preference for interfaces.
src/openhuman/notifications/store.rs (1)

108-153: Consider adding an upper bound on limit to prevent excessive memory usage.

While limit is guaranteed to be numeric (parsed from u64), there's no cap on its value. A client requesting limit: 999999999 could cause memory pressure.

♻️ Proposed fix
 pub fn list(
     config: &Config,
     limit: usize,
     offset: usize,
     provider_filter: Option<&str>,
     min_score: Option<f32>,
 ) -> Result<Vec<IntegrationNotification>> {
+    const MAX_LIMIT: usize = 500;
+    let limit = limit.min(MAX_LIMIT);
     with_connection(config, |conn| {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/store.rs` around lines 108 - 153, In list(),
clamp the input limit to a reasonable maximum before building the SQL to avoid
unbounded allocations: introduce a constant (e.g., MAX_LIMIT), compute let limit
= std::cmp::min(limit, MAX_LIMIT); and use that clamped value when formatting
the LIMIT/OFFSET fragment; update any type conversions so the formatted limit is
a usize/u64 matching MAX_LIMIT and ensure the change is applied inside the
with_connection closure (function name: list, variables: limit, MAX_LIMIT,
with_connection) so the query uses the safe bounded limit.
src/openhuman/notifications/rpc.rs (1)

30-34: Consider adding entry-point debug logging for observability.

Per coding guidelines, handlers should have debug/trace logs at entry points. Adding a brief log here would help trace request flow:

tracing::debug!(
    provider = %req.provider,
    "[notifications::rpc] handle_ingest called"
);

Similar entry-point logging could be added to handle_list, handle_settings_get, and handle_settings_set for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/rpc.rs` around lines 30 - 34, Add entry-point
debug logging to the notification RPC handlers: in handle_ingest, after
deserializing into req, call tracing::debug! to log that handle_ingest was
called and include the provider field (e.g., provider = %req.provider) and a
short context tag like "[notifications::rpc] handle_ingest called"; also add
analogous tracing::debug! calls at the start of handle_list,
handle_settings_get, and handle_settings_set so each handler logs its entry and
any small identifying fields (e.g., provider or request id) to improve
observability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/notifications/NotificationCenter.tsx`:
- Around line 57-58: The current providers pill list is computed from the
filtered items array, so when selectedProvider is applied via
fetchNotifications({provider}) the providers list collapses; instead compute
providers from an unfiltered source (e.g., originalNotifications, allItems, or a
providers list returned by the API) and stop deriving it from items. Update the
providers calculation (the Array.from(new Set(items.map(n =>
n.provider))).sort() expression) to use the unfiltered state or a dedicated
providers array, and ensure code around selectedProvider and fetchNotifications
uses that unfiltered source so the pill row (and the providers.length > 1 check)
remains correct even when items is filtered.
- Line 5: The component NotificationCenter currently imports markReadAction and
setNotifications but never dispatches loading/error actions, so update the fetch
effect inside NotificationCenter to dispatch setNotificationsLoading(true)
before starting the RPC, then on success dispatch setNotifications(payload) and
setNotificationsLoading(false), and on failure dispatch
setNotificationsError(error) (and setNotificationsLoading(false)); ensure you
reference the existing action creators setNotificationsLoading and
setNotificationsError and the imported setNotifications/markReadAction so the
loading and error branches in the render become reachable.

In `@app/src/components/settings/panels/NotificationRoutingPanel.tsx`:
- Around line 50-66: The optimistic update in updateSetting must be rolled back
if persistence fails: capture the previous provider state (e.g., store a copy of
settings[provider] as prevState before calling setSettings), perform the
optimistic setSettings({ ... }), then wrap await setNotificationSettings({
provider, ...next }) in try/catch; on catch restore the previous state via
setSettings(prev => ({ ...prev, [provider]: prevState })) and rethrow or return
a rejected promise so callers can observe the failure; also ensure updateSetting
preserves and propagates the rejection (don’t swallow it) so callers can await
or handle errors.

In `@app/src/components/settings/SettingsHome.tsx`:
- Around line 130-163: Update the settings navigation types and routing logic to
include the new notifications routes: add 'notifications' and
'notification-routing' to the SettingsRoute union in useSettingsNavigation.ts;
update getCurrentRoute() to detect URL paths for '/settings/notifications' and
'/settings/notification-routing' (returning the corresponding SettingsRoute
values) and ensure any switch/case or path-matching logic covers those two new
routes; and extend getBreadcrumbs() to return appropriate breadcrumb arrays when
route is 'notifications' or 'notification-routing' (matching the pattern used by
other routes) so the panels NotificationsPanel and NotificationRoutingPanel are
discoverable and have breadcrumbs.

In `@app/src/services/notificationService.ts`:
- Around line 57-70: ingestNotification currently types its response as { id:
string } even though callCoreRpc (openhuman.notification_ingest) may return {
skipped: true } for disabled-provider flows; update the return type of
ingestNotification to a discriminated union (e.g. { id: string } | { skipped:
true }) and adjust the call signature to callCoreRpc<{ id: string } | { skipped:
true }>, then ensure any downstream usage of ingestNotification checks for the
skipped branch (inspect result.skipped or result.id) so callers no longer assume
an id exists.

In `@docs/SUMMARY.md`:
- Around line 1-127: This SUMMARY.md should either be removed from the PR or
cleaned up and committed: either drop the file from the commit (exclude it from
the PR/diff) or update SUMMARY.md by (1) removing the local worktree path
reference (/Volumes/Extreme SSD/...) near the top, (2) fixing capitalization
from "macos" to "macOS" in the Architecture Notes, and (3) removing the
self-referential deletion note about deleting SUMMARY.md before opening the PR;
then re-stage the cleaned SUMMARY.md (or remove it) and update the commit so the
PR reflects the chosen option.

In `@scripts/install.sh`:
- Around line 296-299: The dry-run branch currently treats a missing release
asset as success; update the branch that checks DRY_RUN to return a non-zero
exit when no compatible ${OS}/${ARCH} asset is found by replacing the current
exit 0 with a failing exit code (e.g., exit 1) and use a more appropriate error
logging call (swap log_warn for log_error) so installer smoke tests detect the
packaging regression; locate the conditional that references DRY_RUN and
log_warn and change the exit code and log level accordingly.

In `@scripts/setup-dev-codesign.sh`:
- Around line 77-80: The openssl pkcs12 invocation currently includes an
unconditional -legacy flag which overrides the earlier probe and
PKCS12_LEGACY_ARGS handling; remove the literal -legacy token from the openssl
pkcs12 command and rely solely on expanding "${PKCS12_LEGACY_ARGS[@]}" so that
-legacy is only passed when the probe added it to PKCS12_LEGACY_ARGS (ensure the
command still uses the array expansion as shown).

In `@src/openhuman/notifications/schemas.rs`:
- Around line 52-95: Update the "ingest" ControllerSchema outputs so clients can
represent the disabled-provider skip path: change the existing FieldSchema for
"id" in the "ingest" ControllerSchema to required: false (since skipped
responses won’t include an id) and add a new FieldSchema named "skipped" with
ty: TypeSchema::Bool and required: false (with a comment like "true when
provider is disabled and ingest was skipped"). This ensures the schema covers
both the normal { id: string } response and the { skipped: true } response used
by openhuman.notification_ingest.

In `@src/openhuman/notifications/store.rs`:
- Around line 283-284: The code currently silently falls back to Utc::now() when
parsing received_at_str fails; change the parse call for received_at
(DateTime<Utc>) to capture the parse error and emit a warning (e.g., using
log::warn! or tracing::warn!) that includes the offending received_at_str and
the parse error, then return Utc::now() as the fallback; specifically replace
the unwrap_or_else on received_at_str.parse() with a closure or match that logs
the warning with received_at_str and the error before returning Utc::now().

---

Duplicate comments:
In `@app/src/services/webviewAccountService.ts`:
- Around line 135-143: The bug is that permissionChecked is set to true
immediately after probing state so a subsequent failing call to
invoke('webview_notification_permission_request') will leave the session marked
complete; modify ensureNotificationPermission so that permissionChecked is only
set to true after the request path succeeds (i.e., set permissionChecked = true
after a successful invoke('webview_notification_permission_request') or when
state === 'granted'), and ensure any thrown error from
invoke('webview_notification_permission_request') does not flip
permissionChecked (so leave permissionChecked false and rethrow or handle the
error); update references in ensureNotificationPermission and callers like
openWebviewAccount if needed to rely on the corrected permissionChecked
behavior.

---

Nitpick comments:
In `@app/src/components/settings/panels/NotificationRoutingPanel.tsx`:
- Around line 19-24: Extract the repeated object shape into a named interface
(e.g., NotificationSetting) and replace the inline type annotations currently
used in the useState declaration for settings and the patch type referenced
around the patch-handling code (the inline Record<string, { enabled: boolean;
importance_threshold: number; route_to_orchestrator: boolean }> and the
identical shape used later at lines ~52-56); update the useState generic to
Record<string, NotificationSetting> and update the patch type to use
NotificationSetting so there is a single source of truth for the settings shape
and it follows the project's preference for interfaces.

In `@app/src/components/settings/panels/NotificationsPanel.tsx`:
- Around line 12-23: Add robust error handling around the async calls in
useEffect and handleDndToggle: wrap the getBypassPrefs() call inside a
try/catch/finally in the useEffect so any rejection is caught, ensure
setLoading(false) runs in finally, and log or surface the error; in
handleDndToggle wrap setGlobalDnd(next) in try/catch and on failure revert the
optimistic UI change by calling setDnd(prev) (or re-read state) and surface/log
the error so the UI and backend remain consistent. Ensure you update functions
referenced (useEffect, getBypassPrefs, setLoading, setDnd, handleDndToggle,
setGlobalDnd) only by adding try/catch/finally and revert logic, not by changing
their signatures.

In `@app/src/services/notificationService.ts`:
- Around line 17-22: Replace the anonymous object types with named interfaces:
define e.g. interface NotificationFetchOptions { provider?: string; limit?:
number; offset?: number; min_score?: number } and use it in the
fetchNotifications signature (fetchNotifications(opts?:
NotificationFetchOptions)), and similarly extract interfaces for the ingest
payload (e.g. NotificationIngestPayload) and settings shape (e.g.
NotificationSettings) used elsewhere in this file (including the other function
signatures referenced around the later blocks). Update function parameters and
return types to reference these interfaces so the RPC contract is explicit and
reusable, and export the interfaces if they need to be consumed externally.

In `@src/openhuman/notifications/rpc.rs`:
- Around line 30-34: Add entry-point debug logging to the notification RPC
handlers: in handle_ingest, after deserializing into req, call tracing::debug!
to log that handle_ingest was called and include the provider field (e.g.,
provider = %req.provider) and a short context tag like "[notifications::rpc]
handle_ingest called"; also add analogous tracing::debug! calls at the start of
handle_list, handle_settings_get, and handle_settings_set so each handler logs
its entry and any small identifying fields (e.g., provider or request id) to
improve observability.

In `@src/openhuman/notifications/store.rs`:
- Around line 108-153: In list(), clamp the input limit to a reasonable maximum
before building the SQL to avoid unbounded allocations: introduce a constant
(e.g., MAX_LIMIT), compute let limit = std::cmp::min(limit, MAX_LIMIT); and use
that clamped value when formatting the LIMIT/OFFSET fragment; update any type
conversions so the formatted limit is a usize/u64 matching MAX_LIMIT and ensure
the change is applied inside the with_connection closure (function name: list,
variables: limit, MAX_LIMIT, with_connection) so the query uses the safe bounded
limit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d117cbe-ad13-4919-be51-41a8b76abe67

📥 Commits

Reviewing files that changed from the base of the PR and between e6f6018 and 70029ba.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • app/src-tauri/Cargo.toml
  • app/src-tauri/src/webview_accounts/mod.rs
  • app/src-tauri/src/webview_accounts/ua_spoof.js
  • app/src/AppRoutes.tsx
  • app/src/components/notifications/NotificationCard.tsx
  • app/src/components/notifications/NotificationCenter.tsx
  • app/src/components/settings/SettingsHome.tsx
  • app/src/components/settings/panels/NotificationRoutingPanel.tsx
  • app/src/components/settings/panels/NotificationsPanel.tsx
  • app/src/pages/Notifications.tsx
  • app/src/pages/Settings.tsx
  • app/src/services/notificationService.ts
  • app/src/services/webviewAccountService.ts
  • app/src/store/index.ts
  • app/src/store/notificationsSlice.ts
  • app/src/types/notifications.ts
  • docs/SUMMARY.md
  • scripts/install.sh
  • scripts/setup-dev-codesign.sh
  • src/openhuman/agent/triage/envelope.rs
  • src/openhuman/notifications/mod.rs
  • src/openhuman/notifications/rpc.rs
  • src/openhuman/notifications/schemas.rs
  • src/openhuman/notifications/store.rs
  • src/openhuman/notifications/types.rs
  • tests/json_rpc_e2e.rs
✅ Files skipped from review due to trivial changes (4)
  • app/src/pages/Notifications.tsx
  • app/src-tauri/Cargo.toml
  • src/openhuman/notifications/mod.rs
  • app/src/types/notifications.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src-tauri/src/webview_accounts/mod.rs

Comment thread app/src/components/notifications/NotificationCenter.tsx Outdated
Comment thread app/src/components/notifications/NotificationCenter.tsx Outdated
Comment thread app/src/components/settings/panels/NotificationRoutingPanel.tsx
Comment on lines +130 to +163
{
id: 'notifications',
title: 'Notifications',
description: 'Do Not Disturb and per-account notification controls',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
),
onClick: () => navigateToSettings('notifications'),
dangerous: false,
},
{
id: 'notification-routing',
title: 'Notification Routing',
description: 'AI importance scoring and orchestrator escalation for integration alerts',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
),
onClick: () => navigateToSettings('notification-routing'),
dangerous: false,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify if SettingsRoute type includes the new notification routes
rg -n "type SettingsRoute" app/src/components/settings/hooks/useSettingsNavigation.ts -A 30 | head -40

# Check if any handling exists for notification routes
rg -n "notification" app/src/components/settings/hooks/useSettingsNavigation.ts

Repository: tinyhumansai/openhuman

Length of output: 712


🏁 Script executed:

#!/bin/bash
# Get the full implementation of getCurrentRoute() and getBreadcrumbs()
rg -n "getCurrentRoute|getBreadcrumbs" app/src/components/settings/hooks/useSettingsNavigation.ts -A 15 | head -80

# Check if there are switch statements that might need updates
rg -n "switch|case" app/src/components/settings/hooks/useSettingsNavigation.ts -B 2 -A 8

Repository: tinyhumansai/openhuman

Length of output: 4082


🏁 Script executed:

#!/bin/bash
# Search for notification-related settings pages
fd -type f "notification" app/src/components/settings/ 2>/dev/null || echo "No notification settings files found"

# Look for any notification settings page implementation
rg -l "notification" app/src/components/settings/ 2>/dev/null | head -10

# Check the actual type definition and confirm the string fallback
sed -n '104,113p' app/src/components/settings/hooks/useSettingsNavigation.ts

Repository: tinyhumansai/openhuman

Length of output: 567


Add missing route definitions for notifications features.

The 'notifications' and 'notification-routing' routes are not defined in the SettingsRoute type union in app/src/components/settings/hooks/useSettingsNavigation.ts, and the corresponding panels (NotificationsPanel.tsx and NotificationRoutingPanel.tsx) exist but are not wired into the navigation system.

Even though navigateToSettings() accepts SettingsRoute | string, allowing the code to compile, the missing type definitions mean:

  • getCurrentRoute() will not properly detect these routes from the URL path
  • getBreadcrumbs() will not provide breadcrumbs for these routes (falls to default empty array)

Add 'notifications' | 'notification-routing' to the SettingsRoute type, add corresponding path checks in getCurrentRoute(), and add breadcrumb cases in getBreadcrumbs().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/SettingsHome.tsx` around lines 130 - 163, Update
the settings navigation types and routing logic to include the new notifications
routes: add 'notifications' and 'notification-routing' to the SettingsRoute
union in useSettingsNavigation.ts; update getCurrentRoute() to detect URL paths
for '/settings/notifications' and '/settings/notification-routing' (returning
the corresponding SettingsRoute values) and ensure any switch/case or
path-matching logic covers those two new routes; and extend getBreadcrumbs() to
return appropriate breadcrumb arrays when route is 'notifications' or
'notification-routing' (matching the pattern used by other routes) so the panels
NotificationsPanel and NotificationRoutingPanel are discoverable and have
breadcrumbs.

Comment thread app/src/services/notificationService.ts
Comment thread docs/SUMMARY.md Outdated
Comment thread scripts/install.sh Outdated
Comment thread scripts/setup-dev-codesign.sh
Comment on lines +52 to +95
"ingest" => ControllerSchema {
namespace: "notification",
function: "ingest",
description: "Ingest a new notification from an embedded webview integration. \
Immediately persists the record and kicks off background triage scoring.",
inputs: vec![
FieldSchema {
name: "provider",
ty: TypeSchema::String,
comment: "Provider slug, e.g. \"gmail\", \"slack\", \"whatsapp\".",
required: true,
},
FieldSchema {
name: "account_id",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "Webview account identifier (optional).",
required: false,
},
FieldSchema {
name: "title",
ty: TypeSchema::String,
comment: "Short notification title / subject.",
required: true,
},
FieldSchema {
name: "body",
ty: TypeSchema::String,
comment: "Notification body or preview text.",
required: true,
},
FieldSchema {
name: "raw_payload",
ty: TypeSchema::Ref("JsonObject"),
comment: "Full raw event payload from the source for downstream use.",
required: true,
},
],
outputs: vec![FieldSchema {
name: "id",
ty: TypeSchema::String,
comment: "UUID of the newly created notification record.",
required: true,
}],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the ingest schema aligned with the disabled-provider skip path.

The new e2e coverage exercises openhuman.notification_ingest returning { skipped: true } when a provider is disabled, but this schema still says the only successful output is id: string. Any schema-driven client, docs view, or controller tooling will assume id is always present and miss the skip case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/schemas.rs` around lines 52 - 95, Update the
"ingest" ControllerSchema outputs so clients can represent the disabled-provider
skip path: change the existing FieldSchema for "id" in the "ingest"
ControllerSchema to required: false (since skipped responses won’t include an
id) and add a new FieldSchema named "skipped" with ty: TypeSchema::Bool and
required: false (with a comment like "true when provider is disabled and ingest
was skipped"). This ensures the schema covers both the normal { id: string }
response and the { skipped: true } response used by
openhuman.notification_ingest.

Comment thread src/openhuman/notifications/store.rs Outdated
…ansai#727

- cdp/emulation: bump Chrome UA 124→136 to pass Slack browser check
- cdp/session: inject Page.addScriptToEvaluateOnNewDocument to stub
  Notification.permission as "granted" and silence provider banners
- notifications/mod.rs + core/all.rs: wire notifications domain into
  the controller registry (fixes unknown-method in json_rpc_e2e tests)
- notifications/schemas: add skipped bool output to ingest schema
- notifications/store: add tracing::warn on datetime parse failure
- notificationService: union return type for ingestNotification
- webviewAccountService: narrow union before accessing result.id
- NotificationCenter: drive loading/error from fetch effect; track
  allProviders separately so filter pills don't collapse on selection
- NotificationRoutingPanel: rollback optimistic update on save failure
- useSettingsNavigation: add notifications/notification-routing routes
- scripts/install.sh: remove silent dry-run exit 0 on asset failure
- scripts/setup-dev-codesign.sh: remove unconditional -legacy flag
- docs/SUMMARY.md: remove worktree path, fix macOS capitalisation,
  remove self-referential deletion note
Auto-apply formatting changes flagged by the pre-push hook:
- prettier reformatted NotificationCenter.tsx and notificationService.ts
- cargo fmt reformatted all.rs, openhuman/mod.rs, notifications/schemas.rs
- NotificationRoutingPanel: move providers array to module scope so
  useEffect dependency array is satisfied without exhaustive-deps warning
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (1)
app/src/services/webviewAccountService.ts (1)

135-146: ⚠️ Potential issue | 🟠 Major

Deduplicate the permission bootstrap and only request on "default".

Two concurrent openWebviewAccount() calls can both enter ensureNotificationPermission() before permissionChecked flips, so this can issue duplicate OS permission requests. It also calls webview_notification_permission_request for "denied", and permissionChecked is set before that second invoke succeeds, so a transient request failure suppresses retries for the rest of the session.

🛠️ Possible fix
-let permissionChecked = false;
+let permissionCheckPromise: Promise<void> | null = null;

 async function ensureNotificationPermission(): Promise<void> {
-  if (permissionChecked) return;
+  if (permissionCheckPromise) return permissionCheckPromise;
   try {
-    const state = await invoke<string>('webview_notification_permission_state');
-    permissionChecked = true;
-    log('notification permission state=%s', state);
-    if (state === 'granted') return;
-    const next = await invoke<string>('webview_notification_permission_request');
-    log('notification permission after request=%s', next);
+    permissionCheckPromise = (async () => {
+      const state = await invoke<string>('webview_notification_permission_state');
+      log('notification permission state=%s', state);
+      if (state !== 'default') return;
+      const next = await invoke<string>('webview_notification_permission_request');
+      log('notification permission after request=%s', next);
+    })();
+    await permissionCheckPromise;
   } catch (err) {
+    permissionCheckPromise = null;
     errLog('notification permission check failed: %o', err);
   }
 }

Also applies to: 716-716

🧹 Nitpick comments (1)
src/openhuman/notifications/schemas.rs (1)

278-279: Rename stale test name for clarity.

The test name says “three functions” but asserts five; renaming avoids confusion during failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/schemas.rs` around lines 278 - 279, Rename the
stale test function all_controller_schemas_covers_three_functions to reflect the
actual assertion count (e.g., all_controller_schemas_covers_five_functions) so
the test name matches its expectations; update the function identifier in the
test declaration and any references to it to avoid confusion when the test
fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src-tauri/src/cdp/session.rs`:
- Around line 146-154: The injected script passed to
cdp.call("Page.addScriptToEvaluateOnNewDocument", ...) currently forces
Notification.permission to "granted" and always resolves
Notification.requestPermission(), which hides the real OS/browser state; change
this so the script consults the real permission state provided by our backend
(do not hardcode "granted") and proxy requestPermission() calls back to our
permission path instead of resolving locally. Locate the injection in session.rs
(the cdp.call invocation and the session_id usage) and replace the hardcoded
shim with one that reads a backend-supplied permission value (or queries it via
an IPC/binding) and routes Notification.requestPermission() through the same
backend permission-check/flow so returned promises reflect the true OS
permission ("default", "denied", or "granted").

In `@app/src/components/notifications/NotificationCenter.tsx`:
- Around line 21-39: The provider pill list (allProviders) can become stale when
a new notification from a previously unseen provider is added in real-time;
update allProviders whenever a notification is added rather than only on
unfiltered fetches. Concretely, in the place that handles incoming notifications
(the same handler that calls setNotifications or addNotification inside
NotificationCenter), check the new notification's provider against allProviders
and, if missing, call setAllProviders(prev => Array.from(new Set([...prev,
newProvider])).sort()); ensure this check runs for single-item additions as well
as batched adds so the pill row is updated immediately.

In `@app/src/components/settings/hooks/useSettingsNavigation.ts`:
- Around line 32-34: getCurrentRoute in useSettingsNavigation doesn't handle the
newly added SettingsRoute entries ('notifications' and 'notification-routing'),
so paths like /settings/notifications fall through to 'home'; update the route
resolution logic inside the getCurrentRoute function to explicitly map
'/settings/notifications' -> 'notifications' and
'/settings/notification-routing' -> 'notification-routing' (use the
SettingsRoute type/enum and any existing pathname parsing used for other
settings routes to keep behavior consistent with the rest of the function).

In `@src/core/all.rs`:
- Around line 148-149: The CLI namespace for "notification" is registered via
controllers.extend(crate::openhuman::notifications::all_notifications_registered_controllers())
but namespace_description() still returns None for "notification", so add a
description entry for "notification" in the namespace_description() function
(match the pattern used for other namespaces) and update/add unit tests to
assert namespace_description("notification") returns the expected string;
reference the controllers.extend(...) call and the namespace_description()
function and ensure tests cover the new mapping.

In `@src/openhuman/notifications/schemas.rs`:
- Around line 89-103: The ingest response schema's outputs vector (the
FieldSchema entries shown) is missing the optional "reason" field returned on
skipped notifications; add a FieldSchema with name: "reason", ty:
TypeSchema::Option(Box::new(TypeSchema::String)), comment: "Optional reason why
the notification was skipped (e.g., provider disabled).", and required: false so
schema-driven clients/docs include the skip reason alongside "skipped" and "id".
- Around line 217-223: The outputs for the settings_set endpoint are missing the
returned normalized settings: update the outputs Vec in the FieldSchema for
settings_set to include a second FieldSchema with name "settings", type matching
the settings schema (e.g., TypeSchema::Ref or the specific settings TypeSchema),
a descriptive comment (e.g., "Normalized settings returned after save."), and
required: true so registry metadata matches the handler return value; modify the
outputs Vec alongside the existing "ok" FieldSchema in the settings_set
definition to reflect both returned fields.

In `@src/openhuman/notifications/store.rs`:
- Around line 47-72: Add debug/trace instrumentation around the notifications
store entry points and DB operations: in with_connection, log entry/exit
(function start and before returning), log the resolved db_path, log before and
after Connection::open, log success/failure of create_dir_all, and log
before/after conn.execute_batch and before calling f(&conn); use the crate's
logging (tracing::debug/trace or log::debug) to record these checkpoints and
include contextual details (db_path.display(), parent.display(), error contexts)
so callers like with_connection, the migration block (conn.execute_batch), and
the invocation f(&conn) are observable; apply the same pattern to the other
store functions mentioned (lines 80-105, 116-152, 163-185, 203-252) at their
entry/exit points, DB calls, branches and error paths.
- Around line 47-72: The with_connection helper opens a new rusqlite Connection
without a busy timeout, causing transient "database is locked" errors under
concurrent access; after creating conn in with_connection (the Connection::open
call) set a busy timeout (e.g., Duration::from_secs(5) or reuse the existing
SQLITE_BUSY_TIMEOUT constant used in memory/tree/store.rs) by calling the
connection's busy_timeout(...) method and propagate any error (with_context
similarly) before running conn.execute_batch(SCHEMA) and invoking f(&conn).
- Around line 293-294: The code silently drops malformed scored_at values by
converting scored_at_str.parse().ok() into None; change the behavior in the
data-loading function that calls row.get(11)? (the code creating scored_at_str
and scored_at) so that parse failures are surfaced instead of silently returning
None — either return an Err with a clear context (including the offending
scored_at_str) or log a warning/error that includes scored_at_str and the parse
error and preserve the original string in the struct (or a separate
"raw_scored_at" field) so on-disk data issues are visible; update the handling
of scored_at (the scored_at_str -> scored_at conversion) accordingly.
- Around line 163-172: The update_triage flow currently ignores the affected-row
count from conn.execute so it returns Ok(()) even when no row matches the given
id; modify the block inside with_connection that calls conn.execute in
update_triage to capture the returned affected row count, and if it is 0 return
a Contextual error (e.g., via anyhow::bail or .with_context) indicating the
notification id was not found; do the same fix for mark_read (the other
conn.execute block) so both functions fail when no rows were updated rather than
silently succeeding.

---

Nitpick comments:
In `@src/openhuman/notifications/schemas.rs`:
- Around line 278-279: Rename the stale test function
all_controller_schemas_covers_three_functions to reflect the actual assertion
count (e.g., all_controller_schemas_covers_five_functions) so the test name
matches its expectations; update the function identifier in the test declaration
and any references to it to avoid confusion when the test fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a3bc41b-4ea1-4fc7-b302-ad474544e756

📥 Commits

Reviewing files that changed from the base of the PR and between 70029ba and 9506513.

⛔ Files ignored due to path filters (1)
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • app/src-tauri/src/cdp/emulation.rs
  • app/src-tauri/src/cdp/session.rs
  • app/src-tauri/vendor/tauri-cef
  • app/src/components/notifications/NotificationCenter.tsx
  • app/src/components/settings/hooks/useSettingsNavigation.ts
  • app/src/components/settings/panels/NotificationRoutingPanel.tsx
  • app/src/services/notificationService.ts
  • app/src/services/webviewAccountService.ts
  • docs/SUMMARY.md
  • scripts/setup-dev-codesign.sh
  • src/core/all.rs
  • src/openhuman/mod.rs
  • src/openhuman/notifications/schemas.rs
  • src/openhuman/notifications/store.rs
✅ Files skipped from review due to trivial changes (4)
  • src/openhuman/mod.rs
  • app/src-tauri/vendor/tauri-cef
  • scripts/setup-dev-codesign.sh
  • app/src-tauri/src/cdp/emulation.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/components/settings/panels/NotificationRoutingPanel.tsx
  • docs/SUMMARY.md
  • app/src/services/notificationService.ts

Comment on lines +146 to +154
cdp.call(
"Page.addScriptToEvaluateOnNewDocument",
json!({
"source": "Object.defineProperty(Notification,'permission',{get:()=>'granted'});\
Notification.requestPermission=()=>Promise.resolve('granted');"
}),
Some(&session_id),
)
.await?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't force the embedded page's Notification API to "granted".

This makes every provider observe a successful permission state/request even when the OS is still "default" or "denied", so apps like Slack/Gmail can suppress their own permission UI while native toasts are still blocked. Feed the real permission state into this shim, and route requestPermission() through the same backend permission path instead of always resolving "granted".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src-tauri/src/cdp/session.rs` around lines 146 - 154, The injected script
passed to cdp.call("Page.addScriptToEvaluateOnNewDocument", ...) currently
forces Notification.permission to "granted" and always resolves
Notification.requestPermission(), which hides the real OS/browser state; change
this so the script consults the real permission state provided by our backend
(do not hardcode "granted") and proxy requestPermission() calls back to our
permission path instead of resolving locally. Locate the injection in session.rs
(the cdp.call invocation and the session_id usage) and replace the hardcoded
shim with one that reads a backend-supplied permission value (or queries it via
an IPC/binding) and routes Notification.requestPermission() through the same
backend permission-check/flow so returned promises reflect the true OS
permission ("default", "denied", or "granted").

Comment on lines +21 to +39
// All providers seen across unfiltered loads — kept separate so the filter
// pill row doesn't collapse when a provider filter is active.
const [allProviders, setAllProviders] = useState<string[]>([]);

// Fetch on mount and when provider filter changes.
useEffect(() => {
let cancelled = false;
const load = async () => {
dispatch(setNotificationsLoading(true));
try {
const result = await fetchNotifications({ provider: selectedProvider, limit: 100 });
if (!cancelled) {
dispatch(setNotifications(result));
// Accumulate providers only from unfiltered loads so the pill row
// stays stable when a filter is active.
if (!selectedProvider) {
const seen = Array.from(new Set(result.items.map(n => n.provider))).sort();
setAllProviders(seen);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the provider pill list synced with live notifications.

allProviders only updates after an unfiltered fetch. If addNotification pushes the first item from a new provider while this screen is open, that notification shows up in items but the new filter pill never appears until a later full reload.

💡 Low-friction fix
-  const providers = allProviders;
+  const providers =
+    selectedProvider === undefined
+      ? Array.from(new Set([...allProviders, ...items.map(n => n.provider)])).sort()
+      : allProviders;

Also applies to: 78-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/notifications/NotificationCenter.tsx` around lines 21 -
39, The provider pill list (allProviders) can become stale when a new
notification from a previously unseen provider is added in real-time; update
allProviders whenever a notification is added rather than only on unfiltered
fetches. Concretely, in the place that handles incoming notifications (the same
handler that calls setNotifications or addNotification inside
NotificationCenter), check the new notification's provider against allProviders
and, if missing, call setAllProviders(prev => Array.from(new Set([...prev,
newProvider])).sort()); ensure this check runs for single-item additions as well
as batched adds so the pill row is updated immediately.

Comment on lines +32 to +34
| 'local-model-debug'
| 'notifications'
| 'notification-routing';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

New settings routes are declared but not resolved in navigation state

Lines 33-34 add new SettingsRoute values, but getCurrentRoute (Line 67 onward) never maps /settings/notifications or /settings/notification-routing. Those pages fall through to 'home', which breaks breadcrumbs and can misroute back-navigation behavior.

Proposed fix
diff --git a/app/src/components/settings/hooks/useSettingsNavigation.ts b/app/src/components/settings/hooks/useSettingsNavigation.ts
@@
     if (path.includes('/settings/ai')) return 'ai';
+    if (path.includes('/settings/notifications')) return 'notifications';
+    if (path.includes('/settings/notification-routing')) return 'notification-routing';
     if (path.includes('/settings/local-model-debug')) return 'local-model-debug';
@@
       case 'developer-options':
+      case 'notifications':
+      case 'notification-routing':
         return [settingsCrumb];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/hooks/useSettingsNavigation.ts` around lines 32 -
34, getCurrentRoute in useSettingsNavigation doesn't handle the newly added
SettingsRoute entries ('notifications' and 'notification-routing'), so paths
like /settings/notifications fall through to 'home'; update the route resolution
logic inside the getCurrentRoute function to explicitly map
'/settings/notifications' -> 'notifications' and
'/settings/notification-routing' -> 'notification-routing' (use the
SettingsRoute type/enum and any existing pathname parsing used for other
settings routes to keep behavior consistent with the rest of the function).

Comment thread src/core/all.rs
Comment on lines +148 to +149
// Integration notification ingest, triage, and per-provider settings
controllers.extend(crate::openhuman::notifications::all_notifications_registered_controllers());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add CLI namespace description for notification (and test coverage).

After registering the new namespace, namespace_description() still returns None for "notification", so CLI help won’t describe these controllers.

🔧 Proposed fix
 pub fn namespace_description(namespace: &str) -> Option<&'static str> {
     match namespace {
+        "notification" => Some(
+            "Integration notifications: ingest, list, read-state, and provider-level settings.",
+        ),
         "about_app" => Some("Catalog the app's user-facing capabilities and where to find them."),
         "app_state" => Some("Expose core-owned app shell state for frontend polling."),
         "auth" => Some("Manage app session and provider credentials."),
     fn namespace_description_known_namespaces() {
+        assert!(namespace_description("notification").is_some());
         assert!(namespace_description("memory").is_some());
         assert!(namespace_description("memory_tree").is_some());
         assert!(namespace_description("billing").is_some());

Also applies to: 202-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/all.rs` around lines 148 - 149, The CLI namespace for "notification"
is registered via
controllers.extend(crate::openhuman::notifications::all_notifications_registered_controllers())
but namespace_description() still returns None for "notification", so add a
description entry for "notification" in the namespace_description() function
(match the pattern used for other namespaces) and update/add unit tests to
assert namespace_description("notification") returns the expected string;
reference the controllers.extend(...) call and the namespace_description()
function and ensure tests cover the new mapping.

Comment on lines +89 to +103
outputs: vec![
FieldSchema {
name: "id",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "UUID of the newly created notification record. Absent when skipped.",
required: false,
},
FieldSchema {
name: "skipped",
ty: TypeSchema::Bool,
comment:
"True when the provider is disabled and the notification was not stored.",
required: true,
},
],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ingest schema is missing the optional reason output used in skip responses.

On disabled providers, runtime returns reason alongside skipped; the schema currently does not model that field, so schema-driven clients/docs lose that signal.

🔧 Proposed schema fix
             outputs: vec![
                 FieldSchema {
                     name: "id",
                     ty: TypeSchema::Option(Box::new(TypeSchema::String)),
                     comment: "UUID of the newly created notification record. Absent when skipped.",
                     required: false,
                 },
                 FieldSchema {
                     name: "skipped",
                     ty: TypeSchema::Bool,
                     comment:
                         "True when the provider is disabled and the notification was not stored.",
                     required: true,
                 },
+                FieldSchema {
+                    name: "reason",
+                    ty: TypeSchema::Option(Box::new(TypeSchema::String)),
+                    comment: "Optional skip reason, e.g. \"provider_disabled\".",
+                    required: false,
+                },
             ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
outputs: vec![
FieldSchema {
name: "id",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "UUID of the newly created notification record. Absent when skipped.",
required: false,
},
FieldSchema {
name: "skipped",
ty: TypeSchema::Bool,
comment:
"True when the provider is disabled and the notification was not stored.",
required: true,
},
],
outputs: vec![
FieldSchema {
name: "id",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "UUID of the newly created notification record. Absent when skipped.",
required: false,
},
FieldSchema {
name: "skipped",
ty: TypeSchema::Bool,
comment:
"True when the provider is disabled and the notification was not stored.",
required: true,
},
FieldSchema {
name: "reason",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "Optional skip reason, e.g. \"provider_disabled\".",
required: false,
},
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/schemas.rs` around lines 89 - 103, The ingest
response schema's outputs vector (the FieldSchema entries shown) is missing the
optional "reason" field returned on skipped notifications; add a FieldSchema
with name: "reason", ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "Optional reason why the notification was skipped (e.g., provider
disabled).", and required: false so schema-driven clients/docs include the skip
reason alongside "skipped" and "id".

Comment on lines +217 to +223
outputs: vec![FieldSchema {
name: "ok",
ty: TypeSchema::Bool,
comment: "True when settings were saved.",
required: true,
}],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

settings_set output schema is incomplete (settings missing).

Line 217 defines only ok, but the handler returns both ok and normalized settings. Please include settings in schema outputs to keep registry metadata accurate.

🔧 Proposed schema fix
-            outputs: vec![FieldSchema {
-                name: "ok",
-                ty: TypeSchema::Bool,
-                comment: "True when settings were saved.",
-                required: true,
-            }],
+            outputs: vec![
+                FieldSchema {
+                    name: "ok",
+                    ty: TypeSchema::Bool,
+                    comment: "True when settings were saved.",
+                    required: true,
+                },
+                FieldSchema {
+                    name: "settings",
+                    ty: TypeSchema::Ref("NotificationSettings"),
+                    comment: "Persisted settings after clamp/normalization.",
+                    required: true,
+                },
+            ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
outputs: vec![FieldSchema {
name: "ok",
ty: TypeSchema::Bool,
comment: "True when settings were saved.",
required: true,
}],
},
outputs: vec![
FieldSchema {
name: "ok",
ty: TypeSchema::Bool,
comment: "True when settings were saved.",
required: true,
},
FieldSchema {
name: "settings",
ty: TypeSchema::Ref("NotificationSettings"),
comment: "Persisted settings after clamp/normalization.",
required: true,
},
],
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/schemas.rs` around lines 217 - 223, The outputs
for the settings_set endpoint are missing the returned normalized settings:
update the outputs Vec in the FieldSchema for settings_set to include a second
FieldSchema with name "settings", type matching the settings schema (e.g.,
TypeSchema::Ref or the specific settings TypeSchema), a descriptive comment
(e.g., "Normalized settings returned after save."), and required: true so
registry metadata matches the handler return value; modify the outputs Vec
alongside the existing "ok" FieldSchema in the settings_set definition to
reflect both returned fields.

Comment on lines +47 to +72
fn with_connection<T>(config: &Config, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {
let db_path = config
.workspace_dir
.join("notifications")
.join("notifications.db");

if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!(
"[notifications::store] failed to create dir {}",
parent.display()
)
})?;
}

let conn = Connection::open(&db_path).with_context(|| {
format!(
"[notifications::store] failed to open DB at {}",
db_path.display()
)
})?;

conn.execute_batch(SCHEMA)
.context("[notifications::store] schema migration failed")?;

f(&conn)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add debug/trace instrumentation around store entry points and DB operations.

This new store opens/migrates the DB and performs inserts, queries, and updates with almost no debug/trace checkpoints. That will make notification-delivery issues much harder to reconstruct in production. As per coding guidelines, "Rust code must use log / tracing at debug or trace level; add substantial development-oriented logs at entry/exit points, branch decisions, external calls, retries, state transitions, and error handling paths".

Also applies to: 80-105, 116-152, 163-185, 203-252

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/store.rs` around lines 47 - 72, Add debug/trace
instrumentation around the notifications store entry points and DB operations:
in with_connection, log entry/exit (function start and before returning), log
the resolved db_path, log before and after Connection::open, log success/failure
of create_dir_all, and log before/after conn.execute_batch and before calling
f(&conn); use the crate's logging (tracing::debug/trace or log::debug) to record
these checkpoints and include contextual details (db_path.display(),
parent.display(), error contexts) so callers like with_connection, the migration
block (conn.execute_batch), and the invocation f(&conn) are observable; apply
the same pattern to the other store functions mentioned (lines 80-105, 116-152,
163-185, 203-252) at their entry/exit points, DB calls, branches and error
paths.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether this store configures any SQLite busy timeout or related lock-handling today.
rg -n -C2 --type=rust 'busy_timeout|PRAGMA\s+busy_timeout|journal_mode' src/openhuman/notifications/store.rs

Repository: tinyhumansai/openhuman

Length of output: 48


🏁 Script executed:

# Check for other SQLite Connection opens in the codebase to see how busy_timeout is handled elsewhere
rg -n 'Connection::open|busy_timeout' --type=rust -A2 | head -100

Repository: tinyhumansai/openhuman

Length of output: 7731


🏁 Script executed:

# Look at the full notifications/store.rs file to understand the module structure and usage patterns
wc -l src/openhuman/notifications/store.rs

Repository: tinyhumansai/openhuman

Length of output: 106


🏁 Script executed:

# Check whether the notifications store module is exposed and used by RPC handlers
rg -n 'notifications.*store|use.*notifications' --type=rust src/core_server/ | head -20

Repository: tinyhumansai/openhuman

Length of output: 122


🏁 Script executed:

# Check the notifications module's public API and entry points
fd 'notifications' --type f --path '*/src/openhuman/*' | head -10

Repository: tinyhumansai/openhuman

Length of output: 299


🏁 Script executed:

# Check the memory/tree/store.rs pattern with busy_timeout
sed -n '340,355p' src/openhuman/memory/tree/store.rs

Repository: tinyhumansai/openhuman

Length of output: 816


🏁 Script executed:

# Find where SQLITE_BUSY_TIMEOUT is defined
rg -n 'SQLITE_BUSY_TIMEOUT\s*=' --type=rust

Repository: tinyhumansai/openhuman

Length of output: 48


🏁 Script executed:

# Check how notifications store is actually used (look for imports/calls)
rg -n 'notifications::store' --type=rust | head -20

Repository: tinyhumansai/openhuman

Length of output: 1687


🏁 Script executed:

# Search for where notifications store is actually invoked (not just internal error messages)
rg -n 'with_connection|notifications.*insert|notifications.*list|notifications.*update_triage' --type=rust src/ | grep -v 'store.rs' | head -20

Repository: tinyhumansai/openhuman

Length of output: 2313


🏁 Script executed:

# Check if SQLITE_BUSY_TIMEOUT is inline or from a dependency
rg -n 'busy_timeout\(' --type=rust -B2 -A2

Repository: tinyhumansai/openhuman

Length of output: 555


🏁 Script executed:

# Look for any API handler or module that calls notifications functions
rg -n 'pub.*fn.*notification|notifications::|Notification' --type=rust src/openhuman/ | grep -v test | head -30

Repository: tinyhumansai/openhuman

Length of output: 3202


🏁 Script executed:

# Check notifications/rpc.rs to see how it calls store operations
sed -n '1,100p' src/openhuman/notifications/rpc.rs

Repository: tinyhumansai/openhuman

Length of output: 3967


🏁 Script executed:

# Check SQLITE_BUSY_TIMEOUT definition
rg -n 'const.*SQLITE_BUSY_TIMEOUT|SQLITE_BUSY_TIMEOUT\s*=' --type=rust -B1 -A1

Repository: tinyhumansai/openhuman

Length of output: 280


🏁 Script executed:

# Check if subconscious store also lacks busy_timeout
sed -n '20,35p' src/openhuman/subconscious/store.rs

Repository: tinyhumansai/openhuman

Length of output: 607


Configure a SQLite busy timeout on each new connection.

This helper opens a fresh connection for every store operation. The notifications store is accessed concurrently (from RPC handlers and background triage tasks), and without a busy timeout configured, transient SQLite file locking can surface as immediate database is locked failures. This follows the established pattern in src/openhuman/memory/tree/store.rs (which uses SQLITE_BUSY_TIMEOUT = Duration::from_secs(5)).

Suggested change
     let conn = Connection::open(&db_path).with_context(|| {
         format!(
             "[notifications::store] failed to open DB at {}",
             db_path.display()
         )
     })?;
+    conn
+        .busy_timeout(std::time::Duration::from_secs(5))
+        .context("[notifications::store] failed to configure sqlite busy timeout")?;
 
     conn.execute_batch(SCHEMA)
         .context("[notifications::store] schema migration failed")?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/store.rs` around lines 47 - 72, The
with_connection helper opens a new rusqlite Connection without a busy timeout,
causing transient "database is locked" errors under concurrent access; after
creating conn in with_connection (the Connection::open call) set a busy timeout
(e.g., Duration::from_secs(5) or reuse the existing SQLITE_BUSY_TIMEOUT constant
used in memory/tree/store.rs) by calling the connection's busy_timeout(...)
method and propagate any error (with_context similarly) before running
conn.execute_batch(SCHEMA) and invoking f(&conn).

Comment on lines +163 to +172
with_connection(config, |conn| {
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE integration_notifications
SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4
WHERE id = ?5",
params![score, action, reason, now, id],
)
.context("[notifications::store] update_triage failed")?;
Ok(())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect whether update_triage checks the affected-row count from rusqlite::Connection::execute.
rg -n -C3 --type=rust 'fn update_triage|let updated = conn\.execute|conn\.execute\(' src/openhuman/notifications/store.rs

Repository: tinyhumansai/openhuman

Length of output: 1922


🏁 Script executed:

#!/bin/bash
# Read the full update_triage function to confirm current state
sed -n '155,175p' src/openhuman/notifications/store.rs

Repository: tinyhumansai/openhuman

Length of output: 661


🏁 Script executed:

#!/bin/bash
# Check the test for update_triage to see if it validates error cases
sed -n '369,420p' src/openhuman/notifications/store.rs

Repository: tinyhumansai/openhuman

Length of output: 2015


🏁 Script executed:

#!/bin/bash
# Compare mark_read (similar UPDATE operation) to see if it has the same pattern
sed -n '176,195p' src/openhuman/notifications/store.rs

Repository: tinyhumansai/openhuman

Length of output: 764


Fail update_triage when the target notification does not exist.

The function ignores the affected-row count from conn.execute(), so a stale id returns Ok(()) even though nothing was updated. The caller would incorrectly believe the triage persisted when the record is already gone. The test only covers the happy path and does not validate this error condition.

Suggested change
         let now = Utc::now().to_rfc3339();
-        conn.execute(
+        let updated = conn.execute(
             "UPDATE integration_notifications
              SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4
              WHERE id = ?5",
             params![score, action, reason, now, id],
         )
         .context("[notifications::store] update_triage failed")?;
+        if updated == 0 {
+            anyhow::bail!("[notifications::store] update_triage missing notification: {id}");
+        }
         Ok(())
     })
 }

Note: mark_read has the same issue at lines 177–189.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with_connection(config, |conn| {
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE integration_notifications
SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4
WHERE id = ?5",
params![score, action, reason, now, id],
)
.context("[notifications::store] update_triage failed")?;
Ok(())
with_connection(config, |conn| {
let now = Utc::now().to_rfc3339();
let updated = conn.execute(
"UPDATE integration_notifications
SET importance_score = ?1, triage_action = ?2, triage_reason = ?3, scored_at = ?4
WHERE id = ?5",
params![score, action, reason, now, id],
)
.context("[notifications::store] update_triage failed")?;
if updated == 0 {
anyhow::bail!("[notifications::store] update_triage missing notification: {id}");
}
Ok(())
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/store.rs` around lines 163 - 172, The
update_triage flow currently ignores the affected-row count from conn.execute so
it returns Ok(()) even when no row matches the given id; modify the block inside
with_connection that calls conn.execute in update_triage to capture the returned
affected row count, and if it is 0 return a Contextual error (e.g., via
anyhow::bail or .with_context) indicating the notification id was not found; do
the same fix for mark_read (the other conn.execute block) so both functions fail
when no rows were updated rather than silently succeeding.

Comment on lines +293 to +294
let scored_at_str: Option<String> = row.get(11)?;
let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| s.parse().ok());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't silently erase malformed scored_at values.

Unlike received_at, a bad scored_at is converted to None with no signal. That makes a persisted notification look unscored and hides on-disk data issues.

Suggested change
     let scored_at_str: Option<String> = row.get(11)?;
-    let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| s.parse().ok());
+    let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| {
+        s.parse().map_err(|e| {
+            tracing::warn!(
+                raw = %s,
+                error = %e,
+                "[notifications::store] invalid scored_at, using None"
+            );
+            e
+        }).ok()
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let scored_at_str: Option<String> = row.get(11)?;
let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| s.parse().ok());
let scored_at_str: Option<String> = row.get(11)?;
let scored_at: Option<DateTime<Utc>> = scored_at_str.and_then(|s| {
s.parse().map_err(|e| {
tracing::warn!(
raw = %s,
error = %e,
"[notifications::store] invalid scored_at, using None"
);
e
}).ok()
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/store.rs` around lines 293 - 294, The code
silently drops malformed scored_at values by converting
scored_at_str.parse().ok() into None; change the behavior in the data-loading
function that calls row.get(11)? (the code creating scored_at_str and scored_at)
so that parse failures are surfaced instead of silently returning None — either
return an Err with a clear context (including the offending scored_at_str) or
log a warning/error that includes scored_at_str and the parse error and preserve
the original string in the struct (or a separate "raw_scored_at" field) so
on-disk data issues are visible; update the handling of scored_at (the
scored_at_str -> scored_at conversion) accordingly.

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.

[Feature] Native OS notifications from embedded webview apps

2 participants