diff --git a/apps/web/src/app/activity/page.tsx b/apps/web/src/app/activity/page.tsx index 7018a85..890d9bc 100644 --- a/apps/web/src/app/activity/page.tsx +++ b/apps/web/src/app/activity/page.tsx @@ -106,24 +106,25 @@ function RunStatusBadge({ status }: { status: string }) { ); } -type FilterValue = - | "all" - | "success" - | "paused" - | "partial" - | "failed" - | "skipped" - | "archived"; +type FilterValue = "all" | "success" | "paused" | "failed" | "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" }, ]; +// Partial runs (some recipients ok, some failed) surface under BOTH the +// Paused and Failed tabs — the operator wants to see anything that didn't +// fully succeed on either page. Skipped runs collapse into Archived since +// they're effectively "history that the operator chose not to send". +const FILTER_STATUSES: Record, string[]> = { + success: ["success"], + paused: ["paused", "partial"], + failed: ["failed", "partial"], +}; + interface PageProps { searchParams: Promise<{ filter?: string }>; } @@ -185,9 +186,7 @@ export default async function ActivityPage({ searchParams }: PageProps) { const filter: FilterValue = sp.filter === "success" || sp.filter === "paused" || - sp.filter === "partial" || sp.filter === "failed" || - sp.filter === "skipped" || sp.filter === "archived" ? sp.filter : "all"; @@ -198,7 +197,7 @@ export default async function ActivityPage({ searchParams }: PageProps) { const filtered = filter === "all" || filter === "archived" ? runs - : runs.filter((r) => r.status === filter); + : runs.filter((r) => FILTER_STATUSES[filter].includes(r.status)); const hasAny = runs.length > 0; return ( @@ -235,26 +234,25 @@ export default async function ActivityPage({ searchParams }: PageProps) { ) : 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 span the full row and wrap onto a second line when the + viewport can't fit them all. Each trigger has a small basis so they + share space evenly while still keeping a readable label on mobile. */} -
- - {FILTER_TABS.map(({ value, label }) => ( - - {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - - {label} - - - ))} - -
+ + {FILTER_TABS.map(({ value, label }) => ( + + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + + {label} + + + ))} +
{filtered.length > 0 ? ( diff --git a/apps/web/src/components/reminder-filter-bar.tsx b/apps/web/src/components/reminder-filter-bar.tsx index 2f523fa..b69586e 100644 --- a/apps/web/src/components/reminder-filter-bar.tsx +++ b/apps/web/src/components/reminder-filter-bar.tsx @@ -99,7 +99,7 @@ export function ReminderFilterBar({ accounts }: FilterBarProps) { id="filter-account" value={initial.accountId} onChange={(e) => setParam("accountId", e.target.value)} - className="h-8 w-full sm:max-w-xs rounded-lg border border-input bg-transparent px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + className="h-8 w-full rounded-lg border border-input bg-transparent px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" > {accounts.map((a) => ( diff --git a/apps/web/src/lib/queries.ts b/apps/web/src/lib/queries.ts index 8a4f0f4..ae57880 100644 --- a/apps/web/src/lib/queries.ts +++ b/apps/web/src/lib/queries.ts @@ -187,11 +187,13 @@ export async function listActivityRuns( // exposes a stable shape. LEFT JOIN keeps orphan runs (whose reminder // has been deleted but history was preserved) in the list. // The `archived` flag flips the visibility filter: - // false (default) — only non-archived rows - // true — only archived rows (for the Archived tab) + // false (default) — non-archived, non-skipped rows (skipped runs + // belong to the Archived tab now) + // true — archived rows OR skipped rows (they're treated + // as "history" rather than active outcomes) const archivedClause = opts.archived - ? sql`rr.archived_at IS NOT NULL` - : sql`rr.archived_at IS NULL`; + ? sql`(rr.archived_at IS NOT NULL OR rr.status = 'skipped')` + : sql`rr.archived_at IS NULL AND rr.status <> 'skipped'`; const rows = await db.execute(sql` SELECT rr.id,