import { useEffect, useMemo, useRef, useState } from "react"; import { ExternalLink } from "lucide-react"; import type { ApiSettings, CampaignListItem } from "../../types"; import { getCampaignWorkspaceDelta, listCampaignsDelta, retryCampaignJobs, sendUnattemptedCampaignJobs, type CampaignSummary } from "../../api/campaigns"; import { Button } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid"; import { DismissibleAlert } from "@govoplan/core-webui"; import { LoadingFrame } from "@govoplan/core-webui"; import { MetricCard } from "@govoplan/core-webui"; import { PageTitle } from "@govoplan/core-webui"; import { StatusBadge, i18nMessage, mergeDeltaRows, useDeltaWatermarks, useGuardedNavigate } from "@govoplan/core-webui"; import { asRecord, formatDateTime, humanize } from "../campaigns/utils/campaignView"; type OperatorRow = { campaign: CampaignListItem; summary: CampaignSummary | null; failed: number; outcomeUnknown: number; notAttempted: number; queuedOrActive: number; imapFailed: number; queueable: number; needsAttention: number; }; export default function OperatorQueuePage({ settings }: {settings: ApiSettings;}) { const navigate = useGuardedNavigate(); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const [rows, setRows] = useState([]); const campaignsRef = useRef([]); const summariesRef = useRef>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [message, setMessage] = useState(""); const [busy, setBusy] = useState(""); const settingsKey = useMemo( () => JSON.stringify({ apiBaseUrl: settings.apiBaseUrl, apiKey: settings.apiKey, accessToken: settings.accessToken }), [settings.apiBaseUrl, settings.apiKey, settings.accessToken] ); useEffect(() => { campaignsRef.current = []; summariesRef.current = {}; resetDeltaWatermark(); void load(); }, [settingsKey, resetDeltaWatermark]); async function load() { setLoading(true); setError(""); try { const campaigns = await loadCampaignsDelta(); const summaries = await loadCampaignSummariesDelta(campaigns); setRows(campaigns.map((campaign) => toRow(campaign, summaries[campaign.id] ?? null))); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } } async function runAction(row: OperatorRow, action: "retry" | "unattempted") { const key = `${action}:${row.campaign.id}`; setBusy(key); setError(""); setMessage(""); try { const response = action === "retry" ? await retryCampaignJobs(settings, row.campaign.id, { enqueue_celery: true }) : await sendUnattemptedCampaignJobs(settings, row.campaign.id, { enqueue_celery: true }); const result = asRecord(response.result ?? response); setMessage(i18nMessage("i18n:govoplan-campaign.value_value_value_enqueued.35b33f6d", { value0: row.campaign.name, value1: humanize(String(result.action ?? action)), value2: String(result.enqueued_count ?? 0) })); resetDeltaWatermark(operatorCampaignSummaryKey(row.campaign.id)); await load(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(""); } } async function loadCampaignsDelta(): Promise { const key = operatorCampaignListKey(); let nextWatermark = getDeltaWatermark(key); let campaigns = campaignsRef.current; let hasMore = false; do { const response = await listCampaignsDelta(settings, { since: nextWatermark }); campaigns = mergeDeltaRows(campaigns, response.campaigns, response.deleted, (campaign) => campaign.id, { deletedResourceType: "campaign", sort: sortCampaignsByUpdatedDesc }); nextWatermark = response.watermark ?? null; hasMore = response.has_more; } while (hasMore); const campaignIds = new Set(campaigns.map((campaign) => campaign.id)); for (const campaignId of Object.keys(summariesRef.current)) { if (!campaignIds.has(campaignId)) delete summariesRef.current[campaignId]; } campaignsRef.current = campaigns; setDeltaWatermark(key, nextWatermark); return campaigns; } async function loadCampaignSummariesDelta(campaigns: CampaignListItem[]): Promise> { const summaries = { ...summariesRef.current }; await Promise.all(campaigns.map(async (campaign) => { const key = operatorCampaignSummaryKey(campaign.id); let nextWatermark = getDeltaWatermark(key); let summary = summaries[campaign.id] ?? null; let hasMore = false; try { do { const response = await getCampaignWorkspaceDelta(settings, campaign.id, { includeCurrentVersion: false, includeVersions: false, includeSummary: true, since: nextWatermark }); if (response.full || response.summary) summary = response.summary; nextWatermark = response.watermark ?? null; hasMore = response.has_more; } while (hasMore); setDeltaWatermark(key, nextWatermark); summaries[campaign.id] = summary; } catch { summaries[campaign.id] = summary; } })); summariesRef.current = summaries; return summaries; } function operatorCampaignListKey(): string { return JSON.stringify({ scope: "operator-campaigns", settingsKey }); } function operatorCampaignSummaryKey(campaignId: string): string { return JSON.stringify({ scope: "operator-campaign-summary", campaignId, settingsKey }); } const totals = rows.reduce((acc, row) => ({ failed: acc.failed + row.failed, outcomeUnknown: acc.outcomeUnknown + row.outcomeUnknown, notAttempted: acc.notAttempted + row.notAttempted, queuedOrActive: acc.queuedOrActive + row.queuedOrActive, imapFailed: acc.imapFailed + row.imapFailed }), { failed: 0, outcomeUnknown: 0, notAttempted: 0, queuedOrActive: 0, imapFailed: 0 }); const columns = useMemo[]>(() => [ { 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) => , value: (row) => row.campaign.status }, { 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 }, { id: "unattempted", header: "i18n:govoplan-campaign.unattempted.e7411dd6", width: 130, align: "right", sortable: true, filterType: "integer", value: (row) => row.notAttempted }, { id: "queued", header: "i18n:govoplan-campaign.queued_active.b08bef73", width: 135, align: "right", sortable: true, filterType: "integer", value: (row) => row.queuedOrActive }, { id: "updated", header: "i18n:govoplan-campaign.updated.f2f8570d", width: 180, sortable: true, filterType: "date", value: (row) => formatDateTime(row.campaign.updated_at), sortValue: (row) => row.campaign.updated_at ?? "" }, { id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 270, sticky: "end", render: (row) =>
}], [busy, navigate]); return (
i18n:govoplan-campaign.operator_queue.72492fb5

i18n:govoplan-campaign.queue_state_retry_candidates_and_reconciliation_.1b592bbe

{error && {error}} {message && {message}}

i18n:govoplan-campaign.queue_pressure.28eee15e

row.campaign.id} emptyText="i18n:govoplan-campaign.no_accessible_campaigns.9e080190" className="data-table compact-table" />
); } function toRow(campaign: CampaignListItem, summary: CampaignSummary | null): OperatorRow { const cards = asRecord(summary?.cards); return { campaign, summary, failed: numberValue(cards.failed), outcomeUnknown: numberValue(cards.outcome_unknown), notAttempted: numberValue(cards.not_attempted), queuedOrActive: numberValue(cards.queued_or_active), imapFailed: numberValue(cards.imap_failed), queueable: numberValue(cards.queueable), needsAttention: numberValue(cards.needs_attention) }; } function numberValue(value: unknown): number { return typeof value === "number" && Number.isFinite(value) ? value : 0; } function sortCampaignsByUpdatedDesc(left: CampaignListItem, right: CampaignListItem): number { return String(right.updated_at ?? right.updatedAt ?? right.created_at ?? "").localeCompare( String(left.updated_at ?? left.updatedAt ?? left.created_at ?? "") ); } function QueuePressureGrid({ items }: {items: Array<{label: string;value: number;tone: "neutral" | "good" | "warning" | "danger" | "info";}>;}) { return (
{items.map((item) => )}
); }