import { useEffect, useMemo, useState } from "react"; import { ArrowLeft, Plus, RefreshCw, RotateCw, Trash2 } from "lucide-react"; import { ActionBlockerHint, Button, Card, ConfirmDialog, DataGrid, Dialog, DismissibleAlert, DocumentationHelpLink, FormField, LoadingFrame, PageScrollViewport, PageTitle, StatusBadge, TableActionGroup, ToggleSwitch, adminErrorMessage, formatDateTime, useGuardedNavigate, type ApiSettings, type DataGridColumn } from "@govoplan/core-webui"; import { listMailBounceObservations, listMailBounceSources, listMailServerProfiles, removeMailBounceSource, saveMailBounceSource, scanMailBounceSource, type MailBounceObservation, type MailBounceSource, type MailServerProfile } from "../../api/mail"; const MAIL_BOUNCE_DOCUMENTATION = { topicId: "mail.bounce-processing", documentationType: "admin" } as const; export default function MailBouncePage({ settings }: { settings: ApiSettings }) { const navigate = useGuardedNavigate(); const [sources, setSources] = useState([]); const [observations, setObservations] = useState([]); const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(""); const [error, setError] = useState(""); const [message, setMessage] = useState(""); const [addOpen, setAddOpen] = useState(false); const [deleteSource, setDeleteSource] = useState(null); const [profileId, setProfileId] = useState(""); const [folder, setFolder] = useState("INBOX"); const [active, setActive] = useState(true); const profileNames = useMemo( () => new Map(profiles.map((profile) => [profile.id, profile.name])), [profiles] ); const imapProfiles = useMemo( () => profiles.filter((profile) => profile.is_active && profile.imap), [profiles] ); const pageMutationBlocker = loading ? "Bounce evidence is already loading." : busy ? "Wait for the current bounce-processing action to finish." : ""; const addWatcherBlocker = pageMutationBlocker || (imapProfiles.length === 0 ? "Configure an active IMAP-enabled Mail profile before adding a watcher." : ""); const saveWatcherBlocker = busy ? "Wait for the current bounce-processing action to finish." : !profileId ? "Select an active IMAP-enabled Mail profile." : !folder.trim() ? "Enter the mailbox folder that contains delivery-status messages." : ""; async function load() { setLoading(true); setError(""); try { const [nextSources, nextObservations, nextProfiles] = await Promise.all([ listMailBounceSources(settings), listMailBounceObservations(settings), listMailServerProfiles(settings, true) ]); setSources(nextSources); setObservations(nextObservations); setProfiles(nextProfiles); setProfileId((current) => current || nextProfiles.find((profile) => profile.imap)?.id || ""); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } } useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); async function addSource() { if (!profileId || !folder.trim()) return; setBusy("add"); setError(""); try { await saveMailBounceSource(settings, { profile_id: profileId, folder: folder.trim(), is_active: active }); setAddOpen(false); setMessage("Bounce mailbox watcher saved."); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(""); } } async function scan(source: MailBounceSource) { setBusy(`scan:${source.id}`); setError(""); try { const result = await scanMailBounceSource(settings, source.id); setMessage(`Processed ${result.processed_messages} message(s) and recorded ${result.observations} bounce observation(s).`); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(""); } } async function removeSource() { if (!deleteSource) return; setBusy(`delete:${deleteSource.id}`); setError(""); try { await removeMailBounceSource(settings, deleteSource.id); setDeleteSource(null); setMessage("Bounce mailbox watcher removed. Existing observations were retained."); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(""); } } const sourceColumns: DataGridColumn[] = [ { id: "profile", header: "Mail profile", width: "minmax(180px, 1fr)", value: (source) => profileNames.get(source.profile_id) || source.profile_id, render: (source) => {profileNames.get(source.profile_id) || source.profile_id} }, { id: "folder", header: "Folder", width: "minmax(150px, .8fr)", value: (source) => source.folder }, { id: "status", header: "Status", width: 130, value: (source) => source.last_error ? "error" : source.is_active ? "active" : "inactive", render: (source) => }, { id: "cursor", header: "Last UID", width: 110, value: (source) => source.highest_processed_uid }, { id: "lastScan", header: "Last scan", width: "minmax(180px, .8fr)", value: (source) => source.last_scanned_at || "", render: (source) => source.last_scanned_at ? formatDateTime(source.last_scanned_at) : "Never" }, { id: "actions", header: "Actions", width: 92, sticky: "end", render: (source) =>