diff --git a/admin/src/app/dashboard/PlacementPage.tsx b/admin/src/app/dashboard/PlacementPage.tsx new file mode 100644 index 00000000..3be05859 --- /dev/null +++ b/admin/src/app/dashboard/PlacementPage.tsx @@ -0,0 +1,549 @@ +// /placement — seed inbox-placement testing. +// +// Send a tokenized copy of a real template through a real sender to the panel +// of Warmbly-controlled SEED mailboxes, then watch where each landed (Inbox / +// Spam / Promotions / other), rolled up per provider. A backend poller fills in +// results as the probes sync into each seed's inbox, so a test starts "pending" +// and resolves over the next few minutes. + +import { useMemo, useState } from "react"; +import { + keepPreviousData, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Inbox, Send, Sparkles, Tag, TriangleAlert } from "lucide-react"; +import { PageHeader } from "@/components/layout/PageHeader"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ErrorState } from "@/components/ErrorState"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { DataTable, type Column } from "@/components/data/DataTable"; +import { useCursorPager } from "@/lib/useCursorPager"; +import { + createPlacementTest, + getPlacementTest, + listPlacementTests, + listSeedCandidates, + listSeedMailboxes, + setSeedMailbox, + type PlacementFolder, + type PlacementTestRow, + type SeedAccount, +} from "@/lib/api/client/admin/placement"; +import { fmtDate } from "./warmup-content/shared"; + +const STATUS_TONE: Record = { + pending: "border-amber-300 bg-amber-50 text-amber-700", + completed: "border-emerald-300 bg-emerald-50 text-emerald-700", +}; + +const FOLDER_TONE: Record = { + inbox: "border-emerald-300 bg-emerald-50 text-emerald-700", + promotions: "border-sky-300 bg-sky-50 text-sky-700", + spam: "border-red-300 bg-red-50 text-red-700", + other: "border-zinc-300 bg-zinc-50 text-zinc-600", + pending: "border-amber-300 bg-amber-50 text-amber-700", +}; + +const PROVIDER_LABEL: Record = { + gmail: "Gmail / Workspace", + outlook: "Outlook / M365", + smtp_imap: "SMTP / IMAP", + unknown: "Unknown", +}; + +function providerLabel(p: string): string { + return PROVIDER_LABEL[p] ?? p; +} + +export default function PlacementPage() { + return ( +
+ + + + +
+

Run a placement test

+ +
+ +
+

Tests

+ +
+ +
+

Seed mailboxes

+ +
+
+ ); +} + +function OperationalNotice() { + return ( +
+ +
+ Placement classification only works for mailboxes Warmbly owns and + syncs. Connect real seed mailboxes across providers (Gmail/Workspace, + Outlook/M365, Yahoo, AOL, iCloud, corporate) and flag them below. + Gmail Promotions-tab detection additionally needs category-label sync + (a worker follow-up); until then a Gmail tab reads as Inbox. +
+
+ ); +} + +// --- Create ------------------------------------------------------------- + +function CreateTestForm() { + const qc = useQueryClient(); + const [senderId, setSenderId] = useState(""); + const [subject, setSubject] = useState(""); + const [bodyPlain, setBodyPlain] = useState(""); + + const create = useMutation({ + mutationFn: () => + createPlacementTest({ + sender_account_id: senderId.trim(), + subject: subject.trim(), + body_plain: bodyPlain, + body_html: "", + }), + onSuccess: () => { + toast.success("Placement test sent to the seed panel"); + setSubject(""); + setBodyPlain(""); + qc.invalidateQueries({ queryKey: ["admin", "placement", "tests"] }); + }, + onError: (err: Error) => toast.error(err.message || "Failed to send test"), + }); + + const canSubmit = + senderId.trim() !== "" && subject.trim() !== "" && bodyPlain.trim() !== ""; + + return ( +
+
+
+ + setSenderId(e.target.value)} + /> +

+ The real mailbox whose deliverability you are testing. Copy its + ID from the Mailboxes explorer. +

+
+
+ + setSubject(e.target.value)} + /> +

+ A hidden placement token is appended so we can find the copy in + each seed inbox. +

+
+
+
+ +