[PULSE-55] In-app notification center, settings tab, and notifications page #28

Merged
uz1mani merged 13 commits from staging into main 2026-02-16 19:46:02 +00:00
uz1mani commented 2026-02-16 11:22:22 +00:00 (Migrated from github.com)

Work Item

PULSE-55

Summary

  • Add bell icon notification center in header with unread badge and dropdown.
  • Add full "View all" notifications page at /notifications with pagination.
  • Add Notifications tab in organization settings (owners/admins only) with billing/uptime toggles.
  • Poll unread count every 90 seconds.

Changes

Components:

  • components/notifications/NotificationCenter.tsx: Bell dropdown with recent notifications, unread badge, "View all" and "Manage settings" links, empty state, 90s polling.

Pages:

  • app/notifications/page.tsx: Full list of notifications, pagination (load more), mark read / mark all read.

Settings:

  • components/settings/OrganizationSettings.tsx: Notifications tab with toggles for Billing and Uptime; hidden for members.

API:

  • lib/api/notifications.ts: listNotifications, markNotificationRead, markAllNotificationsRead.
  • lib/api/notification-settings.ts: getNotificationSettings, updateNotificationSettings.

Layout:

  • app/layout-content.tsx: Notification center in header.

Test Plan

  • Bell icon shows unread badge when there are unread notifications.
  • Dropdown shows recent notifications and links to "View all" and "Manage settings".
  • Full notifications page loads and paginates.
  • Mark read / mark all read updates UI and unread count.
  • Unread count updates every 90 seconds.
  • Notifications tab appears in org settings for owners/admins; hidden for members.
  • Billing and Uptime toggles save and reflect backend state.
## Work Item PULSE-55 ## Summary - Add bell icon notification center in header with unread badge and dropdown. - Add full "View all" notifications page at /notifications with pagination. - Add Notifications tab in organization settings (owners/admins only) with billing/uptime toggles. - Poll unread count every 90 seconds. ## Changes **Components:** - `components/notifications/NotificationCenter.tsx`: Bell dropdown with recent notifications, unread badge, "View all" and "Manage settings" links, empty state, 90s polling. **Pages:** - `app/notifications/page.tsx`: Full list of notifications, pagination (load more), mark read / mark all read. **Settings:** - `components/settings/OrganizationSettings.tsx`: Notifications tab with toggles for Billing and Uptime; hidden for members. **API:** - `lib/api/notifications.ts`: `listNotifications`, `markNotificationRead`, `markAllNotificationsRead`. - `lib/api/notification-settings.ts`: `getNotificationSettings`, `updateNotificationSettings`. **Layout:** - `app/layout-content.tsx`: Notification center in header. ## Test Plan - [x] Bell icon shows unread badge when there are unread notifications. - [x] Dropdown shows recent notifications and links to "View all" and "Manage settings". - [x] Full notifications page loads and paginates. - [x] Mark read / mark all read updates UI and unread count. - [x] Unread count updates every 90 seconds. - [x] Notifications tab appears in org settings for owners/admins; hidden for members. - [x] Billing and Uptime toggles save and reflect backend state.
greptile-apps[bot] commented 2026-02-16 11:25:34 +00:00 (Migrated from github.com)

Greptile Summary

This PR adds a complete in-app notification system: a bell icon dropdown in the header (NotificationCenter), a full /notifications page with pagination, and a Notifications settings tab in organization settings for owners/admins. It also introduces two new API client modules for notifications and notification settings, bumps the app version to 0.6.0-alpha, and updates the UI package.

  • Notification center dropdown with unread badge, 90-second background polling, click-outside-to-close, and mark-all-read functionality
  • Full notifications page at /notifications with offset-based pagination ("Load more"), mark-read on click, and mark-all-read
  • Notification settings tab in org settings with toggle switches for notification categories (billing, uptime), restricted to owners/admins
  • Bug: handleMarkAllRead in the notifications page discards the actual API error and constructs a new generic Error, losing status-code-specific error messages
  • Style: BellIcon SVG component and formatTimeAgo/getTypeIcon helper functions are duplicated across NotificationCenter.tsx and notifications/page.tsx / OrganizationSettings.tsx — consider extracting to shared modules

Confidence Score: 4/5

  • This PR is safe to merge with one minor bug fix needed in error handling.
  • The notification system is well-architected with proper auth guards, defensive API response handling, and good UX patterns (deferred loading, optimistic updates). One concrete bug exists in the notifications page where the actual API error is discarded in handleMarkAllRead. Code duplication across files is a style concern but not a blocker.
  • app/notifications/page.tsx has a bug in error handling for mark-all-read. components/settings/OrganizationSettings.tsx has a minor stale closure concern in the toggle handler.

Important Files Changed

Filename Overview
components/notifications/NotificationCenter.tsx New bell icon dropdown component with unread badge, 90s polling, click-outside close, and mark-all-read. Well-structured with deferred loading indicator. Duplicates BellIcon and formatTimeAgo/getTypeIcon helpers with the notifications page.
app/notifications/page.tsx Full notifications list page with pagination and mark-read functionality. Has a bug in handleMarkAllRead error handling that discards the actual API error. Duplicates helper functions with NotificationCenter.
components/settings/OrganizationSettings.tsx Adds Notifications tab with toggle switches for notification categories. Properly hidden from members. Uses optimistic UI updates for toggles with rollback on error. Duplicates BellIcon SVG component already defined in NotificationCenter.
app/layout-content.tsx Adds NotificationCenter to header via rightSideActions prop. Defensively wraps getUserOrganizations result with Array.isArray check. Clean integration.
lib/api/notifications.ts Clean API client for notifications endpoints. Properly typed interfaces and uses shared apiRequest client. Well-structured query parameter building.
lib/api/notification-settings.ts Minimal API client for notification settings GET and PATCH. Clean and correct.
package.json Version bump to 0.6.0-alpha, UI package update, adds @radix-ui/react-icons. TypeScript changed from ^5.5.4 range to pinned 5.9.3.
CHANGELOG.md Comprehensive changelog entry for 0.6.0-alpha covering all new notification features.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant NC as NotificationCenter
    participant NP as Notifications Page
    participant OS as Org Settings
    participant API as Backend API

    Note over NC: On mount (every 90s)
    NC->>API: GET /api/v1/notifications?limit=1
    API-->>NC: { unread_count }
    NC->>NC: Update badge

    Note over NC: On dropdown open
    NC->>API: GET /api/v1/notifications
    API-->>NC: { notifications, unread_count }

    U->>NC: Click notification
    NC->>API: PATCH /api/v1/notifications/{id}/read
    NC->>NC: Close dropdown, navigate

    U->>NC: Click "Mark all read"
    NC->>API: POST /api/v1/notifications/mark-all-read

    Note over NP: On page load
    NP->>API: GET /api/v1/notifications?limit=50&offset=0
    API-->>NP: { notifications, unread_count }

    U->>NP: Click "Load more"
    NP->>API: GET /api/v1/notifications?limit=50&offset=50

    Note over OS: On Notifications tab open
    OS->>API: GET /api/v1/notification-settings
    API-->>OS: { settings, categories }

    U->>OS: Toggle category
    OS->>API: PATCH /api/v1/notification-settings
    API-->>OS: 200 OK

Last reviewed commit: 4a48945

