Skip to content

notifications (5/5): notifications UI#456

Closed
illegalprime wants to merge 1 commit into
eden/notifications-4-serverfrom
eden/notifications-5-client
Closed

notifications (5/5): notifications UI#456
illegalprime wants to merge 1 commit into
eden/notifications-4-serverfrom
eden/notifications-5-client

Conversation

@illegalprime

@illegalprime illegalprime commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Stack 5/5 — base: eden/notifications-4-server

1. Summary

Adds the Notifications settings UI to the protoFleet client: a /settings/notifications page 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 the notifications.v1 Connect 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 without notification: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.

  • API module (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 / *ToProto helpers. Secrets are write-only: reads never carry them back (a has_secret flag signals presence), and timestamps are converted to/from ISO strings.
  • Store (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 derived active boolean is computed for each maintenance window at fetch/mutation time (not in render, to avoid Date.now() in render).
  • Page (Notifications.tsx) mounts the four sections (Rules, History, Channels, Maintenance Windows) and triggers refresh() on mount; load errors surface as toasts.
  • Sections read store slices and render shared/List tables. Row actions and "Add" buttons are conditionally rendered on useHasPermission("notification:manage").

Primary flows:

  • Add channel + testAddChannelModal builds a typed payload per channel kind (webhook / Slack / SMTP), validates locally, optionally calls testChannel (server delivers a synthetic alert and returns ok/HTTP code), then createChannel persists it. Secret fields (webhook bearer header, Slack URL, SMTP password) use type=password.
  • Rules are read-only (the Grafana YAML owns the set). Operators can only pause/resume a rule or attach/lift a maintenance window via the row kebab; the kebab flips between "Maintenance window" and "Lift" based on whether an active rule-scoped window already exists.
  • Maintenance window lifecycleAddMaintenanceWindowModal supports quick presets (1h/4h/8h/1d/3d) or custom datetime-local start/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).
  • Dashboard cardActiveNotificationsCard renders only when the user has notification:read (avoids a guaranteed 403), and shows a HistoryTable in activeOnly mode. "Active" is derived client-side: dedupe the loaded history (newest-first) to one row per fingerprint, keep only status === "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 & card
Loading

Maintenance 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 note
Loading

Sequence: add channel + test

sequenceDiagram
  actor Op as Operator
  participant M as AddChannelModal
  participant S as Store
  participant A as notificationsApi
  participant Srv as fleet-api

  Op->>M: fill name + destination
  Op->>M: click "Send test"
  M->>A: testChannel(payload)
  A->>Srv: ChannelService.TestChannel
  Srv-->>A: ok / error / HTTP code
  A-->>M: result
  M-->>Op: toast (delivered / failed)
  Op->>M: click "Save channel"
  M->>S: createChannel(payload)
  S->>A: createChannel
  A->>Srv: ChannelService.CreateChannel
  Srv-->>S: canonical Channel row
  S->>S: upsertById(channels)
  M-->>Op: toast "Saved"
Loading

4. Areas of the code involved

Area / file What changed Why it matters for review
Data / API layer
features/notifications/api/notificationsApi.ts (new) Proto ⇄ view-model translation; all list/create/update/delete/test/pause/resume/history calls Single boundary with the wire contract; check enum/secret/timestamp mapping
features/notifications/types/index.ts (new) snake_case view-model types mirroring the server handler Source of truth for client shapes
features/notifications/store/notificationsStore.ts (new) Zustand cache; parallel refresh, keyset history pagination, optimistic-merge mutations, derived active Core state mechanism and pagination logic
api/clients.ts (modified) Registers 4 generated notification Connect clients Wiring only
UI sections
pages/Notifications.tsx (new) Composes the four sections; refresh() on mount Page entry point
components/RulesSection.tsx (new) Read-only rules table; pause/resume + maintenance-window/lift row actions Rule actions are mutation-gated
components/HistorySection.tsx + HistoryTable.tsx (new) History table; activeOnly dedupe mode + load-more pagination Note the active-alert derivation (see §5)
components/ChannelsSection.tsx (new) Channels table; inline rename/re-target, test, delete canManage gating; full-PUT update semantics
components/AddChannelModal.tsx (new) Per-kind channel form + test-before-save; masked secret inputs Validation + secret handling
components/MaintenanceWindowsSection.tsx (new) Windows table; active/expired badges, edit/lift Sorting + gating
components/AddMaintenanceWindowModal.tsx (new) Create/edit window; quick presets, scope preservation on edit Non-rule scope preservation fix
components/ChannelEditableCell.tsx (new) Inline-edit cell; readOnly for non-managers
components/ChannelStatusBadge.tsx, SinglePickerField.tsx (new) Validation badge; accessible single-select picker Presentational
components/ActiveNotificationsCard.tsx (new) Dashboard card wrapping HistoryTable activeOnly
lib/maintenanceWindowOptions.ts (new) Scope/quick-window options + local datetime formatting
Wiring
router.tsx, routePrefetch.ts, config/navItems.ts (modified) Route, lazy import + prefetch, nav entry (gated on notification:read) Routing/nav
features/dashboard/pages/Dashboard.tsx (modified) Mounts the card behind notification:read Avoids guaranteed 403
features/settings/utils/permissionCatalog.ts (modified) Adds a "Notifications" permission group Roles UI grouping
api/generated/notifications/** Connect/protobuf bindings consumed here generated — skip

5. Key technical decisions & trade-offs

  • Optimistic cache merge of the server's canonical row (vs. refetch-after-mutate): each mutation merges the returned row via upsertById, avoiding a full reload but trusting the server response shape.
  • History pagination is keyset (before_id, page size 50) and independent of the rest of the feature's state, rather than offset paging or one combined fetch.
  • Active-alert view is derived client-side, not a server query (see limitation below).
  • active flag precomputed in the store, not in render — required because computing it needs Date.now(), which is lint-blocked in render.
  • Permission gating is UI-only defense-in-depth: controls hidden via useHasPermission("notification:manage"); the server independently rejects unauthorized mutations.
  • Secrets are write-only and masked: read responses never return secret values (a has_secret flag signals presence); secret inputs use type=password.
  • Maintenance-window scope is preserved on edit for non-rule scopes (the modal only edits rule scope), avoiding wiping group/site/device targets that the server would then reject.
  • Channel updates send a full payload (PUT-style) even for a rename; the server applies only the changed fields.
  • "Silences" renamed to "Maintenance Windows" throughout — types, store, API, proto bindings, components, and user-facing labels.

6. Testing & validation

  • Verified manually against the local dev stack: list/create/update/delete/test for channels; pause/resume and maintenance-window attach/lift for rules; window create/edit (quick + custom) and lift; history pagination; dashboard card.
  • Permission gating verified for both notification:read-only and notification:manage users.
  • Addressed prior review findings: masked secret inputs + mutation-control gating (security), and non-rule maintenance-window scope preservation on edit (connector).

Not covered / known limitations:

  • The dashboard "Active notifications" card derives active alerts by deduping only the first loaded history page (default 50 rows). A still-firing alert older than that page can drop off the card. This needs an authoritative active-alert source server-side and is a known follow-up.
  • Saved channels are not yet attached to alert routing — "Test" sends a synthetic alert directly to the destination, but live alerts won't deliver through configured channels until routing ships.
  • No automated unit/E2E coverage added in this PR.

🤖 Generated with Claude Code

@illegalprime illegalprime requested a review from a team as a code owner June 15, 2026 15:53
@github-actions github-actions Bot added javascript Pull requests that update javascript code client labels Jun 15, 2026
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (69f0098d5d9150263913b9156fec8a2c6ad505c2...451f42bcbf108682b79d01cc2eae35ce24cfc64c, exact PR three-dot diff)
  • Model: gpt-5.5

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: MEDIUM

Findings

[MEDIUM] Active notifications can be stale and incomplete

  • Category: Reliability
  • Location: client/src/protoFleet/features/notifications/components/HistoryTable.tsx:97, client/src/protoFleet/features/notifications/store/notificationsStore.ts:56
  • Description: The dashboard’s new ActiveNotificationsCard uses HistoryTable activeOnly, but HistoryTable refreshes history only once on mount. It also derives active alerts from the locally loaded history page, which is capped at 50 entries. A long-lived dashboard will not reflect newly firing or resolved alerts, and an alert that is still firing can disappear if its latest event falls outside the first page.
  • Impact: Operators may see “No active notifications” or stale active alerts while the fleet is actually in an alerting state. This undermines the dashboard as an operational monitoring surface.
  • Recommendation: Back the active card with a server-side “current active notifications” query or an active-only filter, and poll/stream it on the dashboard using the same freshness expectations as the other dashboard panels. Avoid deriving active state from a capped history page.

Notes

I 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 |
Triggered by: @illegalprime |
Review workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +204 to +208
<Button
variant={variants.secondary}
size={sizes.compact}
text="Add channel"
onClick={() => setShowAddModal(true)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +119 to +121
group_id: null,
site_id: null,
device_ids: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +6 to +8
<HistoryTable
activeOnly
noDataElement={<div className="py-6 text-center text-text-primary-50">No active notifications.</div>}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from 6cdd7d2 to 6655a15 Compare June 15, 2026 17:37
illegalprime added a commit that referenced this pull request Jun 15, 2026
…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>
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from d7bef30 to 76179b2 Compare June 15, 2026 17:38
@illegalprime

Copy link
Copy Markdown
Contributor Author

Codex findings on this PR:

  • [MEDIUM] secrets in visible text fields — fixed (commit 76179b2): webhook bearer header, Slack webhook URL, and SMTP password inputs are now type=password (the shared Input ships a show/hide toggle).
  • [MEDIUM] read-only users shown manage controls — fixed (same commit): add buttons, row actions, and inline-editable channel cells are gated on useHasPermission("notification:manage"), so notification:read users see the lists without unusable controls. (The server already enforced this; this removes the misleading UI.)
  • [MEDIUM] active card can miss firing alerts — not fixed inline; deriving active alerts from the loaded history page is the wrong source, and the fix needs an authoritative active-alerts read. Tracked in Notifications dashboard: active-alerts card can miss alerts older than the loaded history page #460.

@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from 6655a15 to 753a5b8 Compare June 15, 2026 17:44
illegalprime added a commit that referenced this pull request Jun 15, 2026
…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>
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from 76179b2 to c069f13 Compare June 15, 2026 17:44
illegalprime added a commit that referenced this pull request Jun 15, 2026
…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>
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from c069f13 to 466fdd6 Compare June 15, 2026 17:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +14 to +15
useEffect(() => {
void refresh().catch((error) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from 75185f6 to 876a9df Compare June 15, 2026 18:15
illegalprime added a commit that referenced this pull request Jun 15, 2026
…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>
illegalprime added a commit that referenced this pull request Jun 15, 2026
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>
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from 466fdd6 to bd9a14c Compare June 15, 2026 18:15
@illegalprime

Copy link
Copy Markdown
Contributor Author

codex-connector triage (earlier #460 reference folded in here):

Fixed:

  • [P2] Gate mutations on manage permission — add/edit/test/delete/pause/resume/silence controls and inline-editable channel cells are gated on useHasPermission("notification:manage").
  • [P2] Preserve non-rule silence targets when editing — the edit modal no longer overwrites group/site/device targets with empties; it preserves the stored scope for non-rule silences.

Deferred — design-level, tracked here (no separate issue):

  • [P2] Don't derive active alerts from one history page — the dashboard "Active notifications" card dedupes only the first 50 history rows, so a still-firing alert older than that page drops off. Needs an authoritative active-alert source (server endpoint or Grafana alert state) rather than reconstructing from paginated history.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +97 to +99
set((state) => {
state.historyLoading = true;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from 876a9df to 5d7cfa0 Compare June 15, 2026 18:36
illegalprime added a commit that referenced this pull request Jun 15, 2026
…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>
illegalprime added a commit that referenced this pull request Jun 15, 2026
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>
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from bd9a14c to 0c88c37 Compare June 15, 2026 18:36
@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from 5d7cfa0 to 382a0e6 Compare June 15, 2026 19:24
illegalprime added a commit that referenced this pull request Jun 15, 2026
…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>
illegalprime added a commit that referenced this pull request Jun 15, 2026
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>
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from 0c88c37 to a345ce6 Compare June 15, 2026 19:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +99 to +103
useEffect(() => {
void refreshHistory().catch((err: unknown) => {
setError(getErrorMessage(err, "Failed to load notification history"));
});
}, [refreshHistory]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +131 to +133
const scopePayload: MaintenanceWindowScope =
isEditing && editingMaintenanceWindow && editingMaintenanceWindow.scope.kind !== "rule"
? editingMaintenanceWindow.scope

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

illegalprime added a commit that referenced this pull request Jun 16, 2026
…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>
illegalprime added a commit that referenced this pull request Jun 16, 2026
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>
@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from 382a0e6 to ffdd5ad Compare June 16, 2026 14:43
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from a345ce6 to 17efacb Compare June 16, 2026 14:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +45 to +49
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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from ffdd5ad to 7b2fdcd Compare June 16, 2026 15:10
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from 17efacb to 3ebf546 Compare June 16, 2026 15:14
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>
@illegalprime illegalprime force-pushed the eden/notifications-5-client branch from 3ebf546 to 451f42b Compare June 16, 2026 18:26
@illegalprime illegalprime force-pushed the eden/notifications-4-server branch from 7b2fdcd to 69f0098 Compare June 16, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant