chore: standardize security and quality foundations
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-17 20:03:47 +12:00
parent 5639dbcb83
commit f852e7c941
127 changed files with 17928 additions and 10741 deletions
+506 -112
View File
@@ -1,132 +1,526 @@
'use client'
"use client";
import { useCallback, useEffect, useRef, useState, type FormEvent } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import AdminShell from '../../ui/AdminShell'
import { authFetch, getApiBase } from '../../lib/auth'
import '../../email-recaps/recaps.css'
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import AdminShell from "../../ui/AdminShell";
import { authFetch, getApiBase } from "../../lib/auth";
import "../../email-recaps/recaps.css";
type Settings = { enabled: boolean; day: number; hour: number; public_url: string; next_send_at?: number | null }
type Delivery = { id: string; month: string; kind: string; email: string; username: string | null; state: string; attempts: number; created_at: number; updated_at: number; next_attempt_at: number; detail: string }
type Overview = { settings: Settings; ready: boolean; detail: string; months: string[]; deliveries: Delivery[]; total: number; subscribers: number; worker_enabled: boolean }
type Preview = { month: string; subject: string; body_html: string; body_text: string; email: string | null }
const monthLabel = (month: string) => new Date(`${month}-01T00:00:00Z`).toLocaleDateString(undefined, { month: 'long', year: 'numeric', timeZone: 'UTC' })
const dateLabel = (value?: number | null) => value ? `${new Date(value * 1000).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC' })} UTC` : 'Not scheduled'
const stateLabels: Record<string, string> = { queued: 'Queued', preparing: 'Preparing report', sending: 'Sending', sent: 'Accepted by mail server', retry: 'Retry scheduled', failed: 'Failed', unknown: 'Needs review', cancelled: 'Cancelled' }
type Settings = { enabled: boolean; day: number; hour: number; public_url: string; next_send_at?: number | null };
type Delivery = {
id: string;
month: string;
kind: string;
email: string;
username: string | null;
state: string;
attempts: number;
created_at: number;
updated_at: number;
next_attempt_at: number;
detail: string;
};
type Overview = {
settings: Settings;
ready: boolean;
detail: string;
months: string[];
deliveries: Delivery[];
total: number;
subscribers: number;
worker_enabled: boolean;
};
type Preview = { month: string; subject: string; body_html: string; body_text: string; email: string | null };
const monthLabel = (month: string) =>
new Date(`${month}-01T00:00:00Z`).toLocaleDateString(undefined, { month: "long", year: "numeric", timeZone: "UTC" });
const dateLabel = (value?: number | null) =>
value
? `${new Date(value * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" })} UTC`
: "Not scheduled";
const stateLabels: Record<string, string> = {
queued: "Queued",
preparing: "Preparing report",
sending: "Sending",
sent: "Accepted by mail server",
retry: "Retry scheduled",
failed: "Failed",
unknown: "Needs review",
cancelled: "Cancelled",
};
export default function EmailRecapsAdminPage() {
const router = useRouter()
const [data, setData] = useState<Overview | null>(null)
const [settings, setSettings] = useState<Settings>({ enabled: false, day: 2, hour: 9, public_url: '' })
const [month, setMonth] = useState('')
const [preview, setPreview] = useState<Preview | null>(null)
const [previewMode, setPreviewMode] = useState<'html' | 'text'>('html')
const [error, setError] = useState('')
const [notice, setNotice] = useState('')
const [busy, setBusy] = useState('')
const [offset, setOffset] = useState(0)
const [revision, setRevision] = useState(0)
const testRequest = useRef<{ month: string; id: string } | null>(null)
const initialized = useRef(false)
const previewController = useRef<AbortController | null>(null)
const router = useRouter();
const [data, setData] = useState<Overview | null>(null);
const [settings, setSettings] = useState<Settings>({ enabled: false, day: 2, hour: 9, public_url: "" });
const [month, setMonth] = useState("");
const [preview, setPreview] = useState<Preview | null>(null);
const [previewMode, setPreviewMode] = useState<"html" | "text">("html");
const [error, setError] = useState("");
const [notice, setNotice] = useState("");
const [busy, setBusy] = useState("");
const [offset, setOffset] = useState(0);
const [revision, setRevision] = useState(0);
const testRequest = useRef<{ month: string; id: string } | null>(null);
const initialized = useRef(false);
const previewController = useRef<AbortController | null>(null);
const responseData = useCallback(async (response: Response) => {
if (response.status === 401) { router.replace('/login?next=%2Fadmin%2Frecaps'); throw new Error('Sign in to continue.') }
if (response.status === 403) { router.replace('/'); throw new Error('Administrator access is required.') }
const result = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(typeof result.detail === 'string' ? result.detail : 'Could not complete this action. Check your settings and try again.')
return result
}, [router])
const responseData = useCallback(
async (response: Response) => {
if (response.status === 401) {
router.replace("/login?next=%2Fadmin%2Frecaps");
throw new Error("Sign in to continue.");
}
if (response.status === 403) {
router.replace("/");
throw new Error("Administrator access is required.");
}
const result = await response.json().catch(() => ({}));
if (!response.ok)
throw new Error(
typeof result.detail === "string"
? result.detail
: "Could not complete this action. Check your settings and try again.",
);
return result;
},
[router],
);
useEffect(() => {
const abort = new AbortController()
void authFetch(`${getApiBase()}/admin/email-recaps?offset=${offset}`, { signal: abort.signal }).then(responseData).then((result: Overview) => {
if (abort.signal.aborted) return
setData(result)
if (!initialized.current) { setSettings(result.settings); setMonth(result.months[0] || ''); initialized.current = true }
}).catch((err: Error) => { if (!abort.signal.aborted) setError(err.message) })
return () => abort.abort()
}, [offset, revision, responseData])
void revision;
const abort = new AbortController();
void authFetch(`${getApiBase()}/admin/email-recaps?offset=${offset}`, { signal: abort.signal })
.then(responseData)
.then((result: Overview) => {
if (abort.signal.aborted) return;
setData(result);
if (!initialized.current) {
setSettings(result.settings);
setMonth(result.months[0] || "");
initialized.current = true;
}
})
.catch((err: Error) => {
if (!abort.signal.aborted) setError(err.message);
});
return () => abort.abort();
}, [offset, revision, responseData]);
useEffect(() => {
if (!data?.deliveries.some((delivery) => ['queued', 'preparing', 'sending', 'retry'].includes(delivery.state))) return
const timer = window.setInterval(() => setRevision((value) => value + 1), 10000)
return () => window.clearInterval(timer)
}, [data])
useEffect(() => () => previewController.current?.abort(), [])
if (!data?.deliveries.some((delivery) => ["queued", "preparing", "sending", "retry"].includes(delivery.state)))
return;
const timer = window.setInterval(() => setRevision((value) => value + 1), 10000);
return () => window.clearInterval(timer);
}, [data]);
useEffect(() => () => previewController.current?.abort(), []);
const dirty = !!data && (settings.enabled !== data.settings.enabled || settings.day !== data.settings.day || settings.hour !== data.settings.hour || settings.public_url !== data.settings.public_url)
const dirty =
!!data &&
(settings.enabled !== data.settings.enabled ||
settings.day !== data.settings.day ||
settings.hour !== data.settings.hour ||
settings.public_url !== data.settings.public_url);
const save = async (event: FormEvent) => {
event.preventDefault()
if (busy) return
setBusy('save'); setError(''); setNotice('')
event.preventDefault();
if (busy) return;
setBusy("save");
setError("");
setNotice("");
try {
const { enabled, day, hour } = settings
const result = await responseData(await authFetch(`${getApiBase()}/admin/email-recaps`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, day, hour }) })) as Settings
setSettings(result); setData((current) => current ? { ...current, settings: result } : current)
setPreview(null)
setNotice(result.enabled ? `Schedule saved. Next send: ${dateLabel(result.next_send_at)}.` : 'Settings saved. Scheduled delivery is paused.')
setRevision((value) => value + 1)
} catch (err) { setError(err instanceof Error ? err.message : 'Could not save the schedule.') }
finally { setBusy('') }
}
const { enabled, day, hour } = settings;
const result = (await responseData(
await authFetch(`${getApiBase()}/admin/email-recaps`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled, day, hour }),
}),
)) as Settings;
setSettings(result);
setData((current) => (current ? { ...current, settings: result } : current));
setPreview(null);
setNotice(
result.enabled
? `Schedule saved. Next send: ${dateLabel(result.next_send_at)}.`
: "Settings saved. Scheduled delivery is paused.",
);
setRevision((value) => value + 1);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not save the schedule.");
} finally {
setBusy("");
}
};
const loadPreview = async () => {
if (busy || !month) return
const abort = new AbortController()
previewController.current?.abort(); previewController.current = abort
setBusy('preview'); setError(''); setNotice(''); setPreview(null)
if (busy || !month) return;
const abort = new AbortController();
previewController.current?.abort();
previewController.current = abort;
setBusy("preview");
setError("");
setNotice("");
setPreview(null);
try {
const result = await responseData(await authFetch(`${getApiBase()}/admin/email-recaps/preview?month=${month}`, { signal: abort.signal })) as Preview
if (!abort.signal.aborted) setPreview(result)
} catch (err) { if (!abort.signal.aborted) setError(err instanceof Error ? err.message : 'Could not prepare your preview.') }
finally { if (!abort.signal.aborted) setBusy('') }
}
const result = (await responseData(
await authFetch(`${getApiBase()}/admin/email-recaps/preview?month=${month}`, { signal: abort.signal }),
)) as Preview;
if (!abort.signal.aborted) setPreview(result);
} catch (err) {
if (!abort.signal.aborted) setError(err instanceof Error ? err.message : "Could not prepare your preview.");
} finally {
if (!abort.signal.aborted) setBusy("");
}
};
const sendTest = async () => {
if (busy || !preview || preview.month !== month) return
if (!testRequest.current || testRequest.current.month !== month) testRequest.current = { month, id: crypto.randomUUID() }
setBusy('test'); setError(''); setNotice('')
if (busy || !preview || preview.month !== month) return;
if (!testRequest.current || testRequest.current.month !== month)
testRequest.current = { month, id: crypto.randomUUID() };
setBusy("test");
setError("");
setNotice("");
try {
const result = await responseData(await authFetch(`${getApiBase()}/admin/email-recaps/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ month, request_id: testRequest.current.id }) }))
setNotice(result.message); testRequest.current = null; setOffset(0); setRevision((value) => value + 1)
} catch (err) { setError(err instanceof Error ? err.message : 'Could not queue your test.') }
finally { setBusy('') }
}
const result = await responseData(
await authFetch(`${getApiBase()}/admin/email-recaps/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ month, request_id: testRequest.current.id }),
}),
);
setNotice(result.message);
testRequest.current = null;
setOffset(0);
setRevision((value) => value + 1);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not queue your test.");
} finally {
setBusy("");
}
};
return <AdminShell title="Monthly email recaps" subtitle="Give each user a personal look back at their month in viewing." actions={<a className="ghost-button" href="/admin/notifications">Email settings </a>}>
<div className="recap-admin">
{error && <p className="error-banner" role="alert">{error}</p>}
{notice && <p className="status-banner" role="status">{notice}</p>}
{!data && !error && <p role="status">Loading email recaps</p>}
{!data && error && <button className="ghost-button" type="button" onClick={() => { setError(''); setRevision((value) => value + 1) }}>Try again</button>}
{data && <>
<div className="recap-overview-strip"><div><span className={`recap-pill ${data.settings.enabled ? 'is-enabled' : ''}`}>{data.settings.enabled ? 'Schedule running' : 'Schedule paused'}</span><p>{data.settings.enabled ? `Next send ${dateLabel(data.settings.next_send_at)}` : 'Start the schedule when youre ready for monthly delivery.'}</p></div><div className="recap-subscriber-count"><strong>{data.subscribers}</strong><span>confirmed {data.subscribers === 1 ? 'subscriber' : 'subscribers'}</span></div></div>
<div className="recap-admin-grid">
<section className="admin-panel recap-panel"><div className="recap-section-heading"><div><span className="recap-eyebrow">Set the rhythm</span><h2>Monthly schedule</h2></div></div>
<p>Send the previous months report to users who have opted in and confirmed their email. All report periods and send times use UTC.</p>
<form className="recap-schedule-form" onSubmit={save}>
<label htmlFor="recap-public-url">Public Magent address<input id="recap-public-url" type="url" value={settings.public_url} readOnly /><small>Inherited from <Link href="/admin/general">Hosting &amp; proxy</Link>. Email links update when that address changes.</small></label>
<div className="recap-schedule-fields"><label htmlFor="recap-day">Day of the month<select id="recap-day" value={settings.day} onChange={(event) => setSettings({ ...settings, day: Number(event.target.value) })} disabled={!!busy}>{Array.from({ length: 28 }, (_, index) => <option key={index + 1} value={index + 1}>{index + 1}</option>)}</select></label><label htmlFor="recap-hour">Send time (UTC)<select id="recap-hour" value={settings.hour} onChange={(event) => setSettings({ ...settings, hour: Number(event.target.value) })} disabled={!!busy}>{Array.from({ length: 24 }, (_, hour) => <option key={hour} value={hour}>{String(hour).padStart(2, '0')}:00 UTC</option>)}</select></label></div>
<label className="recap-checkbox"><input type="checkbox" checked={settings.enabled} disabled={!!busy} onChange={(event) => setSettings({ ...settings, enabled: event.target.checked })} /><span>Enable scheduled monthly recaps</span></label>
<p className="recap-muted">Starting or changing the schedule begins at its next future send time. Pausing cancels queued monthly emails.</p>
<button className="account-primary" type="submit" disabled={!!busy || !dirty}>{busy === 'save' ? 'Saving…' : 'Save schedule'}</button>
</form>
{!data.ready && <p className="recap-setup-note">{data.detail} <a href="/admin/notifications">Review email settings </a></p>}
</section>
<section className="admin-panel recap-panel"><span className="recap-eyebrow">Make it yours</span><h2>Preview your recap</h2><p>See your own viewing highlights in the email design. A test goes only to your confirmed profile email.</p>
<label className="recap-month-label" htmlFor="recap-month">Report month<select id="recap-month" value={month} disabled={!!busy} onChange={(event) => { setMonth(event.target.value); setPreview(null); testRequest.current = null }}>{data.months.map((value) => <option key={value} value={value}>{monthLabel(value)}</option>)}</select></label>
<div className="recap-actions"><button type="button" className="account-primary" onClick={() => void loadPreview()} disabled={!!busy || dirty || !month}>{busy === 'preview' ? 'Preparing preview…' : 'Preview my recap'}</button><button type="button" className="account-secondary" onClick={() => void sendTest()} disabled={!!busy || dirty || !preview || !data.ready}>{busy === 'test' ? 'Queuing test…' : 'Send test to me'}</button></div>
{dirty && <p className="recap-muted">Save your settings before previewing or sending a test.</p>}
<div className="recap-preview-guidance"><h3>One email. Your month.</h3><ul><li>Minutes, movies, episodes and requests</li><li>Changes from the previous month</li><li>Most watched titles and your longest run</li><li>A link to the full report and easy unsubscribe</li></ul><a href="/profile#monthly-recaps">Confirm your email in Profile </a></div>
</section>
</div>
{preview && <section className="admin-panel recap-panel recap-preview"><div className="recap-section-heading"><div><span className="recap-eyebrow">Email preview</span><h2>{preview.subject}</h2><p>For {preview.email || 'your profile email'} · Preview links use your saved public address.</p></div><div className="recap-mode-buttons"><button type="button" aria-pressed={previewMode === 'html'} onClick={() => setPreviewMode('html')}>Email design</button><button type="button" aria-pressed={previewMode === 'text'} onClick={() => setPreviewMode('text')}>Plain text</button></div></div>{previewMode === 'html' ? <iframe title="Monthly recap email preview" sandbox="" referrerPolicy="no-referrer" srcDoc={preview.body_html} /> : <pre className="recap-plain-preview">{preview.body_text}</pre>}</section>}
<section className="admin-panel recap-panel"><div className="recap-section-heading"><div><span className="recap-eyebrow">From queue to inbox</span><h2>Delivery history</h2><p>Server acceptance is recorded here. Inbox placement depends on your mail provider.</p></div><button type="button" className="ghost-button" disabled={!!busy} onClick={() => { setError(''); setRevision((value) => value + 1) }}>Refresh history</button></div>
{data.deliveries.length ? <><div className="recap-history-scroll"><table className="recap-history"><thead><tr><th scope="col">Recipient</th><th scope="col">Report</th><th scope="col">Delivery</th><th scope="col">Updated</th></tr></thead><tbody>{data.deliveries.map((delivery) => <tr key={delivery.id}><td><strong>{delivery.username || 'Removed account'}</strong><small>{delivery.email}</small></td><td>{monthLabel(delivery.month)}<small>{delivery.kind === 'test' ? 'Test email' : delivery.kind === 'on_demand' ? 'Requested by user' : 'Scheduled recap'}</small></td><td><span className={`recap-pill ${delivery.state === 'sent' ? 'is-enabled' : ['failed', 'unknown'].includes(delivery.state) ? 'is-attention' : ''}`}>{stateLabels[delivery.state] || delivery.state}</span><small>{delivery.attempts} {delivery.attempts === 1 ? 'attempt' : 'attempts'} · {delivery.detail || 'Waiting for the next worker check.'}</small>{delivery.state === 'retry' && <small>Next attempt {dateLabel(delivery.next_attempt_at)}</small>}{delivery.state === 'unknown' && <small>Automatic retries are stopped to avoid a duplicate email.</small>}</td><td>{dateLabel(delivery.updated_at)}</td></tr>)}</tbody></table></div><div className="recap-pagination"><span>{offset + 1}{Math.min(offset + 50, data.total)} of {data.total}</span><div className="recap-actions"><button className="ghost-button" type="button" disabled={!offset} onClick={() => setOffset(Math.max(0, offset - 50))}>Previous</button><button className="ghost-button" type="button" disabled={offset + 50 >= data.total} onClick={() => setOffset(offset + 50)}>Next</button></div></div></> : <div className="recap-empty"><span aria-hidden="true"></span><h3>Your first recap starts here</h3><p>Preview your email, send yourself a test, then start the monthly schedule. Delivery results will appear here.</p></div>}
</section>
</>}
</div>
</AdminShell>
return (
<AdminShell
title="Monthly email recaps"
subtitle="Give each user a personal look back at their month in viewing."
actions={
<a className="ghost-button" href="/admin/notifications">
Email settings
</a>
}
>
<div className="recap-admin">
{error && (
<p className="error-banner" role="alert">
{error}
</p>
)}
{notice && (
<p className="status-banner" role="status">
{notice}
</p>
)}
{!data && !error && <p role="status">Loading email recaps</p>}
{!data && error && (
<button
className="ghost-button"
type="button"
onClick={() => {
setError("");
setRevision((value) => value + 1);
}}
>
Try again
</button>
)}
{data && (
<>
<div className="recap-overview-strip">
<div>
<span className={`recap-pill ${data.settings.enabled ? "is-enabled" : ""}`}>
{data.settings.enabled ? "Schedule running" : "Schedule paused"}
</span>
<p>
{data.settings.enabled
? `Next send ${dateLabel(data.settings.next_send_at)}`
: "Start the schedule when youre ready for monthly delivery."}
</p>
</div>
<div className="recap-subscriber-count">
<strong>{data.subscribers}</strong>
<span>confirmed {data.subscribers === 1 ? "subscriber" : "subscribers"}</span>
</div>
</div>
<div className="recap-admin-grid">
<section className="admin-panel recap-panel">
<div className="recap-section-heading">
<div>
<span className="recap-eyebrow">Set the rhythm</span>
<h2>Monthly schedule</h2>
</div>
</div>
<p>
Send the previous months report to users who have opted in and confirmed their email. All report
periods and send times use UTC.
</p>
<form className="recap-schedule-form" onSubmit={save}>
<label htmlFor="recap-public-url">
Public Magent address
<input id="recap-public-url" type="url" value={settings.public_url} readOnly />
<small>
Inherited from <Link href="/admin/general">Hosting &amp; proxy</Link>. Email links update when
that address changes.
</small>
</label>
<div className="recap-schedule-fields">
<label htmlFor="recap-day">
Day of the month
<select
id="recap-day"
value={settings.day}
onChange={(event) => setSettings({ ...settings, day: Number(event.target.value) })}
disabled={!!busy}
>
{Array.from({ length: 28 }, (_, index) => (
<option key={index + 1} value={index + 1}>
{index + 1}
</option>
))}
</select>
</label>
<label htmlFor="recap-hour">
Send time (UTC)
<select
id="recap-hour"
value={settings.hour}
onChange={(event) => setSettings({ ...settings, hour: Number(event.target.value) })}
disabled={!!busy}
>
{Array.from({ length: 24 }, (_, hour) => (
<option key={hour} value={hour}>
{String(hour).padStart(2, "0")}:00 UTC
</option>
))}
</select>
</label>
</div>
<label className="recap-checkbox">
<input
type="checkbox"
checked={settings.enabled}
disabled={!!busy}
onChange={(event) => setSettings({ ...settings, enabled: event.target.checked })}
/>
<span>Enable scheduled monthly recaps</span>
</label>
<p className="recap-muted">
Starting or changing the schedule begins at its next future send time. Pausing cancels queued
monthly emails.
</p>
<button className="account-primary" type="submit" disabled={!!busy || !dirty}>
{busy === "save" ? "Saving…" : "Save schedule"}
</button>
</form>
{!data.ready && (
<p className="recap-setup-note">
{data.detail} <a href="/admin/notifications">Review email settings </a>
</p>
)}
</section>
<section className="admin-panel recap-panel">
<span className="recap-eyebrow">Make it yours</span>
<h2>Preview your recap</h2>
<p>
See your own viewing highlights in the email design. A test goes only to your confirmed profile email.
</p>
<label className="recap-month-label" htmlFor="recap-month">
Report month
<select
id="recap-month"
value={month}
disabled={!!busy}
onChange={(event) => {
setMonth(event.target.value);
setPreview(null);
testRequest.current = null;
}}
>
{data.months.map((value) => (
<option key={value} value={value}>
{monthLabel(value)}
</option>
))}
</select>
</label>
<div className="recap-actions">
<button
type="button"
className="account-primary"
onClick={() => void loadPreview()}
disabled={!!busy || dirty || !month}
>
{busy === "preview" ? "Preparing preview…" : "Preview my recap"}
</button>
<button
type="button"
className="account-secondary"
onClick={() => void sendTest()}
disabled={!!busy || dirty || !preview || !data.ready}
>
{busy === "test" ? "Queuing test…" : "Send test to me"}
</button>
</div>
{dirty && <p className="recap-muted">Save your settings before previewing or sending a test.</p>}
<div className="recap-preview-guidance">
<h3>One email. Your month.</h3>
<ul>
<li>Minutes, movies, episodes and requests</li>
<li>Changes from the previous month</li>
<li>Most watched titles and your longest run</li>
<li>A link to the full report and easy unsubscribe</li>
</ul>
<a href="/profile#monthly-recaps">Confirm your email in Profile </a>
</div>
</section>
</div>
{preview && (
<section className="admin-panel recap-panel recap-preview">
<div className="recap-section-heading">
<div>
<span className="recap-eyebrow">Email preview</span>
<h2>{preview.subject}</h2>
<p>For {preview.email || "your profile email"} · Preview links use your saved public address.</p>
</div>
<div className="recap-mode-buttons">
<button type="button" aria-pressed={previewMode === "html"} onClick={() => setPreviewMode("html")}>
Email design
</button>
<button type="button" aria-pressed={previewMode === "text"} onClick={() => setPreviewMode("text")}>
Plain text
</button>
</div>
</div>
{previewMode === "html" ? (
<iframe
title="Monthly recap email preview"
sandbox=""
referrerPolicy="no-referrer"
srcDoc={preview.body_html}
/>
) : (
<pre className="recap-plain-preview">{preview.body_text}</pre>
)}
</section>
)}
<section className="admin-panel recap-panel">
<div className="recap-section-heading">
<div>
<span className="recap-eyebrow">From queue to inbox</span>
<h2>Delivery history</h2>
<p>Server acceptance is recorded here. Inbox placement depends on your mail provider.</p>
</div>
<button
type="button"
className="ghost-button"
disabled={!!busy}
onClick={() => {
setError("");
setRevision((value) => value + 1);
}}
>
Refresh history
</button>
</div>
{data.deliveries.length ? (
<>
<div className="recap-history-scroll">
<table className="recap-history">
<thead>
<tr>
<th scope="col">Recipient</th>
<th scope="col">Report</th>
<th scope="col">Delivery</th>
<th scope="col">Updated</th>
</tr>
</thead>
<tbody>
{data.deliveries.map((delivery) => (
<tr key={delivery.id}>
<td>
<strong>{delivery.username || "Removed account"}</strong>
<small>{delivery.email}</small>
</td>
<td>
{monthLabel(delivery.month)}
<small>
{delivery.kind === "test"
? "Test email"
: delivery.kind === "on_demand"
? "Requested by user"
: "Scheduled recap"}
</small>
</td>
<td>
<span
className={`recap-pill ${delivery.state === "sent" ? "is-enabled" : ["failed", "unknown"].includes(delivery.state) ? "is-attention" : ""}`}
>
{stateLabels[delivery.state] || delivery.state}
</span>
<small>
{delivery.attempts} {delivery.attempts === 1 ? "attempt" : "attempts"} ·{" "}
{delivery.detail || "Waiting for the next worker check."}
</small>
{delivery.state === "retry" && (
<small>Next attempt {dateLabel(delivery.next_attempt_at)}</small>
)}
{delivery.state === "unknown" && (
<small>Automatic retries are stopped to avoid a duplicate email.</small>
)}
</td>
<td>{dateLabel(delivery.updated_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="recap-pagination">
<span>
{offset + 1}{Math.min(offset + 50, data.total)} of {data.total}
</span>
<div className="recap-actions">
<button
className="ghost-button"
type="button"
disabled={!offset}
onClick={() => setOffset(Math.max(0, offset - 50))}
>
Previous
</button>
<button
className="ghost-button"
type="button"
disabled={offset + 50 >= data.total}
onClick={() => setOffset(offset + 50)}
>
Next
</button>
</div>
</div>
</>
) : (
<div className="recap-empty">
<span aria-hidden="true"></span>
<h3>Your first recap starts here</h3>
<p>
Preview your email, send yourself a test, then start the monthly schedule. Delivery results will
appear here.
</p>
</div>
)}
</section>
</>
)}
</div>
</AdminShell>
);
}