notifications (2/5): notification_history keyset listing#453
Conversation
🔐 Codex Security Review
Review SummaryOverall Risk: MEDIUM Findings[MEDIUM] Unbounded notification-history page limit reaches SQL
NotesThe authoritative diff only changed notification-history sqlc/store/query/index files. I did not see changed auth handlers, Connect-RPC wiring, frontend rendering, plugin logic, command execution, network discovery, pool configuration, or protobuf field definitions in No SQL injection, command injection, cryptostealing/pool hijack, or cross-org data exposure issue was evident in the changed hunks. This was a static review; I did not run tests. Generated by Codex Security Review | |
c64a5c2 to
d032a2d
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 923f5e3745
ℹ️ 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".
CREATE INDEX took ACCESS EXCLUSIVE on the live notification_history table, blocking webhook-driven inserts for the duration of the build. Switch to CREATE INDEX CONCURRENTLY (and a matching DROP INDEX CONCURRENTLY in the down migration) so the build runs online, matching the convention established in 000074. golang-migrate's postgres driver runs statements without an implicit transaction, so CONCURRENTLY is safe. Addresses Codex security review finding (LOW) on #453. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Codex findings on this PR:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d9857a968f
ℹ️ 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".
CREATE INDEX took ACCESS EXCLUSIVE on the live notification_history table, blocking webhook-driven inserts for the duration of the build. Switch to CREATE INDEX CONCURRENTLY (and a matching DROP INDEX CONCURRENTLY in the down migration) so the build runs online, matching the convention established in 000074. golang-migrate's postgres driver runs statements without an implicit transaction, so CONCURRENTLY is safe. Addresses Codex security review finding (LOW) on #453. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d9857a9 to
c49d919
Compare
Two review fixes: - The keyset-index migration was numbered 000088 while the permission-seed migration in the next stack PR is 000087. Since this PR merges first, deploying it would advance schema_migrations to 88 and golang-migrate would then skip the lower-numbered 000087 seed on already-upgraded DBs. Renumber the index to 000087 (this PR merges first); the seed becomes 000088 in the authz PR so the numeric order matches the merge order. - Regenerate sqlc: the committed ListNotificationHistory query comment had been shortened but generated/sqlc was stale, so 'just gen' + the generated-code CI check would have failed on a dirty tree. Addresses Codex connector findings (P1/P2) on #453. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…to the mount PR Two review fixes: - Renumber the permission-seed migration 000087 -> 000088 so it sits above the keyset-index migration (now 000087) that merges first in the stack, keeping numeric order aligned with merge order (see #453). - Move the notification entries out of registeredServices (the RBAC contract fixture): TestRPCContract_RegisteredServicesMatchMainMux compares it against the handlers mounted in main.go, and the mounts land in the next stack PR, so listing them here failed the test standalone. The ProcedurePermissions policy map (and its catalog keys) stay here; only the mount-coupled registration moves to the server PR. Addresses Codex connector findings (P1/P2) on #454. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
codex-connector triage (all fixed):
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 607669536a
ℹ️ 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".
d032a2d to
7a2ceb2
Compare
CREATE INDEX took ACCESS EXCLUSIVE on the live notification_history table, blocking webhook-driven inserts for the duration of the build. Switch to CREATE INDEX CONCURRENTLY (and a matching DROP INDEX CONCURRENTLY in the down migration) so the build runs online, matching the convention established in 000074. golang-migrate's postgres driver runs statements without an implicit transaction, so CONCURRENTLY is safe. Addresses Codex security review finding (LOW) on #453. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two review fixes: - The keyset-index migration was numbered 000088 while the permission-seed migration in the next stack PR is 000087. Since this PR merges first, deploying it would advance schema_migrations to 88 and golang-migrate would then skip the lower-numbered 000087 seed on already-upgraded DBs. Renumber the index to 000087 (this PR merges first); the seed becomes 000088 in the authz PR so the numeric order matches the merge order. - Regenerate sqlc: the committed ListNotificationHistory query comment had been shortened but generated/sqlc was stale, so 'just gen' + the generated-code CI check would have failed on a dirty tree. Addresses Codex connector findings (P1/P2) on #453. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6076695 to
03bb87c
Compare
…to the mount PR Two review fixes: - Renumber the permission-seed migration 000087 -> 000088 so it sits above the keyset-index migration (now 000087) that merges first in the stack, keeping numeric order aligned with merge order (see #453). - Move the notification entries out of registeredServices (the RBAC contract fixture): TestRPCContract_RegisteredServicesMatchMainMux compares it against the handlers mounted in main.go, and the mounts land in the next stack PR, so listing them here failed the test standalone. The ProcedurePermissions policy map (and its catalog keys) stay here; only the mount-coupled registration moves to the server PR. Addresses Codex connector findings (P1/P2) on #454. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7a2ceb2 to
cd175c1
Compare
CREATE INDEX took ACCESS EXCLUSIVE on the live notification_history table, blocking webhook-driven inserts for the duration of the build. Switch to CREATE INDEX CONCURRENTLY (and a matching DROP INDEX CONCURRENTLY in the down migration) so the build runs online, matching the convention established in 000074. golang-migrate's postgres driver runs statements without an implicit transaction, so CONCURRENTLY is safe. Addresses Codex security review finding (LOW) on #453. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two review fixes: - The keyset-index migration was numbered 000088 while the permission-seed migration in the next stack PR is 000087. Since this PR merges first, deploying it would advance schema_migrations to 88 and golang-migrate would then skip the lower-numbered 000087 seed on already-upgraded DBs. Renumber the index to 000087 (this PR merges first); the seed becomes 000088 in the authz PR so the numeric order matches the merge order. - Regenerate sqlc: the committed ListNotificationHistory query comment had been shortened but generated/sqlc was stale, so 'just gen' + the generated-code CI check would have failed on a dirty tree. Addresses Codex connector findings (P1/P2) on #453. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
03bb87c to
b89405f
Compare
…to the mount PR Two review fixes: - Renumber the permission-seed migration 000087 -> 000088 so it sits above the keyset-index migration (now 000087) that merges first in the stack, keeping numeric order aligned with merge order (see #453). - Move the notification entries out of registeredServices (the RBAC contract fixture): TestRPCContract_RegisteredServicesMatchMainMux compares it against the handlers mounted in main.go, and the mounts land in the next stack PR, so listing them here failed the test standalone. The ProcedurePermissions policy map (and its catalog keys) stay here; only the mount-coupled registration moves to the server PR. Addresses Codex connector findings (P1/P2) on #454. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b89405f to
c8602d1
Compare
Add the notification_history keyset Lister/Store, the sqlc query, and the CONCURRENTLY-built (organization_id, id DESC) keyset index (migration 000087). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c8602d1 to
6f51c39
Compare
ankitgoswami
left a comment
There was a problem hiding this comment.
approving based on the PR description
Completes the notifications feature with the read-only delivery history surface, backed by Proto Fleet's own notification_history table (#453) rather than Grafana. Server: the HistoryService (ListNotifications) with page-size clamping and exact has_more via keyset pagination, plus its registration in the mux, RPC contract test, and session-only interceptor list. Client: the History section + table with load-more pagination, the dashboard "Active notifications" card (gated on notification:read, deriving firing alerts client-side from the first history page), and the history parts of the API module, store, and view-model types. With this slice the mux mounts all four notifications.v1 services and the settings page renders all four sections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stack 2/5 — base:
eden/notifications-1-proto(#452)1. Summary
Adds the read/list path for notification history. Background already on
main: an Alertmanager webhook receiver persists each fired-alert notification into thenotification_historytable. This PR adds the ability to page through that history newest-first, so an operator can review what alerts fired for their organization. It delivers the DB/query/store layer only — aListNotificationHistorySQL query, a supporting index, and aListstore method using keyset pagination. The gRPC handler that calls this arrives in #455.2. How it works
The flow is a single-page read keyed off the caller's organization and an optional cursor:
HistoryServicehandler landing in notifications (4/5): Grafana-backed service, handlers, and server wiring #455) invokesstore.List(ctx, organizationID, beforeID, limit).beforeIDisnilfor the first page; for subsequent pages the caller passes theidof the last row it already received.Listmaps the Go arguments into sqlc query params (organizationID→NullInt64,beforeID→ nullable cursor,limit→ page size) and calls the generatedListNotificationHistory.notification_historyfiltered to the organization, withid < before_idapplied only when a cursor is present, orderedid DESC, capped byLIMIT. ItLEFT JOINs thedeviceanddiscovered_devicetables to resolvedevice_idinto a human-readable device name and MAC at read time (falling back to''when the device is unknown or soft-deleted).(organization_id, id DESC)index rather than scanning.Listconverts each generated row into aStoredNotification(the persistedNotificationplus row identityID,ReceivedAt, and the read-timeDeviceName/DeviceMAC) and returns the slice.Keyset (cursor) pagination: instead of
OFFSET, each next page seeks past the last seenid(id < before_id) and reads the nextLIMITrows inid DESCorder. Becausenotification_historyis insert-only, ids are monotonic and stable, so pages never shift or duplicate as new rows arrive. The handler in #455 owns page-size policy (default 50, max 200); this layer just honors thelimitit is given.3. Diagrams
Component / data flow (boundaries this PR touches are bold in prose; the handler is in #455):
flowchart LR H["HistoryService handler (#455)"] -->|"List(orgID, beforeID, limit)"| S["SQLNotificationHistoryStore.List"] S -->|"ListNotificationHistoryParams"| Q["sqlc: ListNotificationHistory (generated)"] Q -->|"SELECT ... WHERE org_id=? AND id<? ORDER BY id DESC LIMIT ?"| PG[("Postgres: notification_history")] PG -. "index scan" .-> IDX["idx_notification_history_org_id (organization_id, id DESC)"] Q -.->|"LEFT JOIN"| DEV[("device / discovered_device")] PG -->|"rows"| S S -->|"[]StoredNotification"| HKeyset pagination over an insert-only table (ordering by
id DESC, seeking past the cursor):sequenceDiagram participant C as Caller (#455) participant L as Store.List participant DB as Postgres C->>L: List(org, beforeID=nil, limit=50) L->>DB: WHERE org_id=org ORDER BY id DESC LIMIT 50 DB-->>L: rows [id=900 .. 851] L-->>C: page 1 (last id = 851) C->>L: List(org, beforeID=851, limit=50) L->>DB: WHERE org_id=org AND id < 851 ORDER BY id DESC LIMIT 50 DB-->>L: rows [id=850 .. 801] L-->>C: page 2 (last id = 801) Note over C,DB: New inserts get higher ids, already-paged rows never shift.4. Areas of the code involved
server/sqlc/queries/notification_history.sqlListNotificationHistory: org filter, optionalbefore_idcursor,id DESC,LIMIT, device-enrichment joins''fallbacks hereserver/internal/domain/notificationhistory/notificationhistory.goStoredNotificationread-back type andListerinterface (ListwithbeforeID *int64cursor)server/internal/domain/stores/sqlstores/notification_history.goListmethod on the SQL store; assertsListeris implemented; maps params and rowsStoredNotificationconversion (helpersptrToNullInt64/nullInt64ToPtr/nullTimeToPtrare pre-existing)server/migrations/000087_notification_history_keyset_index.up.sqlCREATE INDEX CONCURRENTLY (organization_id, id DESC)server/migrations/000087_notification_history_keyset_index.down.sqlDROP INDEX CONCURRENTLY IF EXISTSserver/generated/sqlc/notification_history.sql.goListNotificationHistoryquery, params, row, scannerserver/generated/sqlc/db.go5. Key technical decisions & trade-offs
id < before_id,id DESC) overOFFSET/LIMIT— stable on an insert-only table (no page drift/skew as rows arrive) and index-friendly; cost is the caller must thread the last id as a cursor rather than a page number.CREATE INDEX CONCURRENTLYover a plainCREATE INDEX— a plain build takesACCESS EXCLUSIVEon the live table and blocks webhook inserts for the build duration; CONCURRENTLY builds online (matches the convention in migration 000074). Trade-off: must be the sole statement and cannot run in a transaction, and a failed build can leave a dirty migration + anINVALIDindex (recovery noted inline in the migration).schema_migrationsadvances in numeric = merge order, avoiding golang-migrate skipping a lower-numbered migration on already-upgraded DBs.LEFT JOINrather than denormalized ontonotification_historyat insert — names stay current and deleted/unknown devices degrade to''; cost is a join per page (served by existing device keys).Listeras a separate interface from the existingStore(insert) interface — read and write capabilities are segregated so the notifications (4/5): Grafana-backed service, handlers, and server wiring #455 handler can depend only on the read surface.6. Testing & validation
just genre-run soserver/generated/sqlcmatches the committed query (the generated-code CI check enforces a clean tree).notificationhistory.Storeandnotificationhistory.Listervia compile-timevar _ = (*...)assertions.Listagainst a live DB, and there is no handler/end-to-end coverage — the gRPCHistoryServicethat callsList(and enforces the page-size clamp of default 50 / max 200) lands in notifications (4/5): Grafana-backed service, handlers, and server wiring #455, where request-level validation and tests belong. TheCONCURRENTLYmigration's online behavior is asserted by convention (matching 000074), not by an automated deploy test.🤖 Generated with Claude Code