notifications (5/5): notifications UI#456
Conversation
🔐 Codex Security Review
Review SummaryOverall Risk: MEDIUM Findings[MEDIUM] Active notifications can be stale and incomplete
NotesI did not find auth bypasses, SQL injection, command injection, protobuf wire-format issues, pool hijacking changes, or frontend XSS in the changed hunks. The new notification RPC calls rely on existing server-side permission gates; the client-side permission checks are UI-only, which matches the surrounding code’s pattern. Generated by Codex Security Review | |
2ace81c to
d7bef30
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ace81cb65
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| <Button | ||
| variant={variants.secondary} | ||
| size={sizes.compact} | ||
| text="Add channel" | ||
| onClick={() => setShowAddModal(true)} |
There was a problem hiding this comment.
Gate notification mutations on manage permission
For users who have notification:read but not notification:manage, the Notifications settings entry is visible, but this button and the row edit/test/delete actions still call RPCs that server/internal/handlers/middleware/rpc_permissions.go maps to notification:manage. Those read-only users will see mutation controls that only produce permission-denied errors, so the mutation buttons/actions should be hidden or disabled behind useHasPermission("notification:manage").
Useful? React with 👍 / 👎.
| group_id: null, | ||
| site_id: null, | ||
| device_ids: [], |
There was a problem hiding this comment.
Preserve non-rule silence targets when editing
When the backend returns an existing group/site/device-scoped silence, the edit modal initializes scope to that kind, but saving always sends group_id, site_id, and device_ids as empty. The server's validateSilenceScope requires those targets for non-rule scopes, so any edit to such a silence fails with an invalid-argument error instead of preserving the current target; either preserve the existing scope fields or hide editing for unsupported scope kinds.
Useful? React with 👍 / 👎.
| <HistoryTable | ||
| activeOnly | ||
| noDataElement={<div className="py-6 text-center text-text-primary-50">No active notifications.</div>} |
There was a problem hiding this comment.
Don't derive active alerts from one history page
The dashboard card labels this as active notifications, but activeOnly uses refreshHistory(), which fetches only the first 50 history rows, and then hides pagination. In fleets with more than 50 newer notification events, a still-firing alert whose latest firing row is older than that first page will disappear from the active card and can incorrectly show “No active notifications”; the active view needs a complete/current active-alert source or must keep paging until it can safely determine active state.
Useful? React with 👍 / 👎.
6cdd7d2 to
6655a15
Compare
…ation:manage
Two client-side hardening fixes from review:
- Channel destination secrets (webhook bearer header, Slack webhook URL, SMTP
password) used the default text input, leaving capability secrets shoulder-
surfable. Switch them to type=password (Input already ships a show/hide toggle).
- The Rules/Channels/Silences sections rendered create/edit/test/delete/pause/
resume/silence controls to any notification:read user, though the server gates
every mutation on notification:manage. Gate the add buttons, row actions, and
inline-editable channel cells on useHasPermission("notification:manage") so
read-only users see the lists without unusable controls.
Addresses Codex security review findings (MEDIUM) on #456.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d7bef30 to
76179b2
Compare
|
Codex findings on this PR:
|
6655a15 to
753a5b8
Compare
…ation:manage
Two client-side hardening fixes from review:
- Channel destination secrets (webhook bearer header, Slack webhook URL, SMTP
password) used the default text input, leaving capability secrets shoulder-
surfable. Switch them to type=password (Input already ships a show/hide toggle).
- The Rules/Channels/Silences sections rendered create/edit/test/delete/pause/
resume/silence controls to any notification:read user, though the server gates
every mutation on notification:manage. Gate the add buttons, row actions, and
inline-editable channel cells on useHasPermission("notification:manage") so
read-only users see the lists without unusable controls.
Addresses Codex security review findings (MEDIUM) on #456.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76179b2 to
c069f13
Compare
…ation:manage
Two client-side hardening fixes from review:
- Channel destination secrets (webhook bearer header, Slack webhook URL, SMTP
password) used the default text input, leaving capability secrets shoulder-
surfable. Switch them to type=password (Input already ships a show/hide toggle).
- The Rules/Channels/Silences sections rendered create/edit/test/delete/pause/
resume/silence controls to any notification:read user, though the server gates
every mutation on notification:manage. Gate the add buttons, row actions, and
inline-editable channel cells on useHasPermission("notification:manage") so
read-only users see the lists without unusable controls.
Addresses Codex security review findings (MEDIUM) on #456.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c069f13 to
466fdd6
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 466fdd686a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| useEffect(() => { | ||
| void refresh().catch((error) => { |
There was a problem hiding this comment.
Gate the page before issuing read RPCs
When a user opens /settings/notifications directly without notification:read, the nav item is hidden but the route itself is not guarded, so this effect still calls the notification read RPCs (and HistoryTable does the same) and the server returns permission-denied toasts/empty sections. Match the other permission-gated settings pages by checking useHasPermission("notification:read") and redirecting or avoiding the fetch before mounting the sections.
Useful? React with 👍 / 👎.
75185f6 to
876a9df
Compare
…ation:manage
Two client-side hardening fixes from review:
- Channel destination secrets (webhook bearer header, Slack webhook URL, SMTP
password) used the default text input, leaving capability secrets shoulder-
surfable. Switch them to type=password (Input already ships a show/hide toggle).
- The Rules/Channels/Silences sections rendered create/edit/test/delete/pause/
resume/silence controls to any notification:read user, though the server gates
every mutation on notification:manage. Gate the add buttons, row actions, and
inline-editable channel cells on useHasPermission("notification:manage") so
read-only users see the lists without unusable controls.
Addresses Codex security review findings (MEDIUM) on #456.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The edit modal only has a target editor for the rule scope, but on save it always sent empty group_id/site_id/device_ids. Editing an existing group/site/ device silence therefore wiped its target and the server's validateSilenceScope rejected it with invalid-argument. Preserve the stored scope for non-rule silences instead of overwriting it. Addresses Codex connector finding (P2) on #456. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
466fdd6 to
bd9a14c
Compare
|
codex-connector triage (earlier #460 reference folded in here): Fixed:
Deferred — design-level, tracked here (no separate issue):
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd9a14cef9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| set((state) => { | ||
| state.historyLoading = true; | ||
| }); |
There was a problem hiding this comment.
Clear notification history on fresh loads
When a user logs out and another user/organization uses the same SPA session, this standalone notifications store is not reset by auth.logout; refreshHistory() only sets historyLoading, so existing rows remain rendered because HistoryTable only shows the initial spinner when history.length === 0. In that same-browser user-switch scenario, the dashboard/history table can display the previous user's notification history (including device names/MACs) until the RPC completes, or indefinitely if the new request fails/403s; clear this store on logout/auth change or clear history before a fresh load.
Useful? React with 👍 / 👎.
| Active | ||
| </span> | ||
| ) : ( | ||
| <span className="rounded bg-surface-5 px-2 py-0.5 text-200 text-text-primary-50">Expired</span> |
There was a problem hiding this comment.
Label future silences as scheduled
For a silence whose starts_at is in the future, active is false just like an expired silence, so this branch labels a planned upcoming mute as “Expired” immediately after the user schedules it. Since the modal allows choosing a future start time, distinguish now < starts_at (for example “Scheduled”) from truly expired windows so operators do not think the maintenance mute has already lapsed.
Useful? React with 👍 / 👎.
876a9df to
5d7cfa0
Compare
…ation:manage
Two client-side hardening fixes from review:
- Channel destination secrets (webhook bearer header, Slack webhook URL, SMTP
password) used the default text input, leaving capability secrets shoulder-
surfable. Switch them to type=password (Input already ships a show/hide toggle).
- The Rules/Channels/Silences sections rendered create/edit/test/delete/pause/
resume/silence controls to any notification:read user, though the server gates
every mutation on notification:manage. Gate the add buttons, row actions, and
inline-editable channel cells on useHasPermission("notification:manage") so
read-only users see the lists without unusable controls.
Addresses Codex security review findings (MEDIUM) on #456.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The edit modal only has a target editor for the rule scope, but on save it always sent empty group_id/site_id/device_ids. Editing an existing group/site/ device silence therefore wiped its target and the server's validateSilenceScope rejected it with invalid-argument. Preserve the stored scope for non-rule silences instead of overwriting it. Addresses Codex connector finding (P2) on #456. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bd9a14c to
0c88c37
Compare
5d7cfa0 to
382a0e6
Compare
…ation:manage
Two client-side hardening fixes from review:
- Channel destination secrets (webhook bearer header, Slack webhook URL, SMTP
password) used the default text input, leaving capability secrets shoulder-
surfable. Switch them to type=password (Input already ships a show/hide toggle).
- The Rules/Channels/Silences sections rendered create/edit/test/delete/pause/
resume/silence controls to any notification:read user, though the server gates
every mutation on notification:manage. Gate the add buttons, row actions, and
inline-editable channel cells on useHasPermission("notification:manage") so
read-only users see the lists without unusable controls.
Addresses Codex security review findings (MEDIUM) on #456.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The edit modal only has a target editor for the rule scope, but on save it always sent empty group_id/site_id/device_ids. Editing an existing group/site/ device silence therefore wiped its target and the server's validateSilenceScope rejected it with invalid-argument. Preserve the stored scope for non-rule silences instead of overwriting it. Addresses Codex connector finding (P2) on #456. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0c88c37 to
a345ce6
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a345ce6fa8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| useEffect(() => { | ||
| void refreshHistory().catch((err: unknown) => { | ||
| setError(getErrorMessage(err, "Failed to load notification history")); | ||
| }); | ||
| }, [refreshHistory]); |
There was a problem hiding this comment.
Refresh the active notifications card after mount
In the dashboard context, ActiveNotificationsCard mounts this table with activeOnly, but this effect fetches history only once. If an alert fires or resolves while the dashboard stays open, the “Active notifications” card keeps showing stale rows or an empty state until the route remounts; add polling or another refresh path for the active view.
Useful? React with 👍 / 👎.
| const scopePayload: MaintenanceWindowScope = | ||
| isEditing && editingMaintenanceWindow && editingMaintenanceWindow.scope.kind !== "rule" | ||
| ? editingMaintenanceWindow.scope |
There was a problem hiding this comment.
Honor scope changes for non-rule windows
When editing a maintenance window returned with scope.kind of group/site/device, the scope picker still allows choosing “A rule”, but this branch keys off the original editingMaintenanceWindow.scope.kind and always sends the original scope. In that scenario, selecting a rule appears to save successfully while leaving the silence scoped to the old target; either make the target read-only for unsupported scopes or preserve only when the current scope remains non-rule.
Useful? React with 👍 / 👎.
…ation:manage
Two client-side hardening fixes from review:
- Channel destination secrets (webhook bearer header, Slack webhook URL, SMTP
password) used the default text input, leaving capability secrets shoulder-
surfable. Switch them to type=password (Input already ships a show/hide toggle).
- The Rules/Channels/Silences sections rendered create/edit/test/delete/pause/
resume/silence controls to any notification:read user, though the server gates
every mutation on notification:manage. Gate the add buttons, row actions, and
inline-editable channel cells on useHasPermission("notification:manage") so
read-only users see the lists without unusable controls.
Addresses Codex security review findings (MEDIUM) on #456.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The edit modal only has a target editor for the rule scope, but on save it always sent empty group_id/site_id/device_ids. Editing an existing group/site/ device silence therefore wiped its target and the server's validateSilenceScope rejected it with invalid-argument. Preserve the stored scope for non-rule silences instead of overwriting it. Addresses Codex connector finding (P2) on #456. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
382a0e6 to
ffdd5ad
Compare
a345ce6 to
17efacb
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17efacba54
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const withActive = (s: MaintenanceWindow): MaintenanceWindowWithActive => { | ||
| const now = Date.now(); | ||
| const start = new Date(s.starts_at).getTime(); | ||
| const end = s.ends_at ? new Date(s.ends_at).getTime() : Infinity; | ||
| return { ...s, active: now >= start && now < end }; |
There was a problem hiding this comment.
Refresh maintenance-window activity when time passes
Because active is snapshotted from Date.now() only when rows are fetched or mutated, any window that starts or ends while the settings page remains open keeps the old state indefinitely. In that scenario a scheduled window never flips to active, or an expired 1-hour window can still show as Active and make RulesSection offer “Lift maintenance window” until the user performs a full refresh; use the server-provided activity or schedule a refresh/recompute at the next boundary.
Useful? React with 👍 / 👎.
ffdd5ad to
7b2fdcd
Compare
17efacb to
3ebf546
Compare
Add the Proto Fleet notifications UI: channels, rules, maintenance windows, and delivery history sections, the active-notifications dashboard card, the client API/store, and the routing/nav wiring. Secret inputs are masked and manage controls are gated on notification:manage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3ebf546 to
451f42b
Compare
7b2fdcd to
69f0098
Compare
Stack 5/5 — base:
eden/notifications-4-server1. Summary
Adds the Notifications settings UI to the protoFleet client: a
/settings/notificationspage where operators view provisioned alert Rules, see delivered notification History, manage delivery Channels (webhook / Slack / SMTP), and schedule Maintenance Windows that mute rules during planned work. A dashboard "Active notifications" card surfaces currently-firing alerts. This is the user-facing front end for the notifications service shipped in #455; it talks to thenotifications.v1Connect services via generated TypeScript clients.The page is gated on
notification:read; every mutation control (add / edit / test / delete / pause / resume / lift) is additionally hidden from users withoutnotification:manage. This is defense-in-depth — the server enforces the same permissions.2. How it works
The feature is a self-contained React module under
features/notifications/, layered as components → Zustand store → API module → generated Connect clients → fleet-api.notificationsApi.ts) is the single translation point between the protobuf wire contract (camelCase,Timestamp, enums) and the feature's snake_case view model. It exposes plain async functions (listChannels,createChannel,testChannel,listRules,pauseRule,listMaintenanceWindows,createMaintenanceWindow,listHistory, …) that call the four generated clients and map responses through*FromProto/*ToProtohelpers. Secrets are write-only: reads never carry them back (ahas_secretflag signals presence), and timestamps are converted to/from ISO strings.notificationsStore.ts) is a Zustand cache.refresh()loads channels, rules, and maintenance windows in parallel;refreshHistory()/loadMoreHistory()paginate history independently via keyset (before_id= last loaded row's id, page size 50). Mutations call the API, then merge the server's canonical row back into the cache (upsertById). A derivedactiveboolean is computed for each maintenance window at fetch/mutation time (not in render, to avoidDate.now()in render).Notifications.tsx) mounts the four sections (Rules, History, Channels, Maintenance Windows) and triggersrefresh()on mount; load errors surface as toasts.shared/Listtables. Row actions and "Add" buttons are conditionally rendered onuseHasPermission("notification:manage").Primary flows:
AddChannelModalbuilds a typed payload per channel kind (webhook / Slack / SMTP), validates locally, optionally callstestChannel(server delivers a synthetic alert and returns ok/HTTP code), thencreateChannelpersists it. Secret fields (webhook bearer header, Slack URL, SMTP password) usetype=password.AddMaintenanceWindowModalsupports quick presets (1h/4h/8h/1d/3d) or customdatetime-localstart/end. On save it sends scope + window + reason; for an existing non-rule (group/site/device) window it preserves the stored scope rather than overwriting with empty fields (the server's scope validation would reject those).ActiveNotificationsCardrenders only when the user hasnotification:read(avoids a guaranteed 403), and shows aHistoryTableinactiveOnlymode. "Active" is derived client-side: dedupe the loaded history (newest-first) to one row per fingerprint, keep onlystatus === "firing".3. Diagrams
Component / data flow
flowchart TD subgraph UI["React feature (features/notifications)"] page["Notifications page"] rules["RulesSection"] hist["HistorySection / HistoryTable"] chan["ChannelsSection + AddChannelModal"] mw["MaintenanceWindowsSection + AddMaintenanceWindowModal"] card["ActiveNotificationsCard (Dashboard)"] end store["Zustand store (notificationsStore)"] api["API module (notificationsApi)"] clients["Generated Connect clients (clients.ts)"] server["fleet-api notifications.v1"] page --> rules & hist & chan & mw card --> hist rules & hist & chan & mw --> store store --> api --> clients --> server perm["useHasPermission('notification:manage')"] -.gates mutation controls.-> rules & chan & mw read["notification:read"] -.gates page + nav + card.-> page & cardMaintenance window lifecycle (as reflected in the UI)
stateDiagram-v2 [*] --> Active: "create (starts <= now < ends)" [*] --> Scheduled: "create (now < starts)" Scheduled --> Active: "clock reaches starts_at" Active --> Expired: "clock reaches ends_at" Active --> Lifted: "operator deletes / lifts" Scheduled --> Lifted: "operator deletes" Lifted --> [*] Expired --> [*] note right of Active 'active' is precomputed in the store (now >= starts AND now < ends), not in render end noteSequence: add channel + test
4. Areas of the code involved
features/notifications/api/notificationsApi.ts(new)features/notifications/types/index.ts(new)features/notifications/store/notificationsStore.ts(new)activeapi/clients.ts(modified)pages/Notifications.tsx(new)refresh()on mountcomponents/RulesSection.tsx(new)components/HistorySection.tsx+HistoryTable.tsx(new)activeOnlydedupe mode + load-more paginationcomponents/ChannelsSection.tsx(new)canManagegating; full-PUT update semanticscomponents/AddChannelModal.tsx(new)components/MaintenanceWindowsSection.tsx(new)components/AddMaintenanceWindowModal.tsx(new)components/ChannelEditableCell.tsx(new)readOnlyfor non-managerscomponents/ChannelStatusBadge.tsx,SinglePickerField.tsx(new)components/ActiveNotificationsCard.tsx(new)HistoryTable activeOnlylib/maintenanceWindowOptions.ts(new)router.tsx,routePrefetch.ts,config/navItems.ts(modified)notification:read)features/dashboard/pages/Dashboard.tsx(modified)notification:readfeatures/settings/utils/permissionCatalog.ts(modified)api/generated/notifications/**5. Key technical decisions & trade-offs
upsertById, avoiding a full reload but trusting the server response shape.before_id, page size 50) and independent of the rest of the feature's state, rather than offset paging or one combined fetch.activeflag precomputed in the store, not in render — required because computing it needsDate.now(), which is lint-blocked in render.useHasPermission("notification:manage"); the server independently rejects unauthorized mutations.has_secretflag signals presence); secret inputs usetype=password.6. Testing & validation
notification:read-only andnotification:manageusers.Not covered / known limitations:
🤖 Generated with Claude Code