"use client"; import { useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/app-shell"; import { apiFetch } from "@/lib/api"; type NotificationPreference = { emailEnabled: boolean; pushEnabled: boolean; minSeverity: "info" | "warning" | "critical"; }; type LedgerNotification = { id: string; type: string; severity: "info" | "warning" | "critical"; title: string; body: string; channels: string[]; readAt?: string | null; createdAt: string; }; type VapidStatus = { enabled: boolean; publicKey: string | null; }; function urlBase64ToUint8Array(base64String: string) { const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; i += 1) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } export default function NotificationsPage() { const [notifications, setNotifications] = useState([]); const [preferences, setPreferences] = useState({ emailEnabled: true, pushEnabled: false, minSeverity: "info", }); const [vapid, setVapid] = useState({ enabled: false, publicKey: null }); const [status, setStatus] = useState(""); const [loading, setLoading] = useState(true); const unreadCount = useMemo(() => notifications.filter((item) => !item.readAt).length, [notifications]); const load = async () => { setLoading(true); const [listRes, prefRes, vapidRes] = await Promise.all([ apiFetch("/api/notifications"), apiFetch("/api/notifications/preferences"), apiFetch("/api/notifications/vapid-public-key"), ]); if (!listRes.error) setNotifications(listRes.data ?? []); if (!prefRes.error && prefRes.data) setPreferences(prefRes.data); if (!vapidRes.error && vapidRes.data) setVapid(vapidRes.data); setLoading(false); }; useEffect(() => { load(); }, []); const updatePreferences = async (patch: Partial) => { const next = { ...preferences, ...patch }; setPreferences(next); const res = await apiFetch("/api/notifications/preferences", { method: "PATCH", body: JSON.stringify(patch), }); if (res.error) { setStatus(res.error.message ?? "Could not update notification preferences."); return; } if (res.data) setPreferences(res.data); setStatus("Notification preferences updated."); }; const enablePush = async () => { setStatus(""); if (!vapid.enabled || !vapid.publicKey) { setStatus("Push notifications need VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY on the backend."); return; } if (!("serviceWorker" in navigator) || !("PushManager" in window)) { setStatus("This browser does not support web push notifications."); return; } const permission = await Notification.requestPermission(); if (permission !== "granted") { setStatus("Browser notification permission was not granted."); return; } const registration = await navigator.serviceWorker.register("/sw.js"); const existing = await registration.pushManager.getSubscription(); const subscription = existing ?? await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapid.publicKey), }); const res = await apiFetch("/api/notifications/push-subscriptions", { method: "POST", body: JSON.stringify(subscription.toJSON()), }); if (res.error) { setStatus(res.error.message ?? "Could not save push subscription."); return; } setPreferences((prev) => ({ ...prev, pushEnabled: true })); setStatus("Push notifications enabled for this browser."); }; const sendTest = async () => { setStatus("Sending test notification..."); const res = await apiFetch("/api/notifications/test", { method: "POST" }); if (res.error) { setStatus(res.error.message ?? "Test notification failed."); return; } setStatus("Test notification sent."); await load(); }; const markRead = async (id: string) => { const res = await apiFetch(`/api/notifications/${id}/read`, { method: "PATCH" }); if (!res.error) { setNotifications((items) => items.map((item) => item.id === id ? { ...item, readAt: new Date().toISOString() } : item)); } }; const markAllRead = async () => { const res = await apiFetch("/api/notifications/read-all", { method: "POST" }); if (!res.error) { const now = new Date().toISOString(); setNotifications((items) => items.map((item) => ({ ...item, readAt: item.readAt ?? now }))); } }; const inputCls = "mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary focus:outline-none"; return (

SMTP Email

Send important Aarthalabs alerts through the configured SMTP transport.

Browser Push

{vapid.enabled ? "Register this browser for web push alerts." : "Backend VAPID keys are not configured."}

Delivery Threshold

Email and push delivery only run at or above this severity.

Notification Center

{unreadCount} unread alert{unreadCount === 1 ? "" : "s"}

{status &&

{status}

}
{loading &&

Loading notifications...

} {!loading && notifications.length === 0 &&

No notifications yet.

} {!loading && notifications.map((item) => (
{!item.readAt && }

{item.title}

{item.severity}

{item.body}

{new Date(item.createdAt).toLocaleString()} ยท {item.channels.join(", ") || "in_app"}

{!item.readAt && ( )}
))}
); }