From 8d8bae4e183cde6a86ed24fef48ee38931fbcb74 Mon Sep 17 00:00:00 2001
From: hsiegeln <37154749+hsiegeln@users.noreply.github.com>
Date: Mon, 20 Apr 2026 13:49:23 +0200
Subject: [PATCH] feat(ui/alerts): InboxPage with ack + bulk-read actions
AlertRow is reused by AllAlertsPage and HistoryPage. Marking a row as read
happens when its link is followed (the detail sub-route will be added in
phase 10 polish). FIRING rows get an amber left border.
---
ui/src/pages/Alerts/AlertRow.tsx | 48 ++++++++++++++++++++++
ui/src/pages/Alerts/InboxPage.tsx | 47 ++++++++++++++++++++-
ui/src/pages/Alerts/alerts-page.module.css | 18 ++++++++
3 files changed, 112 insertions(+), 1 deletion(-)
create mode 100644 ui/src/pages/Alerts/AlertRow.tsx
create mode 100644 ui/src/pages/Alerts/alerts-page.module.css
diff --git a/ui/src/pages/Alerts/AlertRow.tsx b/ui/src/pages/Alerts/AlertRow.tsx
new file mode 100644
index 00000000..9f23c9bd
--- /dev/null
+++ b/ui/src/pages/Alerts/AlertRow.tsx
@@ -0,0 +1,48 @@
+import { Link } from 'react-router';
+import { Button, useToast } from '@cameleer/design-system';
+import { AlertStateChip } from '../../components/AlertStateChip';
+import { SeverityBadge } from '../../components/SeverityBadge';
+import type { AlertDto } from '../../api/queries/alerts';
+import { useAckAlert, useMarkAlertRead } from '../../api/queries/alerts';
+import css from './alerts-page.module.css';
+
+export function AlertRow({ alert, unread }: { alert: AlertDto; unread: boolean }) {
+ const ack = useAckAlert();
+ const markRead = useMarkAlertRead();
+ const { toast } = useToast();
+
+ const onAck = async () => {
+ try {
+ await ack.mutateAsync(alert.id);
+ toast({ title: 'Acknowledged', description: alert.title, variant: 'success' });
+ } catch (e) {
+ toast({ title: 'Ack failed', description: String(e), variant: 'error' });
+ }
+ };
+
+ return (
+
+
+
+
markRead.mutate(alert.id)}>
+
{alert.title}
+
+
+
{alert.message}
+
+
+ {alert.state === 'FIRING' && (
+
+ )}
+
+
+ );
+}
diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx
index 10f561b6..ba9f2849 100644
--- a/ui/src/pages/Alerts/InboxPage.tsx
+++ b/ui/src/pages/Alerts/InboxPage.tsx
@@ -1,3 +1,48 @@
+import { useMemo } from 'react';
+import { Button, SectionHeader, useToast } from '@cameleer/design-system';
+import { PageLoader } from '../../components/PageLoader';
+import { useAlerts, useBulkReadAlerts } from '../../api/queries/alerts';
+import { AlertRow } from './AlertRow';
+import css from './alerts-page.module.css';
+
export default function InboxPage() {
- return InboxPage — coming soon
;
+ const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 100 });
+ const bulkRead = useBulkReadAlerts();
+ const { toast } = useToast();
+
+ const unreadIds = useMemo(
+ () => (data ?? []).filter((a) => a.state === 'FIRING').map((a) => a.id),
+ [data],
+ );
+
+ if (isLoading) return ;
+ if (error) return Failed to load alerts: {String(error)}
;
+
+ const rows = data ?? [];
+
+ const onMarkAllRead = async () => {
+ if (unreadIds.length === 0) return;
+ try {
+ await bulkRead.mutateAsync(unreadIds);
+ toast({ title: `Marked ${unreadIds.length} as read`, variant: 'success' });
+ } catch (e) {
+ toast({ title: 'Bulk read failed', description: String(e), variant: 'error' });
+ }
+ };
+
+ return (
+
+
+ Inbox
+
+
+ {rows.length === 0 ? (
+
No open alerts for you in this environment.
+ ) : (
+ rows.map((a) =>
)
+ )}
+
+ );
}
diff --git a/ui/src/pages/Alerts/alerts-page.module.css b/ui/src/pages/Alerts/alerts-page.module.css
new file mode 100644
index 00000000..71047bfd
--- /dev/null
+++ b/ui/src/pages/Alerts/alerts-page.module.css
@@ -0,0 +1,18 @@
+.page { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
+.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
+.row {
+ display: grid;
+ grid-template-columns: 72px 1fr auto;
+ gap: 12px;
+ padding: 12px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+}
+.rowUnread { border-left: 3px solid var(--accent); }
+.body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
+.meta { display: flex; gap: 8px; font-size: 12px; color: var(--muted); }
+.time { font-variant-numeric: tabular-nums; }
+.message { margin: 0; font-size: 13px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.actions { display: flex; align-items: center; }
+.empty { padding: 48px; text-align: center; color: var(--muted); }