feat: filter notifications to warnings/errors/ready only and add clear-all button

Notification filtering:
- lifecycle_hooks.py: only instance.error and instance.health_changed
  with status=running generate notifications. All other lifecycle events
  (created, started, stopped, restarted, deleted) are filtered out.
- health_monitor.py: only error and unhealthy states generate notifications.
  Running/recovered state no longer creates info notifications.
- _derive_title now maps instance.health_changed to "Container ready".

Clear-all button:
- Added dismiss_all() to NotificationService
- Added DELETE /notifications endpoint for bulk dismiss
- Frontend: clearAllNotifications API, clearAll in notification context,
  "Clear all" button in notification drawer alongside "Mark all as read"
- Added CSS for .notification-clear-all with danger hover state
- Updated notification-center tests

Quality gates: pytest (21 passed), vitest (11 passed)
This commit is contained in:
Alex Blank
2026-05-29 16:42:31 +02:00
parent 9c4500f9cb
commit 2b5223097f
11 changed files with 253 additions and 16 deletions
@@ -9,6 +9,7 @@ vi.mock("../api/notifications", () => ({
markNotificationRead: vi.fn(),
markAllNotificationsRead: vi.fn(),
dismissNotification: vi.fn(),
clearAllNotifications: vi.fn(),
}));
import { getNotifications, getUnreadCount } from "../api/notifications";
@@ -146,6 +147,26 @@ describe("NotificationCenter", () => {
expect(vi.mocked(mockMarkAll)).toHaveBeenCalled();
});
it("calls clearAll on clear-all button click", async () => {
mockedGetNotifications.mockResolvedValue({
items: [makeNotification("1")],
total: 1,
limit: 20,
offset: 0,
});
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
await vi.advanceTimersByTimeAsync(100);
fireEvent.click(screen.getByRole("button", { name: /clear all/i }));
const { clearAllNotifications: mockClearAll } = await import(
"../api/notifications"
);
expect(vi.mocked(mockClearAll)).toHaveBeenCalled();
});
it("refreshes list immediately on open", async () => {
render(<NotificationCenter />, { wrapper });
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
@@ -15,6 +15,7 @@ export function NotificationCenter({
unreadCount,
markRead,
markAllRead,
clearAll,
dismiss,
refreshList,
isDropdownOpen,
@@ -115,6 +116,15 @@ export function NotificationCenter({
>
Mark all as read
</button>
<button
type="button"
className="notification-clear-all"
onClick={() => {
void clearAll();
}}
>
Clear all
</button>
</div>
)}
</div>