Implement notification preferences and status event timeline#716
Implement notification preferences and status event timeline#716shyim wants to merge 11 commits into
Conversation
9e471e9 to
c8d1ef0
Compare
This comment has been minimized.
This comment has been minimized.
c8d1ef0 to
cd87f87
Compare
|
@copilot resolve the merge conflicts in this pull request |
Merged |
Greptile SummaryThis PR delivers a comprehensive notification system overhaul: a new
Confidence Score: 5/5Safe to merge — the core notification dispatch, preference resolver, and DB migrations are correct; the three findings are bounded in impact and do not affect data integrity or security. The notification architecture is well-structured and the tests cover the preference resolver and dispatcher thoroughly. The two issues in the scrape path are both bounded: the json.Marshal error discard is practically unreachable for a slice of simple structs, and the pre-migration check fallback only affects the brief window between schema deployment and the first rescrape. The notification-before-transaction ordering was already present in the previous architecture; the new timeline entry makes the inconsistency more visible but does not corrupt any existing data. api/internal/monitoring/scrape/service.go and api/internal/monitoring/scrape/notify.go — the silent error discard and the pre-migration check key fallback both live here. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Job as Scrape Job
participant SVC as scrape.Service
participant Notifier as notify.Dispatcher
participant Prefs as PreferenceResolver
participant InApp as inAppChannel
participant Email as emailChannel
participant DB as PostgreSQL
participant TX as DB Transaction
Job->>SVC: scrapeEnvironment(env)
SVC->>DB: GetEnvironmentChecks (old checks)
SVC->>SVC: handleStatusTransition(oldChecks, newChecks)
SVC->>DB: GetEnvironmentByID (current status)
SVC->>SVC: computeStatusReasons()
SVC->>Notifier: Dispatch(event, recipients)
loop per recipient
Notifier->>Prefs: Channels(recipient, event, defaults)
Prefs->>DB: ListNotificationPreferences(userId)
Prefs-->>Notifier: resolved channel list
Notifier->>InApp: Send() → UpsertNotification (committed immediately)
Notifier->>Email: Send() (if dedup lock acquired)
end
Notifier-->>SVC: transition struct
SVC->>TX: persistScrapeResult (begin transaction)
TX->>DB: UpdateEnvironmentStatus
TX->>DB: InsertEnvironmentCheck (×N)
TX->>DB: InsertEnvironmentStatusEvent
TX-->>SVC: commit / rollback
Note over Notifier,TX: Notifications committed before transaction —<br/>timeline entry absent if transaction rolls back
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Job as Scrape Job
participant SVC as scrape.Service
participant Notifier as notify.Dispatcher
participant Prefs as PreferenceResolver
participant InApp as inAppChannel
participant Email as emailChannel
participant DB as PostgreSQL
participant TX as DB Transaction
Job->>SVC: scrapeEnvironment(env)
SVC->>DB: GetEnvironmentChecks (old checks)
SVC->>SVC: handleStatusTransition(oldChecks, newChecks)
SVC->>DB: GetEnvironmentByID (current status)
SVC->>SVC: computeStatusReasons()
SVC->>Notifier: Dispatch(event, recipients)
loop per recipient
Notifier->>Prefs: Channels(recipient, event, defaults)
Prefs->>DB: ListNotificationPreferences(userId)
Prefs-->>Notifier: resolved channel list
Notifier->>InApp: Send() → UpsertNotification (committed immediately)
Notifier->>Email: Send() (if dedup lock acquired)
end
Notifier-->>SVC: transition struct
SVC->>TX: persistScrapeResult (begin transaction)
TX->>DB: UpdateEnvironmentStatus
TX->>DB: InsertEnvironmentCheck (×N)
TX->>DB: InsertEnvironmentStatusEvent
TX-->>SVC: commit / rollback
Note over Notifier,TX: Notifications committed before transaction —<br/>timeline entry absent if transaction rolls back
Reviews (3): Last reviewed commit: "fix: align notify subscriptions with cap..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd053b51f6
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…eferences A complete rework of how alerts and notifications work: - notify domain package (typed events, registry, pluggable channels, dispatcher) with a shared i18n catalog; alerts render per recipient locale and emails localize via user.locale. - Checks and notifications are stored as translation keys + params and rendered in the viewer's language; locale persists to the server. - Normalized notification_preference table replaces the user.notifications JSONB array, with a most-specific-first resolver (global -> environment, event-specific > wildcard) and a useNotificationPreferences composable shared by the settings page and the environment notification modal. - Per-channel, per-event-type and per-environment preferences, event types served from an endpoint, watched environments grouped by shop. - Status-change visibility: check diffing, environment_status_event timeline, recovery alerts, and reasons surfaced in notifications, emails and a status-history view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq
…moval Three lifecycle gaps around notification data: 1. user_notification was the only user-referencing table without ON DELETE cascade, so deleting a user who had any notification failed on the foreign key. Add the cascade (migration 000020). 2. Removing a user from an organization (RemoveMember) or leaving it (LeaveOrganization) left their environment-scoped preferences and inbox notifications for that org's environments behind — the subscriber query already gates on membership so no future alerts leak, but the stale rows kept the environments showing in the user's watched list. Both flows now purge that data in the same transaction as the member removal. 3. Deleting an environment left its environment-scoped preferences and linked notifications orphaned (they reference the environment by text scope_id / link param, not a foreign key). DeleteEnvironment now removes them within its existing transaction. Adds integration tests covering the cascade and both cleanup paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq
Replaces the two separate global notification controls ("Delivery
channels" on/off-all-events and "Event types" on/off-all-channels) with
a single matrix: rows are event types, columns are channels, and each
cell toggles whether that event is delivered on that channel.
This was already supported by the model and resolver
(notification_preference is keyed by event_type x channel, and
channelEnabled resolves per event+channel); only the UI exposed the
coarser two-axis controls. Cells are shown only for an event's default
channels (e.g. data-fetch errors are in-app only), using the
defaultChannels from the event-types endpoint.
useNotificationPreferences gains eventChannelEnabled/setEventChannel and
drops the now-redundant global channel/event booleans.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq
Per-environment notification settings were two separate axes (a channel list + an event-type list, each inherit/on/off), while the global settings were an event x channel matrix. That was inconsistent and actually less expressive than global — you couldn't override a single event+channel for an environment. Unify both scopes on one matrix: - Extract a reusable <NotificationMatrix> component. Global cells are on/off switches; per-environment cells are inherit/on/off, where "inherit" falls back to the global cell for that exact event+channel (environment + event + channel is the most specific resolver scope). - Settings (global + per-environment expandable) and the environment notification modal all render the same matrix. - Composable: replace the coarse env channel/event helpers (envChannelState/setEnvChannel/envEventState/setEnvEvent) with envEventChannelState/setEnvEventChannel. No backend change; the resolver already supported this granularity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq
The matrix overflowed the environment modal: DialogContent is a CSS
grid, whose item defaults to min-width:auto and so refused to shrink
below the table's content width, defeating the inner overflow-x-auto and
spilling the table outside the dialog.
- Add min-w-0 to the modal body (and the matrix scroll container) so the
grid item can shrink and horizontal scroll engages when needed.
- Use short channel column headers ("In-app" / "Email") and let the
event-label column wrap, so the matrix fits the dialog width without
scrolling in the common case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq
…ests - Enable email for data_fetch_error: add the email channel to its registry defaults and gate it behind the same one-hour dedup lock as the other emailing events, so a persistently failing fetch emails at most once an hour. The matrix surfaces the email cell automatically (it's driven by the event-types endpoint's defaultChannels). - Convert the notify package tests to testify (assert/require) to match the rest of the codebase, and update the data-fetch dispatch test to reflect that it now emails (deduped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq
The status-history route and view existed, but the environment detail tabs are a hardcoded list, so without an entry there was no way to reach it from the UI. Add a "Status history" tab (after Checks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq
0e83d05 to
c3266a4
Compare
Summary
This PR introduces a comprehensive notification system overhaul, adding user-configurable notification preferences, multi-channel delivery (in-app and email), localized message rendering, and an environment status change timeline.
Key Changes
Notification System Architecture
notifypackage: Separates concerns into Event (what happened), Preferences (who gets notified), Channel (how it's delivered), and Translator (what it says)Channelinterface with in-app and email implementations; extensible for future channels (webhook, Slack, etc.)PreferenceResolverhonors per-user, per-scope, per-event override chain with inheritance (global → organization → environment)Notification Preferences
notification_preferencetable with scope (global/organization/environment) × event type × channel modelGET/PUT/DELETE /account/notification-preferencesfor managing preferencesGET /notifications/event-typesto list available event types and their default channelsuseNotificationPreferencescentralizes preference state across componentsLocalization & Message Rendering
messageKey/titleKeywith structuredparamsinstead of pre-rendered English stringsTranslatorloads locale JSON files (en, de) with{param}interpolation, matching frontend vue-i18n syntaxuser.localecolumn drives server-side email renderingStatus Event Timeline
environment_status_eventrecords every status transition with reasons (which checks caused it)GET /environments/{environmentId}/status-eventsreturns timelineDetailStatusHistory.vuedisplays status changes with check detailsJob Refactoring
environment_scrape_notify.gorefactored to useDispatcherinstead of inline notification logicStatusReasonarray (which checks changed level) for timeline persistencenotify.lockAcquirerfor email-level dedup per event typeAccount Management
PATCH /account/menow supports updating locale preferenceGetEnvironmentNotificationSubscribersquery for environment-scoped notificationsNotable Implementation Details
Files Added
api/internal/notify/package (event, channel, dispatcher, preferences, translator, registry, lock)api/internal/handler/notification_preference.goapi/internal/database/queries/notification_preference.sql.goapi/migrations/000013-000016(user locale, check keys, preferences, status events)useNotificationPreferences.ts,EnvironmentNotificationModal.vue,DetailStatusHistory.vue,i18n.tsapi/internal/notify/locales/{en,de}.jsonhttps://claude.ai/code/session_01X2GLoyem7aStcR2K5hx5jq