<h3>Greptile Summary</h3> This PR adds a complete in-app notification system: a bell icon dropdown in the header (`NotificationCenter`), a full `/notifications` page with pagination, and a Notifications settings tab in organization settings for owners/admins. It also introduces two new API client modules for notifications and notification settings, bumps the app version to 0.6.0-alpha, and updates the UI package. - **Notification center dropdown** with unread badge, 90-second background polling, click-outside-to-close, and mark-all-read functionality - **Full notifications page** at `/notifications` with offset-based pagination ("Load more"), mark-read on click, and mark-all-read - **Notification settings tab** in org settings with toggle switches for notification categories (billing, uptime), restricted to owners/admins - **Bug**: `handleMarkAllRead` in the notifications page discards the actual API error and constructs a new generic `Error`, losing status-code-specific error messages - **Style**: `BellIcon` SVG component and `formatTimeAgo`/`getTypeIcon` helper functions are duplicated across `NotificationCenter.tsx` and `notifications/page.tsx` / `OrganizationSettings.tsx` — consider extracting to shared modules <h3>Confidence Score: 4/5</h3> - This PR is safe to merge with one minor bug fix needed in error handling. - The notification system is well-architected with proper auth guards, defensive API response handling, and good UX patterns (deferred loading, optimistic updates). One concrete bug exists in the notifications page where the actual API error is discarded in handleMarkAllRead. Code duplication across files is a style concern but not a blocker. - `app/notifications/page.tsx` has a bug in error handling for mark-all-read. `components/settings/OrganizationSettings.tsx` has a minor stale closure concern in the toggle handler. <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | components/notifications/NotificationCenter.tsx | New bell icon dropdown component with unread badge, 90s polling, click-outside close, and mark-all-read. Well-structured with deferred loading indicator. Duplicates BellIcon and formatTimeAgo/getTypeIcon helpers with the notifications page. | | app/notifications/page.tsx | Full notifications list page with pagination and mark-read functionality. Has a bug in handleMarkAllRead error handling that discards the actual API error. Duplicates helper functions with NotificationCenter. | | components/settings/OrganizationSettings.tsx | Adds Notifications tab with toggle switches for notification categories. Properly hidden from members. Uses optimistic UI updates for toggles with rollback on error. Duplicates BellIcon SVG component already defined in NotificationCenter. | | app/layout-content.tsx | Adds NotificationCenter to header via rightSideActions prop. Defensively wraps getUserOrganizations result with Array.isArray check. Clean integration. | | lib/api/notifications.ts | Clean API client for notifications endpoints. Properly typed interfaces and uses shared apiRequest client. Well-structured query parameter building. | | lib/api/notification-settings.ts | Minimal API client for notification settings GET and PATCH. Clean and correct. | | package.json | Version bump to 0.6.0-alpha, UI package update, adds @radix-ui/react-icons. TypeScript changed from ^5.5.4 range to pinned 5.9.3. | | CHANGELOG.md | Comprehensive changelog entry for 0.6.0-alpha covering all new notification features. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant U as User (Browser) participant NC as NotificationCenter participant NP as Notifications Page participant OS as Org Settings participant API as Backend API Note over NC: On mount (every 90s) NC->>API: GET /api/v1/notifications?limit=1 API-->>NC: { unread_count } NC->>NC: Update badge Note over NC: On dropdown open NC->>API: GET /api/v1/notifications API-->>NC: { notifications, unread_count } U->>NC: Click notification NC->>API: PATCH /api/v1/notifications/{id}/read NC->>NC: Close dropdown, navigate U->>NC: Click "Mark all read" NC->>API: POST /api/v1/notifications/mark-all-read Note over NP: On page load NP->>API: GET /api/v1/notifications?limit=50&offset=0 API-->>NP: { notifications, unread_count } U->>NP: Click "Load more" NP->>API: GET /api/v1/notifications?limit=50&offset=50 Note over OS: On Notifications tab open OS->>API: GET /api/v1/notification-settings API-->>OS: { settings, categories } U->>OS: Toggle category OS->>API: PATCH /api/v1/notification-settings API-->>OS: 200 OK ``` <sub>Last reviewed commit: 4a48945</sub>
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-16 11:25:40 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

9 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

<sub>9 files reviewed, 2 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -0,0 +1,231 @@
'use client'
greptile-apps[bot] (Migrated from github.com) commented 2026-02-16 11:25:38 +00:00

Actual API error is discarded

The catch block creates a new Error('Failed to mark all as read') instead of passing the caught error to getAuthErrorMessage. Since getAuthErrorMessage checks for .status on the error to return the appropriate message, this new plain Error will always yield 'Something went wrong, please try again.' regardless of the actual failure reason (e.g. 401 session expired vs. 500 server error).

    } catch (err) {
      toast.error(getAuthErrorMessage(err as Error) || 'Failed to mark all as read')
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/notifications/page.tsx
Line: 102:103

Comment:
**Actual API error is discarded**

The catch block creates a `new Error('Failed to mark all as read')` instead of passing the caught error to `getAuthErrorMessage`. Since `getAuthErrorMessage` checks for `.status` on the error to return the appropriate message, this new plain `Error` will always yield `'Something went wrong, please try again.'` regardless of the actual failure reason (e.g. 401 session expired vs. 500 server error).

```suggestion
    } catch (err) {
      toast.error(getAuthErrorMessage(err as Error) || 'Failed to mark all as read')
```

How can I resolve this? If you propose a fix, please make it concise.
**Actual API error is discarded** The catch block creates a `new Error('Failed to mark all as read')` instead of passing the caught error to `getAuthErrorMessage`. Since `getAuthErrorMessage` checks for `.status` on the error to return the appropriate message, this new plain `Error` will always yield `'Something went wrong, please try again.'` regardless of the actual failure reason (e.g. 401 session expired vs. 500 server error). ```suggestion } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to mark all as read') ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/notifications/page.tsx Line: 102:103 Comment: **Actual API error is discarded** The catch block creates a `new Error('Failed to mark all as read')` instead of passing the caught error to `getAuthErrorMessage`. Since `getAuthErrorMessage` checks for `.status` on the error to return the appropriate message, this new plain `Error` will always yield `'Something went wrong, please try again.'` regardless of the actual failure reason (e.g. 401 session expired vs. 500 server error). ```suggestion } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to mark all as read') ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-16 11:25:39 +00:00

