Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions openspec/changes/add-svelte-notifications-management/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Design: Add Svelte Notifications Management

## Backend

### NotificationService extraction

Extract shared notification logic from `StatusController` into a service to avoid duplication:

```csharp
// src/Exceptionless.Core/Services/NotificationService.cs
public class NotificationService(ICacheClient cacheClient, IMessagePublisher messagePublisher, TimeProvider timeProvider)
{
public Task<SystemNotification?> GetSystemNotificationAsync();
public Task<SystemNotification> SetSystemNotificationAsync(string message, bool publish = true);
public Task ClearSystemNotificationAsync(bool publish = true);
public Task<ReleaseNotification> SendReleaseNotificationAsync(string? message, bool critical);
}
```

- Cache key: `system-notification` (unchanged)
- Publish: via `IMessagePublisher` (unchanged)
- `StatusController` delegates to this service (behavior unchanged)

### Admin endpoints

Added to `AdminController` (already `[Authorize(Policy = GlobalAdminPolicy)]`):

| Method | Route | Body | Response |
|--------|-------|------|----------|
| GET | `admin/notifications` | — | `{ configured_message, system_notification }` |
| PUT | `admin/notifications/system` | `{ message }` | `SystemNotification` |
| DELETE | `admin/notifications/system?publish=true` | — | 204 |
| POST | `admin/notifications/release` | `{ message, critical }` | `ReleaseNotification` |
| POST | `admin/notifications/force-refresh` | — | `ReleaseNotification` |

DTOs in `src/Exceptionless.Web/Models/Admin/`:
- `NotificationSettingsResponse` — configured fallback + current persisted notification
- `SetSystemNotificationRequest` — message string
- `SendReleaseNotificationRequest` — message + critical flag
Comment on lines +24 to +39

### Authorization

All admin endpoints require `GlobalAdminPolicy` (existing controller-level attribute).

## Frontend (Svelte)

### Feature module

`src/Exceptionless.Web/ClientApp/src/lib/features/notifications/`

- `models.ts` — Re-export websocket models + admin DTOs
- `api.svelte.ts` — TanStack Query wrappers using `useFetchClient`
- `components/notification-banners.svelte` — Banner rendering component

### Notification banners

Rendered in `src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte`.

Behavior:
1. On mount: fetch `GET /api/v2/notifications/system` (existing StatusController endpoint)
2. Fallback: if no persisted notification, use `PUBLIC_SYSTEM_NOTIFICATION_MESSAGE` env var
3. Listen for `SystemNotification` and `ReleaseNotification` DOM CustomEvents (existing websocket dispatch)
4. System notification → destructive/danger Alert with `role="alert"` and `aria-live="assertive"`
5. Release notification → info Alert with `role="status"` and `aria-live="polite"`
6. Critical release → `window.location.reload()` immediately

Security: Plain text only. No `{@html}`. If sanitization is needed later, use dompurify.

### Admin page

Route: `/system/notifications`
- Added to system nav in `routes.svelte.ts`, visible to global admins only
- Card layout showing current state
- Dialog-based actions: set notification, clear, send release, force refresh
- Uses shadcn-svelte Alert, Button, Dialog, Input/Textarea, Card
- Mutations use svelte-sonner for success/error toasts
- Invalidates queries on mutation success

### Query keys

```typescript
export const notificationKeys = {
settings: ['admin', 'notifications'] as const,
current: ['notifications', 'system'] as const,
};
```

## Security

- Admin endpoints already behind GlobalAdminPolicy
- No HTML injection: plain text rendering only
- No secrets exposed in admin response (only configured message text)

## Accessibility

- Banners use `role="alert"` / `role="status"` with appropriate `aria-live`
- Dismiss button (if added) is keyboard-accessible
- Color is not sole indicator (icon + text in banners)
29 changes: 29 additions & 0 deletions openspec/changes/add-svelte-notifications-management/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Proposal: Add Svelte Notifications Management

## Summary

Implement global notification support in the Svelte 5 UI matching the legacy Angular behavior: system notification banners, release notification banners, critical release force-refresh, configuration fallback messages, and a global-admin notification management page.

Additionally, add dedicated admin endpoints under `/api/v2/admin/notifications` to provide a cleaner management interface while preserving the existing `StatusController` notification endpoints for backwards compatibility.

## Classification

- **Type:** Feature + UI migration
- **Affected areas:** Backend/API, Svelte UI, Redis (cache key `system-notification`), WebSocket messages, tests
- **OpenSpec justification:** New API endpoints, WebSocket message consumption in new UI, admin authorization, cross-cutting UI/API contract, Angular-to-Svelte behavior migration

## Compatibility Risks

| Risk | Mitigation |
|------|-----------|
| Existing `StatusController` notification endpoints | Preserved unchanged; new admin endpoints are additive |
| WebSocket message format (`SystemNotification`, `ReleaseNotification`) | Consumed as-is; no format changes |
| Redis cache key `system-notification` | Shared between StatusController and new AdminController endpoints via extracted service |
| Config key `PUBLIC_SYSTEM_NOTIFICATION_MESSAGE` | Read-only consumption; no changes to how it's set |
| SDK/client expectations | No SDK changes; WebSocket messages unchanged |

