import Link from "next/link"; import { ActivityIcon, AlertTriangleIcon, ArchiveIcon, ArchiveRestoreIcon, CheckCircle2Icon, MinusCircleIcon, PauseCircleIcon, PlayIcon, Trash2Icon, XCircleIcon, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { PageShell } from "@/components/page-shell"; import { EmptyState } from "@/components/empty-state"; import { getSeededOperator } from "@/lib/operator"; import { listActivityRuns } from "@/lib/queries"; import { archiveRunAction, clearHistoryAction, deleteRunAction, unarchiveRunAction, } from "@/actions/history"; import { SwipeableRow } from "@/components/swipeable-row"; import { ResumeRunButton } from "@/components/activity/resume-run-button"; function relativeTime(date: Date | string): string { const d = typeof date === "string" ? new Date(date) : date; const diffSec = Math.floor((Date.now() - d.getTime()) / 1000); const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); if (diffSec < 60) return rtf.format(-diffSec, "second"); if (diffSec < 3600) return rtf.format(-Math.floor(diffSec / 60), "minute"); if (diffSec < 86400) return rtf.format(-Math.floor(diffSec / 3600), "hour"); return rtf.format(-Math.floor(diffSec / 86400), "day"); } const RUN_STATUS_CONFIG: Record< string, { label: string; className: string; icon: React.ElementType } > = { success: { label: "Success", className: "bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border-transparent", icon: CheckCircle2Icon, }, paused: { label: "Paused", className: "bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 border-transparent", icon: PauseCircleIcon, }, partial: { label: "Partial", className: "bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 border-transparent", icon: AlertTriangleIcon, }, failed: { label: "Failed", className: "bg-red-500/15 text-red-600 dark:bg-red-500/20 dark:text-red-400 border-transparent", icon: XCircleIcon, }, skipped: { label: "Skipped", className: "bg-slate-200/60 text-slate-500 dark:bg-slate-700/40 dark:text-slate-400 border-transparent", icon: MinusCircleIcon, }, }; function RunStatusBadge({ status }: { status: string }) { const cfg = RUN_STATUS_CONFIG[status] ?? { label: status, className: "bg-secondary text-secondary-foreground border-transparent", icon: ActivityIcon, }; const Icon = cfg.icon; return ( {cfg.label} ); } type FilterValue = | "all" | "success" | "paused" | "partial" | "failed" | "skipped" | "archived"; const FILTER_TABS: { value: FilterValue; label: string }[] = [ { value: "all", label: "All" }, { value: "success", label: "Success" }, { value: "paused", label: "Paused" }, { value: "partial", label: "Partial" }, { value: "failed", label: "Failed" }, { value: "skipped", label: "Skipped" }, { value: "archived", label: "Archived" }, ]; interface PageProps { searchParams: Promise<{ filter?: string }>; } interface ShelfButtonProps { runId: string; isArchived: boolean; } /** * Left-shelf (revealed by swiping the row RIGHT). Hard-delete button. * iOS-Mail-style: destructive action lives on the leading edge. */ function DeleteShelfButton({ runId }: ShelfButtonProps) { return (
); } /** * Right-shelf (revealed by swiping the row LEFT). Archive (or Restore * when the row is already archived). Non-destructive trailing action. */ function ArchiveShelfButton({ runId, isArchived }: ShelfButtonProps) { return (
); } export default async function ActivityPage({ searchParams }: PageProps) { const sp = await searchParams; const filter: FilterValue = sp.filter === "success" || sp.filter === "paused" || sp.filter === "partial" || sp.filter === "failed" || sp.filter === "skipped" || sp.filter === "archived" ? sp.filter : "all"; const showingArchived = filter === "archived"; const op = await getSeededOperator(); const runs = await listActivityRuns(op.id, { archived: showingArchived }); const filtered = filter === "all" || filter === "archived" ? runs : runs.filter((r) => r.status === filter); const hasAny = runs.length > 0; return ( Clear all run history? This permanently removes every reminder run record, including runs from reminders that have already been deleted. Reminders themselves are not affected.
) : undefined } > {/* Six tabs (All / Success / Partial / Failed / Skipped / Archived) packed into a phone-width row left every label squeezed to ~50px. Wrap the list in an overflow-x scroller so each tab keeps a readable label + comfortable touch target on mobile; on desktop the row fits naturally and no scroll bar appears. Negative margins extend the scroller to the page edges so the first/last tabs don't look clipped against the container. */}
{FILTER_TABS.map(({ value, label }) => ( {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {label} ))}
{filtered.length > 0 ? ( <>

Swipe left to Delete, or right to {showingArchived ? "Restore" : "Archive"}.

{/* Mobile: swipeable cards */}
{filtered.map((run) => { const clickable = run.reminderId && !run.isDeleted; const inner = (

{run.reminderName} {run.isDeleted && ( (deleted) )}

{relativeTime(run.firedAt)}

); const card = ( {clickable ? ( {inner} ) : ( inner )} ); return ( } // Left swipe → reveal right shelf → Delete (destructive). rightActions={} > {card} ); })}
{/* Desktop: table with hover-revealed actions */}
Reminder Status Fired Actions {filtered.map((run) => { const clickable = run.reminderId && !run.isDeleted; const isArchived = Boolean(run.archivedAt); return ( {clickable ? ( {run.reminderName} ) : ( {run.reminderName} {run.isDeleted && " (deleted)"} )} {relativeTime(run.firedAt)}
{run.status === "paused" && ( )}
); })}
) : ( )}
); }