feat: harden campaign delivery and editing

This commit is contained in:
2026-07-30 14:27:04 +02:00
parent 4a120e8009
commit cd223cbb95
34 changed files with 3370 additions and 165 deletions
@@ -102,6 +102,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
const version = data.currentVersion;
const cards = data.summary?.cards;
const delivery = asRecord(data.summary?.delivery);
const postboxReceipts = asRecord(data.summary?.postbox_receipts);
const rateLimit = asRecord(delivery.rate_limit);
const imapPolicy = asRecord(delivery.imap_append_sent);
@@ -262,8 +263,10 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
attempted += 1;
try {
const sendResponse = await sendCampaignJob(settings, campaignId, jobId, {
kind: "single_resend",
idempotency_key: crypto.randomUUID(),
reason: "Operator requested synchronous resend from the campaign report.",
include_warnings: true,
dry_run: false,
use_rate_limit: true,
enqueue_imap_task: false
});
@@ -338,9 +341,10 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
version_id: version?.id,
include_jobs: false,
attach_jobs_csv: attachCsv,
attach_report_json: attachJson
attach_report_json: attachJson,
idempotency_key: crypto.randomUUID()
});
setActionMessage(`Report sent to ${recipients.join(", ")}.`);
setActionMessage(`Report queued for ${recipients.join(", ")}.`);
setEmailOpen(false);
} catch (err) {
setActionError(err instanceof Error ? err.message : String(err));
@@ -471,6 +475,30 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
<div><dt>i18n:govoplan-campaign.execution_snapshot.5a67f098</dt><dd title={String(delivery.execution_snapshot_hash ?? "")}>{delivery.execution_snapshot_hash ? i18nMessage("i18n:govoplan-campaign.value.382bcd25", { value0: String(delivery.execution_snapshot_hash).slice(0, 12) }) : "i18n:govoplan-campaign.missing.92185dc5"}</dd></div>
</dl>
</Card>
<Card title="Postbox receipt evidence">
<dl className="detail-list">
<div>
<dt>Evidence state</dt>
<dd>
<StatusBadge
status={
postboxReceipts.status === "available"
? "success"
: postboxReceipts.status === "unavailable"
? "warning"
: "info"
}
label={humanize(String(postboxReceipts.status ?? "not applicable"))}
/>
</dd>
</div>
<div><dt>Accepted deliveries</dt><dd>{String(postboxReceipts.delivery_count ?? 0)}</dd></div>
<div><dt>Currently readable</dt><dd>{String(postboxReceipts.currently_readable_delivery_count ?? "—")}</dd></div>
<div><dt>Read</dt><dd>{String(postboxReceipts.read_delivery_count ?? "—")}</dd></div>
<div><dt>Acknowledged</dt><dd>{String(postboxReceipts.acknowledged_delivery_count ?? "—")}</dd></div>
<div><dt>Withdrawn / expired copies</dt><dd>{String(Number(postboxReceipts.withdrawn_message_count ?? 0) + Number(postboxReceipts.expired_message_count ?? 0))}</dd></div>
</dl>
</Card>
<Card title="i18n:govoplan-campaign.explicit_delivery_actions.b35e72a4">
<p className="muted">i18n:govoplan-campaign.these_actions_never_include_smtp_accepted_or_unr.449d0a80</p>
<div className="button-row compact-actions">
@@ -563,6 +591,13 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
<div><dt>i18n:govoplan-campaign.attachments.6771ade6</dt><dd>{String(detail.job.matched_file_count ?? detail.job.attachment_count ?? 0)}</dd></div>
<div><dt>Message SHA-256</dt><dd><code>{String(detail.job.eml_sha256 ?? "—")}</code></dd></div>
</dl>
<PostboxTargetEvidenceSection
targets={
Array.isArray(detail.job.resolved_postbox_targets)
? detail.job.resolved_postbox_targets
: []
}
/>
<AttachmentEvidenceSection attachments={Array.isArray(detail.job.attachments) ? detail.job.attachments : []} />
<AttemptHistoryTable kind="smtp" rows={detail.attempts.smtp ?? []} />
<AttemptHistoryTable kind="postbox" rows={detail.attempts.postbox ?? []} />
@@ -587,6 +622,93 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
}
function PostboxTargetEvidenceSection({ targets }: { targets: unknown[] }) {
const rows = targets.map(asRecord);
if (rows.length === 0) return null;
const columns: DataGridColumn<Record<string, unknown>>[] = [
{
id: "target",
header: "Frozen Postbox target",
width: "minmax(240px, 1fr)",
minWidth: 220,
resizable: true,
filterable: true,
value: (row) => String(row.address ?? row.name ?? row.postbox_id ?? "—"),
render: (row) => (
<span className="campaign-evidence-identifiers">
<strong>{String(row.name ?? row.address ?? row.postbox_id ?? "—")}</strong>
<small>{String(row.address ?? row.address_key ?? "")}</small>
</span>
)
},
{
id: "organization",
header: "Organization / function",
width: 260,
resizable: true,
filterable: true,
value: (row) =>
`${String(row.organization_unit_name ?? row.organization_unit_id ?? "")} ${String(row.function_name ?? row.function_id ?? "")}`,
render: (row) => (
<span className="campaign-evidence-identifiers">
<span>{String(row.organization_unit_name ?? row.organization_unit_id ?? "—")}</span>
<small>{String(row.function_name ?? row.function_id ?? "—")}</small>
</span>
)
},
{
id: "resolution",
header: "Frozen resolution",
width: 220,
resizable: true,
value: (row) =>
`${String(row.mode ?? "direct")} ${String(row.context_key ?? "")} ${String(row.template_revision_id ?? "")}`,
render: (row) => (
<span className="campaign-evidence-identifiers">
<span>{humanize(String(row.mode ?? "direct"))}</span>
<small>
{row.template_revision_id
? `Template ${shortEvidenceId(String(row.template_revision_id))}`
: "Exact Postbox"}
{row.context_key ? ` · Context ${String(row.context_key)}` : ""}
</small>
</span>
)
},
{
id: "holders",
header: "Build-time holders",
width: 150,
align: "right",
sortable: true,
value: (row) => Number(row.holder_count ?? 0),
render: (row) =>
row.vacant === true ? (
<StatusBadge status="warning" label="Vacant" />
) : (
String(row.holder_count ?? 0)
)
}
];
return (
<section className="attempt-history-section">
<h3>Frozen Postbox targets</h3>
<p className="muted small-note">
These addresses, organization/function references, and template revisions
were resolved during build and are the delivery evidence for this job.
</p>
<DataGrid
id="campaign-postbox-target-evidence"
rows={rows}
columns={columns}
getRowKey={(row, index) =>
String(row.target_id ?? row.postbox_id ?? `postbox-target-${index}`)
}
/>
</section>
);
}
type AttachmentEvidenceRow = {
id: string;
rule: string;
@@ -718,6 +840,50 @@ function AttemptHistoryTable({ kind, rows }: {kind: "smtp" | "imap" | "postbox";
kind === "postbox" ?
{ id: "target", header: "Postbox", width: 220, sortable: true, filterable: true, value: (row) => String(row.address ?? asRecord(row.target).address ?? row.postbox_id ?? "—"), render: (row) => String(row.address ?? asRecord(row.target).address ?? row.postbox_id ?? "—") } :
{ id: "code", header: "i18n:govoplan-campaign.code.adac6937", width: 110, sortable: true, value: (row) => String(row.smtp_status_code ?? "—"), render: (row) => String(row.smtp_status_code ?? "—") },
...(kind === "postbox" ? [
{
id: "readability",
header: "Current access",
width: 155,
value: (row: Record<string, unknown>) => {
const summary = asRecord(row.receipt_summary);
return String(
row.receipt_summary_status === "available"
? summary.currently_readable === true
? "readable"
: "unavailable"
: row.receipt_summary_status ?? "unavailable"
);
},
render: (row: Record<string, unknown>) => {
const summary = asRecord(row.receipt_summary);
const available = row.receipt_summary_status === "available";
const readable = available && summary.currently_readable === true;
return (
<StatusBadge
status={readable ? "success" : available ? "warning" : "info"}
label={
readable
? "Readable"
: available
? "Not currently readable"
: "Evidence unavailable"
}
/>
);
}
},
{
id: "receipts",
header: "Read / acknowledged",
width: 170,
align: "right" as const,
value: (row: Record<string, unknown>) => {
const summary = asRecord(row.receipt_summary);
return `${String(summary.read_receipt_count ?? 0)} / ${String(summary.acknowledged_receipt_count ?? 0)}`;
}
}
] : []),
{ id: "started", header: "i18n:govoplan-campaign.started.faa9e7e7", width: 180, sortable: true, value: (row) => String(row.started_at ?? row.created_at ?? ""), render: (row) => formatDateTime(String(row.started_at ?? row.created_at ?? "")) },
{ id: "finished", header: "i18n:govoplan-campaign.finished.355bcc57", width: 180, sortable: true, value: (row) => String(row.finished_at ?? row.updated_at ?? ""), render: (row) => formatDateTime(String(row.finished_at ?? row.updated_at ?? "")) },
{ id: "result", header: "i18n:govoplan-campaign.result.5faa59d4", width: "minmax(240px, 1fr)", minWidth: 200, resizable: true, filterable: true, value: (row) => String(row.smtp_response ?? row.error_message ?? row.error_code ?? "—"), render: (row) => <span title={String(row.smtp_response ?? row.error_message ?? row.error_code ?? "")}>{String(row.smtp_response ?? row.error_message ?? row.error_code ?? "—")}</span> }