From 8e53fc7027aa7ab932fcc86244ebd613617706a1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:38:58 +0000 Subject: [PATCH 1/6] feat: integrate warmup observation evidence canonical diagnostics send-result recovery cloud trust exact-parent lineage durable dispatch portability and legacy compatibility --- .../dashboard/placement/TestDetailSheet.tsx | 5 +- admin/src/app/dashboard/placement/badges.tsx | 6 + .../dashboard/warmup-content/LibraryPage.tsx | 11 +- .../dashboard/warmup-content/OverviewPage.tsx | 72 +++- admin/src/lib/api/client/admin/placement.ts | 7 + .../src/lib/api/client/admin/warmupContent.ts | 32 ++ cmd/backend/main.go | 3 + cmd/worker/main.go | 19 +- docs/content/docs/api/endpoints.mdx | 7 + docs/content/docs/api/error-codes.mdx | 2 + docs/content/docs/development/admin-panel.mdx | 2 +- .../docs/development/configuration.mdx | 3 + .../content/docs/development/data-control.mdx | 6 + docs/content/docs/development/meta.json | 1 + .../docs/development/warmup-generation.mdx | 65 ++++ docs/content/docs/guides/deliverability.mdx | 22 +- docs/content/docs/guides/warmbly-cloud.mdx | 52 ++- .../docs/guides/workspace-export-import.mdx | 2 + internal/api/handler/admin_warmup_content.go | 98 ++++- .../admin_warmup_content_settings_test.go | 28 ++ internal/api/handler/email_oauth_callback.go | 2 +- internal/api/handler/handler.go | 1 + .../api/handler/internal_warmup_dispatch.go | 77 ++++ internal/api/handler/poollink.go | 21 +- internal/api/routes.go | 5 + internal/app/analytics/warmup_placement.go | 22 +- internal/app/cloudlink/managed.go | 157 +++----- internal/app/cloudlink/managed_consent.go | 278 ++++++++++++++ .../app/cloudlink/managed_consent_test.go | 330 ++++++++++++++++ internal/app/cloudlink/reconciliation.go | 100 +++++ internal/app/cloudlink/reconciliation_test.go | 168 ++++++++ .../app/cloudlink/revoke_for_delete_test.go | 26 +- internal/app/cloudlink/service.go | 127 ++++-- internal/app/cloudlink/standing.go | 45 ++- internal/app/cloudlink/standing_test.go | 10 +- internal/app/consumer/event_new_email.go | 43 ++- internal/app/consumer/event_send_result.go | 27 +- .../consumer/event_send_result_live_test.go | 3 +- .../app/consumer/greylisting_evidence_test.go | 2 +- .../send_legacy_recovery_live_test.go | 139 +++++++ .../app/consumer/send_recovery_live_test.go | 252 ++++++++++++ .../consumer/send_reservation_live_test.go | 39 +- internal/app/consumer/stuck_send_reclaimer.go | 31 +- internal/app/consumer/warmup_reply_back.go | 74 ++-- .../warmup_send_giveback_live_test.go | 5 +- internal/app/email/broker.go | 8 + internal/app/email/service.go | 1 + .../orgtransfer/managed_consent_live_test.go | 39 ++ internal/app/orgtransfer/spec.go | 17 +- internal/app/placement/tick.go | 12 +- internal/app/placement/view.go | 40 +- internal/app/poollink/managed_operations.go | 203 ++++++++++ .../app/poollink/managed_operations_test.go | 363 ++++++++++++++++++ internal/app/poollink/oauth.go | 194 +++++++++- internal/app/poollink/service.go | 50 ++- internal/app/warmup/service.go | 7 + internal/app/warmupcontent/batch.go | 187 +++++---- internal/app/warmupcontent/batch_test.go | 2 +- internal/app/warmupcontent/controls_test.go | 274 +++++++++++++ internal/app/warmupcontent/judge.go | 36 +- internal/app/warmupcontent/judge_test.go | 4 +- internal/app/warmupcontent/service.go | 57 +-- internal/app/warmupcontent/service_test.go | 1 + internal/app/worker/event_send_email.go | 113 +++++- internal/app/worker/warmup_dispatch_test.go | 159 ++++++++ internal/app/worker/wmail/admit.go | 5 + internal/app/worker/wmail/send.go | 35 ++ .../app/worker/wmail/send_failure_test.go | 75 ++++ internal/client/goog/error.go | 28 +- internal/client/goog/message.go | 8 + internal/client/goog/message_test.go | 12 + internal/client/goog/send.go | 8 + internal/client/goog/send_failure_test.go | 35 ++ internal/client/msgraph/error.go | 44 +-- internal/client/msgraph/message.go | 6 + internal/client/msgraph/send.go | 50 ++- internal/client/msgraph/send_failure_test.go | 54 +++ internal/client/smtpimap/imap/client.go | 10 +- internal/client/smtpimap/smtp/client.go | 53 +-- internal/client/smtpimap/smtp/failure.go | 38 ++ internal/client/smtpimap/smtp/failure_test.go | 42 ++ internal/client/smtpimap/smtp/mime_test.go | 26 ++ internal/client/smtpimap/smtp/server_test.go | 9 +- internal/errx/email.go | 1 + internal/errx/send_failure.go | 70 ++++ internal/errx/send_failure_test.go | 48 +++ internal/events/publisher.go | 29 +- .../000266_observation_evidence.down.sql | 4 + .../000266_observation_evidence.up.sql | 21 + .../000267_warmup_content_provenance.down.sql | 8 + .../000267_warmup_content_provenance.up.sql | 9 + .../000268_send_result_recovery.down.sql | 12 + .../000268_send_result_recovery.up.sql | 14 + ...000269_cloud_trust_reconciliation.down.sql | 93 +++++ .../000269_cloud_trust_reconciliation.up.sql | 124 ++++++ .../migrations/000270_warmup_lineage.down.sql | 13 + .../migrations/000270_warmup_lineage.up.sql | 18 + .../000271_warmup_dispatch.down.sql | 8 + .../migrations/000271_warmup_dispatch.up.sql | 6 + .../db/send_recovery_upgrade_test.go | 155 ++++++++ internal/models/email.go | 1 + internal/models/fleet_node.go | 11 +- internal/models/managed_consent.go | 43 +++ internal/models/observation_evidence_test.go | 103 +++++ internal/models/observation_metric.go | 51 +++ internal/models/placement.go | 76 ++-- internal/models/poollink.go | 91 +++-- internal/models/received_evidence.go | 83 ++++ internal/models/send_failure_test.go | 97 +++++ internal/models/warmup_content.go | 120 +++--- internal/models/warmup_content_test.go | 62 +++ internal/models/warmup_deliverability.go | 69 +++- internal/models/warmup_deliverability_test.go | 10 +- internal/models/warmup_report.go | 36 +- internal/models/worker.go | 18 +- internal/pkg/dnsauth/evidence.go | 43 +++ internal/pkg/dnsauth/evidence_test.go | 35 ++ internal/pkg/emsg/emsg.go | 46 ++- internal/pkg/generation/batch.go | 11 + internal/pkg/generation/conversation.go | 85 ++-- internal/pkg/generation/diagnostic.go | 57 +++ internal/pkg/generation/diagnostic_test.go | 162 ++++++++ internal/pkg/mailhdr/evidence.go | 102 +++++ internal/pkg/mailhdr/evidence_test.go | 52 +++ internal/pkg/mailhdr/references.go | 42 ++ internal/pkg/mailhdr/references_test.go | 15 + internal/pkg/nodeagent/agent.go | 32 +- .../cloud_link_standing_live_test.go | 222 ++++++++++- internal/repository/cloud_reconciliation.go | 53 +++ internal/repository/http_warmup_dispatch.go | 55 +++ internal/repository/managed_consent.go | 280 ++++++++++++++ .../managed_consent_fault_live_test.go | 118 ++++++ .../repository/managed_consent_live_test.go | 221 +++++++++++ .../observation_evidence_live_test.go | 139 +++++++ internal/repository/pg_campaign.go | 4 +- internal/repository/pg_campaign_progress.go | 17 +- internal/repository/pg_cloudlink.go | 70 +++- internal/repository/pg_email.go | 7 + internal/repository/pg_fleet_node.go | 6 +- internal/repository/pg_placement.go | 56 ++- internal/repository/pg_send_recovery.go | 236 ++++++++++++ internal/repository/pg_task.go | 16 +- internal/repository/pg_warmup.go | 79 +++- internal/repository/pg_warmup_content.go | 102 ++++- internal/repository/pg_warmup_placement.go | 80 ++-- .../warmup_content_upgrade_live_test.go | 157 ++++++++ internal/repository/warmup_dispatch.go | 225 +++++++++++ internal/repository/warmup_lineage.go | 282 ++++++++++++++ .../repository/warmup_lineage_live_test.go | 287 ++++++++++++++ .../warmup_partner_org_live_test.go | 33 ++ .../repository/warmup_placement_live_test.go | 4 +- internal/repository/warmup_placement_sql.go | 3 +- internal/repository/warmup_standing_sql.go | 19 +- internal/scheduler/warmup_dispatch.go | 174 +++++++++ internal/tasks/campaign_task.go | 4 + internal/tasks/content_quality_test.go | 198 ++++++++++ internal/tasks/dispatch.go | 3 + internal/tasks/email_sender.go | 50 ++- internal/tasks/email_sender_reply_to_test.go | 27 ++ internal/tasks/email_task.go | 286 +++++++------- internal/tasks/placement_task.go | 3 + internal/tasks/template.go | 146 ++++--- internal/tasks/user_email_task.go | 3 + internal/tasks/warmup_content.go | 58 +-- internal/tasks/warmup_dispatch_live_test.go | 56 +++ internal/tasks/warmup_lineage.go | 252 ++++++++++++ .../tasks/warmup_partner_routing_live_test.go | 2 +- internal/tasks/warmup_reconciler.go | 26 +- scripts/check-pages-build.sh | 2 +- web/src/app/app/placement/[id]/page.tsx | 3 +- .../components/app/emails/InboxDetails.tsx | 20 +- .../app/placement/MailboxPlacementTab.tsx | 19 +- .../app/placement/PlacementCharts.tsx | 35 +- .../app/placement/WarmupPlacementSection.tsx | 27 +- .../app/placement/placement.test.ts | 34 ++ web/src/components/app/placement/placement.ts | 21 +- .../app/placement/tests/PlacementParts.tsx | 7 +- .../app/placement/tests/placementTests.ts | 7 +- .../models/app/analytics/WarmupPlacement.ts | 34 +- .../lib/api/models/app/emails/AuthCheck.ts | 7 +- .../lib/api/models/app/placement/Placement.ts | 11 +- 181 files changed, 10032 insertions(+), 1172 deletions(-) create mode 100644 docs/content/docs/development/warmup-generation.mdx create mode 100644 internal/api/handler/admin_warmup_content_settings_test.go create mode 100644 internal/api/handler/internal_warmup_dispatch.go create mode 100644 internal/app/cloudlink/managed_consent.go create mode 100644 internal/app/cloudlink/managed_consent_test.go create mode 100644 internal/app/cloudlink/reconciliation.go create mode 100644 internal/app/cloudlink/reconciliation_test.go create mode 100644 internal/app/consumer/send_legacy_recovery_live_test.go create mode 100644 internal/app/consumer/send_recovery_live_test.go create mode 100644 internal/app/orgtransfer/managed_consent_live_test.go create mode 100644 internal/app/poollink/managed_operations.go create mode 100644 internal/app/poollink/managed_operations_test.go create mode 100644 internal/app/warmupcontent/controls_test.go create mode 100644 internal/app/worker/warmup_dispatch_test.go create mode 100644 internal/app/worker/wmail/send_failure_test.go create mode 100644 internal/client/goog/send_failure_test.go create mode 100644 internal/client/msgraph/send_failure_test.go create mode 100644 internal/client/smtpimap/smtp/failure.go create mode 100644 internal/client/smtpimap/smtp/failure_test.go create mode 100644 internal/errx/send_failure.go create mode 100644 internal/errx/send_failure_test.go create mode 100644 internal/infrastructure/db/migrations/000266_observation_evidence.down.sql create mode 100644 internal/infrastructure/db/migrations/000266_observation_evidence.up.sql create mode 100644 internal/infrastructure/db/migrations/000267_warmup_content_provenance.down.sql create mode 100644 internal/infrastructure/db/migrations/000267_warmup_content_provenance.up.sql create mode 100644 internal/infrastructure/db/migrations/000268_send_result_recovery.down.sql create mode 100644 internal/infrastructure/db/migrations/000268_send_result_recovery.up.sql create mode 100644 internal/infrastructure/db/migrations/000269_cloud_trust_reconciliation.down.sql create mode 100644 internal/infrastructure/db/migrations/000269_cloud_trust_reconciliation.up.sql create mode 100644 internal/infrastructure/db/migrations/000270_warmup_lineage.down.sql create mode 100644 internal/infrastructure/db/migrations/000270_warmup_lineage.up.sql create mode 100644 internal/infrastructure/db/migrations/000271_warmup_dispatch.down.sql create mode 100644 internal/infrastructure/db/migrations/000271_warmup_dispatch.up.sql create mode 100644 internal/infrastructure/db/send_recovery_upgrade_test.go create mode 100644 internal/models/managed_consent.go create mode 100644 internal/models/observation_evidence_test.go create mode 100644 internal/models/observation_metric.go create mode 100644 internal/models/received_evidence.go create mode 100644 internal/models/send_failure_test.go create mode 100644 internal/models/warmup_content_test.go create mode 100644 internal/pkg/dnsauth/evidence.go create mode 100644 internal/pkg/dnsauth/evidence_test.go create mode 100644 internal/pkg/generation/diagnostic.go create mode 100644 internal/pkg/generation/diagnostic_test.go create mode 100644 internal/pkg/mailhdr/evidence.go create mode 100644 internal/pkg/mailhdr/evidence_test.go create mode 100644 internal/pkg/mailhdr/references.go create mode 100644 internal/pkg/mailhdr/references_test.go create mode 100644 internal/repository/cloud_reconciliation.go create mode 100644 internal/repository/http_warmup_dispatch.go create mode 100644 internal/repository/managed_consent.go create mode 100644 internal/repository/managed_consent_fault_live_test.go create mode 100644 internal/repository/managed_consent_live_test.go create mode 100644 internal/repository/observation_evidence_live_test.go create mode 100644 internal/repository/pg_send_recovery.go create mode 100644 internal/repository/warmup_content_upgrade_live_test.go create mode 100644 internal/repository/warmup_dispatch.go create mode 100644 internal/repository/warmup_lineage.go create mode 100644 internal/repository/warmup_lineage_live_test.go create mode 100644 internal/scheduler/warmup_dispatch.go create mode 100644 internal/tasks/content_quality_test.go create mode 100644 internal/tasks/warmup_dispatch_live_test.go create mode 100644 internal/tasks/warmup_lineage.go create mode 100644 web/src/components/app/placement/placement.test.ts diff --git a/admin/src/app/dashboard/placement/TestDetailSheet.tsx b/admin/src/app/dashboard/placement/TestDetailSheet.tsx index e3812d955..51b3e27d2 100644 --- a/admin/src/app/dashboard/placement/TestDetailSheet.tsx +++ b/admin/src/app/dashboard/placement/TestDetailSheet.tsx @@ -185,7 +185,8 @@ function scoreTone(score: number): Tone { function SummaryGrid({ counts }: { counts: PlacementCounts }) { const tabs = counts.promotions + counts.other; const cells: { label: string; value: number; sub?: string; tone?: Tone }[] = [ - { label: "Delivered", value: counts.delivered, sub: `of ${counts.total}` }, + { label: "Resolved (legacy basis)", value: counts.delivered, sub: `includes timeouts, of ${counts.total}` }, + { label: "Observed receipts", value: counts.observed_receipts ?? counts.inbox + counts.promotions + counts.other + counts.spam, sub: `${counts.unknown ?? 0} unknown, ${counts.archive ?? 0} archive, ${counts.custom ?? 0} custom` }, { label: "Primary inbox", value: counts.inbox, sub: pct(counts.inbox_rate), tone: "success" }, { label: "Gmail tabs", value: tabs, sub: pct(counts.tabs_rate), tone: "info" }, { label: "Spam", value: counts.spam, sub: pct(counts.spam_rate), tone: counts.spam > 0 ? "danger" : undefined }, @@ -237,7 +238,7 @@ function FamiliesTable({ families }: { families: { family: string; label: string Provider - Delivered + Resolved (legacy) Inbox Tabs Spam diff --git a/admin/src/app/dashboard/placement/badges.tsx b/admin/src/app/dashboard/placement/badges.tsx index 2858e94de..20678a335 100644 --- a/admin/src/app/dashboard/placement/badges.tsx +++ b/admin/src/app/dashboard/placement/badges.tsx @@ -26,6 +26,9 @@ const FOLDER_LABEL: Record = { other: "Other tab", spam: "Spam", missing: "Missing", + unknown: "Folder unknown", + archive: "Archive", + custom: "Custom folder", failed: "Failed", cancelled: "Cancelled", }; @@ -37,6 +40,9 @@ const FOLDER_TONE: Record = { other: "info", spam: "danger", missing: "warning", + unknown: "neutral", + archive: "neutral", + custom: "neutral", failed: "danger", cancelled: "neutral", }; diff --git a/admin/src/app/dashboard/warmup-content/LibraryPage.tsx b/admin/src/app/dashboard/warmup-content/LibraryPage.tsx index 65c8b213f..b770214b6 100644 --- a/admin/src/app/dashboard/warmup-content/LibraryPage.tsx +++ b/admin/src/app/dashboard/warmup-content/LibraryPage.tsx @@ -153,6 +153,12 @@ export default function LibraryPage() { ), csv: (c) => (c.lint_passed ? "pass" : "fail"), }, + { + id: "review", + header: "Semantic review", + cell: (c) => {c.semantic_review ?? "legacy_unknown"}, + csv: (c) => c.semantic_review ?? "legacy_unknown", + }, { id: "status", header: "Status", @@ -188,7 +194,7 @@ export default function LibraryPage() { size="xs" variant="ghost" onClick={() => unarchive.mutate(c.id)} - disabled={unarchive.isPending} + disabled={unarchive.isPending || (c.semantic_review !== undefined && !["passed", "legacy_unknown"].includes(c.semantic_review))} > Restore @@ -363,6 +369,9 @@ function ConversationDialog({ lint {c.lint_passed ? "pass" : "fail"} + review: {c.semantic_review ?? "legacy_unknown"} + {c.scenario_version && scenario: {c.scenario_version}} + {c.rendering_version && rendering: {c.rendering_version}} {c.segment && segment: {c.segment}} source: {c.source || "—"} diff --git a/admin/src/app/dashboard/warmup-content/OverviewPage.tsx b/admin/src/app/dashboard/warmup-content/OverviewPage.tsx index 511859a65..e3e29efb8 100644 --- a/admin/src/app/dashboard/warmup-content/OverviewPage.tsx +++ b/admin/src/app/dashboard/warmup-content/OverviewPage.tsx @@ -3,8 +3,13 @@ // library stock vs the scheduler's targets, today's generation budget, // headline counts, and the content-source vs spam-placement A/B comparison. -import { useMemo, type ReactNode } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useMemo, useState, type ReactNode } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { useAdminPerm } from "@/hooks/useAdminPerm"; +import { AdminPerm } from "@/lib/auth/permissions"; import { Archive, CheckCircle2, @@ -30,6 +35,8 @@ import { cn } from "@/lib/utils"; import { getWarmupContentAb, getWarmupContentOverview, + putWarmupGenerationSettings, + type WarmupGenerationSettings, type WarmupContentOverview, } from "@/lib/api/client/admin/warmupContent"; import { Td, Th } from "./components"; @@ -41,19 +48,16 @@ interface PipelineStep { detail: string; } -// The controller is always enabled. The only external dependency is an AI -// provider; without one, reviewed static content keeps sends operational. +// Cached-content selection and new generation have independent controls. function pipelineSteps(d: WarmupContentOverview): PipelineStep[] { const totalTarget = d.stock.reduce((n, s) => n + s.target, 0); const totalStock = d.stock.reduce((n, s) => n + Math.min(s.active, s.target), 0); const stocked = totalTarget > 0 && totalStock >= totalTarget; return [ { - label: "AI client configured", - ok: d.ai_configured, - detail: d.ai_configured - ? "OPENAI_API_KEY is set on the backend" - : "Set OPENAI_API_KEY on the backend, then restart it", + label: "Generation enabled", + ok: d.ai_configured && d.generation_enabled !== false, + detail: !d.ai_configured ? "No compatible generation client configured" : d.generation_enabled === false ? "New jobs stopped; existing batches drain" : "Configured model checked before submission", }, { label: "AI content enabled", @@ -100,7 +104,7 @@ function AutomationPanel({ data }: { data: WarmupContentOverview }) { const firstGap = steps.findIndex((s) => !s.ok); const capped = data.daily_generation_cap > 0; const budgetUsed = capped - ? Math.min(100, Math.round((data.generated_today / data.daily_generation_cap) * 100)) + ? Math.min(100, Math.round(((data.reserved_today ?? data.generated_today) / data.daily_generation_cap) * 100)) : 0; return ( @@ -108,7 +112,7 @@ function AutomationPanel({ data }: { data: WarmupContentOverview }) { title="Automatic extension" actions={ - Autopilot + {data.generation_enabled === false ? "Generation stopped" : "Generation control"} } bodyClassName="p-0" @@ -116,7 +120,7 @@ function AutomationPanel({ data }: { data: WarmupContentOverview }) {

{allOk ? data.refresh_enabled - ? "The library extends itself: every run generates new threads, humanizes and lints them, and recycles the most-used ones so fresh content keeps flowing indefinitely." + ? "Scheduled generation preserves canonical content and reviews complete rendered diagnostic threads before activating them." : "The library tops itself up to the target. Continuous refresh is off, so generation pauses once the target is reached." : "Not fully automatic yet. Fix the first amber step below and the library will keep itself stocked without manual runs."}

@@ -148,8 +152,8 @@ function AutomationPanel({ data }: { data: WarmupContentOverview }) { {capped ? ( - {data.generated_today.toLocaleString()} / {data.daily_generation_cap.toLocaleString()}{" "} - threads + {(data.reserved_today ?? data.generated_today).toLocaleString()} / {data.daily_generation_cap.toLocaleString()}{" "} + requests reserved today = 100 ? "warning" : "success"} className="w-24" /> @@ -179,6 +183,44 @@ function AutomationPanel({ data }: { data: WarmupContentOverview }) { ); } +function GenerationSettingsPanel({ data }: { data: WarmupContentOverview }) { + const canManage = useAdminPerm(AdminPerm.ManageSettings); + const qc = useQueryClient(); + const [draft, setDraft] = useState(null); + const save = useMutation({ + mutationFn: putWarmupGenerationSettings, + onSuccess: () => { + setDraft(null); + void qc.invalidateQueries({ queryKey: ["admin", "warmup-content"] }); + toast.success("Generation settings saved"); + }, + onError: (err: Error) => toast.error(err.message || "Failed to save generation settings"), + }); + if (!data.effective_settings) return This backend does not expose generation controls. Upgrade the backend before changing settings.; + const text = draft ?? JSON.stringify(data.effective_settings, null, 2); + const submit = () => { + try { + const settings: WarmupGenerationSettings = JSON.parse(text); + if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error("Expected a settings object"); + save.mutate(settings); + } catch { + toast.error("Enter a valid JSON settings object"); + } + }; + return ( + +

Stopping generation blocks new jobs without deleting credentials. Already submitted batches continue polling and ingest safely; cancel them explicitly in Jobs. Cached-content use is controlled separately by enabled. A stop is effective only after all backends are upgraded.

+

{data.provider_capability}

+