import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react"; import { BarChart3, Check, ChevronDown, FlaskConical, LockKeyhole, PackageCheck, Send, ShieldCheck, type LucideIcon } from "lucide-react"; import type { ApiSettings } from "../../types"; import { appendSent, buildVersion, getCampaignJobsDelta, getCampaignJobDetail, getCampaignSummary, mockSendCampaign, sendCampaignNow, updateCampaignReviewState, validateVersion, type CampaignJobsQuery, type CampaignJobsResponse, type CampaignSummary, type CampaignVersionDetail } from "../../api/campaigns"; import { getMockMailboxMessage, type MockMailboxMessage } from "../../api/mail"; import { Button, useDeltaWatermarks, useGuardedNavigate, usePlatformUiCapability, type MailDevMailboxUiCapability } from "@govoplan/core-webui"; import DataGrid, { type DataGridColumn, type DataGridListOption } from "../../components/table/DataGrid"; import { DismissibleAlert } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import { LoadingFrame } from "@govoplan/core-webui"; import { PageTitle } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui"; import { InlineHelp, i18nMessage } from "@govoplan/core-webui"; import CampaignMessagePreviewOverlay, { type CampaignMessagePreviewAttachment } from "./components/MessagePreviewOverlay"; import LockedVersionNotice from "./components/LockedVersionNotice"; import VersionLine from "./components/VersionLine"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { asArray, asRecord, formatDateTime, getCampaignJson, getDeliverySection, humanize, isFinalLockedVersion, isHistoricalCampaignVersion, isUserLockedVersion, isVersionReadyForDelivery, stringifyPreview } from "./utils/campaignView"; import { getBool, getText } from "./utils/draftEditor"; import { emptyCampaignJobsResponse, mergeCampaignJobsDelta } from "./utils/jobDeltas"; import { buildTemplatePreviewContext, renderTemplatePreviewText } from "./utils/templatePlaceholders"; type FlowState = "complete" | "warning" | "danger" | "active" | "locked" | "running" | "partial" | "stale" | "pending"; type FlowStageDefinition = { id: string; title: string; shortTitle: string; description: string; icon: LucideIcon; state: FlowState; stateLabel: string; lockReason?: string; }; type DeliverabilityPreflightItem = { label: string; detail: string; state: "ready" | "warning" | "blocked" | "info"; }; type WorkflowBusy = "validate" | "build" | "inspect" | "mock" | "mailbox" | "send" | "imap" | ""; const stateColors: Record = { complete: "var(--green)", warning: "var(--amber)", danger: "var(--red)", active: "var(--blue)", locked: "var(--line-dark)", running: "var(--blue)", partial: "#9b86c7", stale: "#9b86c7", pending: "var(--muted)" }; const MESSAGE_VALIDATION_OPTIONS: DataGridListOption[] = [ { value: "ready", label: "i18n:govoplan-campaign.ready.20c7c552" }, { value: "warning", label: "i18n:govoplan-campaign.warning.e9c45563" }, { value: "needs_review", label: "i18n:govoplan-campaign.needs_review.33a506cf" }, { value: "blocked", label: "i18n:govoplan-campaign.blocked.99613c74" }, { value: "excluded", label: "i18n:govoplan-campaign.excluded.9804952b" }]; const MESSAGE_REVIEW_ISSUE_STATUSES = ["warning", "needs_review", "blocked", "excluded"]; export default function ReviewSendPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) { const navigate = useGuardedNavigate(); const devMailboxCapability = usePlatformUiCapability("mail.devMailbox"); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const mockWorkflowAvailable = devMailboxCapability?.enabled === true; const [mockVerificationRequired, setMockVerificationRequired] = useState(true); const [mockMailboxPreviewEnabled, setMockMailboxPreviewEnabled] = useState(true); const mockWorkflowRequired = mockWorkflowAvailable && mockVerificationRequired; const mockMailboxPreviewActive = mockWorkflowAvailable && mockMailboxPreviewEnabled; const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true }); const [liveSummary, setLiveSummary] = useState(null); const [queueStatusLoading, setQueueStatusLoading] = useState(false); const version = data.currentVersion; const campaignJson = useMemo(() => getCampaignJson(version), [version]); const server = asRecord(campaignJson.server); const smtpServer = asRecord(server.smtp); const imapServer = asRecord(server.imap); const serverCredentials = asRecord(server.credentials); const smtpCredentials = asRecord(serverCredentials.smtp); const imapCredentials = asRecord(serverCredentials.imap); const selectedMailProfileId = getText(server, "mail_profile_id", getText(server, "profile_id")); const inlineEntries = useMemo( () => asArray(asRecord(campaignJson.entries).inline).map(asRecord), [campaignJson] ); const validation = asRecord(version?.validation_summary); const build = asRecord(version?.build_summary); const summary = liveSummary ?? data.summary; const cards = summary?.cards; const attachmentSummary = asRecord(summary?.attachments); const delivery = getDeliverySection(version); const rateLimit = asRecord(delivery.rate_limit); const imapAppend = asRecord(delivery.imap_append_sent); const [busy, setBusy] = useState(""); const [message, setMessage] = useState(""); const [messageReviewComplete, setMessageReviewComplete] = useState(false); const [builtReviewRows, setBuiltReviewRows] = useState[]>([]); const [reviewJobs, setReviewJobs] = useState(() => emptyCampaignJobsResponse()); const reviewJobsRef = useRef(emptyCampaignJobsResponse()); const jobPageCursorsRef = useRef>>({}); const [reviewPage, setReviewPage] = useState(1); const [showAllReviewJobs, setShowAllReviewJobs] = useState(false); const [jobsLoadedKey, setJobsLoadedKey] = useState(""); const [reviewedMessageKeys, setReviewedMessageKeys] = useState>(() => new Set()); const [newlyReviewedRequiredKeys, setNewlyReviewedRequiredKeys] = useState>(() => new Set()); const [selectedBuiltIndex, setSelectedBuiltIndex] = useState(null); const [mockResult, setMockResult] = useState | null>(null); const [mockClearFirst, setMockClearFirst] = useState(true); const [mockAppendSent, setMockAppendSent] = useState(true); const [selectedMockMessage, setSelectedMockMessage] = useState(null); const [reviewConfirmOpen, setReviewConfirmOpen] = useState(false); const [dryRun, setDryRun] = useState(false); const [sendConfirmOpen, setSendConfirmOpen] = useState(false); const [sendResult, setSendResult] = useState | null>(null); const [imapAppendResult, setImapAppendResult] = useState | null>(null); const [imapDiagnostics, setImapDiagnostics] = useState(() => emptyCampaignJobsResponse()); const imapDiagnosticsRef = useRef(emptyCampaignJobsResponse()); const [selectedDeliveryJobDetail, setSelectedDeliveryJobDetail] = useState | null>(null); const persistedReview = storedMessageReviewState(version); const persistedReviewKey = `${persistedReview.buildToken}|${persistedReview.inspectionComplete ? "1" : "0"}|${persistedReview.reviewedMessageKeys.join(",")}`; useEffect(() => { setBuiltReviewRows([]); setReviewJobs(emptyCampaignJobsResponse()); reviewJobsRef.current = emptyCampaignJobsResponse(); setReviewPage(1); setJobsLoadedKey(""); setNewlyReviewedRequiredKeys(new Set()); setSelectedBuiltIndex(null); setMockResult(null); setSelectedMockMessage(null); setSendResult(null); setImapAppendResult(null); setImapDiagnostics(emptyCampaignJobsResponse()); imapDiagnosticsRef.current = emptyCampaignJobsResponse(); setSelectedDeliveryJobDetail(null); setSendConfirmOpen(false); setLiveSummary(null); resetDeltaWatermark(); }, [version?.id, resetDeltaWatermark]); useEffect(() => { setMessageReviewComplete(persistedReview.inspectionComplete); setReviewedMessageKeys(new Set(persistedReview.reviewedMessageKeys)); }, [version?.id, persistedReviewKey]); useEffect(() => { if (!mockMailboxPreviewActive) setSelectedMockMessage(null); }, [mockMailboxPreviewActive]); const validationPresent = Object.keys(validation).length > 0; const validationOk = validation.ok === true; const validationErrors = numberFrom(validation, ["error_count", "errors", "blocked"]); const validationWarnings = numberFrom(validation, ["warning_count", "warnings"]); const validationIssues = asArray(validation.issues).map(asRecord); const visibleValidationIssues = validationIssues.slice(0, 10); const readyForDelivery = isVersionReadyForDelivery(version); const validationStale = validationOk && !readyForDelivery; const buildPresent = Object.keys(build).length > 0; const builtCount = numberFrom(build, ["built_count", "ready_count", "built", "messages_built"]); const buildBlocked = numberFrom(build, ["blocked_count", "blocked"]); const buildNeedsReview = numberFrom(build, ["needs_review_count", "needs_review"]); const buildWarnings = numberFrom(build, ["warning_count", "warnings"]); const hasBuild = buildPresent && (builtCount > 0 || version?.workflow_state === "built"); useEffect(() => { if (!version?.id || !hasBuild) return; const expectedKey = reviewJobsDeltaKey(version.id, reviewPage, showAllReviewJobs); if (jobsLoadedKey === expectedKey) return; void loadBuiltMessages(true, reviewPage); }, [version?.id, hasBuild, reviewPage, showAllReviewJobs, jobsLoadedKey, campaignId, settings.apiBaseUrl, settings.apiKey, settings.accessToken]); const statusCounts = asRecord(summary?.status_counts); const sendStatusCounts = asRecord(statusCounts.send); const imapStatusCounts = asRecord(statusCounts.imap); const attempts = asRecord(summary?.attempts); const summaryDelivery = asRecord(summary?.delivery); const queuedSendCount = numberFrom(sendStatusCounts, ["queued"]); const claimedSendCount = numberFrom(sendStatusCounts, ["claimed"]); const sendingSendCount = numberFrom(sendStatusCounts, ["sending"]); const activeSendCount = claimedSendCount + sendingSendCount; const queuedOrActiveCount = cards?.queued_or_active ?? queuedSendCount + activeSendCount; const notQueuedCount = cards?.not_attempted ?? numberFrom(sendStatusCounts, ["not_queued"]); const cancelledCount = cards?.cancelled ?? numberFrom(sendStatusCounts, ["cancelled"]); const outcomeUnknownCount = cards?.outcome_unknown ?? numberFrom(sendStatusCounts, ["outcome_unknown"]); const sendAttemptCount = numberFrom(attempts, ["send_attempts"]); const backgroundWorkersEnabled = summaryDelivery.background_workers_enabled === true || summaryDelivery.celery_enabled === true; const backgroundWorkersDisabled = summaryDelivery.background_workers_enabled === false || summaryDelivery.celery_enabled === false; const recentFailures = asArray(summary?.recent_failures).map(asRecord).slice(0, 5); const jobsTotal = cards?.jobs_total ?? inlineEntries.filter((entry) => entry.active !== false).length; const sentCount = cards?.sent ?? 0; const failedCount = cards?.failed ?? 0; const imapAppended = cards?.imap_appended ?? 0; const imapFailed = cards?.imap_failed ?? 0; const imapPending = numberFrom(imapStatusCounts, ["pending"]); const imapSkipped = numberFrom(imapStatusCounts, ["skipped"]); const deliveryHasTerminalOutcome = sentCount + failedCount + outcomeUnknownCount + cancelledCount > 0; const currentWorkflowState = (version?.workflow_state ?? "").toLowerCase(); const deliverySending = activeSendCount > 0 || currentWorkflowState === "sending" && queuedOrActiveCount > 0; const deliveryQueued = queuedOrActiveCount > 0 || currentWorkflowState === "queued" && !deliveryHasTerminalOutcome; const deliveryStarted = deliverySending || deliveryHasTerminalOutcome || ["sent", "completed", "partially_completed", "outcome_unknown", "failed"].includes(currentWorkflowState); const queuedWithoutWorker = backgroundWorkersDisabled && queuedSendCount > 0 && activeSendCount === 0; const directQueuedSendAllowed = queuedWithoutWorker && !deliveryStarted; const selectedDryRun = dryRun && !directQueuedSendAllowed; const deliveryPartial = cards?.partially_completed === true || ["partially_sent", "failed_partial", "partially_completed"].includes(currentWorkflowState); const deliveryComplete = queuedOrActiveCount === 0 && sentCount > 0 && failedCount === 0 && outcomeUnknownCount === 0 && cancelledCount === 0 && cards?.partially_completed !== true; const deliveryDanger = failedCount > 0 || outcomeUnknownCount > 0 || ["outcome_unknown", "failed"].includes(currentWorkflowState); const deliveryDisplayStatus = deliverySending ? "sending" : deliveryQueued ? "queued" : deliveryPartial ? "partially_completed" : deliveryComplete ? "completed" : deliveryDanger ? failedCount > 0 ? "failed" : "outcome_unknown" : data.campaign?.status ?? version?.workflow_state ?? "not_started"; const queueStatusNote = queuedWithoutWorker ? "i18n:govoplan-campaign.queued_jobs_are_waiting_in_the_database_but_back.5b1fadfd" : deliverySending ? "i18n:govoplan-campaign.delivery_is_being_processed_this_page_refreshes_.fe83676e" : deliveryQueued && backgroundWorkersEnabled ? "i18n:govoplan-campaign.queued_jobs_are_waiting_for_a_background_deliver.cc703afb" : deliveryQueued ? "i18n:govoplan-campaign.queued_jobs_are_waiting_if_the_counts_do_not_cha.7d2164d6" : ""; const queueStatusTone = queuedWithoutWorker ? "is-warning" : deliveryQueued || deliverySending ? "is-stale" : ""; const historicalVersion = isHistoricalCampaignVersion(version, data.campaign?.current_version_id); const finalVersion = isFinalLockedVersion(version); const userLockedVersion = isUserLockedVersion(version); const readOnlyVersion = historicalVersion || userLockedVersion || finalVersion; const refreshQueueStatus = useCallback(async (silent = true) => { if (!version?.id) return; setQueueStatusLoading(true); if (!silent) setMessage("i18n:govoplan-campaign.refreshing_queue_status.2a7dea57"); setError(""); try { const result = await getCampaignSummary(settings, campaignId, version.id); setLiveSummary(result); if (!silent) setMessage("i18n:govoplan-campaign.queue_status_refreshed.253e2e5b"); } catch (err) { if (!silent) setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setQueueStatusLoading(false); } }, [campaignId, setError, settings, version?.id]); useEffect(() => { if (!(deliveryQueued || deliverySending) || loading || queueStatusLoading || busy === "send") return; const handle = window.setTimeout(() => {void refreshQueueStatus(true);}, 3000); return () => window.clearTimeout(handle); }, [deliveryQueued, deliverySending, loading, queueStatusLoading, busy, refreshQueueStatus]); const selectedBuiltMessage = selectedBuiltIndex === null ? null : builtReviewRows[selectedBuiltIndex] ?? null; const reviewMetadata = reviewJobs.review ?? {}; const blockingReviewCount = Number(reviewMetadata.blocking_count ?? 0); const explicitReviewCount = Number(reviewMetadata.required_count ?? 0); const bulkAcceptableCount = Number(reviewMetadata.bulk_acceptable_count ?? 0); const reviewedExplicitCount = Math.min( explicitReviewCount, Number(reviewMetadata.reviewed_required_count ?? 0) + newlyReviewedRequiredKeys.size ); const reviewRequiredCount = explicitReviewCount + bulkAcceptableCount; const reviewedRequiredCount = reviewedExplicitCount; const automaticInspectionComplete = reviewJobs.total_unfiltered > 0 && blockingReviewCount === 0 && reviewRequiredCount === 0; const mockSend = asRecord(mockResult?.send); const mockSent = numberFrom(mockSend, ["sent_count", "attempted_count"]); const mockFailed = numberFrom(mockSend, ["failed_count"]); const mockSkipped = numberFrom(mockSend, ["skipped_count"]); const mockComplete = Boolean(mockResult) && mockSent > 0 && mockFailed === 0 && mockSkipped === 0; const mockPartial = Boolean(mockResult) && mockSent > 0 && (mockFailed > 0 || mockSkipped > 0); const mockRows = asArray(mockSend.results).map(asRecord); const mockMailbox = asRecord(mockResult?.mailbox); const mockMailboxMessages = asArray(mockMailbox.messages).map(asRecord); const sendResultRows = asArray(sendResult?.results).map(asRecord); const imapAppendResultRows = asArray(imapAppendResult?.results).map(asRecord); const imapDiagnosticRows = imapDiagnostics.jobs.map(asRecord); const imapDiagnosticsPending = imapDiagnosticRows.filter((job) => String(job.imap_status ?? "").toLowerCase() === "pending").length; const imapDiagnosticsFailed = imapDiagnosticRows.filter((job) => String(job.imap_status ?? "").toLowerCase() === "failed").length; const imapPendingForDisplay = Math.max(imapPending, imapDiagnosticsPending); const imapFailedForDisplay = Math.max(imapFailed, imapDiagnosticsFailed); const canAppendPendingImap = Boolean(imapAppend.enabled) && imapPendingForDisplay > 0 && !historicalVersion && !userLockedVersion; const validationReviewState: FlowState = busy === "validate" ? "running" : validationStale ? "stale" : validationPresent && !validationOk ? "danger" : readyForDelivery && (validationWarnings > 0 || (cards?.needs_attention ?? 0) > 0) ? "warning" : readyForDelivery ? "complete" : "active"; const downstreamDeliveryActivity = deliveryQueued || deliveryStarted; const inspectionSatisfied = automaticInspectionComplete || messageReviewComplete || downstreamDeliveryActivity; const buildReviewState: FlowState = !readyForDelivery ? "locked" : busy === "build" || busy === "inspect" ? "running" : hasBuild && buildBlocked > 0 ? "danger" : hasBuild && (buildNeedsReview > 0 || buildWarnings > 0 || !inspectionSatisfied) ? "warning" : hasBuild && inspectionSatisfied ? "complete" : "active"; const mockState: FlowState = !mockWorkflowAvailable ? "locked" : !inspectionSatisfied ? "locked" : busy === "mock" || busy === "mailbox" ? "running" : mockPartial ? "partial" : mockFailed > 0 ? "danger" : mockComplete ? "complete" : downstreamDeliveryActivity ? "warning" : "active"; const mockStateDisplayLabel = !mockWorkflowAvailable ? "i18n:govoplan-campaign.unavailable.2c9c1f79" : !mockVerificationRequired ? "i18n:govoplan-campaign.optional.0c6c4102" : stateLabel(mockState); const mockGateSatisfied = inspectionSatisfied && (!mockWorkflowRequired || mockComplete || downstreamDeliveryActivity); const sendLockReason = !inspectionSatisfied ? "i18n:govoplan-campaign.build_and_complete_the_required_message_review_f.c0cd00fe" : mockWorkflowRequired ? "i18n:govoplan-campaign.complete_a_successful_mock_delivery_first.bc2a587d" : "i18n:govoplan-campaign.build_the_exact_queue_first.98d7ce1b"; const sendState: FlowState = !mockGateSatisfied ? "locked" : busy === "send" || deliverySending ? "running" : queuedWithoutWorker ? "warning" : deliveryQueued ? "running" : deliveryPartial ? "partial" : deliveryComplete || ["sent", "completed"].includes(currentWorkflowState) ? "complete" : deliveryDanger ? "danger" : "active"; const resultState: FlowState = !deliveryStarted ? "locked" : deliverySending ? "running" : deliveryPartial ? "partial" : deliveryComplete || ["sent", "completed"].includes(currentWorkflowState) ? "complete" : deliveryDanger ? "danger" : "active"; const stages: FlowStageDefinition[] = useMemo(() => [ { id: "workflow-validate-review", title: "i18n:govoplan-campaign.validate_and_inspect.b617b9b2", shortTitle: "i18n:govoplan-campaign.validate.6752f198", description: "i18n:govoplan-campaign.lock_and_validate_the_campaign_then_inspect_bloc.e22b9266", icon: ShieldCheck, state: validationReviewState, stateLabel: stateLabel(validationReviewState) }, { id: "workflow-build-review", title: "i18n:govoplan-campaign.build_and_review.635177f0", shortTitle: "i18n:govoplan-campaign.build.bbd80cf7", description: "i18n:govoplan-campaign.build_the_exact_queue_resolve_recipient_values_a.d86446cb", icon: PackageCheck, state: buildReviewState, stateLabel: stateLabel(buildReviewState), lockReason: validationErrors > 0 ? `Resolve ${validationErrors} blocking validation issue${validationErrors === 1 ? "" : "s"} first.` : "i18n:govoplan-campaign.lock_and_validate_the_current_working_version_fi.e824704f" }, { id: "workflow-mock-verify", title: "i18n:govoplan-campaign.mock_send_and_verify.03ec38d0", shortTitle: "i18n:govoplan-campaign.mock_send.37a5f500", description: "i18n:govoplan-campaign.exercise_the_delivery_path_and_verify_recipient_.12ed1928", icon: FlaskConical, state: mockState, stateLabel: mockStateDisplayLabel, lockReason: mockWorkflowAvailable ? "i18n:govoplan-campaign.build_and_complete_the_required_message_review_f.c0cd00fe" : "i18n:govoplan-campaign.enable_the_mail_dev_mailbox_capability_to_run_mo.4f9a506d" }, { id: "workflow-send", title: "i18n:govoplan-campaign.confirm_and_send.fe43b726", shortTitle: "i18n:govoplan-campaign.send.9bc2575c", description: "i18n:govoplan-campaign.review_the_final_execution_summary_optionally_ru.6b32b00a", icon: Send, state: sendState, stateLabel: stateLabel(sendState), lockReason: sendLockReason }, { id: "workflow-results", title: "i18n:govoplan-campaign.delivery_results.b5d7d1cb", shortTitle: "i18n:govoplan-campaign.results.612e12d2", description: "i18n:govoplan-campaign.review_smtp_outcomes_imap_append_results_partial.a0d4e83e", icon: BarChart3, state: resultState, stateLabel: stateLabel(resultState), lockReason: "i18n:govoplan-campaign.delivery_results_become_available_after_the_real.4a86ab77" }], [ validationReviewState, buildReviewState, mockState, sendState, resultState, validationErrors, mockStateDisplayLabel, mockWorkflowAvailable, sendLockReason] ); function reviewJobsDeltaKey(versionId: string, targetPage: number, includeAll: boolean): string { return JSON.stringify({ scope: "review-jobs", campaignId, versionId, page: targetPage, pageSize: 50, includeAll, apiBaseUrl: settings.apiBaseUrl, apiKey: settings.apiKey, accessToken: settings.accessToken }); } function reviewJobsCursorScopeKey(versionId: string, includeAll: boolean): string { return JSON.stringify({ scope: "review-jobs", campaignId, versionId, pageSize: 50, includeAll, validationStatus: includeAll ? [] : MESSAGE_REVIEW_ISSUE_STATUSES }); } function imapDiagnosticsDeltaKey(versionId: string): string { return JSON.stringify({ scope: "imap-diagnostics", campaignId, versionId, page: 1, pageSize: 50, imapStatus: ["pending", "failed"], apiBaseUrl: settings.apiBaseUrl, apiKey: settings.apiKey, accessToken: settings.accessToken }); } async function fetchJobsDelta(key: string, current: CampaignJobsResponse, options: CampaignJobsQuery, cursorScopeKey?: string): Promise { let nextWatermark = getDeltaWatermark(key); let merged = current; let hasMore = false; const page = options.page ?? 1; const cursorMap = cursorScopeKey ? jobPageCursorsRef.current[cursorScopeKey] ?? { 1: null } : undefined; const pageCursor = cursorMap ? page === 1 ? null : cursorMap[page] : undefined; do { const response = await getCampaignJobsDelta(settings, campaignId, { ...options, cursor: pageCursor, since: nextWatermark }); merged = mergeCampaignJobsDelta(merged, response); if (cursorScopeKey) { const nextCursorMap = jobPageCursorsRef.current[cursorScopeKey] ?? { 1: null }; if (response.cursor !== undefined) nextCursorMap[page] = response.cursor ?? null; if (response.next_cursor !== undefined) { if (response.next_cursor) nextCursorMap[page + 1] = response.next_cursor; else delete nextCursorMap[page + 1]; } jobPageCursorsRef.current[cursorScopeKey] = nextCursorMap; } nextWatermark = response.watermark ?? null; hasMore = response.has_more; } while (hasMore); setDeltaWatermark(key, nextWatermark); return merged; } async function runValidation() { if (!version || busy || readOnlyVersion || readyForDelivery) return; setBusy("validate"); setMessage("i18n:govoplan-campaign.validating_the_campaign_including_managed_attach.b133cfd9"); setError(""); try { const result = await validateVersion(settings, version.id, true); setMessage(result.ok ? "i18n:govoplan-campaign.validation_passed.c3e25768" : "i18n:govoplan-campaign.validation_finished_with_issues_review_the_excep.be65d2a1"); resetDownstreamReview(); await reload(); } catch (err) { setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function runBuild() { if (!version || busy || readOnlyVersion || !readyForDelivery || deliveryQueued || deliveryStarted) return; setBusy("build"); setMessage("i18n:govoplan-campaign.building_exact_messages_and_resolving_managed_at.7f24ee41"); setError(""); try { const result = await buildVersion(settings, version.id, true); const reviewKey = reviewJobsDeltaKey(version.id, 1, showAllReviewJobs); const reviewCursorKey = reviewJobsCursorScopeKey(version.id, showAllReviewJobs); resetDeltaWatermark(reviewKey); jobPageCursorsRef.current[reviewCursorKey] = { 1: null }; reviewJobsRef.current = emptyCampaignJobsResponse(); const review = await fetchJobsDelta(reviewKey, reviewJobsRef.current, { versionId: version.id, page: 1, pageSize: 50, validationStatus: showAllReviewJobs ? undefined : MESSAGE_REVIEW_ISSUE_STATUSES }, reviewCursorKey); setReviewPage(1); reviewJobsRef.current = review; setReviewJobs(review); setBuiltReviewRows(review.jobs.map(asRecord)); setJobsLoadedKey(reviewKey); setMessage(i18nMessage("i18n:govoplan-campaign.build_finished_built_value_message_s_the_message.e1ec0296", { value0: String(result.built_count ?? result.ready_count ?? "—") })); setMessageReviewComplete(false); setReviewedMessageKeys(new Set()); setMockResult(null); setSelectedMockMessage(null); await reload(); } catch (err) { setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function loadBuiltMessages(silent = false, targetPage = reviewPage) { if (!version || busy || !hasBuild) return; setBusy("inspect"); if (!silent) setMessage("i18n:govoplan-campaign.loading_the_built_message_review.a5339588"); setError(""); try { const reviewKey = reviewJobsDeltaKey(version.id, targetPage, showAllReviewJobs); const reviewCursorKey = reviewJobsCursorScopeKey(version.id, showAllReviewJobs); const current = jobsLoadedKey === reviewKey ? reviewJobsRef.current : emptyCampaignJobsResponse(); if (jobsLoadedKey !== reviewKey) resetDeltaWatermark(reviewKey); const result = await fetchJobsDelta(reviewKey, current, { versionId: version.id, page: targetPage, pageSize: 50, validationStatus: showAllReviewJobs ? undefined : MESSAGE_REVIEW_ISSUE_STATUSES }, reviewCursorKey); const jobs = result.jobs.map(asRecord); reviewJobsRef.current = result; setReviewJobs(result); setBuiltReviewRows(jobs); setJobsLoadedKey(reviewKey); if (result.pages > 0 && targetPage > result.pages) setReviewPage(result.pages); if (!silent) setMessage(i18nMessage("i18n:govoplan-campaign.loaded_value_message_s_on_page_value_of_value.febbeb1e", { value0: jobs.length, value1: result.page, value2: result.pages || 1 })); } catch (err) { if (!silent) setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function runMockSend() { if (!version || busy || !mockWorkflowAvailable || readOnlyVersion || !inspectionSatisfied || deliveryQueued || deliveryStarted) return; setBusy("mock"); setMessage("i18n:govoplan-campaign.running_the_complete_mock_delivery_flow.3d0f6cd8"); setError(""); setSelectedMockMessage(null); try { const response = await mockSendCampaign(settings, campaignId, { version_id: version.id, send: true, include_warnings: true, include_needs_review: false, append_sent: mockAppendSent, clear_mailbox: mockClearFirst, check_files: true }); const result = asRecord(response.result ?? response); setMockResult(result); const sendResult = asRecord(result.send); setMessage(i18nMessage("i18n:govoplan-campaign.mock_delivery_finished_captured_value_message_s_.ac23dedf", { value0: String(sendResult.sent_count ?? 0), value1: String(sendResult.failed_count ?? 0), value2: String(sendResult.skipped_count ?? 0) })); } catch (err) { setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function runSendNow() { if (!version || busy || readOnlyVersion || !readyForDelivery || !hasBuild || !mockGateSatisfied || deliveryQueued && !directQueuedSendAllowed || deliveryStarted) return; const effectiveDryRun = dryRun && !directQueuedSendAllowed; setBusy("send"); setMessage(effectiveDryRun ? "i18n:govoplan-campaign.checking_the_built_queue_without_sending.e15bb876" : directQueuedSendAllowed ? "i18n:govoplan-campaign.sending_the_queued_jobs_synchronously.6162f750" : "i18n:govoplan-campaign.sending_the_locked_campaign_version.061ee1bb"); setSendResult(null); setError(""); try { const response = await sendCampaignNow(settings, campaignId, { version_id: version.id, include_warnings: true, check_files: false, validate_before_send: false, build_before_send: false, dry_run: effectiveDryRun, use_rate_limit: true, enqueue_imap_task: false }); const result = asRecord(response.result ?? response); setSendResult(result); const sent = result.sent_count ?? 0; const failed = result.failed_count ?? 0; const unknown = result.outcome_unknown_count ?? 0; setMessage( effectiveDryRun ? "i18n:govoplan-campaign.dry_run_finished_no_message_was_sent.c026c6cd" : `Send finished. SMTP accepted ${String(sent)} message(s), failed ${String(failed)}, outcome unknown ${String(unknown)}.` ); setSendConfirmOpen(false); await reload(); } catch (err) { setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function runAppendSent() { if (!version || busy || !canAppendPendingImap) return; const runInline = !backgroundWorkersEnabled; setBusy("imap"); setError(""); setMessage(runInline ? "i18n:govoplan-campaign.appending_pending_sent_copies_via_imap.3f0daf68" : "i18n:govoplan-campaign.queueing_pending_imap_append_jobs.31dd388e"); try { const response = await appendSent(settings, campaignId, { enqueue_celery: backgroundWorkersEnabled, run_inline: runInline, dry_run: false }); const result = asRecord(response.result ?? response); setImapAppendResult(result); const appended = numberFrom(result, ["appended_count"]); const failed = numberFrom(result, ["failed_count"]); const enqueued = numberFrom(result, ["enqueued_count"]); const pending = numberFrom(result, ["pending_count"]); setMessage(runInline ? `IMAP append processed ${pending} pending job(s): appended ${appended}, failed ${failed}.` : `Queued ${enqueued} pending IMAP append job(s).`); await refreshQueueStatus(true); await loadImapDiagnostics(true); } catch (err) { setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function loadImapDiagnostics(silent = false) { if (!version?.id) return; setBusy("inspect"); if (!silent) setMessage("i18n:govoplan-campaign.loading_imap_diagnostics.a5b00c0e"); setError(""); try { const diagnosticsKey = imapDiagnosticsDeltaKey(version.id); const result = await fetchJobsDelta(diagnosticsKey, imapDiagnosticsRef.current, { versionId: version.id, page: 1, pageSize: 50, imapStatus: ["pending", "failed"] }); imapDiagnosticsRef.current = result; setImapDiagnostics(result); if (!silent) setMessage(i18nMessage("i18n:govoplan-campaign.loaded_value_pending_failed_imap_job_s.39b1b268", { value0: result.total })); } catch (err) { if (!silent) setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function openDeliveryJobDetail(jobId: string) { if (!jobId || busy) return; setBusy("inspect"); setError(""); try { const detail = await getCampaignJobDetail(settings, campaignId, jobId); setSelectedDeliveryJobDetail(detail as unknown as Record); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function completeInspection(_acceptBulk = false) { if (!version || busy || readOnlyVersion || automaticInspectionComplete || !canCompleteInspection || downstreamDeliveryActivity) return; setBusy("inspect"); setError(""); setMessage("i18n:govoplan-campaign.recording_the_completed_message_review.16f58b81"); try { const reviewed = new Set(reviewedMessageKeys); await updateCampaignReviewState(settings, campaignId, version.id, { inspection_complete: true, reviewed_message_keys: [...reviewed] }); setReviewedMessageKeys(reviewed); setNewlyReviewedRequiredKeys(new Set()); setMessageReviewComplete(true); setReviewJobs((current) => { const updated = { ...current, review: { ...current.review, inspection_complete: true, reviewed_required_count: explicitReviewCount } }; reviewJobsRef.current = updated; return updated; }); setReviewConfirmOpen(false); setMessage("i18n:govoplan-campaign.message_review_completed_and_recorded_for_this_b.452d87de"); await reload(); } catch (err) { setMessage(""); setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function openMockMessage(id: string) { if (!id || busy === "mailbox" || !mockMailboxPreviewActive) return; setBusy("mailbox"); setError(""); try { const response = await getMockMailboxMessage(settings, id); setSelectedMockMessage(response.message); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function openBuiltMessage(index: number) { const row = builtReviewRows[index]; if (!row) return; const jobId = String(row.id ?? ""); const reviewKey = String(row.review_key ?? builtMessageKey(row, index)); setReviewedMessageKeys((current) => { const next = new Set(current); next.add(reviewKey); return next; }); if (messageNeedsExplicitReview(row) && row.reviewed !== true) { setNewlyReviewedRequiredKeys((current) => new Set(current).add(reviewKey)); } if (!jobId || row.resolved_recipients || row.attachments || row.issues) { setSelectedBuiltIndex(index); return; } setBusy("inspect"); setError(""); try { const detail = await getCampaignJobDetail(settings, campaignId, jobId); setBuiltReviewRows((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, ...detail.job, review_key: item.review_key, reviewed: item.reviewed, attempts: detail.attempts } : item)); setSelectedBuiltIndex(index); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } function resetDownstreamReview() { setMessageReviewComplete(false); setBuiltReviewRows([]); setReviewJobs(emptyCampaignJobsResponse()); reviewJobsRef.current = emptyCampaignJobsResponse(); setReviewPage(1); setJobsLoadedKey(""); setReviewedMessageKeys(new Set()); setNewlyReviewedRequiredKeys(new Set()); setSelectedBuiltIndex(null); setMockResult(null); setSelectedMockMessage(null); resetDeltaWatermark(); } function scrollToStage(id: string) { document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" }); } const matchedAttachments = numberFrom(attachmentSummary, ["total_matched_files"]); const missingAttachments = numberFrom(attachmentSummary, ["missing_configs"]); const ambiguousAttachments = numberFrom(attachmentSummary, ["ambiguous_configs"]); const messagesPerMinute = numberFrom(rateLimit, ["messages_per_minute"]); const estimatedMinutes = messagesPerMinute > 0 && jobsTotal > 0 ? Math.ceil(jobsTotal / messagesPerMinute) : null; const smtpConfigured = Boolean(selectedMailProfileId || getText(smtpServer, "host") || getText(smtpCredentials, "username")); const imapConfigured = Boolean(selectedMailProfileId || getText(imapServer, "host") || getText(imapCredentials, "username")); const deliverabilityPreflightItems: DeliverabilityPreflightItem[] = [ { label: "Transport", detail: smtpConfigured ? selectedMailProfileId ? "Reusable mail profile selected." : "Inline SMTP settings are present." : "Select a reusable mail profile or configure SMTP before live delivery.", state: smtpConfigured ? "ready" : "blocked" }, { label: "Policy", detail: validationPresent ? validationErrors > 0 ? "Validation still reports blocking policy or data issues." : "Latest validation has no blocking issue." : "Run validation to evaluate effective mail and attachment policy.", state: validationPresent ? validationErrors > 0 ? "blocked" : validationWarnings > 0 ? "warning" : "ready" : "warning" }, { label: "Messages", detail: hasBuild ? inspectionSatisfied ? "Built messages are reviewed or did not require manual review." : "Built messages still need review before live delivery." : "Build exact messages before sending.", state: hasBuild ? inspectionSatisfied ? "ready" : "warning" : "blocked" }, { label: "Attachments", detail: missingAttachments + ambiguousAttachments === 0 ? "No missing or ambiguous attachment matches in the current summary." : `${missingAttachments} missing and ${ambiguousAttachments} ambiguous attachment match(es).`, state: missingAttachments + ambiguousAttachments === 0 ? "ready" : "blocked" }, { label: "Rate limit", detail: messagesPerMinute > 0 ? `${messagesPerMinute} message(s) per minute; estimated minimum duration ${estimatedMinutes ?? "unknown"} minute(s).` : "No explicit rate limit is configured for this execution.", state: messagesPerMinute > 0 ? "ready" : "warning" }, { label: "Sent copy", detail: Boolean(imapAppend.enabled) ? imapConfigured ? `IMAP append enabled for ${String(imapAppend.folder ?? "auto")}.` : "IMAP append is enabled, but no IMAP server/profile is visible." : "IMAP append is disabled for this campaign.", state: Boolean(imapAppend.enabled) ? imapConfigured ? "ready" : "blocked" : "info" }]; const canCompleteInspection = blockingReviewCount === 0 && reviewRequiredCount > 0 && reviewedExplicitCount === explicitReviewCount; return (
i18n:govoplan-campaign.review_send.1627617d
{error && {error}} {message && {message}} {version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion) && }
{validationStale &&

i18n:govoplan-campaign.the_stored_validation_result_is_no_longer_an_act.45d2dd14

} {validationPresent && validationErrors === 0 &&

} {validationErrors > 0 &&

i18n:govoplan-campaign.resolve_the_blocking_entries_then_validate_again.e282ae0f

} {visibleValidationIssues.length > 0 &&

i18n:govoplan-campaign.validation_details.aa503267

{visibleValidationIssues.map((issue, index) =>
{humanize(String(issue.severity ?? "issue"))}
{String(issue.message ?? issue.code ?? "i18n:govoplan-campaign.validation_issue.800cc3d0")} {issue.path ? · {String(issue.path)} : null} {issue.code ? · {String(issue.code)} : null}
)}
{validationIssues.length > visibleValidationIssues.length &&

i18n:govoplan-campaign.showing.163d8174 {visibleValidationIssues.length} of {validationIssues.length} i18n:govoplan-campaign.validation_issue_s.c6a8b911

}
}

i18n:govoplan-campaign.building_freezes_the_current_recipients_rendered.273a8170

{reviewRequiredCount > 0 && }
{automaticInspectionComplete &&

} {blockingReviewCount > 0 &&

{blockingReviewCount} i18n:govoplan-campaign.blocked_or_failed_message_s_must_be_resolved_bef.5c6b5140

} {hasBuild &&
{reviewJobs.total} i18n:govoplan-campaign.matching_of.66a3778e {reviewJobs.total_unfiltered} i18n:govoplan-campaign.built_job_s.82d49ade
i18n:govoplan-campaign.page.fb06270f {reviewJobs.pages === 0 ? 0 : reviewJobs.page} of {reviewJobs.pages}

i18n:govoplan-campaign.ready_messages_are_hidden_initially_messages_mar.1cb9770d

}
{!mockWorkflowAvailable &&

i18n:govoplan-campaign.mock_delivery_uses_the_mail_module_development_m.9d645a5f

}
{mockResult &&

i18n:govoplan-campaign.recipient_outcomes.d88abb50

String(row.entry_id ?? row.entry_index ?? index)} emptyText="i18n:govoplan-campaign.no_mock_delivery_results_were_returned.96b6736d" className="data-table-wrap data-table compact-table" />
{mockMailboxPreviewActive ?

i18n:govoplan-campaign.captured_mock_messages.5839fae1

String(row.id ?? index)} emptyText="i18n:govoplan-campaign.no_mock_messages_were_captured_in_this_run.2b6e90f0" className="data-table-wrap data-table compact-table" />
:

i18n:govoplan-campaign.captured_mock_messages.5839fae1

{mockWorkflowAvailable ? "i18n:govoplan-campaign.captured_mailbox_preview_is_disabled_for_this_ru.6559458d" : "i18n:govoplan-campaign.captured_mailbox_preview_requires_the_mail_devel.9b3d9f0d"}

}
}
i18n:govoplan-campaign.recipients.78cbf8eb{jobsTotal || "—"}
i18n:govoplan-campaign.messages_to_send.f7763bf9{builtCount || jobsTotal || "—"}
i18n:govoplan-campaign.matched_attachments.ead1eeb1{matchedAttachments || "—"}
i18n:govoplan-campaign.missing_ambiguous.fffdd3f5{missingAttachments} / {ambiguousAttachments}
i18n:govoplan-campaign.rate_limit.d08e55f5{messagesPerMinute > 0 ? i18nMessage("i18n:govoplan-campaign.value_min.c9d89eae", { value0: messagesPerMinute }) : "i18n:govoplan-campaign.not_set.93039e60"}
i18n:govoplan-campaign.minimum_duration.91a71a6d{estimatedMinutes ? i18nMessage("i18n:govoplan-campaign.about_value_min.7c2e77fc", { value0: estimatedMinutes }) : "—"}
i18n:govoplan-campaign.imap_append.8c0d9e96{Boolean(imapAppend.enabled) ? "i18n:govoplan-campaign.enabled.df174a3f" : "i18n:govoplan-campaign.disabled.f4f4473d"}
i18n:govoplan-campaign.version.2da600bf{version ? `v${version.version_number}` : "—"}
{queueStatusNote &&

{queueStatusNote}

}
i18n:govoplan-campaign.a_dry_run_checks_the_frozen_queue_and_delivery_c.c42924bb
{sendResult &&

i18n:govoplan-campaign.attempted.a9eb9c90 {String(sendResult.attempted_count ?? "—")}i18n:govoplan-campaign.smtp_accepted.a5d0dccc {String(sendResult.sent_count ?? "—")}i18n:govoplan-campaign.failed.fac9f871 {String(sendResult.failed_count ?? "—")}i18n:govoplan-campaign.outcome_unknown.4383023a {String(sendResult.outcome_unknown_count ?? 0)}i18n:govoplan-campaign.skipped.6b98496c {String(sendResult.skipped_count ?? "—")}.

{sendResultRows.length > 0 && `send-result-${index}`} emptyText="i18n:govoplan-campaign.no_send_results_returned.d1ec3e23" className="data-table-wrap data-table compact-table" /> }
}
{deliveryStarted ? "i18n:govoplan-campaign.delivery_activity_is_available_in_the_report_and.cb163d1d" : "i18n:govoplan-campaign.no_real_delivery_has_started_for_this_campaign_v.3b9235ff"}
{Boolean(imapAppend.enabled) && imapPendingForDisplay > 0 &&

i18n:govoplan-campaign.imap_sent_append_is_still_pending_for.475700e4 {imapPendingForDisplay} i18n:govoplan-campaign.job_s_pending_with_no_imap_attempt_usually_means.0776d29f

} {Boolean(imapAppend.enabled) && imapFailedForDisplay > 0 &&

{imapFailedForDisplay} i18n:govoplan-campaign.imap_append_job_s_failed_load_diagnostics_to_ins.9de2b9cd

} {Boolean(imapAppend.enabled) && (imapPendingForDisplay > 0 || imapFailedForDisplay > 0) &&
{imapPendingForDisplay > 0 && }
} {imapAppendResult &&

i18n:govoplan-campaign.imap_append_result.544fc7ed

i18n:govoplan-campaign.pending.96f608c1 {String(imapAppendResult.pending_count ?? "0")}i18n:govoplan-campaign.enqueued.d93063b1 {String(imapAppendResult.enqueued_count ?? "0")}i18n:govoplan-campaign.processed.90e0ee45 {String(imapAppendResult.processed_count ?? "0")}i18n:govoplan-campaign.appended.f15bf444 {String(imapAppendResult.appended_count ?? "0")}i18n:govoplan-campaign.failed.fac9f871 {String(imapAppendResult.failed_count ?? "0")}.

{imapAppendResultRows.length > 0 && String(row.job_id ?? index)} emptyText="i18n:govoplan-campaign.no_imap_append_result_rows_returned.13a62b2e" className="data-table-wrap data-table compact-table" /> }
} {imapDiagnostics.total > 0 &&

i18n:govoplan-campaign.imap_diagnostics.f3666fc4

String(row.id ?? index)} emptyText="i18n:govoplan-campaign.no_pending_or_failed_imap_jobs_found.eb8adfdc" className="data-table-wrap data-table compact-table" />
} {recentFailures.length > 0 &&

i18n:govoplan-campaign.recent_delivery_failures.282285fb

{recentFailures.map((failure, index) =>
{String(failure.recipient_email ?? failure.entry_id ?? i18nMessage("i18n:govoplan-campaign.value.44b8c76f", { value0: String(failure.entry_index ?? index + 1) }))}
{humanize(String(failure.send_status ?? failure.imap_status ?? "failed"))} {failure.last_error ? `: ${String(failure.last_error)}` : ""} {failure.updated_at ? · {formatDateTime(String(failure.updated_at))} : null}
)}
}
{selectedBuiltMessage && selectedBuiltIndex !== null && setSelectedBuiltIndex(null)} /> } void completeInspection(true)} onCancel={() => setReviewConfirmOpen(false)} /> setSendConfirmOpen(false)} onConfirm={() => void runSendNow()} /> {selectedDeliveryJobDetail && setSelectedDeliveryJobDetail(null)} /> } {selectedMockMessage && mockMailboxPreviewActive && setSelectedMockMessage(null)} /> }
); } function WorkflowNavigation({ stages, onSelect }: {stages: FlowStageDefinition[];onSelect: (id: string) => void;}) { return ( ); } function WorkflowStage({ stage, nextState, children }: {stage: FlowStageDefinition;nextState?: FlowState;children: React.ReactNode;}) { const Icon = stage.icon; const locked = stage.state === "locked"; const [collapsed, setCollapsed] = useState(false); const style = { "--review-stage-color": stateColors[stage.state], "--review-next-stage-color": stateColors[nextState ?? stage.state] } as CSSProperties; return (
); } function DeliverabilityPreflight({ items }: {items: DeliverabilityPreflightItem[];}) { return (

Deliverability preflight

Operator checks before the first live send.
{items.map((item) =>
{item.label} {item.detail}
)}
); } function BuiltMessagePreview({ campaignJson, entries, rows, index, onSelect, onClose }: {campaignJson: Record;entries: Record[];rows: Record[];index: number;onSelect: (index: number) => void;onClose: () => void;}) { const row = rows[index] ?? {}; const entryIndex = Math.max(0, numberFrom(row, ["entry_index"]) - 1); const entry = entries[entryIndex] ?? {}; const template = asRecord(campaignJson.template); const context = buildTemplatePreviewContext(campaignJson, entry); const ignoreEmptyFields = getBool(asRecord(campaignJson.validation_policy), "ignore_empty_fields", false); const html = renderTemplatePreviewText(getText(template, "html"), context, ignoreEmptyFields); const text = renderTemplatePreviewText(getText(template, "text"), context, ignoreEmptyFields); const subject = String(row.subject || renderTemplatePreviewText(getText(template, "subject"), context, ignoreEmptyFields) || "i18n:govoplan-campaign.no_subject.7b4e8035"); const issues = asArray(row.issues).map(asRecord); const resolvedRecipients = asRecord(row.resolved_recipients); return ( 0 ? `${issues.length} issue${issues.length === 1 ? "" : "s"}: ${issues.map((issue) => String(issue.message ?? issue.code ?? "i18n:govoplan-campaign.issue.73781a12")).join(" · ")}` : "i18n:govoplan-campaign.built_without_reported_issues.99c1f1a6"} metaItems={builtMessageMetaItems(row)} attachments={builtMessageAttachments(row)} navigation={{ index, total: rows.length, onFirst: () => onSelect(0), onPrevious: () => onSelect(Math.max(0, index - 1)), onNext: () => onSelect(Math.min(rows.length - 1, index + 1)), onLast: () => onSelect(rows.length - 1) }} onClose={onClose} />); } function DeliveryJobDetailOverlay({ detail, onClose }: {detail: Record;onClose: () => void;}) { const job = asRecord(detail.job); const attempts = asRecord(detail.attempts); const smtpAttempts = asArray(attempts.smtp).map(asRecord); const imapAttempts = asArray(attempts.imap).map(asRecord); const issues = asArray(job.issues).map(asRecord); return ( i18n:govoplan-campaign.close.bbfa773e}>
i18n:govoplan-campaign.recipient.90343260
{formatAddressList(asRecord(job.resolved_recipients).to) || String(job.recipient_email ?? "-")}
i18n:govoplan-campaign.subject.8d183dbd
{String(job.subject ?? "-")}
i18n:govoplan-campaign.smtp_status.6211cb2b
{humanize(String(job.send_status ?? "-"))}
i18n:govoplan-campaign.imap_status.dbc2e430
{humanize(String(job.imap_status ?? "-"))}
{job.last_error ?
i18n:govoplan-campaign.last_error.5e4df866
{String(job.last_error)}
: null}
{issues.length > 0 && }
); } function AttemptList({ title, rows, emptyText = "i18n:govoplan-campaign.no_rows.fdbeab75" }: {title: string;rows: Record[];emptyText?: string;}) { return (

{title}

{rows.length === 0 ?

{emptyText}

:
{rows.map((row, index) =>
{String(row.status ?? row.severity ?? row.code ?? i18nMessage("i18n:govoplan-campaign.value.44b8c76f", { value0: index + 1 }))}
{String(row.message ?? row.error_message ?? row.smtp_response ?? row.folder ?? row.path ?? "-")} {row.started_at || row.created_at ? · {formatDateTime(String(row.started_at ?? row.created_at))} : null} {row.finished_at || row.updated_at ? → {formatDateTime(String(row.finished_at ?? row.updated_at))} : null}
)}
}
); } function WorkflowFact({ label, value }: {label: string;value: React.ReactNode;}) { return (
{label} {value}
); } function builtMessageColumns( openMessage: (index: number) => void, reviewedKeys: Set) : DataGridColumn>[] { return [ { id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", value: (row, index) => Number(row.entry_index ?? index + 1) }, { id: "recipient", header: "i18n:govoplan-campaign.recipient.90343260", width: 250, resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(asRecord(row.resolved_recipients).to) || String(row.recipient_email ?? "—") }, { 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: MESSAGE_VALIDATION_OPTIONS, display: "pill" }, value: (row) => String(row.validation_status ?? "unknown") }, { id: "attachments", header: "i18n:govoplan-campaign.attachments.6771ade6", width: 125, sortable: true, filterable: true, filterType: "integer", align: "right", value: (row) => Number(row.attachment_count ?? countResolvedAttachments(row.attachments)) }, { id: "reviewed", header: "i18n:govoplan-campaign.reviewed.31ef8593", width: 110, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "yes", label: "i18n:govoplan-campaign.reviewed.31ef8593" }, { value: "no", label: "i18n:govoplan-campaign.not_reviewed.0a0e3cff" }] }, render: (row, index) => row.reviewed === true || reviewedKeys.has(String(row.review_key ?? builtMessageKey(row, index))) ? : , value: (row, index) => row.reviewed === true || reviewedKeys.has(String(row.review_key ?? builtMessageKey(row, index))) ? "yes" : "no" }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 110, sticky: "end", render: (_row, index) => }]; } function mockSendResultColumns(): DataGridColumn>[] { const options: DataGridListOption[] = [ { value: "sent", label: "i18n:govoplan-campaign.sent.35f49dcf" }, { value: "failed", label: "i18n:govoplan-campaign.failed.09fef5d8" }, { value: "skipped", label: "i18n:govoplan-campaign.skipped.5a000ad7" }, { value: "ready", label: "i18n:govoplan-campaign.ready.20c7c552" }]; return [ { id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", value: (row, index) => Number(row.entry_index ?? index + 1) }, { id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options, display: "pill" }, value: (row) => String(row.status ?? "info") }, { id: "recipient", header: "i18n:govoplan-campaign.recipient.90343260", width: 250, resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(row.to) || asArray(row.envelope_recipients).join(", ") || "—" }, { id: "smtp", header: "i18n:govoplan-campaign.smtp.efff9cca", width: 190, sortable: true, filterable: true, value: (row) => String(row.smtp_message_id ?? row.status ?? "—") }, { id: "imap", header: "i18n:govoplan-campaign.imap.271f9ef2", width: 190, sortable: true, filterable: true, value: (row) => String(row.imap_message_id ?? row.imap_status ?? "—") }, { id: "message", header: "i18n:govoplan-campaign.message.68f4145f", width: "minmax(260px, 1fr)", resizable: true, filterable: true, value: (row) => String(row.message ?? "—") }]; } function sendResultColumns(): DataGridColumn>[] { const statuses: DataGridListOption[] = [ { value: "smtp_accepted", label: "i18n:govoplan-campaign.smtp_accepted.e3aa7603" }, { value: "already_accepted", label: "i18n:govoplan-campaign.already_accepted.826e68f2" }, { value: "outcome_unknown", label: "i18n:govoplan-campaign.outcome_unknown.6e929fca" }, { value: "failed", label: "i18n:govoplan-campaign.failed.09fef5d8" }, { value: "cancelled", label: "i18n:govoplan-campaign.cancelled.a1bf92ef" }, { value: "not_claimed", label: "i18n:govoplan-campaign.not_claimed.aa712c1b" }, { value: "dry_run", label: "i18n:govoplan-campaign.dry_run.485a3d15" }]; return [ { id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 160, sortable: true, filterable: true, sticky: "start", columnType: "from-list", list: { options: statuses, display: "pill" }, render: (row) => , value: (row) => String(row.status ?? "info") }, { id: "job", header: "i18n:govoplan-campaign.job.30c8cb83", width: 180, sortable: true, filterable: true, value: (row) => String(row.job_id ?? row.version_id ?? "—") }, { id: "message", header: "i18n:govoplan-campaign.message.68f4145f", width: "minmax(320px, 1fr)", resizable: true, filterable: true, value: (row) => String(row.message ?? stringifyPreview(row, 180)) }]; } function imapAppendResultColumns(): DataGridColumn>[] { return [ { id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 150, sticky: "start", sortable: true, filterable: true, render: (row) => , value: (row) => String(row.status ?? "info") }, { id: "job", header: "i18n:govoplan-campaign.job.30c8cb83", width: 180, sortable: true, filterable: true, value: (row) => String(row.job_id ?? "-") }, { id: "folder", header: "i18n:govoplan-campaign.folder.30baa249", width: 190, sortable: true, filterable: true, value: (row) => String(row.folder ?? "-") }, { id: "message", header: "i18n:govoplan-campaign.message.68f4145f", width: "minmax(300px, 1fr)", resizable: true, filterable: true, value: (row) => String(row.message ?? stringifyPreview(row, 180)) }]; } function imapDiagnosticColumns(openDetail: (jobId: string) => Promise): DataGridColumn>[] { return [ { id: "recipient", header: "i18n:govoplan-campaign.recipient.90343260", width: 240, sticky: "start", resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(asRecord(row.resolved_recipients).to) || String(row.recipient_email ?? "-") }, { id: "subject", header: "i18n:govoplan-campaign.subject.8d183dbd", width: "minmax(260px, 1fr)", resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "-") }, { id: "send", header: "i18n:govoplan-campaign.smtp.efff9cca", width: 150, sortable: true, filterable: true, render: (row) => , value: (row) => String(row.send_status ?? "-") }, { id: "imap", header: "i18n:govoplan-campaign.imap.271f9ef2", width: 150, sortable: true, filterable: true, render: (row) => , value: (row) => String(row.imap_status ?? "-") }, { id: "error", header: "i18n:govoplan-campaign.last_error.5e4df866", width: "minmax(260px, 1fr)", resizable: true, filterable: true, value: (row) => String(row.last_error ?? "-") }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 110, sticky: "end", render: (row) => }]; } function mockMailboxColumns(openMessage: (id: string) => Promise): DataGridColumn>[] { const options: DataGridListOption[] = [ { value: "smtp", label: "i18n:govoplan-campaign.smtp.efff9cca" }, { value: "imap_append", label: "i18n:govoplan-campaign.imap_append.8c0d9e96" }]; return [ { id: "kind", header: "i18n:govoplan-campaign.kind.e00ac23f", width: 125, sortable: true, filterable: true, sticky: "start", columnType: "from-list", list: { options, display: "pill" }, value: (row) => String(row.kind ?? "mock") }, { id: "subject", header: "i18n:govoplan-campaign.subject.8d183dbd", width: "minmax(260px, 1fr)", resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "—") }, { id: "envelope", header: "i18n:govoplan-campaign.envelope_folder.9f30740d", width: 300, resizable: true, filterable: true, value: (row) => `${String(row.envelope_from ?? row.folder ?? "—")} → ${asArray(row.envelope_recipients).join(", ") || String(row.folder ?? "—")}` }, { id: "attachments", header: "i18n:govoplan-campaign.attachments.6771ade6", width: 125, sortable: true, filterable: true, filterType: "integer", align: "right", value: (row) => Number(row.attachment_count ?? 0) }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 110, sticky: "end", render: (row) => }]; } function builtMessageMetaItems(row: Record) { const recipients = asRecord(row.resolved_recipients); return [ { label: "i18n:govoplan-campaign.from.3f66052a", value: formatSingleAddress(recipients.from) || "—" }, { label: "i18n:govoplan-campaign.to.ae79ea1e", value: formatAddressList(recipients.to) || String(row.recipient_email ?? "—") }, { label: "i18n:govoplan-campaign.cc.c5a976de", value: formatAddressList(recipients.cc) || null }, { label: "i18n:govoplan-campaign.bcc.4c0145a3", value: formatAddressList(recipients.bcc) || null }, { label: "i18n:govoplan-campaign.validation.dd74d182", value: String(row.validation_status ?? "—") }, { label: "i18n:govoplan-campaign.mime_size.c8b9d519", value: row.eml_size_bytes ? `${String(row.eml_size_bytes)} bytes` : "—" }]; } function builtMessageAttachments(row: Record): CampaignMessagePreviewAttachment[] { return asArray(row.attachments).flatMap((value, index) => { const attachment = asRecord(value); const zipProtection = zipProtectionFromBuiltAttachment(attachment); const managedMatches = asArray(attachment.managed_matches).map(asRecord); if (managedMatches.length > 0) { return managedMatches.map((match, matchIndex) => ({ filename: String(match.filename ?? match.display_path ?? `Attachment ${index + 1}.${matchIndex + 1}`), detail: String(match.display_path ?? match.relative_path ?? attachment.label ?? ""), contentType: stringOrUndefined(match.content_type), sizeBytes: numberOrUndefined(match.size_bytes), archiveGroup: stringOrUndefined(attachment.zip_filename), archiveLabel: stringOrUndefined(attachment.zip_filename), protected: zipProtection.protected, protectionNote: zipProtection.note })); } const matches = asArray(attachment.matches).filter((match): match is string => typeof match === "string" && Boolean(match.trim())); if (matches.length > 0) { return matches.map((match) => ({ filename: match.split(/[\\/]/).pop() || String(attachment.label ?? `Attachment ${index + 1}`), detail: match, contentType: stringOrUndefined(attachment.content_type), sizeBytes: numberOrUndefined(attachment.size_bytes), archiveGroup: stringOrUndefined(attachment.zip_filename), archiveLabel: stringOrUndefined(attachment.zip_filename), protected: zipProtection.protected, protectionNote: zipProtection.note })); } return [{ filename: String(attachment.filename ?? attachment.filename_used ?? attachment.display_path ?? attachment.label ?? `Attachment ${index + 1}`), detail: String(attachment.display_path ?? attachment.source_path ?? attachment.file_filter ?? ""), contentType: stringOrUndefined(attachment.content_type), sizeBytes: numberOrUndefined(attachment.size_bytes), archiveGroup: stringOrUndefined(attachment.zip_filename), archiveLabel: stringOrUndefined(attachment.zip_filename), protected: zipProtection.protected, protectionNote: zipProtection.note }]; }); } function zipProtectionFromBuiltAttachment(attachment: Record): {protected: boolean;note: string | null;} { const zipFilename = stringOrUndefined(attachment.zip_filename); if (!zipFilename) return { protected: false, note: null }; const legacyMode = String(attachment.password_mode ?? attachment.zip_password_mode ?? "").trim(); const protectedArchive = getBool(attachment, "password_enabled", getBool(attachment, "zip_password_protected", getBool(attachment, "zip_protected", ["direct", "field", "template"].includes(legacyMode)))); if (!protectedArchive) return { protected: false, note: null }; const field = stringOrUndefined(attachment.password_field) ?? stringOrUndefined(attachment.zip_password_field); const rawScope = String(attachment.password_scope ?? attachment.zip_password_scope ?? "local"); const scope = rawScope === "global" ? "global" : "local"; const method = String(attachment.method ?? attachment.zip_method ?? "aes") === "zip_standard" ? "i18n:govoplan-campaign.zipcrypto.03bf7fb4" : "i18n:govoplan-campaign.aes.41f215a6"; const source = field ? i18nMessage("i18n:govoplan-campaign.scope_field_value", { value0: humanizeScope(scope), value1: field }) : ""; return { protected: true, note: source ? i18nMessage("i18n:govoplan-campaign.value_encryption_value", { value0: source, value1: method }) : i18nMessage("i18n:govoplan-campaign.encryption_value", { value0: method }) }; } function humanizeScope(scope: string): string { return scope === "global" ? "i18n:govoplan-campaign.global.5f1184f7" : "i18n:govoplan-campaign.local.dc99d54d"; } function mockMessageMetaItems(message: MockMailboxMessage) { return [ { label: "i18n:govoplan-campaign.from.3f66052a", value: message.from_header || message.envelope_from || "—" }, { label: "i18n:govoplan-campaign.to.ae79ea1e", value: message.to_header || message.envelope_recipients?.join(", ") || "—" }, { label: "i18n:govoplan-campaign.cc.1fd6a880", value: message.cc_header || null }, { label: "i18n:govoplan-campaign.bcc.8431acad", value: message.bcc_header || null }, { label: "i18n:govoplan-campaign.kind.e00ac23f", value: message.kind || "—" }, { label: "i18n:govoplan-campaign.folder.30baa249", value: message.folder || "—" }, { label: "i18n:govoplan-campaign.message_id.fa0e4fdd", value: message.message_id || "—" }, { label: "i18n:govoplan-campaign.size.b7152342", value: `${message.size_bytes || 0} bytes` }]; } function mockMessageAttachments(message: MockMailboxMessage): CampaignMessagePreviewAttachment[] { return (message.attachments ?? []).map((attachment, index) => ({ filename: attachment.filename || `Attachment ${index + 1}`, contentType: attachment.content_type || undefined, sizeBytes: attachment.size_bytes ?? undefined })); } function countResolvedAttachments(value: unknown): number { const archives = new Set(); let directCount = 0; for (const item of asArray(value)) { const attachment = asRecord(item); const zipFilename = String(attachment.zip_filename ?? "").trim(); if (zipFilename) { archives.add(zipFilename); continue; } const managedCount = asArray(attachment.managed_matches).length; if (managedCount > 0) { directCount += managedCount; continue; } const matchCount = asArray(attachment.matches).length; directCount += matchCount > 0 ? matchCount : 1; } return directCount + archives.size; } function messageNeedsExplicitReview(row: Record): boolean { return String(row.validation_status ?? "").toLowerCase() === "needs_review"; } function storedMessageReviewState(version: CampaignVersionDetail | null): { buildToken: string; inspectionComplete: boolean; reviewedMessageKeys: string[]; } { const build = asRecord(version?.build_summary); const buildToken = String(build.build_token ?? build.built_at ?? ""); const review = asRecord(asRecord(version?.editor_state).review_send); if (!buildToken || String(review.build_token ?? "") !== buildToken) { return { buildToken, inspectionComplete: false, reviewedMessageKeys: [] }; } return { buildToken, inspectionComplete: review.inspection_complete === true, reviewedMessageKeys: asArray(review.reviewed_message_keys).filter((value): value is string => typeof value === "string") }; } function builtMessageKey(row: Record, index: number): string { return String(row.entry_id ?? row.entry_index ?? index); } function formatAddressList(value: unknown): string { return asArray(value).map(asRecord).map(formatSingleAddress).filter(Boolean).join(", "); } function formatSingleAddress(value: unknown): string { const address = asRecord(value); const email = String(address.email ?? "").trim(); const name = String(address.name ?? "").trim(); if (name && email) return `${name} <${email}>`; return email || name; } function numberFrom(record: Record, keys: string[]): number { for (const key of keys) { const value = record[key]; if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value); } return 0; } function numberOrUndefined(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value); return undefined; } function stringOrUndefined(value: unknown): string | undefined { if (typeof value !== "string") return undefined; return value.trim() || undefined; } function stateLabel(state: FlowState): string { switch (state) { case "complete":return "i18n:govoplan-campaign.passed.271d60f4"; case "warning":return "i18n:govoplan-campaign.warnings.1430f976"; case "danger":return "i18n:govoplan-campaign.blocked.99613c74"; case "active":return "i18n:govoplan-campaign.available.7c62a142"; case "locked":return "i18n:govoplan-campaign.locked.a798882f"; case "running":return "i18n:govoplan-campaign.running.73989d9c"; case "partial":return "i18n:govoplan-campaign.partial.65de2e2a"; case "stale":return "i18n:govoplan-campaign.stale.189cc40c"; default:return humanize(state); } }