fix(webui): localize Campaign delivery reporting
This commit is contained in:
@@ -41,7 +41,7 @@ const SEND_STATUS_OPTIONS: DataGridListOption[] = [
|
||||
"failed_temporary",
|
||||
"failed_permanent",
|
||||
"cancelled"].
|
||||
map((value) => ({ value, label: humanize(value) }));
|
||||
map((value) => ({ value, label: deliveryStatusLabel(value) ?? humanize(value) }));
|
||||
|
||||
const IMAP_STATUS_OPTIONS: DataGridListOption[] = [
|
||||
"not_requested",
|
||||
@@ -51,7 +51,7 @@ const IMAP_STATUS_OPTIONS: DataGridListOption[] = [
|
||||
"outcome_unknown",
|
||||
"failed",
|
||||
"skipped"].
|
||||
map((value) => ({ value, label: humanize(value) }));
|
||||
map((value) => ({ value, label: deliveryStatusLabel(value) ?? humanize(value) }));
|
||||
|
||||
const VALIDATION_STATUS_OPTIONS: DataGridListOption[] = [
|
||||
"ready",
|
||||
@@ -318,8 +318,8 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
{ id: "subject", header: "i18n:govoplan-campaign.subject.8d183dbd", width: "minmax(260px, 1fr)", resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "—") },
|
||||
{ id: "validation", header: "i18n:govoplan-campaign.validation.dd74d182", width: 145, sortable: true, filterable: true, columnType: "from-list", list: { options: VALIDATION_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.validation_status ?? "unknown")} />, value: (row) => String(row.validation_status ?? "unknown") },
|
||||
{ id: "queue", header: "i18n:govoplan-campaign.queue.d325fcd9", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: QUEUE_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.queue_status ?? "unknown")} />, value: (row) => String(row.queue_status ?? "unknown") },
|
||||
{ id: "send", header: "i18n:govoplan-campaign.smtp.efff9cca", width: 160, sortable: true, filterable: true, columnType: "from-list", list: { options: SEND_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.send_status ?? "unknown")} />, value: (row) => String(row.send_status ?? "unknown") },
|
||||
{ id: "imap", header: "i18n:govoplan-campaign.imap.271f9ef2", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: IMAP_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.imap_status ?? "unknown")} />, value: (row) => String(row.imap_status ?? "unknown") },
|
||||
{ id: "send", header: "i18n:govoplan-campaign.smtp.efff9cca", width: 160, sortable: true, filterable: true, columnType: "from-list", list: { options: SEND_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.send_status ?? "unknown")} label={deliveryStatusLabel(String(row.send_status ?? "unknown"))} />, value: (row) => String(row.send_status ?? "unknown") },
|
||||
{ id: "imap", header: "i18n:govoplan-campaign.imap.271f9ef2", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: IMAP_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.imap_status ?? "unknown")} label={deliveryStatusLabel(String(row.imap_status ?? "unknown"))} />, value: (row) => String(row.imap_status ?? "unknown") },
|
||||
{ id: "attempts", header: "i18n:govoplan-campaign.attempts.5a29585e", width: 105, align: "right", sortable: true, filterType: "integer", value: (row) => Number(row.attempt_count ?? 0) },
|
||||
{
|
||||
id: "evidence",
|
||||
@@ -388,7 +388,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
<div><dt>i18n:govoplan-campaign.failed.09fef5d8</dt><dd>{cards?.failed ?? 0}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.outcome_unknown.6e929fca</dt><dd>{cards?.outcome_unknown ?? 0}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.not_attempted.e1be3c69</dt><dd>{cards?.not_attempted ?? 0}</dd></div>
|
||||
<div><dt>SMTP skipped (excluded)</dt><dd>{cards?.skipped ?? jobs.counts.send?.skipped ?? 0}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.smtp_skipped_excluded_.df6eca19</dt><dd>{cards?.skipped ?? jobs.counts.send?.skipped ?? 0}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.cancelled.a1bf92ef</dt><dd>{cards?.cancelled ?? 0}</dd></div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -396,7 +396,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
<dl className="detail-list">
|
||||
<div><dt>i18n:govoplan-campaign.imap_appended.56017ea3</dt><dd>{cards?.imap_appended ?? 0}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.imap_failed.50dbca55</dt><dd>{cards?.imap_failed ?? 0}</dd></div>
|
||||
<div><dt>IMAP skipped</dt><dd>{cards?.imap_skipped ?? jobs.counts.imap?.skipped ?? 0}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.imap_skipped.5a97b542</dt><dd>{cards?.imap_skipped ?? jobs.counts.imap?.skipped ?? 0}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.append_policy.f195cb05</dt><dd>{imapPolicy.enabled === true ? i18nMessage("i18n:govoplan-campaign.enabled_value.e395e48f", { value0: String(imapPolicy.folder ?? "i18n:govoplan-campaign.auto.0d612c12") }) : "i18n:govoplan-campaign.disabled.f4f4473d"}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.rate_limit.d08e55f5</dt><dd>{rateLimit.messages_per_minute ? i18nMessage("i18n:govoplan-campaign.value_minute.aeb1a9ea", { value0: String(rateLimit.messages_per_minute) }) : "—"}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.minimum_remaining_duration.639b792c</dt><dd>{String(delivery.estimated_remaining_send_human ?? "—")}</dd></div>
|
||||
@@ -417,7 +417,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
|
||||
<Card title="i18n:govoplan-campaign.recipient_delivery_jobs.52492608">
|
||||
<p className="muted small-note">
|
||||
Excluded rows are intentionally omitted from delivery. Their SMTP and IMAP states are shown as Skipped because no transport effect is attempted; use the separate status filters to isolate them.
|
||||
i18n:govoplan-campaign.excluded_rows_are_intentionally_omitted_from_del.421a1f00
|
||||
</p>
|
||||
<div className="page-heading split">
|
||||
<div className="button-row compact-actions">
|
||||
@@ -487,8 +487,8 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
<div><dt>i18n:govoplan-campaign.recipient.90343260</dt><dd>{String(detail.job.recipient_email ?? "—")}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.subject.8d183dbd</dt><dd>{String(detail.job.subject ?? "—")}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.message_id.465056ba</dt><dd>{String(detail.job.message_id_header ?? "—")}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.smtp_state.ff372566</dt><dd><StatusBadge status={String(detail.job.send_status ?? "unknown")} /></dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.imap_state.03b83be0</dt><dd><StatusBadge status={String(detail.job.imap_status ?? "unknown")} /></dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.smtp_state.ff372566</dt><dd><StatusBadge status={String(detail.job.send_status ?? "unknown")} label={deliveryStatusLabel(String(detail.job.send_status ?? "unknown"))} /></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>
|
||||
</dl>
|
||||
<AttemptHistoryTable kind="smtp" rows={detail.attempts.smtp ?? []} />
|
||||
@@ -556,6 +556,10 @@ function initialReportGridFilters(): Record<string, string | string[]> {
|
||||
return result;
|
||||
}
|
||||
|
||||
function deliveryStatusLabel(status: string): string | undefined {
|
||||
return status === "skipped" ? "i18n:govoplan-campaign.skipped.5a000ad7" : undefined;
|
||||
}
|
||||
|
||||
function initialReportQuery(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
return new URLSearchParams(window.location.search).get("q")?.trim() ?? "";
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
isVersionReadyForDelivery,
|
||||
stringifyPreview } from
|
||||
"./utils/campaignView";
|
||||
import { deliveryModeLabel } from "./utils/deliveryMode";
|
||||
import { getBool, getText } from "./utils/draftEditor";
|
||||
import { attachmentPreviewLinkableFiles, attachmentPreviewMatchedFiles } from "./utils/attachmentPreview";
|
||||
import { emptyCampaignJobsResponse, mergeCampaignJobsDelta } from "./utils/jobDeltas";
|
||||
@@ -863,7 +864,7 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
async function runQueueForWorkers() {
|
||||
if (!version || busy || !canQueueForWorkers || !workerQueueAvailable || readOnlyVersion || !readyForDelivery || !hasBuild || !mockGateSatisfied || deliveryQueued || deliveryStarted || synchronousEligibleCount <= 0) return;
|
||||
setBusy("queue");
|
||||
setMessage("Committing the reviewed execution to the background-worker queue…");
|
||||
setMessage("i18n:govoplan-campaign.committing_the_reviewed_execution_to_the_backgro.6ae349e2");
|
||||
setQueueResult(null);
|
||||
setError("");
|
||||
try {
|
||||
@@ -875,7 +876,10 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
});
|
||||
const result = asRecord(response);
|
||||
setQueueResult(result);
|
||||
setMessage(`Queued ${String(result.queued_count ?? 0)} message(s); ${String(result.enqueued_count ?? 0)} worker task(s) published.`);
|
||||
setMessage(i18nMessage("i18n:govoplan-campaign.queued_value0_message_s_value1_worker_task_s_pub.18d67ec8", {
|
||||
value0: String(result.queued_count ?? 0),
|
||||
value1: String(result.enqueued_count ?? 0)
|
||||
}));
|
||||
setQueueConfirmOpen(false);
|
||||
await reload();
|
||||
await refreshDeliveryOptions(true);
|
||||
@@ -890,7 +894,7 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
async function runDeliveryControl(action: "pause" | "resume" | "cancel") {
|
||||
if (busy || !canControlDelivery) return;
|
||||
setBusy("control");
|
||||
setMessage(`${action === "pause" ? "Pausing" : action === "resume" ? "Resuming" : "Cancelling"} eligible delivery jobs…`);
|
||||
setMessage(deliveryControlProgressMessage(action));
|
||||
setError("");
|
||||
try {
|
||||
const response = action === "pause" ?
|
||||
@@ -900,9 +904,18 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
await cancelCampaign(settings, campaignId);
|
||||
const result = asRecord(response.result ?? response);
|
||||
setMessage(
|
||||
action === "pause" ? `Paused ${String(result.paused_count ?? 0)} queued message(s).` :
|
||||
action === "resume" ? `Resumed ${String(result.resumed_count ?? 0)} message(s); ${String(result.enqueued_count ?? 0)} worker task(s) published.` :
|
||||
`Cancelled ${String(result.cancelled_count ?? 0)} unsent message(s); ${String(result.protected_count ?? 0)} protected outcome(s) and ${String(result.skipped_count ?? 0)} excluded/skipped message(s) were retained.`
|
||||
action === "pause" ? i18nMessage("i18n:govoplan-campaign.paused_value0_queued_message_s_.c7d568d2", {
|
||||
value0: String(result.paused_count ?? 0)
|
||||
}) :
|
||||
action === "resume" ? i18nMessage("i18n:govoplan-campaign.resumed_value0_message_s_value1_worker_task_s_pu.0f59afb4", {
|
||||
value0: String(result.resumed_count ?? 0),
|
||||
value1: String(result.enqueued_count ?? 0)
|
||||
}) :
|
||||
i18nMessage("i18n:govoplan-campaign.cancelled_value0_unsent_message_s_value1_protect.d24c5e24", {
|
||||
value0: String(result.cancelled_count ?? 0),
|
||||
value1: String(result.protected_count ?? 0),
|
||||
value2: String(result.skipped_count ?? 0)
|
||||
})
|
||||
);
|
||||
if (action === "cancel") setCancelDeliveryConfirmOpen(false);
|
||||
await reload();
|
||||
@@ -918,12 +931,15 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
async function runRetryFailed() {
|
||||
if (busy || !canRetryDelivery || retryableCount <= 0 || !workerQueueAvailable) return;
|
||||
setBusy("retry");
|
||||
setMessage("Queueing retryable failed messages for background workers…");
|
||||
setMessage("i18n:govoplan-campaign.queueing_retryable_failed_messages_for_backgroun.4bc80cd9");
|
||||
setError("");
|
||||
try {
|
||||
const response = await retryCampaignJobs(settings, campaignId, { enqueue_celery: true });
|
||||
const result = asRecord(response.result ?? response);
|
||||
setMessage(`Queued ${String(result.selected_count ?? 0)} retryable message(s); ${String(result.enqueued_count ?? 0)} worker task(s) published.`);
|
||||
setMessage(i18nMessage("i18n:govoplan-campaign.queued_value0_retryable_message_s_value1_worker_.f4795bdb", {
|
||||
value0: String(result.selected_count ?? 0),
|
||||
value1: String(result.enqueued_count ?? 0)
|
||||
}));
|
||||
await reload();
|
||||
await refreshDeliveryOptions(true);
|
||||
} catch (err) {
|
||||
@@ -1192,14 +1208,24 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
state: messagesPerMinute > 0 ? "ready" : "warning"
|
||||
},
|
||||
{
|
||||
label: "Delivery mode",
|
||||
label: "i18n:govoplan-campaign.delivery_mode.109ed9d1",
|
||||
detail: deliveryOptionsLoading ?
|
||||
"Loading the effective delivery policy…" :
|
||||
"i18n:govoplan-campaign.loading_the_effective_delivery_policy_.d6893011" :
|
||||
synchronousSendAllowed ?
|
||||
`Send now is available for ${synchronousEligibleCount} eligible message(s), within the effective limit of ${synchronousSendLimit}. ${workerQueueAvailable ? "The worker queue is also available." : "Background workers are not configured."}` :
|
||||
i18nMessage("i18n:govoplan-campaign.send_now_is_available_for_value0_eligible_messag.6ec0ed6c", {
|
||||
value0: synchronousEligibleCount,
|
||||
value1: synchronousSendLimit,
|
||||
value2: workerQueueAvailable
|
||||
? "i18n:govoplan-campaign.the_worker_queue_is_also_available_.7b77144e"
|
||||
: "i18n:govoplan-campaign.background_workers_are_not_configured_.efa72396"
|
||||
}) :
|
||||
workerQueueAvailable ?
|
||||
`Send now is unavailable (${synchronousSendReason(synchronousSendOption)}). Queue for workers remains available.` :
|
||||
`No real delivery mode is currently available (${synchronousSendReason(synchronousSendOption)}).`,
|
||||
i18nMessage("i18n:govoplan-campaign.send_now_is_unavailable_value0_queue_for_workers.59bfc873", {
|
||||
value0: synchronousSendReason(synchronousSendOption)
|
||||
}) :
|
||||
i18nMessage("i18n:govoplan-campaign.no_real_delivery_mode_is_currently_available_val.618cce1f", {
|
||||
value0: synchronousSendReason(synchronousSendOption)
|
||||
}),
|
||||
state: synchronousSendAllowed || workerQueueAvailable ? "ready" : deliveryOptionsLoading ? "info" : "blocked"
|
||||
},
|
||||
{
|
||||
@@ -1443,9 +1469,9 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
<div><span>i18n:govoplan-campaign.rate_limit.d08e55f5</span><strong>{messagesPerMinute > 0 ? i18nMessage("i18n:govoplan-campaign.value_min.c9d89eae", { value0: messagesPerMinute }) : "i18n:govoplan-campaign.not_set.93039e60"}</strong></div>
|
||||
<div><span>i18n:govoplan-campaign.minimum_duration.91a71a6d</span><strong>{estimatedMinutes ? i18nMessage("i18n:govoplan-campaign.about_value_min.7c2e77fc", { value0: estimatedMinutes }) : "—"}</strong></div>
|
||||
<div><span>i18n:govoplan-campaign.imap_append.8c0d9e96</span><strong>{Boolean(imapAppend.enabled) ? "i18n:govoplan-campaign.enabled.df174a3f" : "i18n:govoplan-campaign.disabled.f4f4473d"}</strong></div>
|
||||
<div><span>Synchronous limit</span><strong>{deliveryOptionsLoading ? "…" : synchronousSendLimit}</strong></div>
|
||||
<div><span>Limit source</span><strong>{humanize(String(synchronousSendPolicy.source ?? "not available"))}</strong></div>
|
||||
<div><span>Worker queue</span><strong>{workerQueueAvailable ? "Available" : "Not configured"}</strong></div>
|
||||
<div><span>i18n:govoplan-campaign.synchronous_limit.f88c0bcd</span><strong>{deliveryOptionsLoading ? "…" : synchronousSendLimit}</strong></div>
|
||||
<div><span>i18n:govoplan-campaign.limit_source.bd933adb</span><strong>{deliveryPolicySourceLabel(String(synchronousSendPolicy.source ?? ""))}</strong></div>
|
||||
<div><span>i18n:govoplan-campaign.worker_queue.c911e32c</span><strong>{workerQueueAvailable ? "i18n:govoplan-campaign.available.7c62a142" : "i18n:govoplan-campaign.not_configured.811931bb"}</strong></div>
|
||||
<div><span>i18n:govoplan-campaign.version.2da600bf</span><strong>{version ? `v${version.version_number}` : "—"}</strong></div>
|
||||
</div>
|
||||
<DeliverabilityPreflight items={deliverabilityPreflightItems} />
|
||||
@@ -1472,7 +1498,7 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
variant="primary"
|
||||
onClick={() => setQueueConfirmOpen(true)}
|
||||
disabled={!version || Boolean(busy) || !canQueueForWorkers || !workerQueueAvailable || readOnlyVersion || !readyForDelivery || !hasBuild || !mockGateSatisfied || deliveryQueued || deliveryStarted || synchronousEligibleCount <= 0}>
|
||||
{busy === "queue" ? "Queueing for workers…" : "Queue for workers"}
|
||||
{busy === "queue" ? "i18n:govoplan-campaign.queueing_for_workers_.d24584fe" : "i18n:govoplan-campaign.queue_for_workers.dae9a3e4"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => selectedDryRun ? void runSendNow() : setSendConfirmOpen(true)}
|
||||
@@ -1488,13 +1514,24 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
</Button>
|
||||
</div>
|
||||
<p className="muted small-note">
|
||||
Queue for workers commits durable jobs and returns immediately; progress remains visible here after you leave. Send now keeps this request open and is limited to {synchronousSendLimit || "the configured maximum"} eligible recipient jobs.
|
||||
{i18nMessage("i18n:govoplan-campaign.queue_for_workers_commits_durable_jobs_and_retur.d0dbe81a", {
|
||||
value0: synchronousSendLimit || "i18n:govoplan-campaign.the_configured_maximum.eb10006b"
|
||||
})}
|
||||
</p>
|
||||
{!synchronousSendAllowed && canSendSynchronously &&
|
||||
<p className="review-flow-inline-note is-warning">Send now is unavailable: {synchronousSendReason(synchronousSendOption)}.</p>
|
||||
<p className="review-flow-inline-note is-warning">
|
||||
{i18nMessage("i18n:govoplan-campaign.send_now_is_unavailable_value0_.d93c0b29", {
|
||||
value0: synchronousSendReason(synchronousSendOption)
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
{queueResult &&
|
||||
<p className="review-flow-inline-note is-stale">Worker queue committed {String(queueResult.queued_count ?? 0)} message(s) and published {String(queueResult.enqueued_count ?? 0)} task(s). Refreshing progress does not requeue them.</p>
|
||||
<p className="review-flow-inline-note is-stale">
|
||||
{i18nMessage("i18n:govoplan-campaign.worker_queue_committed_value0_message_s_and_publ.24400d3c", {
|
||||
value0: String(queueResult.queued_count ?? 0),
|
||||
value1: String(queueResult.enqueued_count ?? 0)
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
{sendResult &&
|
||||
<div className="review-flow-data-section">
|
||||
@@ -1518,8 +1555,8 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
<WorkflowFact label="i18n:govoplan-campaign.smtp_accepted.e3aa7603" value={sentCount} />
|
||||
<WorkflowFact label="i18n:govoplan-campaign.smtp_failed.0ce5516d" value={failedCount} />
|
||||
<WorkflowFact label="i18n:govoplan-campaign.queued_active.a2784a4a" value={queuedOrActiveCount} />
|
||||
<WorkflowFact label="Paused" value={pausedQueueCount} />
|
||||
<WorkflowFact label="Delivery mode" value={persistedDeliveryMode ? humanize(persistedDeliveryMode) : "—"} />
|
||||
<WorkflowFact label="i18n:govoplan-campaign.paused.c7dfb6f1" value={pausedQueueCount} />
|
||||
<WorkflowFact label="i18n:govoplan-campaign.delivery_mode.109ed9d1" value={deliveryModeLabel(persistedDeliveryMode)} />
|
||||
<WorkflowFact label="i18n:govoplan-campaign.outcome_unknown.6e929fca" value={outcomeUnknownCount} />
|
||||
<WorkflowFact label="i18n:govoplan-campaign.imap_appended.56017ea3" value={imapAppended} />
|
||||
<WorkflowFact label="i18n:govoplan-campaign.imap_pending.ed50375e" value={imapPendingForDisplay} />
|
||||
@@ -1532,29 +1569,36 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
</div>
|
||||
{persistedDeliveryMode &&
|
||||
<p className="muted small-note">
|
||||
This version last entered {humanize(persistedDeliveryMode)} mode{persistedDeliveryModeSelectedAt ? ` at ${formatDateTime(persistedDeliveryModeSelectedAt)}` : ""}. This record remains available after leaving and returning.
|
||||
{persistedDeliveryModeSelectedAt
|
||||
? i18nMessage("i18n:govoplan-campaign.this_version_last_entered_value0_mode_at_value1_.c087d2f3", {
|
||||
value0: deliveryModeLabel(persistedDeliveryMode),
|
||||
value1: formatDateTime(persistedDeliveryModeSelectedAt)
|
||||
})
|
||||
: i18nMessage("i18n:govoplan-campaign.this_version_last_entered_value0_mode_this_recor.ea2f201f", {
|
||||
value0: deliveryModeLabel(persistedDeliveryMode)
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
<div className="button-row compact-actions review-flow-stage-actions">
|
||||
<Button
|
||||
onClick={() => void runDeliveryControl("pause")}
|
||||
disabled={Boolean(busy) || !canControlDelivery || queuedSendCount <= 0}>
|
||||
Pause queued work
|
||||
i18n:govoplan-campaign.pause_queued_work.35ab4a5b
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void runDeliveryControl("resume")}
|
||||
disabled={Boolean(busy) || !canControlDelivery || pausedQueueCount <= 0 || !workerQueueAvailable}>
|
||||
Resume with workers
|
||||
i18n:govoplan-campaign.resume_with_workers.510a2a9a
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCancelDeliveryConfirmOpen(true)}
|
||||
disabled={Boolean(busy) || !canControlDelivery || queuedSendCount + pausedQueueCount <= 0}>
|
||||
Cancel unsent work
|
||||
i18n:govoplan-campaign.cancel_unsent_work.66df9f0d
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void runRetryFailed()}
|
||||
disabled={Boolean(busy) || !canRetryDelivery || retryableCount <= 0 || !workerQueueAvailable}>
|
||||
Retry failed with workers
|
||||
i18n:govoplan-campaign.retry_failed_with_workers.a4b7dcc5
|
||||
</Button>
|
||||
</div>
|
||||
{Boolean(imapAppend.enabled) && imapPendingForDisplay > 0 &&
|
||||
@@ -1664,9 +1708,12 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
|
||||
<ConfirmDialog
|
||||
open={queueConfirmOpen}
|
||||
title="Queue this version for background workers?"
|
||||
message={`This commits ${synchronousEligibleCount} eligible message(s) from version ${String(version?.version_number ?? "—")} to durable worker jobs. You may leave this page and return to the same progress and recovery state.`}
|
||||
confirmLabel="Queue for workers"
|
||||
title="i18n:govoplan-campaign.queue_this_version_for_background_workers_.943d032c"
|
||||
message={i18nMessage("i18n:govoplan-campaign.this_commits_value0_eligible_message_s_from_vers.f53e7222", {
|
||||
value0: synchronousEligibleCount,
|
||||
value1: String(version?.version_number ?? "—")
|
||||
})}
|
||||
confirmLabel="i18n:govoplan-campaign.queue_for_workers.dae9a3e4"
|
||||
cancelLabel="i18n:govoplan-campaign.cancel.77dfd213"
|
||||
busy={busy === "queue"}
|
||||
onCancel={() => setQueueConfirmOpen(false)}
|
||||
@@ -1675,7 +1722,11 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
<ConfirmDialog
|
||||
open={sendConfirmOpen}
|
||||
title="i18n:govoplan-campaign.send_this_version_now.10a0ca56"
|
||||
message={`${i18nMessage("i18n:govoplan-campaign.this_sends_the_frozen_execution_snapshot_for_ver.1f7c53cb", { value0: String(version?.version_number ?? "—"), value1: jobsTotal, value2: builtCount, value3: buildBlocked, value4: String(attachmentSummary.total_matched_files ?? 0), value5: String(rateLimit.messages_per_minute ?? "i18n:govoplan-campaign.not_set.ef374c57"), value6: imapAppend.enabled === true ? "enabled" : "disabled", value7: version?.execution_snapshot_hash ? i18nMessage("i18n:govoplan-campaign.value.382bcd25", { value0: version.execution_snapshot_hash.slice(0, 12) }) : "missing" })} This is a synchronous request for ${synchronousEligibleCount} eligible message(s), within the effective limit of ${synchronousSendLimit}. All messages are preflighted before SMTP is contacted.`}
|
||||
message={i18nMessage("i18n:govoplan-campaign.value0_this_is_a_synchronous_request_for_value1_.0267bc6b", {
|
||||
value0: i18nMessage("i18n:govoplan-campaign.this_sends_the_frozen_execution_snapshot_for_ver.1f7c53cb", { value0: String(version?.version_number ?? "—"), value1: jobsTotal, value2: builtCount, value3: buildBlocked, value4: String(attachmentSummary.total_matched_files ?? 0), value5: String(rateLimit.messages_per_minute ?? "i18n:govoplan-campaign.not_set.ef374c57"), value6: imapAppend.enabled === true ? "enabled" : "disabled", value7: version?.execution_snapshot_hash ? i18nMessage("i18n:govoplan-campaign.value.382bcd25", { value0: version.execution_snapshot_hash.slice(0, 12) }) : "missing" }),
|
||||
value1: synchronousEligibleCount,
|
||||
value2: synchronousSendLimit
|
||||
})}
|
||||
confirmLabel={directQueuedSendAllowed ? "i18n:govoplan-campaign.send_queued_now.bbace803" : "i18n:govoplan-campaign.send_now.dae33010"}
|
||||
tone="danger"
|
||||
busy={busy === "send"}
|
||||
@@ -1684,10 +1735,10 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
|
||||
<ConfirmDialog
|
||||
open={cancelDeliveryConfirmOpen}
|
||||
title="Cancel all unsent delivery work?"
|
||||
message={`This cancels queued or paused messages that have not crossed the SMTP boundary. Accepted, active, or outcome-unknown messages remain protected for audit and reconciliation.`}
|
||||
confirmLabel="Cancel unsent work"
|
||||
cancelLabel="Keep delivery work"
|
||||
title="i18n:govoplan-campaign.cancel_all_unsent_delivery_work_.a2f56cda"
|
||||
message="i18n:govoplan-campaign.this_cancels_queued_or_paused_messages_that_have.add583bc"
|
||||
confirmLabel="i18n:govoplan-campaign.cancel_unsent_work.66df9f0d"
|
||||
cancelLabel="i18n:govoplan-campaign.keep_delivery_work.10dbcb13"
|
||||
tone="danger"
|
||||
busy={busy === "control"}
|
||||
onCancel={() => setCancelDeliveryConfirmOpen(false)}
|
||||
@@ -2559,14 +2610,28 @@ function synchronousSendReason(option: Record<string, unknown>): string {
|
||||
const configuredMessage = String(option.message ?? "").trim();
|
||||
if (configuredMessage) return configuredMessage;
|
||||
switch (String(option.reason ?? "")) {
|
||||
case "recipient_limit_exceeded":return "the exact built run exceeds the effective recipient-job limit";
|
||||
case "no_eligible_recipient_jobs":return "the built run has no eligible message";
|
||||
case "version_not_ready":return "validate, lock, build, and review the current version first";
|
||||
case "policy_configuration_invalid":return "the delivery policy configuration is invalid";
|
||||
default:return "delivery options are still being evaluated";
|
||||
case "recipient_limit_exceeded":return "i18n:govoplan-campaign.the_exact_built_run_exceeds_the_effective_limit_.d7812d6a";
|
||||
case "no_eligible_recipient_jobs":return "i18n:govoplan-campaign.the_built_run_has_no_eligible_message.48e5410c";
|
||||
case "version_not_ready":return "i18n:govoplan-campaign.validate_lock_build_and_review_the_current_versi.d0567dcc";
|
||||
case "policy_configuration_invalid":return "i18n:govoplan-campaign.the_delivery_policy_configuration_is_invalid.a804a8f8";
|
||||
default:return "i18n:govoplan-campaign.delivery_options_are_still_being_evaluated.8395096e";
|
||||
}
|
||||
}
|
||||
|
||||
function deliveryControlProgressMessage(action: "pause" | "resume" | "cancel"): string {
|
||||
if (action === "pause") return "i18n:govoplan-campaign.pausing_eligible_delivery_jobs_.eb6b9d58";
|
||||
if (action === "resume") return "i18n:govoplan-campaign.resuming_eligible_delivery_jobs_.dd12c5a2";
|
||||
return "i18n:govoplan-campaign.cancelling_eligible_delivery_jobs_.4090fa47";
|
||||
}
|
||||
|
||||
function deliveryPolicySourceLabel(source: string): string {
|
||||
if (source === "tenant") return "i18n:govoplan-campaign.tenant.3ca93c78";
|
||||
if (source === "deployment") return "i18n:govoplan-campaign.deployment.327a55f8";
|
||||
if (source === "deployment_default") return "i18n:govoplan-campaign.deployment_default.aa39f7b4";
|
||||
if (source === "deployment_ceiling") return "i18n:govoplan-campaign.deployment_ceiling.abbec75b";
|
||||
return "i18n:govoplan-campaign.not_available.d1a17af1";
|
||||
}
|
||||
|
||||
function numberFrom(record: Record<string, unknown>, keys: string[]): number {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
|
||||
6
webui/src/features/campaigns/utils/deliveryMode.ts
Normal file
6
webui/src/features/campaigns/utils/deliveryMode.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function deliveryModeLabel(mode: string | null | undefined): string {
|
||||
if (mode === "synchronous") return "i18n:govoplan-campaign.synchronous.77c61919";
|
||||
if (mode === "worker_queue") return "i18n:govoplan-campaign.worker_queue.c911e32c";
|
||||
if (mode === "database_queue") return "i18n:govoplan-campaign.database_queue.8bf98437";
|
||||
return mode ? "i18n:govoplan-campaign.unknown.bc7819b3" : "—";
|
||||
}
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
useGuardedNavigate,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
import { asRecord, formatDateTime, humanize } from "../campaigns/utils/campaignView";
|
||||
import { asRecord, formatDateTime } from "../campaigns/utils/campaignView";
|
||||
import { deliveryModeLabel } from "../campaigns/utils/deliveryMode";
|
||||
import {
|
||||
operatorQueueActionBlocks,
|
||||
type OperatorQueueAction,
|
||||
@@ -237,7 +238,7 @@ export default function OperatorQueuePage({ settings, auth }: {settings: ApiSett
|
||||
const columns = useMemo<DataGridColumn<OperatorRow>[]>(() => [
|
||||
{ id: "campaign", header: "i18n:govoplan-campaign.campaign.69390e16", width: "minmax(260px, 1.2fr)", sticky: "start", sortable: true, filterable: true, value: (row) => row.campaign.name },
|
||||
{ id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 145, sortable: true, filterable: true, render: (row) => <StatusBadge status={row.campaign.status} />, value: (row) => row.campaign.status },
|
||||
{ id: "mode", header: "i18n:govoplan-campaign.mode.a7b93d21", width: 155, sortable: true, filterable: true, value: (row) => row.mode ? humanize(row.mode) : "—", sortValue: (row) => row.mode ?? "" },
|
||||
{ id: "mode", header: "i18n:govoplan-campaign.mode.a7b93d21", width: 155, sortable: true, filterable: true, value: (row) => deliveryModeLabel(row.mode), sortValue: (row) => row.mode ?? "" },
|
||||
{ id: "attention", header: "i18n:govoplan-campaign.attention.74e0b9c8", width: 120, align: "right", sortable: true, filterType: "integer", value: (row) => row.needsAttention },
|
||||
{ id: "failed", header: "i18n:govoplan-campaign.failed.09fef5d8", width: 100, align: "right", sortable: true, filterType: "integer", value: (row) => row.failed },
|
||||
{ id: "unknown", header: "i18n:govoplan-campaign.unknown.bc7819b3", width: 110, align: "right", sortable: true, filterType: "integer", value: (row) => row.outcomeUnknown },
|
||||
|
||||
@@ -25,7 +25,7 @@ export type OperatorQueueFacts = {
|
||||
};
|
||||
|
||||
export const OPERATOR_QUEUE_REASON = {
|
||||
permission: "i18n:govoplan-campaign.you_do_not_have_permission_for_this_action.dfcfbad6",
|
||||
permissionDenied: "i18n:govoplan-campaign.you_do_not_have_permission_for_this_action.dfcfbad6",
|
||||
noQueued: "i18n:govoplan-campaign.no_queued_jobs_can_be_paused.acaa0b4e",
|
||||
noPaused: "i18n:govoplan-campaign.no_paused_jobs_can_be_resumed.869b6275",
|
||||
noFailed: "i18n:govoplan-campaign.no_failed_jobs_are_eligible_for_retry.e65a69f9",
|
||||
@@ -43,34 +43,34 @@ export function operatorQueueActionBlocks(
|
||||
permissions: OperatorQueuePermissions
|
||||
): Record<OperatorQueueAction, string | null> {
|
||||
return {
|
||||
details: permissions.canRead ? null : OPERATOR_QUEUE_REASON.permission,
|
||||
details: permissions.canRead ? null : OPERATOR_QUEUE_REASON.permissionDenied,
|
||||
pause: !permissions.canControl
|
||||
? OPERATOR_QUEUE_REASON.permission
|
||||
? OPERATOR_QUEUE_REASON.permissionDenied
|
||||
: facts.queued > 0
|
||||
? null
|
||||
: OPERATOR_QUEUE_REASON.noQueued,
|
||||
resume: !permissions.canControl
|
||||
? OPERATOR_QUEUE_REASON.permission
|
||||
? OPERATOR_QUEUE_REASON.permissionDenied
|
||||
: facts.paused > 0
|
||||
? null
|
||||
: OPERATOR_QUEUE_REASON.noPaused,
|
||||
retry: !permissions.canRetry
|
||||
? OPERATOR_QUEUE_REASON.permission
|
||||
? OPERATOR_QUEUE_REASON.permissionDenied
|
||||
: facts.retryable > 0
|
||||
? null
|
||||
: OPERATOR_QUEUE_REASON.noFailed,
|
||||
"queue-unsent": !permissions.canQueue
|
||||
? OPERATOR_QUEUE_REASON.permission
|
||||
? OPERATOR_QUEUE_REASON.permissionDenied
|
||||
: facts.queueableUnattempted > 0
|
||||
? null
|
||||
: OPERATOR_QUEUE_REASON.noUnsent,
|
||||
reconcile: !permissions.canReconcile
|
||||
? OPERATOR_QUEUE_REASON.permission
|
||||
? OPERATOR_QUEUE_REASON.permissionDenied
|
||||
: facts.outcomeUnknown > 0
|
||||
? null
|
||||
: OPERATOR_QUEUE_REASON.noUnknown,
|
||||
cancel: !permissions.canControl
|
||||
? OPERATOR_QUEUE_REASON.permission
|
||||
? OPERATOR_QUEUE_REASON.permissionDenied
|
||||
: facts.cancellable > 0
|
||||
? null
|
||||
: OPERATOR_QUEUE_REASON.noCancellable
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
StatusBadge,
|
||||
TableActionGroup,
|
||||
formatDateTime,
|
||||
i18nMessage,
|
||||
type ApiSettings,
|
||||
type DataGridColumn
|
||||
} from "@govoplan/core-webui";
|
||||
@@ -131,11 +132,11 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
|
||||
render: (campaign) => (
|
||||
<TableActionGroup actions={[{
|
||||
id: "view-aggregate-report",
|
||||
label: "View aggregate report",
|
||||
label: "i18n:govoplan-campaign.view_aggregate_report.94b42124",
|
||||
icon: <Eye aria-hidden="true" />,
|
||||
variant: campaign.id === selectedFromUrl ? "primary" : "secondary",
|
||||
disabled: campaign.id === selectedFromUrl,
|
||||
disabledReason: campaign.id === selectedFromUrl ? "This report is already selected." : undefined,
|
||||
disabledReason: campaign.id === selectedFromUrl ? "i18n:govoplan-campaign.this_report_is_already_selected_.983c5be4" : undefined,
|
||||
onClick: () => selectCampaign(campaign.id)
|
||||
}]} />
|
||||
)
|
||||
@@ -150,75 +151,75 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
|
||||
<div className="page-heading split workspace-heading">
|
||||
<div>
|
||||
<PageTitle loading={listLoading || reportLoading}>i18n:govoplan-campaign.reports.88bc3fe3</PageTitle>
|
||||
<p className="muted">Privacy-protected Campaign outcomes without recipient or delivery diagnostics.</p>
|
||||
<p className="muted">i18n:govoplan-campaign.privacy_protected_campaign_outcomes_without_reci.5b8a100d</p>
|
||||
</div>
|
||||
<div className="button-row compact-actions">
|
||||
<Button
|
||||
onClick={() => void Promise.all([loadCampaigns(), selectedFromUrl ? loadReport(selectedFromUrl) : Promise.resolve()])}
|
||||
disabled={listLoading || reportLoading}
|
||||
>
|
||||
<RefreshCw size={16} aria-hidden="true" /> Refresh
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-campaign.refresh.56e3badc
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||
|
||||
<Card title="Campaign reports available to you">
|
||||
<LoadingFrame loading={listLoading} label="Loading Campaign reports…">
|
||||
<Card title="i18n:govoplan-campaign.campaign_reports_available_to_you.f14fa403">
|
||||
<LoadingFrame loading={listLoading} label="i18n:govoplan-campaign.loading_campaign_reports_.61ec1ee8">
|
||||
<DataGrid
|
||||
id="campaign-aggregate-report-list"
|
||||
rows={campaigns}
|
||||
columns={columns}
|
||||
getRowKey={(campaign) => campaign.id}
|
||||
initialSort={{ columnId: "updated", direction: "desc" }}
|
||||
emptyText="No aggregate Campaign reports are available to you."
|
||||
emptyText="i18n:govoplan-campaign.no_aggregate_campaign_reports_are_available_to_y.dafb2781"
|
||||
/>
|
||||
</LoadingFrame>
|
||||
</Card>
|
||||
|
||||
<LoadingFrame loading={reportLoading} label="Loading aggregate Campaign report…">
|
||||
<LoadingFrame loading={reportLoading} label="i18n:govoplan-campaign.loading_aggregate_campaign_report_.72f2f091">
|
||||
{report && outcomes && (
|
||||
<>
|
||||
<Card title={report.campaign.name}>
|
||||
<dl className="detail-list">
|
||||
<div><dt>Status</dt><dd><StatusBadge status={report.campaign.status} /></dd></div>
|
||||
<div><dt>Completion</dt><dd><StatusBadge status={report.completion_state} /></dd></div>
|
||||
<div><dt>Version</dt><dd>{report.version_number ?? "—"}</dd></div>
|
||||
<div><dt>Generated</dt><dd>{formatDateTime(report.generated_at)}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.status.bae7d5be</dt><dd><StatusBadge status={report.campaign.status} /></dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.completion.2ff25568</dt><dd><StatusBadge status={report.completion_state} label={aggregateCompletionLabel(report.completion_state)} /></dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.version.2da600bf</dt><dd>{report.version_number ?? "—"}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.generated.8eefdd52</dt><dd>{formatDateTime(report.generated_at)}</dd></div>
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
<div className="dashboard-grid">
|
||||
<MetricCard label="SMTP accepted" value={countValue(outcomes.smtp_accepted)} tone="good" />
|
||||
<MetricCard label="Failed" value={countValue(outcomes.failed)} tone="danger" />
|
||||
<MetricCard label="Outcome unknown" value={countValue(outcomes.outcome_unknown)} tone="warning" />
|
||||
<MetricCard label="Queued or active" value={countValue(outcomes.queued_or_active)} tone="info" />
|
||||
<MetricCard label="Not attempted" value={countValue(outcomes.not_attempted)} />
|
||||
<MetricCard label="Cancelled" value={countValue(outcomes.cancelled)} />
|
||||
<MetricCard label="Excluded" value={countValue(outcomes.excluded)} />
|
||||
<MetricCard label="i18n:govoplan-campaign.smtp_accepted.e3aa7603" value={countValue(outcomes.smtp_accepted)} tone="good" />
|
||||
<MetricCard label="i18n:govoplan-campaign.failed.09fef5d8" value={countValue(outcomes.failed)} tone="danger" />
|
||||
<MetricCard label="i18n:govoplan-campaign.outcome_unknown.6e929fca" value={countValue(outcomes.outcome_unknown)} tone="warning" />
|
||||
<MetricCard label="i18n:govoplan-campaign.queued_or_active.afdcd7da" value={countValue(outcomes.queued_or_active)} tone="info" />
|
||||
<MetricCard label="i18n:govoplan-campaign.not_attempted.e1be3c69" value={countValue(outcomes.not_attempted)} />
|
||||
<MetricCard label="i18n:govoplan-campaign.cancelled.a1bf92ef" value={countValue(outcomes.cancelled)} />
|
||||
<MetricCard label="i18n:govoplan-campaign.excluded.9804952b" value={countValue(outcomes.excluded)} />
|
||||
</div>
|
||||
|
||||
<Card title="Population and exclusions">
|
||||
<Card title="i18n:govoplan-campaign.population_and_exclusions.cdff62ad">
|
||||
<dl className="detail-list">
|
||||
<div><dt>Report denominator</dt><dd>{countValue(report.population.denominator)}</dd></div>
|
||||
<div><dt>Inactive source entries</dt><dd>{countValue(report.population.inactive_source_entries)}</dd></div>
|
||||
<div><dt>Excluded or blocked jobs</dt><dd>{countValue(report.population.excluded_or_blocked_jobs)}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.report_denominator.d089074a</dt><dd>{countValue(report.population.denominator)}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.inactive_source_entries.88fa0cf8</dt><dd>{countValue(report.population.inactive_source_entries)}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.excluded_or_blocked_jobs.e2190d47</dt><dd>{countValue(report.population.excluded_or_blocked_jobs)}</dd></div>
|
||||
</dl>
|
||||
<p className="muted">{report.population.denominator_definition}</p>
|
||||
<p className="muted">i18n:govoplan-campaign.all_persisted_recipient_delivery_jobs_for_the_se.208e90cc</p>
|
||||
</Card>
|
||||
|
||||
<Card title="Delivery activity range">
|
||||
<Card title="i18n:govoplan-campaign.delivery_activity_range.0d1b62ec">
|
||||
<dl className="detail-list">
|
||||
<div><dt>First activity</dt><dd>{report.time_range.suppressed ? "Suppressed" : formatDateTime(report.time_range.first_activity_at ?? undefined)}</dd></div>
|
||||
<div><dt>Last activity</dt><dd>{report.time_range.suppressed ? "Suppressed" : formatDateTime(report.time_range.last_activity_at ?? undefined)}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.first_activity.3fe36ef4</dt><dd>{report.time_range.suppressed ? "i18n:govoplan-campaign.suppressed.fce88db5" : formatDateTime(report.time_range.first_activity_at ?? undefined)}</dd></div>
|
||||
<div><dt>i18n:govoplan-campaign.last_activity.1c73b806</dt><dd>{report.time_range.suppressed ? "i18n:govoplan-campaign.suppressed.fce88db5" : formatDateTime(report.time_range.last_activity_at ?? undefined)}</dd></div>
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
<Card title="Privacy boundary">
|
||||
<p>{report.privacy.rule}</p>
|
||||
<Card title="i18n:govoplan-campaign.privacy_boundary.d37e12bc">
|
||||
<p>i18n:govoplan-campaign.positive_counts_below_the_threshold_are_hidden_a.1dc97093</p>
|
||||
<p className="muted">
|
||||
Small-cell threshold: {threshold}. Suppressed values cannot be opened, filtered, exported, or inspected from this view.
|
||||
{i18nMessage("i18n:govoplan-campaign.small_cell_threshold_value0_suppressed_values_ca.d73f87a6", { value0: threshold })}
|
||||
</p>
|
||||
</Card>
|
||||
</>
|
||||
@@ -229,5 +230,18 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
|
||||
}
|
||||
|
||||
function countValue(count: AggregateReportCount): string | number {
|
||||
return count.suppressed ? "Suppressed" : count.value ?? "—";
|
||||
return count.suppressed ? "i18n:govoplan-campaign.suppressed.fce88db5" : count.value ?? "—";
|
||||
}
|
||||
|
||||
function aggregateCompletionLabel(state: AggregateCampaignReport["completion_state"]): string {
|
||||
const labels: Record<AggregateCampaignReport["completion_state"], string> = {
|
||||
not_started: "i18n:govoplan-campaign.not_started.db2c45d5",
|
||||
in_progress: "i18n:govoplan-campaign.in_progress.b6bd42e4",
|
||||
completed: "i18n:govoplan-campaign.completed.1798b3ba",
|
||||
partially_completed: "i18n:govoplan-campaign.partially_completed.760bc5e6",
|
||||
incomplete: "i18n:govoplan-campaign.incomplete.387fd1bb",
|
||||
outcome_unknown: "i18n:govoplan-campaign.outcome_unknown.6e929fca",
|
||||
suppressed: "i18n:govoplan-campaign.suppressed.fce88db5"
|
||||
};
|
||||
return labels[state];
|
||||
}
|
||||
|
||||
@@ -21,6 +21,82 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.reconcile_outcomes.bd81d8a7": "Reconcile outcomes",
|
||||
"i18n:govoplan-campaign.resume_queue.1f22f964": "Resume queue",
|
||||
"i18n:govoplan-campaign.you_do_not_have_permission_for_this_action.dfcfbad6": "You do not have permission for this action.",
|
||||
"i18n:govoplan-campaign.campaign_reports_available_to_you.f14fa403": "Campaign reports available to you",
|
||||
"i18n:govoplan-campaign.cancelled_value0_unsent_message_s_value1_protect.d24c5e24": "Cancelled {value0} unsent message(s); {value1} protected outcome(s) and {value2} excluded/skipped message(s) were retained.",
|
||||
"i18n:govoplan-campaign.completion.2ff25568": "Completion",
|
||||
"i18n:govoplan-campaign.database_queue.8bf98437": "Database queue",
|
||||
"i18n:govoplan-campaign.delivery_activity_range.0d1b62ec": "Delivery activity range",
|
||||
"i18n:govoplan-campaign.delivery_mode.109ed9d1": "Delivery mode",
|
||||
"i18n:govoplan-campaign.excluded_or_blocked_jobs.e2190d47": "Excluded or blocked jobs",
|
||||
"i18n:govoplan-campaign.excluded_rows_are_intentionally_omitted_from_del.421a1f00": "Excluded rows are intentionally omitted from delivery. Their SMTP and IMAP states are shown as Skipped because no transport effect is attempted; use the separate status filters to isolate them.",
|
||||
"i18n:govoplan-campaign.first_activity.3fe36ef4": "First activity",
|
||||
"i18n:govoplan-campaign.in_progress.b6bd42e4": "In progress",
|
||||
"i18n:govoplan-campaign.inactive_source_entries.88fa0cf8": "Inactive source entries",
|
||||
"i18n:govoplan-campaign.incomplete.387fd1bb": "Incomplete",
|
||||
"i18n:govoplan-campaign.last_activity.1c73b806": "Last activity",
|
||||
"i18n:govoplan-campaign.loading_aggregate_campaign_report_.72f2f091": "Loading aggregate Campaign report…",
|
||||
"i18n:govoplan-campaign.loading_campaign_reports_.61ec1ee8": "Loading Campaign reports…",
|
||||
"i18n:govoplan-campaign.no_aggregate_campaign_reports_are_available_to_y.dafb2781": "No aggregate Campaign reports are available to you.",
|
||||
"i18n:govoplan-campaign.not_started.db2c45d5": "Not started",
|
||||
"i18n:govoplan-campaign.partially_completed.760bc5e6": "Partially completed",
|
||||
"i18n:govoplan-campaign.population_and_exclusions.cdff62ad": "Population and exclusions",
|
||||
"i18n:govoplan-campaign.privacy_boundary.d37e12bc": "Privacy boundary",
|
||||
"i18n:govoplan-campaign.privacy_protected_campaign_outcomes_without_reci.5b8a100d": "Privacy-protected Campaign outcomes without recipient or delivery diagnostics.",
|
||||
"i18n:govoplan-campaign.queued_or_active.afdcd7da": "Queued or active",
|
||||
"i18n:govoplan-campaign.report_denominator.d089074a": "Report denominator",
|
||||
"i18n:govoplan-campaign.small_cell_threshold_value0_suppressed_values_ca.d73f87a6": "Small-cell threshold: {value0}. Suppressed values cannot be opened, filtered, exported, or inspected from this view.",
|
||||
"i18n:govoplan-campaign.smtp_skipped_excluded_.df6eca19": "SMTP skipped (excluded)",
|
||||
"i18n:govoplan-campaign.suppressed.fce88db5": "Suppressed",
|
||||
"i18n:govoplan-campaign.synchronous.77c61919": "Synchronous",
|
||||
"i18n:govoplan-campaign.this_report_is_already_selected_.983c5be4": "This report is already selected.",
|
||||
"i18n:govoplan-campaign.this_version_last_entered_value0_mode_at_value1_.c087d2f3": "This version last entered {value0} mode at {value1}. This record remains available after leaving and returning.",
|
||||
"i18n:govoplan-campaign.this_version_last_entered_value0_mode_this_recor.ea2f201f": "This version last entered {value0} mode. This record remains available after leaving and returning.",
|
||||
"i18n:govoplan-campaign.view_aggregate_report.94b42124": "View aggregate report",
|
||||
"i18n:govoplan-campaign.worker_queue.c911e32c": "Worker queue",
|
||||
"i18n:govoplan-campaign.all_persisted_recipient_delivery_jobs_for_the_se.208e90cc": "All persisted recipient delivery jobs for the selected campaign version, including excluded or blocked jobs. Inactive source entries without a job record are excluded and reported separately.",
|
||||
"i18n:govoplan-campaign.background_workers_are_not_configured_.efa72396": "Background workers are not configured.",
|
||||
"i18n:govoplan-campaign.cancel_all_unsent_delivery_work_.a2f56cda": "Cancel all unsent delivery work?",
|
||||
"i18n:govoplan-campaign.cancel_unsent_work.66df9f0d": "Cancel unsent work",
|
||||
"i18n:govoplan-campaign.cancelling_eligible_delivery_jobs_.4090fa47": "Cancelling eligible delivery jobs…",
|
||||
"i18n:govoplan-campaign.committing_the_reviewed_execution_to_the_backgro.6ae349e2": "Committing the reviewed execution to the background-worker queue…",
|
||||
"i18n:govoplan-campaign.delivery_options_are_still_being_evaluated.8395096e": "delivery options are still being evaluated",
|
||||
"i18n:govoplan-campaign.deployment.327a55f8": "Deployment",
|
||||
"i18n:govoplan-campaign.deployment_ceiling.abbec75b": "Deployment ceiling",
|
||||
"i18n:govoplan-campaign.deployment_default.aa39f7b4": "Deployment default",
|
||||
"i18n:govoplan-campaign.keep_delivery_work.10dbcb13": "Keep delivery work",
|
||||
"i18n:govoplan-campaign.limit_source.bd933adb": "Limit source",
|
||||
"i18n:govoplan-campaign.loading_the_effective_delivery_policy_.d6893011": "Loading the effective delivery policy…",
|
||||
"i18n:govoplan-campaign.no_real_delivery_mode_is_currently_available_val.618cce1f": "No real delivery mode is currently available ({value0}).",
|
||||
"i18n:govoplan-campaign.not_available.d1a17af1": "Not available",
|
||||
"i18n:govoplan-campaign.pause_queued_work.35ab4a5b": "Pause queued work",
|
||||
"i18n:govoplan-campaign.paused_value0_queued_message_s_.c7d568d2": "Paused {value0} queued message(s).",
|
||||
"i18n:govoplan-campaign.pausing_eligible_delivery_jobs_.eb6b9d58": "Pausing eligible delivery jobs…",
|
||||
"i18n:govoplan-campaign.positive_counts_below_the_threshold_are_hidden_a.1dc97093": "Positive counts below the threshold are hidden. At least one additional count or the denominator is hidden when needed to prevent subtraction.",
|
||||
"i18n:govoplan-campaign.queued_value0_message_s_value1_worker_task_s_pub.18d67ec8": "Queued {value0} message(s); {value1} worker task(s) published.",
|
||||
"i18n:govoplan-campaign.queued_value0_retryable_message_s_value1_worker_.f4795bdb": "Queued {value0} retryable message(s); {value1} worker task(s) published.",
|
||||
"i18n:govoplan-campaign.queue_for_workers.dae9a3e4": "Queue for workers",
|
||||
"i18n:govoplan-campaign.queue_for_workers_commits_durable_jobs_and_retur.d0dbe81a": "Queue for workers commits durable jobs and returns immediately; progress remains visible here after you leave. Send now keeps this request open and is limited to {value0} eligible recipient jobs.",
|
||||
"i18n:govoplan-campaign.queue_this_version_for_background_workers_.943d032c": "Queue this version for background workers?",
|
||||
"i18n:govoplan-campaign.queueing_for_workers_.d24584fe": "Queueing for workers…",
|
||||
"i18n:govoplan-campaign.queueing_retryable_failed_messages_for_backgroun.4bc80cd9": "Queueing retryable failed messages for background workers…",
|
||||
"i18n:govoplan-campaign.resume_with_workers.510a2a9a": "Resume with workers",
|
||||
"i18n:govoplan-campaign.resumed_value0_message_s_value1_worker_task_s_pu.0f59afb4": "Resumed {value0} message(s); {value1} worker task(s) published.",
|
||||
"i18n:govoplan-campaign.resuming_eligible_delivery_jobs_.dd12c5a2": "Resuming eligible delivery jobs…",
|
||||
"i18n:govoplan-campaign.retry_failed_with_workers.a4b7dcc5": "Retry failed with workers",
|
||||
"i18n:govoplan-campaign.send_now_is_available_for_value0_eligible_messag.6ec0ed6c": "Send now is available for {value0} eligible message(s), within the effective limit of {value1}. {value2}",
|
||||
"i18n:govoplan-campaign.send_now_is_unavailable_value0_.d93c0b29": "Send now is unavailable: {value0}.",
|
||||
"i18n:govoplan-campaign.send_now_is_unavailable_value0_queue_for_workers.59bfc873": "Send now is unavailable ({value0}). Queue for workers remains available.",
|
||||
"i18n:govoplan-campaign.synchronous_limit.f88c0bcd": "Synchronous limit",
|
||||
"i18n:govoplan-campaign.the_built_run_has_no_eligible_message.48e5410c": "the built run has no eligible message",
|
||||
"i18n:govoplan-campaign.the_configured_maximum.eb10006b": "the configured maximum",
|
||||
"i18n:govoplan-campaign.the_delivery_policy_configuration_is_invalid.a804a8f8": "the delivery policy configuration is invalid",
|
||||
"i18n:govoplan-campaign.the_exact_built_run_exceeds_the_effective_limit_.d7812d6a": "the exact built run exceeds the effective limit for recipient jobs",
|
||||
"i18n:govoplan-campaign.the_worker_queue_is_also_available_.7b77144e": "The worker queue is also available.",
|
||||
"i18n:govoplan-campaign.this_cancels_queued_or_paused_messages_that_have.add583bc": "This cancels queued or paused messages that have not crossed the SMTP boundary. Accepted, active, or outcome-unknown messages remain protected for audit and reconciliation.",
|
||||
"i18n:govoplan-campaign.this_commits_value0_eligible_message_s_from_vers.f53e7222": "This commits {value0} eligible message(s) from version {value1} to durable worker jobs. You may leave this page and return to the same progress and recovery state.",
|
||||
"i18n:govoplan-campaign.validate_lock_build_and_review_the_current_versi.d0567dcc": "validate, lock, build, and review the current version first",
|
||||
"i18n:govoplan-campaign.value0_this_is_a_synchronous_request_for_value1_.0267bc6b": "{value0} This is a synchronous request for {value1} eligible message(s), within the effective limit of {value2}. All messages are preflighted before SMTP is contacted.",
|
||||
"i18n:govoplan-campaign.worker_queue_committed_value0_message_s_and_publ.24400d3c": "Worker queue committed {value0} message(s) and published {value1} task(s). Refreshing progress does not requeue them.",
|
||||
"i18n:govoplan-campaign.campaign_scoped_mail_profile.9cbf3505": "Campaign-scoped Mail profile",
|
||||
"i18n:govoplan-campaign.campaign_stores_only_this_stable_profile_referen.de554809": "Campaign stores only this stable profile reference. The Mail module owns, encrypts, authorizes, tests, and resolves all SMTP/IMAP settings and credentials.",
|
||||
"i18n:govoplan-campaign.configured.668c5fff": "Configured",
|
||||
@@ -1210,6 +1286,82 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.reconcile_outcomes.bd81d8a7": "Ergebnisse abstimmen",
|
||||
"i18n:govoplan-campaign.resume_queue.1f22f964": "Warteschlange fortsetzen",
|
||||
"i18n:govoplan-campaign.you_do_not_have_permission_for_this_action.dfcfbad6": "Sie haben keine Berechtigung für diese Aktion.",
|
||||
"i18n:govoplan-campaign.campaign_reports_available_to_you.f14fa403": "Für Sie verfügbare Kampagnenberichte",
|
||||
"i18n:govoplan-campaign.cancelled_value0_unsent_message_s_value1_protect.d24c5e24": "{value0} ungesendete Nachricht(en) wurden abgebrochen; {value1} geschützte Ergebnisse und {value2} ausgeschlossene/übersprungene Nachricht(en) wurden beibehalten.",
|
||||
"i18n:govoplan-campaign.completion.2ff25568": "Abschluss",
|
||||
"i18n:govoplan-campaign.database_queue.8bf98437": "Datenbank-Warteschlange",
|
||||
"i18n:govoplan-campaign.delivery_activity_range.0d1b62ec": "Zeitraum der Versandaktivität",
|
||||
"i18n:govoplan-campaign.delivery_mode.109ed9d1": "Versandmodus",
|
||||
"i18n:govoplan-campaign.excluded_or_blocked_jobs.e2190d47": "Ausgeschlossene oder blockierte Aufträge",
|
||||
"i18n:govoplan-campaign.excluded_rows_are_intentionally_omitted_from_del.421a1f00": "Ausgeschlossene Zeilen werden absichtlich nicht versendet. Ihr SMTP- und IMAP-Status wird als „Übersprungen“ angezeigt, da kein Transport versucht wird; über die separaten Statusfilter können Sie diese Zeilen gezielt anzeigen.",
|
||||
"i18n:govoplan-campaign.first_activity.3fe36ef4": "Erste Aktivität",
|
||||
"i18n:govoplan-campaign.in_progress.b6bd42e4": "In Bearbeitung",
|
||||
"i18n:govoplan-campaign.inactive_source_entries.88fa0cf8": "Inaktive Quelleinträge",
|
||||
"i18n:govoplan-campaign.incomplete.387fd1bb": "Unvollständig",
|
||||
"i18n:govoplan-campaign.last_activity.1c73b806": "Letzte Aktivität",
|
||||
"i18n:govoplan-campaign.loading_aggregate_campaign_report_.72f2f091": "Aggregierter Kampagnenbericht wird geladen…",
|
||||
"i18n:govoplan-campaign.loading_campaign_reports_.61ec1ee8": "Kampagnenberichte werden geladen…",
|
||||
"i18n:govoplan-campaign.no_aggregate_campaign_reports_are_available_to_y.dafb2781": "Für Sie sind keine aggregierten Kampagnenberichte verfügbar.",
|
||||
"i18n:govoplan-campaign.not_started.db2c45d5": "Nicht begonnen",
|
||||
"i18n:govoplan-campaign.partially_completed.760bc5e6": "Teilweise abgeschlossen",
|
||||
"i18n:govoplan-campaign.population_and_exclusions.cdff62ad": "Grundgesamtheit und Ausschlüsse",
|
||||
"i18n:govoplan-campaign.privacy_boundary.d37e12bc": "Datenschutzgrenze",
|
||||
"i18n:govoplan-campaign.privacy_protected_campaign_outcomes_without_reci.5b8a100d": "Datenschutzgeschützte Kampagnenergebnisse ohne Empfänger- oder Versanddiagnosen.",
|
||||
"i18n:govoplan-campaign.queued_or_active.afdcd7da": "In Warteschlange oder aktiv",
|
||||
"i18n:govoplan-campaign.report_denominator.d089074a": "Bezugsgröße des Berichts",
|
||||
"i18n:govoplan-campaign.small_cell_threshold_value0_suppressed_values_ca.d73f87a6": "Schwellenwert für kleine Fallzahlen: {value0}. Unterdrückte Werte können in dieser Ansicht nicht geöffnet, gefiltert, exportiert oder geprüft werden.",
|
||||
"i18n:govoplan-campaign.smtp_skipped_excluded_.df6eca19": "SMTP übersprungen (ausgeschlossen)",
|
||||
"i18n:govoplan-campaign.suppressed.fce88db5": "Unterdrückt",
|
||||
"i18n:govoplan-campaign.synchronous.77c61919": "Synchron",
|
||||
"i18n:govoplan-campaign.this_report_is_already_selected_.983c5be4": "Dieser Bericht ist bereits ausgewählt.",
|
||||
"i18n:govoplan-campaign.this_version_last_entered_value0_mode_at_value1_.c087d2f3": "Diese Version wechselte zuletzt am {value1} in den Modus „{value0}“. Der Eintrag bleibt auch nach dem Verlassen und erneuten Öffnen verfügbar.",
|
||||
"i18n:govoplan-campaign.this_version_last_entered_value0_mode_this_recor.ea2f201f": "Diese Version wechselte zuletzt in den Modus „{value0}“. Der Eintrag bleibt auch nach dem Verlassen und erneuten Öffnen verfügbar.",
|
||||
"i18n:govoplan-campaign.view_aggregate_report.94b42124": "Aggregierten Bericht anzeigen",
|
||||
"i18n:govoplan-campaign.worker_queue.c911e32c": "Worker-Warteschlange",
|
||||
"i18n:govoplan-campaign.all_persisted_recipient_delivery_jobs_for_the_se.208e90cc": "Alle dauerhaft gespeicherten Empfänger-Versandaufträge der ausgewählten Kampagnenversion werden berücksichtigt, einschließlich ausgeschlossener oder blockierter Aufträge. Inaktive Quelleinträge ohne Auftragsdatensatz sind ausgeschlossen und werden separat ausgewiesen.",
|
||||
"i18n:govoplan-campaign.background_workers_are_not_configured_.efa72396": "Hintergrund-Worker sind nicht konfiguriert.",
|
||||
"i18n:govoplan-campaign.cancel_all_unsent_delivery_work_.a2f56cda": "Alle ungesendeten Versandaufträge abbrechen?",
|
||||
"i18n:govoplan-campaign.cancel_unsent_work.66df9f0d": "Ungesendete Aufträge abbrechen",
|
||||
"i18n:govoplan-campaign.cancelling_eligible_delivery_jobs_.4090fa47": "Geeignete Versandaufträge werden abgebrochen…",
|
||||
"i18n:govoplan-campaign.committing_the_reviewed_execution_to_the_backgro.6ae349e2": "Die geprüfte Ausführung wird an die Hintergrund-Worker-Warteschlange übergeben…",
|
||||
"i18n:govoplan-campaign.delivery_options_are_still_being_evaluated.8395096e": "die Versandoptionen werden noch ausgewertet",
|
||||
"i18n:govoplan-campaign.deployment.327a55f8": "Bereitstellung",
|
||||
"i18n:govoplan-campaign.deployment_ceiling.abbec75b": "Bereitstellungsobergrenze",
|
||||
"i18n:govoplan-campaign.deployment_default.aa39f7b4": "Bereitstellungsstandard",
|
||||
"i18n:govoplan-campaign.keep_delivery_work.10dbcb13": "Versandaufträge beibehalten",
|
||||
"i18n:govoplan-campaign.limit_source.bd933adb": "Quelle des Grenzwerts",
|
||||
"i18n:govoplan-campaign.loading_the_effective_delivery_policy_.d6893011": "Wirksame Versandrichtlinie wird geladen…",
|
||||
"i18n:govoplan-campaign.no_real_delivery_mode_is_currently_available_val.618cce1f": "Derzeit ist kein echter Versandmodus verfügbar ({value0}).",
|
||||
"i18n:govoplan-campaign.not_available.d1a17af1": "Nicht verfügbar",
|
||||
"i18n:govoplan-campaign.pause_queued_work.35ab4a5b": "Wartende Aufträge pausieren",
|
||||
"i18n:govoplan-campaign.paused_value0_queued_message_s_.c7d568d2": "{value0} wartende Nachricht(en) pausiert.",
|
||||
"i18n:govoplan-campaign.pausing_eligible_delivery_jobs_.eb6b9d58": "Geeignete Versandaufträge werden pausiert…",
|
||||
"i18n:govoplan-campaign.positive_counts_below_the_threshold_are_hidden_a.1dc97093": "Positive Anzahlen unterhalb des Schwellenwerts werden ausgeblendet. Falls erforderlich, wird mindestens eine weitere Anzahl oder die Bezugsgröße ausgeblendet, damit keine Rückrechnung möglich ist.",
|
||||
"i18n:govoplan-campaign.queued_value0_message_s_value1_worker_task_s_pub.18d67ec8": "{value0} Nachricht(en) wurden eingereiht; {value1} Worker-Aufgabe(n) wurden veröffentlicht.",
|
||||
"i18n:govoplan-campaign.queued_value0_retryable_message_s_value1_worker_.f4795bdb": "{value0} wiederholbare Nachricht(en) wurden eingereiht; {value1} Worker-Aufgabe(n) wurden veröffentlicht.",
|
||||
"i18n:govoplan-campaign.queue_for_workers.dae9a3e4": "Für Worker einreihen",
|
||||
"i18n:govoplan-campaign.queue_for_workers_commits_durable_jobs_and_retur.d0dbe81a": "Durch das Einreihen für Worker werden dauerhafte Aufträge erstellt und die Aktion kehrt sofort zurück; der Fortschritt bleibt nach dem Verlassen sichtbar. „Jetzt senden“ hält diese Anfrage offen und ist auf {value0} geeignete Empfängeraufträge begrenzt.",
|
||||
"i18n:govoplan-campaign.queue_this_version_for_background_workers_.943d032c": "Diese Version für Hintergrund-Worker einreihen?",
|
||||
"i18n:govoplan-campaign.queueing_for_workers_.d24584fe": "Wird für Worker eingereiht…",
|
||||
"i18n:govoplan-campaign.queueing_retryable_failed_messages_for_backgroun.4bc80cd9": "Wiederholbare fehlgeschlagene Nachrichten werden für Hintergrund-Worker eingereiht…",
|
||||
"i18n:govoplan-campaign.resume_with_workers.510a2a9a": "Mit Workern fortsetzen",
|
||||
"i18n:govoplan-campaign.resumed_value0_message_s_value1_worker_task_s_pu.0f59afb4": "{value0} Nachricht(en) wurden fortgesetzt; {value1} Worker-Aufgabe(n) wurden veröffentlicht.",
|
||||
"i18n:govoplan-campaign.resuming_eligible_delivery_jobs_.dd12c5a2": "Geeignete Versandaufträge werden fortgesetzt…",
|
||||
"i18n:govoplan-campaign.retry_failed_with_workers.a4b7dcc5": "Fehlgeschlagene Aufträge mit Workern wiederholen",
|
||||
"i18n:govoplan-campaign.send_now_is_available_for_value0_eligible_messag.6ec0ed6c": "„Jetzt senden“ ist für {value0} geeignete Nachricht(en) innerhalb des wirksamen Grenzwerts von {value1} verfügbar. {value2}",
|
||||
"i18n:govoplan-campaign.send_now_is_unavailable_value0_.d93c0b29": "„Jetzt senden“ ist nicht verfügbar: {value0}.",
|
||||
"i18n:govoplan-campaign.send_now_is_unavailable_value0_queue_for_workers.59bfc873": "„Jetzt senden“ ist nicht verfügbar ({value0}). Die Worker-Warteschlange bleibt verfügbar.",
|
||||
"i18n:govoplan-campaign.synchronous_limit.f88c0bcd": "Grenzwert für synchronen Versand",
|
||||
"i18n:govoplan-campaign.the_built_run_has_no_eligible_message.48e5410c": "der erstellte Lauf enthält keine geeignete Nachricht",
|
||||
"i18n:govoplan-campaign.the_configured_maximum.eb10006b": "das konfigurierte Maximum",
|
||||
"i18n:govoplan-campaign.the_delivery_policy_configuration_is_invalid.a804a8f8": "die Konfiguration der Versandrichtlinie ist ungültig",
|
||||
"i18n:govoplan-campaign.the_exact_built_run_exceeds_the_effective_limit_.d7812d6a": "der exakt erstellte Lauf überschreitet den wirksamen Grenzwert für Empfängeraufträge",
|
||||
"i18n:govoplan-campaign.the_worker_queue_is_also_available_.7b77144e": "Die Worker-Warteschlange ist ebenfalls verfügbar.",
|
||||
"i18n:govoplan-campaign.this_cancels_queued_or_paused_messages_that_have.add583bc": "Dadurch werden wartende oder pausierte Nachrichten abgebrochen, die die SMTP-Grenze noch nicht überschritten haben. Angenommene, aktive oder ungeklärte Nachrichten bleiben für Audit und Abstimmung geschützt.",
|
||||
"i18n:govoplan-campaign.this_commits_value0_eligible_message_s_from_vers.f53e7222": "Dadurch werden {value0} geeignete Nachricht(en) aus Version {value1} als dauerhafte Worker-Aufträge eingereiht. Sie können diese Seite verlassen und später zum selben Fortschritts- und Wiederherstellungszustand zurückkehren.",
|
||||
"i18n:govoplan-campaign.validate_lock_build_and_review_the_current_versi.d0567dcc": "validieren, sperren, erstellen und prüfen Sie zuerst die aktuelle Version",
|
||||
"i18n:govoplan-campaign.value0_this_is_a_synchronous_request_for_value1_.0267bc6b": "{value0} Dies ist eine synchrone Anfrage für {value1} geeignete Nachricht(en) innerhalb des wirksamen Grenzwerts von {value2}. Alle Nachrichten werden vor dem SMTP-Kontakt vollständig vorgeprüft.",
|
||||
"i18n:govoplan-campaign.worker_queue_committed_value0_message_s_and_publ.24400d3c": "Die Worker-Warteschlange hat {value0} Nachricht(en) übernommen und {value1} Aufgabe(n) veröffentlicht. Eine Fortschrittsaktualisierung reiht sie nicht erneut ein.",
|
||||
"i18n:govoplan-campaign.campaign_scoped_mail_profile.9cbf3505": "Kampagnenspezifisches Mail-Profil",
|
||||
"i18n:govoplan-campaign.campaign_stores_only_this_stable_profile_referen.de554809": "Campaign speichert nur diese stabile Profilreferenz. Das Mail-Modul verwaltet, verschlüsselt, autorisiert, testet und löst alle SMTP-/IMAP-Einstellungen und Zugangsdaten auf.",
|
||||
"i18n:govoplan-campaign.configured.668c5fff": "Konfiguriert",
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const api = readFileSync("src/api/campaigns.ts", "utf8");
|
||||
const page = readFileSync("src/features/reports/AggregateReportsPage.tsx", "utf8");
|
||||
const reportPage = readFileSync("src/features/campaigns/CampaignReportPage.tsx", "utf8");
|
||||
const reviewPage = readFileSync("src/features/campaigns/ReviewSendPage.tsx", "utf8");
|
||||
const operatorPage = readFileSync("src/features/operator/OperatorQueuePage.tsx", "utf8");
|
||||
const deliveryMode = readFileSync("src/features/campaigns/utils/deliveryMode.ts", "utf8");
|
||||
const translationCatalog = readFileSync("src/i18n/generatedTranslations.ts", "utf8");
|
||||
|
||||
assert(api.includes('"/api/v1/campaigns/aggregate-reports"'), "the report list uses the safe aggregate collection");
|
||||
assert(api.includes("/api/v1/campaigns/aggregate-reports/${encodeURIComponent(campaignId)}"), "the selected report uses the safe aggregate projection");
|
||||
@@ -17,5 +23,32 @@ assert(!page.includes("localStorage"), "the aggregate page does not persist repo
|
||||
assert(!page.includes("sessionStorage"), "the aggregate page does not persist report data in session storage");
|
||||
assert(page.includes("TableActionGroup"), "campaign selection uses the central icon-only table action group");
|
||||
assert(page.includes("disabled: campaign.id === selectedFromUrl"), "the selected row action stays visible and disabled");
|
||||
assert(!page.includes("{report.population.denominator_definition}"), "the known denominator contract uses a localized UI explanation");
|
||||
assert(!page.includes("{report.privacy.rule}"), "the known privacy contract uses a localized UI explanation");
|
||||
assert(page.includes("all_persisted_recipient_delivery_jobs_for_the_se.208e90cc"), "the denominator explanation is bilingual");
|
||||
assert(page.includes("positive_counts_below_the_threshold_are_hidden_a.1dc97093"), "the suppression rule is bilingual");
|
||||
|
||||
const touchedSources = [page, reportPage, reviewPage, operatorPage, deliveryMode].join("\n");
|
||||
const touchedKeys = new Set(touchedSources.match(/i18n:govoplan-campaign\.[a-z0-9_]+\.[a-f0-9]{8}/g) ?? []);
|
||||
for (const key of touchedKeys) {
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const catalogEntries = [...translationCatalog.matchAll(new RegExp(`"${escapedKey}":\\s*("(?:[^"\\\\]|\\\\.)*")`, "g"))];
|
||||
assert.equal(catalogEntries.length, 2, `${key} is present exactly once in both language catalogs`);
|
||||
const english = JSON.parse(catalogEntries[0][1]);
|
||||
const german = JSON.parse(catalogEntries[1][1]);
|
||||
assert.notEqual(english.trim(), "", `${key} has a non-empty English translation`);
|
||||
assert.notEqual(german.trim(), "", `${key} has a non-empty German translation`);
|
||||
}
|
||||
|
||||
const sliceSections = [...translationCatalog.matchAll(/ "i18n:govoplan-campaign\.campaign_reports_available_to_you\.f14fa403":[\s\S]*? "i18n:govoplan-campaign\.worker_queue_committed_value0_message_s_and_publ\.24400d3c":.*\n/g)];
|
||||
assert.equal(sliceSections.length, 2, "the current slice has one contiguous English and German catalog section");
|
||||
const newEnglishEntries = [...sliceSections[0][0].matchAll(/"(i18n:govoplan-campaign\.[a-z0-9_]+\.[a-f0-9]{8})":\s*("(?:[^"\\]|\\.)*")/g)];
|
||||
for (const [, key, encodedEnglish] of newEnglishEntries) {
|
||||
const english = JSON.parse(encodedEnglish);
|
||||
const digest = createHash("sha1").update(english).digest("hex").slice(0, 8);
|
||||
const slug = english.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+/, "").slice(0, 48);
|
||||
assert.equal(key, `i18n:govoplan-campaign.${slug}.${digest}`, `${key} uses the normalized source slug and exact SHA-1 suffix`);
|
||||
assert(touchedKeys.has(key), `${key} is referenced by the current Campaign UI slices`);
|
||||
}
|
||||
|
||||
console.log("aggregate report UI structure checks passed");
|
||||
|
||||
@@ -5,12 +5,22 @@ import { dirname, resolve } from "node:path";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const source = readFileSync(resolve(here, "../src/features/campaigns/ReviewSendPage.tsx"), "utf8");
|
||||
const modeSource = readFileSync(resolve(here, "../src/features/campaigns/utils/deliveryMode.ts"), "utf8");
|
||||
|
||||
assert.match(source, /getCampaignDeliveryOptions/);
|
||||
assert.match(source, /Queue for workers/);
|
||||
assert.match(source, /Send now keeps this request open/);
|
||||
assert.match(source, /queue_for_workers\.dae9a3e4/);
|
||||
assert.match(source, /queue_for_workers_commits_durable_jobs_and_retur\.d0dbe81a/);
|
||||
assert.match(source, /loading_the_effective_delivery_policy_\.d6893011/);
|
||||
assert.match(source, /deliveryControlProgressMessage/);
|
||||
assert.match(source, /synchronousSendAllowed/);
|
||||
assert.match(source, /commandInProgress[\s\S]*refreshQueueStatus/);
|
||||
assert.match(source, /<ConfirmDialog[\s\S]*open=\{queueConfirmOpen\}/);
|
||||
assert.match(source, /<ConfirmDialog[\s\S]*open=\{cancelDeliveryConfirmOpen\}/);
|
||||
assert.match(source, /deliveryModeLabel\(persistedDeliveryMode\)/);
|
||||
assert.match(source, /this_version_last_entered_value0_mode_at_value1_\.c087d2f3/);
|
||||
assert.match(source, /this_version_last_entered_value0_mode_this_recor\.ea2f201f/);
|
||||
assert.doesNotMatch(source, /This version last entered/);
|
||||
assert.match(modeSource, /synchronous\.77c61919/);
|
||||
assert.match(modeSource, /worker_queue\.c911e32c/);
|
||||
assert.match(modeSource, /database_queue\.8bf98437/);
|
||||
assert.doesNotMatch(source, /window\.alert\s*\(/);
|
||||
|
||||
@@ -66,6 +66,6 @@ test("permission explanations take precedence over queue state", () => {
|
||||
const blocks = operatorQueueActionBlocks(everything, noPermissions);
|
||||
assert.deepEqual(
|
||||
Object.values(blocks),
|
||||
Array.from({ length: 7 }, () => OPERATOR_QUEUE_REASON.permission)
|
||||
Array.from({ length: 7 }, () => OPERATOR_QUEUE_REASON.permissionDenied)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,8 @@ assert(reportSource.includes("onQueryChange={handleJobGridQuery}"), "header sort
|
||||
assert(reportSource.includes("initialReportGridFilters()"), "status deep links initialize the DataGrid filters");
|
||||
assert(reportSource.includes("initialReportQuery()"), "q deep links initialize the report search");
|
||||
assert(reportSource.includes('"skipped",\n"queued"'), "SMTP skipped is a first-class report filter option");
|
||||
assert(reportSource.includes("Excluded rows are intentionally omitted from delivery"), "the report explains excluded transport semantics");
|
||||
assert(reportSource.includes("i18n:govoplan-campaign.excluded_rows_are_intentionally_omitted_from_del.421a1f00"), "the report explains excluded transport semantics through the bilingual catalog");
|
||||
assert(reportSource.includes('return status === "skipped" ? "i18n:govoplan-campaign.skipped.5a000ad7"'), "the new skipped filter and status badges use the localized label");
|
||||
assert(reportSource.includes("cards?.skipped ?? jobs.counts.send?.skipped"), "SMTP skipped has a separate report count");
|
||||
assert(reportSource.includes("cards?.imap_skipped ?? jobs.counts.imap?.skipped"), "IMAP skipped has a separate report count");
|
||||
assert(!reportSource.includes("setPage((value) => Math.max(1, value - 1))"), "the one-off report pager is removed in favor of the central DataGrid pager");
|
||||
|
||||
Reference in New Issue
Block a user