Stale closure in optimistic rollback

The .catch rollback on line 1024 captures notificationSettings from the closure at the time of the click. If a user rapidly toggles two different categories (the isSavingNotificationSettings flag disables buttons, but there's still a brief race window between setIsSavingNotificationSettings(true) and the React re-render that disables the buttons), the rollback could restore incorrect state.

Consider capturing the previous state explicitly before the optimistic update:

const prev = { ...notificationSettings }
const next = { ...notificationSettings, [cat.id]: notificationSettings[cat.id] === false }
setNotificationSettings(next)
setIsSavingNotificationSettings(true)
updateNotificationSettings(next)
  .then(() => toast.success('Notification settings updated'))
  .catch((err) => {
    toast.error(getAuthErrorMessage(err) || 'Failed to update settings')
    setNotificationSettings(prev)
  })
  .finally(() => setIsSavingNotificationSettings(false))
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 1014:1025

Comment:
**Stale closure in optimistic rollback**

The `.catch` rollback on line 1024 captures `notificationSettings` from the closure at the time of the click. If a user rapidly toggles two different categories (the `isSavingNotificationSettings` flag disables buttons, but there's still a brief race window between `setIsSavingNotificationSettings(true)` and the React re-render that disables the buttons), the rollback could restore incorrect state.

Consider capturing the previous state explicitly before the optimistic update:

```
const prev = { ...notificationSettings }
const next = { ...notificationSettings, [cat.id]: notificationSettings[cat.id] === false }
setNotificationSettings(next)
setIsSavingNotificationSettings(true)
updateNotificationSettings(next)
  .then(() => toast.success('Notification settings updated'))
  .catch((err) => {
    toast.error(getAuthErrorMessage(err) || 'Failed to update settings')
    setNotificationSettings(prev)
  })
  .finally(() => setIsSavingNotificationSettings(false))
```

How can I resolve this? If you propose a fix, please make it concise.
**Stale closure in optimistic rollback** The `.catch` rollback on line 1024 captures `notificationSettings` from the closure at the time of the click. If a user rapidly toggles two different categories (the `isSavingNotificationSettings` flag disables buttons, but there's still a brief race window between `setIsSavingNotificationSettings(true)` and the React re-render that disables the buttons), the rollback could restore incorrect state. Consider capturing the previous state explicitly before the optimistic update: ``` const prev = { ...notificationSettings } const next = { ...notificationSettings, [cat.id]: notificationSettings[cat.id] === false } setNotificationSettings(next) setIsSavingNotificationSettings(true) updateNotificationSettings(next) .then(() => toast.success('Notification settings updated')) .catch((err) => { toast.error(getAuthErrorMessage(err) || 'Failed to update settings') setNotificationSettings(prev) }) .finally(() => setIsSavingNotificationSettings(false)) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 1014:1025 Comment: **Stale closure in optimistic rollback** The `.catch` rollback on line 1024 captures `notificationSettings` from the closure at the time of the click. If a user rapidly toggles two different categories (the `isSavingNotificationSettings` flag disables buttons, but there's still a brief race window between `setIsSavingNotificationSettings(true)` and the React re-render that disables the buttons), the rollback could restore incorrect state. Consider capturing the previous state explicitly before the optimistic update: ``` const prev = { ...notificationSettings } const next = { ...notificationSettings, [cat.id]: notificationSettings[cat.id] === false } setNotificationSettings(next) setIsSavingNotificationSettings(true) updateNotificationSettings(next) .then(() => toast.success('Notification settings updated')) .catch((err) => { toast.error(getAuthErrorMessage(err) || 'Failed to update settings') setNotificationSettings(prev) }) .finally(() => setIsSavingNotificationSettings(false)) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-16 19:34:44 +00:00
uz1mani (Migrated from github.com) commented 2026-02-16 19:34:44 +00:00

Issue: The .catch rollback used notificationSettings from the closure, which can be stale if the user toggles quickly before React re-renders, so the rollback could restore the wrong state.
Fix: Store the previous state before the optimistic update: const prev = { ...notificationSettings } and roll back with setNotificationSettings(prev) in the .catch handler.
Why: The closure captures whatever notificationSettings was at click time. On rapid toggles, the catch handler can see an older value, so the rollback must use the exact state from when the update was started.

Issue: The .catch rollback used notificationSettings from the closure, which can be stale if the user toggles quickly before React re-renders, so the rollback could restore the wrong state. Fix: Store the previous state before the optimistic update: const prev = { ...notificationSettings } and roll back with setNotificationSettings(prev) in the .catch handler. Why: The closure captures whatever notificationSettings was at click time. On rapid toggles, the catch handler can see an older value, so the rollback must use the exact state from when the update was started.
uz1mani (Migrated from github.com) reviewed 2026-02-16 19:34:52 +00:00
@@ -0,0 +1,231 @@
'use client'
uz1mani (Migrated from github.com) commented 2026-02-16 19:34:52 +00:00

Issue: The catch block was creating new Error('Failed to mark all as read') and passing it to getAuthErrorMessage, so the original API error was lost and users always saw a generic message even for auth-specific failures (e.g. 401).
Fix: Use the caught error: catch (err) and getAuthErrorMessage(err as Error) || 'Failed to mark all as read' so auth-related errors surface correctly and other errors still show a fallback message.
Why: getAuthErrorMessage relies on the real error (e.g. .status) to choose the message. Without the actual error, auth failures like expired sessions cannot be distinguished from generic server errors.

Issue: The catch block was creating new Error('Failed to mark all as read') and passing it to getAuthErrorMessage, so the original API error was lost and users always saw a generic message even for auth-specific failures (e.g. 401). Fix: Use the caught error: catch (err) and getAuthErrorMessage(err as Error) || 'Failed to mark all as read' so auth-related errors surface correctly and other errors still show a fallback message. Why: getAuthErrorMessage relies on the real error (e.g. .status) to choose the message. Without the actual error, auth failures like expired sessions cannot be distinguished from generic server errors.
greptile-apps[bot] commented 2026-02-16 19:38:15 +00:00 (Migrated from github.com)

Greptile Summary

This PR adds a complete in-app notification system: a bell icon dropdown in the header (NotificationCenter), a full /notifications page with pagination, and a Notifications settings tab in organization settings for owners/admins to toggle billing and uptime notification categories.

  • NotificationCenter renders in the header for authenticated users, polls unread count every 90s, and shows a dropdown with recent notifications, mark-all-read, and links to the full page and settings.
  • Notifications page (/notifications) lists all notifications with load-more pagination, mark-read on click, and mark-all-read with toast feedback.
  • Organization Settings gains a Notifications tab (hidden from members) with toggle switches for each notification category using optimistic updates with rollback.
  • API clients (lib/api/notifications.ts, lib/api/notification-settings.ts) follow existing apiRequest patterns.
  • Version bumped to 0.6.0-alpha, @ciphera-net/ui updated to ^0.0.50, @radix-ui/react-icons added explicitly, TypeScript pinned to exact 5.9.3.

Issues found:

  • The member-redirect effect for the Notifications tab fires after loadNotificationSettings, causing a wasted 403 API call and brief error toast for members who navigate to ?tab=notifications directly.

Confidence Score: 4/5

  • This PR is safe to merge with one minor race condition to address in the member redirect flow.
  • The notification system is well-structured with proper error handling, defensive checks, and follows existing codebase patterns. The only functional issue is a race condition where members navigating directly to the notifications tab trigger an unnecessary API call before the redirect fires. Previous thread issues (error discarding, stale closure rollback) have been addressed.
  • Pay attention to components/settings/OrganizationSettings.tsx — the member redirect race condition on the notifications tab.

Important Files Changed

Filename Overview
components/notifications/NotificationCenter.tsx New bell icon dropdown component with 90s polling, click-outside close, and mark-read functionality. Clean implementation with deferred loading state and proper event cleanup.
app/notifications/page.tsx Full notifications page with pagination, mark-read, and mark-all-read. Handles auth/org guards and error states well. Previous thread issue (error discarding) has been addressed.
components/settings/OrganizationSettings.tsx New Notifications tab with toggle switches for notification categories. Member redirect has a race condition where the API call fires before the redirect effect kicks in.
lib/api/notifications.ts Clean API client for notifications with list, mark-read, and mark-all-read endpoints. Follows existing patterns in the codebase.
lib/api/notification-settings.ts Minimal API client for get/update notification settings. Follows existing patterns.
app/layout-content.tsx Added NotificationCenter to header via rightSideActions prop, guarded behind auth check. Also added defensive Array.isArray check for organizations response.
CHANGELOG.md Comprehensive changelog entry for 0.6.0-alpha documenting all new notification features.
package.json Version bump to 0.6.0-alpha, UI lib updated, @radix-ui/react-icons added explicitly, TypeScript pinned from ^5.5.4 to exact 5.9.3.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant NC as NotificationCenter
    participant NP as Notifications Page
    participant OS as Org Settings
    participant API as Backend API

    Note over NC: Mounted in Header (auth users only)
    NC->>API: GET /notifications?limit=1 (poll unread count)
    API-->>NC: { unread_count }
    loop Every 90 seconds
        NC->>API: GET /notifications?limit=1
        API-->>NC: { unread_count }
    end

    U->>NC: Click bell icon
    NC->>API: GET /notifications
    API-->>NC: { notifications, unread_count }
    NC-->>U: Show dropdown with recent notifications

    U->>NC: Click notification
    NC->>API: PATCH /notifications/{id}/read
    API-->>NC: 200 OK

    U->>NP: Navigate to /notifications
    NP->>API: GET /notifications?limit=50&offset=0
    API-->>NP: { notifications, unread_count }
    U->>NP: Click "Load more"
    NP->>API: GET /notifications?limit=50&offset=50
    API-->>NP: { notifications, unread_count }
    U->>NP: Click "Mark all read"
    NP->>API: POST /notifications/mark-all-read
    API-->>NP: 200 OK

    U->>OS: Navigate to Org Settings → Notifications
    OS->>API: GET /notification-settings
    API-->>OS: { settings, categories }
    U->>OS: Toggle category
    OS->>API: PATCH /notification-settings { settings }
    API-->>OS: 200 OK

Last reviewed commit: 56b99df

<h3>Greptile Summary</h3> This PR adds a complete in-app notification system: a bell icon dropdown in the header (`NotificationCenter`), a full `/notifications` page with pagination, and a Notifications settings tab in organization settings for owners/admins to toggle billing and uptime notification categories. - **NotificationCenter** renders in the header for authenticated users, polls unread count every 90s, and shows a dropdown with recent notifications, mark-all-read, and links to the full page and settings. - **Notifications page** (`/notifications`) lists all notifications with load-more pagination, mark-read on click, and mark-all-read with toast feedback. - **Organization Settings** gains a Notifications tab (hidden from members) with toggle switches for each notification category using optimistic updates with rollback. - **API clients** (`lib/api/notifications.ts`, `lib/api/notification-settings.ts`) follow existing `apiRequest` patterns. - Version bumped to `0.6.0-alpha`, `@ciphera-net/ui` updated to `^0.0.50`, `@radix-ui/react-icons` added explicitly, TypeScript pinned to exact `5.9.3`. **Issues found:** - The member-redirect effect for the Notifications tab fires *after* `loadNotificationSettings`, causing a wasted 403 API call and brief error toast for members who navigate to `?tab=notifications` directly. <h3>Confidence Score: 4/5</h3> - This PR is safe to merge with one minor race condition to address in the member redirect flow. - The notification system is well-structured with proper error handling, defensive checks, and follows existing codebase patterns. The only functional issue is a race condition where members navigating directly to the notifications tab trigger an unnecessary API call before the redirect fires. Previous thread issues (error discarding, stale closure rollback) have been addressed. - Pay attention to `components/settings/OrganizationSettings.tsx` — the member redirect race condition on the notifications tab. <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | components/notifications/NotificationCenter.tsx | New bell icon dropdown component with 90s polling, click-outside close, and mark-read functionality. Clean implementation with deferred loading state and proper event cleanup. | | app/notifications/page.tsx | Full notifications page with pagination, mark-read, and mark-all-read. Handles auth/org guards and error states well. Previous thread issue (error discarding) has been addressed. | | components/settings/OrganizationSettings.tsx | New Notifications tab with toggle switches for notification categories. Member redirect has a race condition where the API call fires before the redirect effect kicks in. | | lib/api/notifications.ts | Clean API client for notifications with list, mark-read, and mark-all-read endpoints. Follows existing patterns in the codebase. | | lib/api/notification-settings.ts | Minimal API client for get/update notification settings. Follows existing patterns. | | app/layout-content.tsx | Added NotificationCenter to header via rightSideActions prop, guarded behind auth check. Also added defensive Array.isArray check for organizations response. | | CHANGELOG.md | Comprehensive changelog entry for 0.6.0-alpha documenting all new notification features. | | package.json | Version bump to 0.6.0-alpha, UI lib updated, @radix-ui/react-icons added explicitly, TypeScript pinned from ^5.5.4 to exact 5.9.3. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant U as User (Browser) participant NC as NotificationCenter participant NP as Notifications Page participant OS as Org Settings participant API as Backend API Note over NC: Mounted in Header (auth users only) NC->>API: GET /notifications?limit=1 (poll unread count) API-->>NC: { unread_count } loop Every 90 seconds NC->>API: GET /notifications?limit=1 API-->>NC: { unread_count } end U->>NC: Click bell icon NC->>API: GET /notifications API-->>NC: { notifications, unread_count } NC-->>U: Show dropdown with recent notifications U->>NC: Click notification NC->>API: PATCH /notifications/{id}/read API-->>NC: 200 OK U->>NP: Navigate to /notifications NP->>API: GET /notifications?limit=50&offset=0 API-->>NP: { notifications, unread_count } U->>NP: Click "Load more" NP->>API: GET /notifications?limit=50&offset=50 API-->>NP: { notifications, unread_count } U->>NP: Click "Mark all read" NP->>API: POST /notifications/mark-all-read API-->>NP: 200 OK U->>OS: Navigate to Org Settings → Notifications OS->>API: GET /notification-settings API-->>OS: { settings, categories } U->>OS: Toggle category OS->>API: PATCH /notification-settings { settings } API-->>OS: 200 OK ``` <sub>Last reviewed commit: 56b99df</sub>
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-16 19:38:20 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

9 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

<sub>9 files reviewed, 2 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -0,0 +43,4 @@
if (diffHours < 24) return `${diffHours}h ago`
if (diffDays < 7) return `${diffDays}d ago`
return d.toLocaleDateString()
}
greptile-apps[bot] (Migrated from github.com) commented 2026-02-16 19:38:19 +00:00

Duplicated formatTimeAgo and getTypeIcon utilities

formatTimeAgo and getTypeIcon are identical copies between this file and app/notifications/page.tsx. Consider extracting them into a shared utility (e.g., lib/utils/notifications.ts) to avoid maintaining the same logic in two places.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/notifications/NotificationCenter.tsx
Line: 34:46

Comment:
**Duplicated `formatTimeAgo` and `getTypeIcon` utilities**

`formatTimeAgo` and `getTypeIcon` are identical copies between this file and `app/notifications/page.tsx`. Consider extracting them into a shared utility (e.g., `lib/utils/notifications.ts`) to avoid maintaining the same logic in two places.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.
**Duplicated `formatTimeAgo` and `getTypeIcon` utilities** `formatTimeAgo` and `getTypeIcon` are identical copies between this file and `app/notifications/page.tsx`. Consider extracting them into a shared utility (e.g., `lib/utils/notifications.ts`) to avoid maintaining the same logic in two places. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/notifications/NotificationCenter.tsx Line: 34:46 Comment: **Duplicated `formatTimeAgo` and `getTypeIcon` utilities** `formatTimeAgo` and `getTypeIcon` are identical copies between this file and `app/notifications/page.tsx`. Consider extracting them into a shared utility (e.g., `lib/utils/notifications.ts`) to avoid maintaining the same logic in two places. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-16 19:38:18 +00:00

API call fires before member redirect

When a member navigates to /org-settings?tab=notifications, two effects run: the one on line 283 fires loadNotificationSettings() (which will fail with 403 for members), and the redirect on line 290 switches the tab to 'general'. Because React batches state updates but fires effects in order, the API call executes before the redirect, resulting in a brief loading spinner and a toast error ("Session expired" or similar) before the tab switches away.

Consider guarding the load effect with a role check to avoid the wasted call:

  useEffect(() => {
    if (activeTab === 'notifications' && currentOrgId && user?.role !== 'member') {
      loadNotificationSettings()
    }
  }, [activeTab, currentOrgId, loadNotificationSettings, user?.role])

  // * Redirect members away from Notifications tab (owners/admins only)
  useEffect(() => {
    if (activeTab === 'notifications' && user?.role === 'member') {
      handleTabChange('general')
    }
  }, [activeTab, user?.role])
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 283:294

Comment:
**API call fires before member redirect**

When a member navigates to `/org-settings?tab=notifications`, two effects run: the one on line 283 fires `loadNotificationSettings()` (which will fail with 403 for members), and the redirect on line 290 switches the tab to `'general'`. Because React batches state updates but fires effects in order, the API call executes before the redirect, resulting in a brief loading spinner and a toast error ("Session expired" or similar) before the tab switches away.

Consider guarding the load effect with a role check to avoid the wasted call:

```suggestion
  useEffect(() => {
    if (activeTab === 'notifications' && currentOrgId && user?.role !== 'member') {
      loadNotificationSettings()
    }
  }, [activeTab, currentOrgId, loadNotificationSettings, user?.role])

  // * Redirect members away from Notifications tab (owners/admins only)
  useEffect(() => {
    if (activeTab === 'notifications' && user?.role === 'member') {
      handleTabChange('general')
    }
  }, [activeTab, user?.role])
```

How can I resolve this? If you propose a fix, please make it concise.
**API call fires before member redirect** When a member navigates to `/org-settings?tab=notifications`, two effects run: the one on line 283 fires `loadNotificationSettings()` (which will fail with 403 for members), and the redirect on line 290 switches the tab to `'general'`. Because React batches state updates but fires effects in order, the API call executes before the redirect, resulting in a brief loading spinner and a toast error ("Session expired" or similar) before the tab switches away. Consider guarding the load effect with a role check to avoid the wasted call: ```suggestion useEffect(() => { if (activeTab === 'notifications' && currentOrgId && user?.role !== 'member') { loadNotificationSettings() } }, [activeTab, currentOrgId, loadNotificationSettings, user?.role]) // * Redirect members away from Notifications tab (owners/admins only) useEffect(() => { if (activeTab === 'notifications' && user?.role === 'member') { handleTabChange('general') } }, [activeTab, user?.role]) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 283:294 Comment: **API call fires before member redirect** When a member navigates to `/org-settings?tab=notifications`, two effects run: the one on line 283 fires `loadNotificationSettings()` (which will fail with 403 for members), and the redirect on line 290 switches the tab to `'general'`. Because React batches state updates but fires effects in order, the API call executes before the redirect, resulting in a brief loading spinner and a toast error ("Session expired" or similar) before the tab switches away. Consider guarding the load effect with a role check to avoid the wasted call: ```suggestion useEffect(() => { if (activeTab === 'notifications' && currentOrgId && user?.role !== 'member') { loadNotificationSettings() } }, [activeTab, currentOrgId, loadNotificationSettings, user?.role]) // * Redirect members away from Notifications tab (owners/admins only) useEffect(() => { if (activeTab === 'notifications' && user?.role === 'member') { handleTabChange('general') } }, [activeTab, user?.role]) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-16 19:41:44 +00:00
@@ -0,0 +43,4 @@
if (diffHours < 24) return `${diffHours}h ago`
if (diffDays < 7) return `${diffDays}d ago`
return d.toLocaleDateString()
}
uz1mani (Migrated from github.com) commented 2026-02-16 19:41:44 +00:00

