Add campaign scheduling lifecycle jobs

This commit is contained in:
2026-06-05 21:38:34 +02:00
parent 3f148bcec2
commit 70951789d2
7 changed files with 384 additions and 0 deletions

View File

@@ -16,7 +16,9 @@ import { Separator } from "@/components/ui/separator";
import { CampaignFormDialog } from "@/components/campaigns/campaign-form-dialog";
type CampaignsListResult = FunctionReturnType<typeof api.campaigns.list>;
type CampaignRunsListResult = FunctionReturnType<typeof api.runs.list>;
type CampaignRow = NonNullable<CampaignsListResult>[number];
type CampaignRunRow = NonNullable<CampaignRunsListResult>[number];
type RecurrenceLabel = Record<CampaignRow["recurrence"], string>;
type CurrentRunStatusLabel = {
@@ -40,6 +42,13 @@ const statusLabel: CurrentRunStatusLabel = {
paused: "Pausiert",
};
const stepLabel: Record<string, string> = {
campaign_cron_queued: "Cron geplant",
campaign_cron_skipped: "Cron übersprungen",
campaign_cron_stale_pending: "Timeout bereinigt",
lead_discovery: "Lead-Recherche",
};
const dateFormatter = new Intl.DateTimeFormat("de-DE", {
dateStyle: "short",
timeStyle: "short",
@@ -84,6 +93,10 @@ const formatNiche = (campaign: CampaignRow): string => {
export function CampaignsBoard() {
const campaigns = useQuery(api.campaigns.list, { limit: 100 });
const recentCampaignRuns = useQuery(api.runs.list, {
limit: 8,
type: "campaign",
});
const createCampaign = useMutation(api.campaigns.create);
const updateCampaign = useMutation(api.campaigns.update);
const setStatus = useMutation(api.campaigns.setStatus);
@@ -130,6 +143,10 @@ export function CampaignsBoard() {
return [...campaigns].sort((a, b) => b.createdAt - a.createdAt);
}, [campaigns]);
const visibleRuns = useMemo<CampaignRunRow[]>(() => {
return recentCampaignRuns ?? [];
}, [recentCampaignRuns]);
const closeDialog = () => {
setEditingCampaign(null);
setIsFormOpen(false);
@@ -343,6 +360,48 @@ export function CampaignsBoard() {
))}
</div>
)}
<Card>
<CardHeader>
<CardTitle>Aktuelle Run-Logs</CardTitle>
<CardDescription>
Letzte Kampagnenläufe inklusive Cron-Skips und Fehlerhinweisen.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-2 text-sm">
{recentCampaignRuns === undefined ? (
<Skeleton className="h-16 rounded-lg" />
) : visibleRuns.length === 0 ? (
<p className="text-muted-foreground">Noch keine Kampagnenläufe.</p>
) : (
visibleRuns.map((run) => (
<div className="rounded-md border p-3" key={run._id}>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="font-medium">
{statusLabel[run.status] ?? run.status}
</p>
<p className="text-xs text-muted-foreground">
{formatDateTime(run.updatedAt)}
</p>
</div>
<p className="mt-1 text-muted-foreground">
{stepLabel[run.currentStep ?? ""] ?? run.currentStep ?? "Schritt offen"}
</p>
{run.currentStep === "campaign_cron_skipped" ? (
<p className="mt-1 text-xs text-muted-foreground">
Cron wurde übersprungen, weil bereits ein Agentenlauf aktiv war.
</p>
) : null}
{run.errorSummary ? (
<p className="mt-1 text-xs text-destructive">
{run.errorSummary}
</p>
) : null}
</div>
))
)}
</CardContent>
</Card>
</section>
);
}