Reminder detail page: * Surfaces a PausedRunBanner above the rest of the surface when the most recent run is in 'paused' state. The banner shows the delivered/total counts, the deadline that closed the window, and Resume / Cancel run buttons that call the matching server actions. * getReminderWithRuns now LEFT JOIN-aggregates run_target counts so the banner has sent/total per run without an N+1 fan-out. Activity tab: * New Paused filter tab between Success and Partial. * Paused rows in the desktop table get an inline ResumeRunButton (emerald play icon, useTransition + error surfacing). * RunStatusBadge picks up a Paused entry — amber, PauseCircle icon. Tests: * PausedRunBanner — 4 SSR cases (resume/cancel CTA rendered, X-of-Y copy, generic fallback, amber styling). * ResumeRunButton — 4 SSR cases (aria, emerald accent, compact / default size variants). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useTransition } from "react";
|
|
import {
|
|
AlertCircleIcon,
|
|
PlayIcon,
|
|
XIcon,
|
|
Loader2Icon,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
resumeReminderRunAction,
|
|
cancelReminderRunAction,
|
|
} from "@/actions/reminders";
|
|
|
|
interface PausedRunBannerProps {
|
|
runId: string;
|
|
/** Best-effort sent count for the body copy. Falls back to a
|
|
* generic message when undefined. */
|
|
sent?: number;
|
|
/** Best-effort total target count. */
|
|
total?: number;
|
|
/** Deadline hour the bot stopped at. Shown in the body copy. */
|
|
windowEndHour: number;
|
|
/** Operator timezone (for the deadline label). */
|
|
timezone: string;
|
|
}
|
|
|
|
/**
|
|
* Amber callout shown above the reminder detail view when the most
|
|
* recent run is in 'paused' state. Two interactive choices:
|
|
* • Resume → re-enqueues the run via the bot.
|
|
* • Cancel run → stops the run cleanly (remaining pending → skipped).
|
|
*
|
|
* Pause notifications deep-link the operator into this surface.
|
|
*/
|
|
export function PausedRunBanner({
|
|
runId,
|
|
sent,
|
|
total,
|
|
windowEndHour,
|
|
timezone,
|
|
}: PausedRunBannerProps) {
|
|
const [pending, startTransition] = useTransition();
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const onResume = () =>
|
|
startTransition(async () => {
|
|
setError(null);
|
|
const r = await resumeReminderRunAction({ runId });
|
|
if (!r.ok) setError(r.error);
|
|
});
|
|
|
|
const onCancel = () =>
|
|
startTransition(async () => {
|
|
setError(null);
|
|
const r = await cancelReminderRunAction({ runId });
|
|
if (!r.ok) setError(r.error);
|
|
});
|
|
|
|
const remaining =
|
|
typeof sent === "number" && typeof total === "number"
|
|
? Math.max(0, total - sent)
|
|
: null;
|
|
|
|
return (
|
|
<div
|
|
className="rounded-lg border border-amber-500/40 bg-amber-500/5 p-4 space-y-3"
|
|
data-testid="paused-run-banner"
|
|
>
|
|
<div className="flex items-start gap-2">
|
|
<AlertCircleIcon className="size-4 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
|
<div className="space-y-1 text-sm">
|
|
<p className="font-medium">Reminder paused</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{typeof sent === "number" && typeof total === "number"
|
|
? `${sent} of ${total} groups delivered.`
|
|
: "The delivery window closed before all groups got the message."}{" "}
|
|
The deadline was {windowEndHour}:00 ({timezone}).{" "}
|
|
{remaining !== null && remaining > 0
|
|
? `Resume to send the remaining ${remaining}, or cancel the run.`
|
|
: "Resume to keep going, or cancel the run."}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{error && (
|
|
<div className="text-xs text-destructive">{error}</div>
|
|
)}
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={onResume}
|
|
disabled={pending}
|
|
className="gap-2"
|
|
data-testid="paused-resume"
|
|
>
|
|
{pending ? (
|
|
<Loader2Icon className="size-3.5 animate-spin" />
|
|
) : (
|
|
<PlayIcon className="size-3.5" />
|
|
)}
|
|
Resume
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={onCancel}
|
|
disabled={pending}
|
|
className="gap-2"
|
|
data-testid="paused-cancel"
|
|
>
|
|
<XIcon className="size-3.5" />
|
|
Cancel run
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|