Issue: formatTimeAgo and getTypeIcon were duplicated in NotificationCenter.tsx and app/notifications/page.tsx, so both need updates when the behavior changes.
Fix: Moved both helpers to lib/utils/notifications.tsx and switched both consumers to import from the shared utility.
Why: Centralizes logic in one place so future changes are consistent across the notification center and notifications page.

Issue: formatTimeAgo and getTypeIcon were duplicated in NotificationCenter.tsx and app/notifications/page.tsx, so both need updates when the behavior changes. Fix: Moved both helpers to lib/utils/notifications.tsx and switched both consumers to import from the shared utility. Why: Centralizes logic in one place so future changes are consistent across the notification center and notifications page.
uz1mani (Migrated from github.com) reviewed 2026-02-16 19:41:52 +00:00
uz1mani (Migrated from github.com) commented 2026-02-16 19:41:52 +00:00

Issue: For members on /org-settings?tab=notifications, the load effect calls loadNotificationSettings() before the redirect effect runs, causing a 403, loading spinner, and error toast before the tab switches.
Fix: Guard the load effect with user?.role !== 'member' so it only runs for owners/admins. The redirect effect still runs when user?.role === 'member' and switches the tab.
Why: Avoids the unnecessary 403 request and error toast, and reduces wasted API calls for members.

Issue: For members on /org-settings?tab=notifications, the load effect calls loadNotificationSettings() before the redirect effect runs, causing a 403, loading spinner, and error toast before the tab switches. Fix: Guard the load effect with user?.role !== 'member' so it only runs for owners/admins. The redirect effect still runs when user?.role === 'member' and switches the tab. Why: Avoids the unnecessary 403 request and error toast, and reduces wasted API calls for members.
Sign in to join this conversation.
No description provided.