fix(webui): localize Campaign delivery reporting
This commit is contained in:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user