## Rollback Plan

- New admin endpoints are additive; removing them has no impact on existing clients.
- Frontend notification banners degrade gracefully (no banner shown if fetch fails).
- Admin page is behind global-admin nav guard; removing it is safe.
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Spec: Notifications

## ADDED: StatusController notification management endpoints

### Requirement: Admin can read notification settings

Given an authenticated global admin user
When they send `GET /api/v2/notifications/settings`
Then the response is 200 with a `NotificationSettingsResponse`:
- `configured_system_notification_message`: the `AppOptions.NotificationMessage` value (may be null)
- `system_notification`: the currently cached `SystemNotification` (may be null)

### Requirement: Non-admin cannot access admin notification endpoints

Given an authenticated non-admin user
When they send any request to `POST/DELETE /api/v2/notifications/*` or `GET /api/v2/notifications/settings`
Then the response is 403 Forbidden.

### Requirement: Admin can set a system notification

Given an authenticated global admin user
When they send `POST /api/v2/notifications/system` with body `{ "value": "Maintenance tonight" }`
Then:
- The system notification is stored in cache key `system-notification`
- A `SystemNotification` message is published to the message bus (when `publish=true`, the default)
- The response is 200 with the `SystemNotification` object

#### Scenario: Empty message is rejected

Given an authenticated global admin user
When they send `POST /api/v2/notifications/system` with body `{ "value": "" }`
Then the response is 400 Bad Request.

#### Scenario: Publish suppression

Given an authenticated global admin user
When they send `POST /api/v2/notifications/system?publish=false` with body `{ "value": "Silent" }`
Then:
- The system notification is stored in cache
- No message is published to the message bus
- The response is 200 with the `SystemNotification` object

### Requirement: Admin can clear a system notification

Given an authenticated global admin user
When they send `DELETE /api/v2/notifications/system`
Then:
- The `system-notification` cache key is removed
- A blank `SystemNotification` is published to the message bus (when `publish=true`, the default)
- The response is 200

#### Scenario: Clear without publish

Given an authenticated global admin user
When they send `DELETE /api/v2/notifications/system?publish=false`
Then:
- The `system-notification` cache key is removed
- No message is published to the message bus
- The response is 200

### Requirement: Admin can send a release notification

Given an authenticated global admin user
When they send `POST /api/v2/notifications/release` with body `{ "value": "v8.0 released" }` and optional `?critical=false`
Then:
- A `ReleaseNotification` is published to the message bus
- The response is 200 with the `ReleaseNotification` object

### Requirement: Admin can force-refresh all clients

Given an authenticated global admin user
When they send `POST /api/v2/notifications/force-refresh` with optional body `{ "value": "reason" }`
Then:
- A `ReleaseNotification` is published with `critical=true` and optional message
- The response is 200 with the `ReleaseNotification` object

## ADDED: Svelte notification banners (Svelte UI only)

### Requirement: System notification banner displays on page load

Given a system notification is persisted in cache
When a user loads any authenticated page in the Svelte app
Then a destructive/danger banner is displayed at the top of the layout with the notification message.

#### Scenario: Fallback to configured message

Given no system notification is persisted in cache
And `PUBLIC_SYSTEM_NOTIFICATION_MESSAGE` is set to "Scheduled maintenance"
When a user loads any authenticated page
Then a destructive/danger banner displays "Scheduled maintenance".

#### Scenario: No notification and no fallback

Given no system notification is persisted in cache
And `PUBLIC_SYSTEM_NOTIFICATION_MESSAGE` is empty
When a user loads any authenticated page
Then no system notification banner is displayed.

### Requirement: Realtime system notification updates via WebSocket

Given a user has the Svelte app open
When a `SystemNotification` WebSocket message is received with a non-empty message
Then the system notification banner updates to show the new message.

#### Scenario: Clear notification via WebSocket

Given a user has the Svelte app open with a system notification banner visible
When a `SystemNotification` WebSocket message is received with an empty/null message
Then the system notification banner is hidden (falls back to env var if configured).

### Requirement: Release notification banner displays on WebSocket message

Given a user has the Svelte app open
When a `ReleaseNotification` WebSocket message is received with `critical=false`
Then an info banner is displayed with the release message.

### Requirement: Critical release notification triggers page reload

Given a user has the Svelte app open
When a `ReleaseNotification` WebSocket message is received with `critical=true`
Then `window.location.reload()` is called immediately.

### Requirement: Notification banners use accessible markup

Given a system notification banner is displayed
Then it has `role="alert"` and `aria-live="assertive"`.

Given a release notification banner is displayed
Then it has `role="status"` and `aria-live="polite"`.

### Requirement: No unsafe HTML rendering

