Complete recipient search and delivery evidence
This commit is contained in:
@@ -561,7 +561,9 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
<div><dt>Postbox targets</dt><dd>{String(detail.job.postbox_target_count ?? 0)}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.imap_state.03b83be0</dt><dd><StatusBadge status={String(detail.job.imap_status ?? "unknown")} label={deliveryStatusLabel(String(detail.job.imap_status ?? "unknown"))} /></dd></div>
|
||||
<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>
|
||||
<AttachmentEvidenceSection attachments={Array.isArray(detail.job.attachments) ? detail.job.attachments : []} />
|
||||
<AttemptHistoryTable kind="smtp" rows={detail.attempts.smtp ?? []} />
|
||||
<AttemptHistoryTable kind="postbox" rows={detail.attempts.postbox ?? []} />
|
||||
<AttemptHistoryTable kind="imap" rows={detail.attempts.imap ?? []} />
|
||||
@@ -585,6 +587,114 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
|
||||
}
|
||||
|
||||
type AttachmentEvidenceRow = {
|
||||
id: string;
|
||||
rule: string;
|
||||
status: string;
|
||||
delivery: string;
|
||||
file: string;
|
||||
assetId: string;
|
||||
versionId: string;
|
||||
sourceRevision: string;
|
||||
checksum: string;
|
||||
sizeBytes: number | null;
|
||||
};
|
||||
|
||||
function AttachmentEvidenceSection({ attachments }: {attachments: unknown[];}) {
|
||||
const rows = attachmentEvidenceRows(attachments);
|
||||
const columns: DataGridColumn<AttachmentEvidenceRow>[] = [
|
||||
{ id: "rule", header: "Rule", width: 180, resizable: true, sortable: true, filterable: true, value: (row) => row.rule },
|
||||
{ id: "status", header: "Status", width: 130, sortable: true, filterable: true, render: (row) => <StatusBadge status={row.status} />, value: (row) => row.status },
|
||||
{ id: "delivery", header: "Attachment output", width: 220, resizable: true, filterable: true, value: (row) => row.delivery },
|
||||
{ id: "file", header: "Frozen file", width: "minmax(240px, 1fr)", minWidth: 220, resizable: true, filterable: true, value: (row) => row.file },
|
||||
{
|
||||
id: "version",
|
||||
header: "Managed version",
|
||||
width: 230,
|
||||
resizable: true,
|
||||
filterable: true,
|
||||
value: (row) => `${row.versionId} ${row.assetId} ${row.sourceRevision}`,
|
||||
render: (row) =>
|
||||
<span className="campaign-evidence-identifiers">
|
||||
<code title={row.versionId}>{row.versionId || "Legacy source"}</code>
|
||||
{row.assetId && <small title={row.assetId}>Asset {shortEvidenceId(row.assetId)}</small>}
|
||||
{row.sourceRevision && <small title={row.sourceRevision}>Source {shortEvidenceId(row.sourceRevision)}</small>}
|
||||
</span>
|
||||
},
|
||||
{
|
||||
id: "checksum",
|
||||
header: "SHA-256",
|
||||
width: 170,
|
||||
filterable: true,
|
||||
value: (row) => row.checksum,
|
||||
render: (row) => <code title={row.checksum}>{row.checksum ? shortEvidenceId(row.checksum) : "—"}</code>
|
||||
},
|
||||
{ id: "size", header: "Size", width: 110, align: "right", sortable: true, value: (row) => row.sizeBytes ?? -1, render: (row) => row.sizeBytes === null ? "—" : `${row.sizeBytes.toLocaleString()} B` }
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="attempt-history-section campaign-attachment-evidence">
|
||||
<h3>Frozen attachment evidence</h3>
|
||||
{rows.length === 0 ?
|
||||
<p className="muted small-note">No attachment rule or frozen file is recorded for this delivery job.</p> :
|
||||
<>
|
||||
<p className="muted small-note">Exact managed versions and checksums shown here are the immutable files used when this message was built.</p>
|
||||
<DataGrid
|
||||
id="campaign-job-attachment-evidence"
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
getRowKey={(row) => row.id}
|
||||
resizeBehavior="free" />
|
||||
</>
|
||||
}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function attachmentEvidenceRows(attachments: unknown[]): AttachmentEvidenceRow[] {
|
||||
const rows: AttachmentEvidenceRow[] = [];
|
||||
attachments.forEach((value, ruleIndex) => {
|
||||
const attachment = asRecord(value);
|
||||
const rule = String(attachment.label ?? attachment.attachment_id ?? `Rule ${ruleIndex + 1}`);
|
||||
const status = String(attachment.status ?? "unknown");
|
||||
const zipEnabled = attachment.zip_enabled === true;
|
||||
const delivery = zipEnabled
|
||||
? `ZIP ${String(attachment.zip_filename ?? attachment.zip_archive_id ?? "")}`.trim() + ` (${String(attachment.zip_mode ?? "inherit")})`
|
||||
: "Direct attachment";
|
||||
const managedMatches = Array.isArray(attachment.managed_matches) ? attachment.managed_matches.map(asRecord) : [];
|
||||
const legacyMatches = Array.isArray(attachment.matches) ? attachment.matches : [];
|
||||
const matches: Array<Record<string, unknown> | string | null> = managedMatches.length > 0
|
||||
? managedMatches
|
||||
: legacyMatches.length > 0
|
||||
? legacyMatches.map((match) => typeof match === "string" ? match : asRecord(match))
|
||||
: [null];
|
||||
|
||||
matches.forEach((match, matchIndex) => {
|
||||
const managed = typeof match === "string" || match === null ? {} : match;
|
||||
const filename = typeof match === "string"
|
||||
? match
|
||||
: String(managed.display_path ?? managed.relative_path ?? managed.filename ?? "No matched file");
|
||||
rows.push({
|
||||
id: `${ruleIndex}:${matchIndex}:${String(managed.version_id ?? filename)}`,
|
||||
rule,
|
||||
status,
|
||||
delivery,
|
||||
file: filename,
|
||||
assetId: String(managed.asset_id ?? ""),
|
||||
versionId: String(managed.version_id ?? ""),
|
||||
sourceRevision: String(managed.source_revision ?? ""),
|
||||
checksum: String(managed.checksum_sha256 ?? ""),
|
||||
sizeBytes: Number.isFinite(Number(managed.size_bytes)) ? Number(managed.size_bytes) : null
|
||||
});
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function shortEvidenceId(value: string): string {
|
||||
return value.length > 16 ? `${value.slice(0, 16)}...` : value;
|
||||
}
|
||||
|
||||
function AttemptHistoryTable({ kind, rows }: {kind: "smtp" | "imap" | "postbox";rows: Record<string, unknown>[];}) {
|
||||
const title = kind === "smtp"
|
||||
? "i18n:govoplan-campaign.smtp_attempts.eb0a9ca6"
|
||||
|
||||
Reference in New Issue
Block a user