From c86b8ff33a7b8a30175c22b136a499d47ce7ec1f Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Sun, 12 Apr 2026 11:44:51 -0400 Subject: [PATCH] Rework alert comments: authors, system events, wider layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add author field ("user" | "system") to AlertComment - System comments are automatically added when an alert is reopened by the engine ("Alert reopened — condition is still active.") or manually via the UI ("Manually reopened.") - Alert detail page redesigned with a two-column layout (3/5 detail, 2/5 comments) at lg breakpoint - System comments render as centered event dividers with a gear icon; user comments render as avatar + bubble Co-Authored-By: Claude Sonnet 4.6 --- src/app/alerts/[id]/AlertDetail.tsx | 214 +++++++++++++++++++--------- src/lib/db.ts | 22 ++- src/lib/types.ts | 1 + 3 files changed, 167 insertions(+), 70 deletions(-) diff --git a/src/app/alerts/[id]/AlertDetail.tsx b/src/app/alerts/[id]/AlertDetail.tsx index 6437e0b..1f5fa23 100644 --- a/src/app/alerts/[id]/AlertDetail.tsx +++ b/src/app/alerts/[id]/AlertDetail.tsx @@ -1,8 +1,10 @@ "use client"; -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import Link from "next/link"; -import { Alert, AlertSeverity } from "@/lib/types"; +import { Alert, AlertSeverity, AlertComment } from "@/lib/types"; + +// ── Severity theming ───────────────────────────────────────────────────────── const severityAccent: Record = { danger: "border-l-red-500", @@ -10,6 +12,20 @@ const severityAccent: Record = { info: "border-l-blue-500", }; +const severityText: Record = { + danger: "text-red-400", + warning: "text-yellow-400", + info: "text-blue-400", +}; + +const severityLabel: Record = { + danger: "Critical", + warning: "Warning", + info: "Info", +}; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + function timeAgo(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60_000); @@ -26,12 +42,62 @@ function shortDate(iso: string): string { }); } +// ── Comment row ─────────────────────────────────────────────────────────────── + +function CommentRow({ comment }: { comment: AlertComment }) { + const isSystem = comment.author === "system"; + + if (isSystem) { + return ( +
+
+
+ + + + + {comment.body} + · + {timeAgo(comment.createdAt)} +
+
+
+ ); + } + + return ( +
+
+
+ + + +
+ User + {shortDate(comment.createdAt)} +
+
+

+ {comment.body} +

+
+
+ ); +} + +// ── Main component ──────────────────────────────────────────────────────────── + export default function AlertDetail({ initialAlert }: { initialAlert: Alert }) { const [alert, setAlert] = useState(initialAlert); const [actionLoading, setActionLoading] = useState(false); const [commentText, setCommentText] = useState(""); const [commentLoading, setCommentLoading] = useState(false); const [error, setError] = useState(null); + const bottomRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [alert.comments.length]); async function toggleStatus() { setActionLoading(true); @@ -79,7 +145,7 @@ export default function AlertDetail({ initialAlert }: { initialAlert: Alert }) { const statusTime = isOpen ? alert.firstSeen : (alert.closedAt ?? alert.firstSeen); return ( -
+
)} - {/* Alert card */} -
+
- {/* Status row */} -
- - {isOpen && } - {isOpen ? "Open" : isResolved ? "Auto-resolved" : "Closed"} - · - {timeAgo(statusTime)} - + {/* ── Alert detail ─────────────────────────────────────────────── */} +
+
- + {/* Status row */} +
+ + {isOpen && } + {isOpen ? "Open" : isResolved ? "Auto-resolved" : "Closed"} + · + {timeAgo(statusTime)} + + + +
+ + {/* Severity + title + description */} +
+ + {severityLabel[alert.severity]} + +

{alert.title}

+

{alert.description}

+
+ +
- {/* Title + description */} -
-

{alert.title}

-

{alert.description}

+ {/* ── Comments ─────────────────────────────────────────────────── */} +
+ +

+ Comments +

+ +
+ {alert.comments.length === 0 && ( +

No comments yet.

+ )} + {alert.comments.map((c) => ( + + ))} +
+
+ +
+