Given any notification message content
When it is rendered in the Svelte UI
Then it is rendered as plain text (no innerHTML or {@html}).

## ADDED: Svelte admin notifications page (Svelte UI only)

### Requirement: System → Notifications page exists for global admins

Given an authenticated global admin user
When they navigate to `/system/notifications`
Then they see the notification management page with:
- Current configured fallback message (read-only)
- Current persisted system notification (if any)
- Actions: Set system notification, Clear, Send release notification, Force refresh

### Requirement: Notifications page is hidden from non-admins

Given an authenticated non-admin user
When they view the system navigation
Then the "Notifications" link is not visible.

## HTTP test file updates

### Requirement: tests/http files updated for notification endpoints

Given the notification endpoints exist in StatusController
Then `tests/http/status.http` includes sample requests for:
- GET notifications/settings
- POST notifications/system
- DELETE notifications/system
- POST notifications/release
- POST notifications/force-refresh

66 changes: 66 additions & 0 deletions openspec/changes/add-svelte-notifications-management/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Tasks: Add Svelte Notifications Management

## Backend

- [x] **Task 1: Extract NotificationService**
- Created `src/Exceptionless.Core/Services/NotificationService.cs`
- Methods: `GetSystemNotificationAsync`, `SetSystemNotificationAsync`, `ClearSystemNotificationAsync`, `SendReleaseNotificationAsync`
- Registered as singleton in DI (`Bootstrapper.cs`)
- Refactored `StatusController` to delegate to `NotificationService` (no behavior change)

- [x] **Task 2: Add new StatusController endpoints**
- `GET notifications/settings` — returns configured fallback + current notification (admin only)
- `POST notifications/force-refresh` — force reload all clients via critical release notification (admin only)
- Added `bool publish = true` query param to POST and DELETE system notification endpoints
- All endpoints use existing `ValueFromBody<string>` pattern for backward compatibility

- [x] **Task 3: Update HTTP test samples**
- Updated `tests/http/status.http` with force-refresh and settings endpoint samples

- [x] **Task 4: Add backend integration tests**
- Added tests to existing `tests/Exceptionless.Tests/Controllers/StatusControllerTests.cs`
- Tests: settings (admin/non-admin), force-refresh (with message/without/non-admin), publish flag
- All tests follow AAA (Arrange/Act/Assert) pattern

## Frontend — System Notification Banner

- [x] **Task 5: Create system-notifications feature module**
- Created `src/Exceptionless.Web/ClientApp/src/lib/features/system-notifications/models.ts`
- Created `src/Exceptionless.Web/ClientApp/src/lib/features/system-notifications/api.svelte.ts`
- All API calls target StatusController routes (`notifications/*`)

- [x] **Task 6: Create system-notification-banner component**
- Created `src/Exceptionless.Web/ClientApp/src/lib/features/system-notifications/components/system-notification-banner.svelte`
- Three-tier message resolution: realtime WebSocket → persisted API → env fallback
- System = destructive Alert with `role="alert"` `aria-live="assertive"`
- Release = info Alert with `role="status"` `aria-live="polite"`
- Critical release = `window.location.reload()`

- [x] **Task 7: Integrate banner into app layout**
- Added `<SystemNotificationBanner />` to `(app)/+layout.svelte`

- [x] **Task 8: Add unit tests**
- Created `system-notification-banner.test.ts` with 6 tests covering event handling and message resolution logic

## Frontend — Admin Page

- [x] **Task 9: Add System → Notifications route and nav entry**
- Created `src/Exceptionless.Web/ClientApp/src/routes/(app)/system/notifications/+page.svelte`
- Added Bell icon + Notifications nav entry in `routes.svelte.ts`, visible only to global admins

- [x] **Task 10: Implement notifications admin page**
- Displays current state (configured fallback, persisted notification)
- Actions via dialogs: Set system notification, Clear/reset, Send release notification, Force refresh
- Uses shadcn-svelte components, svelte-sonner toasts, TanStack Query mutations
- Invalidates notification queries on success

## Final Validation

- [x] **Task 11: Full build and test validation**
- `dotnet build` ✓
- `dotnet test -- --filter-class StatusControllerTests` → 19/19 pass ✓
- `npm run check` (svelte-check) → 0 errors ✓
- `npm run lint` → clean ✓
- `npm run build` → success ✓
- `npm run test:unit` → 249/249 pass ✓
- UI dogfood via browser → all flows verified ✓
1 change: 1 addition & 0 deletions src/Exceptionless.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO
services.AddSingleton<UserAgentParser>();
services.AddSingleton<ICoreLastReferenceIdManager, NullCoreLastReferenceIdManager>();

services.AddSingleton<NotificationService>();
services.AddSingleton<OrganizationService>();
services.AddStartupAction<OrganizationService>();
services.AddSingleton<UsageService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Exceptionless.Core.Messaging.Models;

public record NotificationSettingsResponse
{
public string? ConfiguredSystemNotificationMessage { get; init; }
public SystemNotification? SystemNotification { get; init; }
}
Loading