page.tsx116 lines · main
1import { MailIcon } from '@/components/ui/mail';
2
3import { apiJson } from '@/lib/api';
4import { toValidDate } from '@/lib/utils';
5
6import { EmptyState } from '../_components/empty-state';
7import { Section } from '../_components/section';
8import { RemoveSuppressionButton, SuppressionControls } from './suppression-controls';
9
10interface Suppression {
11 id: string;
12 email: string;
13 reason: 'permanent_bounce' | 'complaint' | 'mittera_suppressed' | 'manual';
14 detail: string | null;
15 sourceEventId: string | null;
16 createdAt: string | Date;
17}
18
19export const dynamic = 'force-dynamic';
20export const metadata = { title: 'admin · email suppressions' };
21
22const REASON_LABEL: Record<Suppression['reason'], string> = {
23 permanent_bounce: 'permanent bounce',
24 complaint: 'complaint',
25 mittera_suppressed: 'mittera-suppressed',
26 manual: 'manual',
27};
28
29const REASON_TINT: Record<Suppression['reason'], string> = {
30 permanent_bounce: 'bg-red-500/10 text-red-400',
31 complaint: 'bg-red-500/10 text-red-400',
32 mittera_suppressed: 'bg-yellow-500/10 text-yellow-300',
33 manual: 'bg-[var(--color-surface-raised)] text-[var(--color-text-muted)]',
34};
35
36function formatTs(t: string | Date): string {
37 const d = toValidDate(t);
38 return d ? d.toISOString().replace('T', ' ').slice(0, 19) + 'Z' : '—';
39}
40
41function publicApiOrigin(): string {
42 return process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? '';
43}
44
45export default async function EmailSuppressionsAdminPage() {
46 const { suppressions } = await apiJson<{ suppressions: Suppression[] }>(
47 '/v1/admin/email-suppressions',
48 ).catch(() => ({ suppressions: [] as Suppression[] }));
49 const apiOrigin = publicApiOrigin();
50
51 return (
52 <div className="flex flex-col gap-10">
53 <header className="flex flex-col gap-2">
54 <div className="flex items-center gap-2">
55 <span className="text-[var(--color-primary)]">
56 <MailIcon size={20} />
57 </span>
58 <h1 className="font-mono text-xl tracking-tight">email suppressions</h1>
59 </div>
60 <p className="max-w-prose font-mono text-sm text-[var(--color-text-muted)]">
61 recipients briven won&rsquo;t send to. populated automatically by the mittera webhook
62 on permanent bounces, complaints, and mittera-side suppressions. add a manual entry to
63 block a sender; remove an entry to allow sending again. mutations require fresh
64 step-up auth — the prompt appears inline on stale sessions.
65 </p>
66 </header>
67
68 <SuppressionControls apiOrigin={apiOrigin} />
69
70 <Section title={`suppressed · ${suppressions.length}`} icon={<MailIcon size={16} />}>
71 {suppressions.length === 0 ? (
72 <EmptyState
73 icon={<MailIcon size={28} />}
74 title="no suppressions — the list is clear"
75 message="mittera will populate this list automatically when permanent bounces or complaints land at https://api.briven.tech/mittera-webhook."
76 />
77 ) : (
78 <div className="overflow-x-auto rounded-xl border border-[var(--color-border-subtle)] bg-[var(--color-surface)]">
79 <table className="w-full font-mono text-xs">
80 <thead className="text-left text-[11px] uppercase tracking-wider text-[var(--color-text-subtle)]">
81 <tr>
82 <th className="px-6 py-4 font-medium">when</th>
83 <th className="px-6 py-4 font-medium">email</th>
84 <th className="px-6 py-4 font-medium">reason</th>
85 <th className="px-6 py-4 font-medium">detail</th>
86 <th className="px-6 py-4 font-medium" />
87 </tr>
88 </thead>
89 <tbody>
90 {suppressions.map((s) => (
91 <tr key={s.id} className="border-t border-[var(--color-border-subtle)] align-top">
92 <td className="whitespace-nowrap px-6 py-4 text-[var(--color-text-subtle)]">
93 {formatTs(s.createdAt)}
94 </td>
95 <td className="px-6 py-4 text-[var(--color-text)]">{s.email}</td>
96 <td className="px-6 py-4">
97 <span
98 className={`inline-flex rounded-full px-2.5 py-0.5 ${REASON_TINT[s.reason]}`}
99 >
100 {REASON_LABEL[s.reason]}
101 </span>
102 </td>
103 <td className="px-6 py-4 text-[var(--color-text-muted)]">{s.detail ?? '—'}</td>
104 <td className="px-6 py-4">
105 <RemoveSuppressionButton email={s.email} apiOrigin={apiOrigin} />
106 </td>
107 </tr>
108 ))}
109 </tbody>
110 </table>
111 </div>
112 )}
113 </Section>
114 </div>
115 );
116}