From be3f28a1e6ab77d554f06cb362e7c7cafe82b0e2 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 10 May 2026 16:32:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor(web,bot,db):=20rename=20reminder=20sta?= =?UTF-8?q?tus=20'ended'=20=E2=86=92=20'inactive'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'ended' label read like a terminal failure state ("the reminder gave up") when in practice it just means "this reminder isn't going to fire on its own — restart it if you want it back". 'inactive' is the more accurate read. * SQL migration 0009 backfills existing rows. * Bot fire-reminder writes 'inactive' on one-off completion / no further occurrences. * Web actions, queries, filters, and reminder lifecycle gates updated. * Dashboard counter card label "Active / Paused / Ended / Total" becomes "Active / Paused / Inactive / Total". * Reminders list filter tab "Ended" becomes "Inactive". * Status pill style key renamed to match. * Tests updated alongside the runtime changes. Also: the "Pause sending by" deadline opt-in now renders as a visible card-shaped row with hover state + Set/Off label on the right, so the toggle is discoverable instead of a tiny native checkbox tucked next to the label. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/bot/src/scheduler/fire-reminder.test.ts | 2 +- apps/bot/src/scheduler/fire-reminder.ts | 4 ++-- apps/web/src/actions/reminders.run-actions.test.ts | 2 +- apps/web/src/actions/reminders.ts | 2 +- apps/web/src/app/page.tsx | 4 ++-- .../web/src/app/reminders/[id]/actions-bar.test.tsx | 4 ++-- apps/web/src/app/reminders/[id]/actions-bar.tsx | 4 ++-- apps/web/src/app/reminders/[id]/page.tsx | 2 +- apps/web/src/app/reminders/page.tsx | 10 +++++----- .../src/components/reminder-edit/edit-when-form.tsx | 13 ++++++++----- .../components/reminder-wizard/when-form-client.tsx | 13 ++++++++----- apps/web/src/lib/queries.ts | 2 +- apps/web/src/lib/reminder-filter.test.ts | 6 +++--- apps/web/src/lib/reminder-filter.ts | 2 +- apps/web/src/lib/reminder-update.test.ts | 4 ++-- apps/web/src/lib/reminder-update.ts | 2 +- .../db/migrations/0009_rename_ended_to_inactive.sql | 4 ++++ packages/db/migrations/meta/_journal.json | 7 +++++++ 18 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 packages/db/migrations/0009_rename_ended_to_inactive.sql diff --git a/apps/bot/src/scheduler/fire-reminder.test.ts b/apps/bot/src/scheduler/fire-reminder.test.ts index de2f3a3..5b173be 100644 --- a/apps/bot/src/scheduler/fire-reminder.test.ts +++ b/apps/bot/src/scheduler/fire-reminder.test.ts @@ -82,7 +82,7 @@ describe("fireReminder", () => { getReminderMock.mockResolvedValue({ id: "r-1", accountId: "acct-A", - status: "ended", + status: "inactive", targets: [], messages: [], createdBy: "op-1", diff --git a/apps/bot/src/scheduler/fire-reminder.ts b/apps/bot/src/scheduler/fire-reminder.ts index 8e2a2e4..5cbf51b 100644 --- a/apps/bot/src/scheduler/fire-reminder.ts +++ b/apps/bot/src/scheduler/fire-reminder.ts @@ -394,7 +394,7 @@ async function fireReminderInner( if (reminder.scheduleKind === "one_off") { await db .update(reminders) - .set({ status: "ended", updatedAt: new Date() }) + .set({ status: "inactive", updatedAt: new Date() }) .where(eq(reminders.id, reminder.id)); } else if (reminder.scheduleKind === "recurring" && reminder.rrule) { const next = nextOccurrence(reminder.rrule, reminder.timezone, new Date()); @@ -417,7 +417,7 @@ async function fireReminderInner( } } else { logger.info({ reminderId: reminder.id }, "fire-reminder: no further occurrences, ending"); - await db.update(reminders).set({ status: "ended" }).where(eq(reminders.id, reminder.id)); + await db.update(reminders).set({ status: "inactive" }).where(eq(reminders.id, reminder.id)); } } } diff --git a/apps/web/src/actions/reminders.run-actions.test.ts b/apps/web/src/actions/reminders.run-actions.test.ts index b781a71..1c87bb2 100644 --- a/apps/web/src/actions/reminders.run-actions.test.ts +++ b/apps/web/src/actions/reminders.run-actions.test.ts @@ -206,6 +206,6 @@ describe("cancelReminderRunAction", () => { await cancelReminderRunAction({ runId: PAUSED_RUN.id }); const calls = setSpy.mock.calls; const lastPayload = calls[calls.length - 1]?.[0] as Record; - expect(lastPayload.status).toBe("ended"); + expect(lastPayload.status).toBe("inactive"); }); }); diff --git a/apps/web/src/actions/reminders.ts b/apps/web/src/actions/reminders.ts index 3910d2d..5bf4885 100644 --- a/apps/web/src/actions/reminders.ts +++ b/apps/web/src/actions/reminders.ts @@ -660,7 +660,7 @@ export async function cancelReminderRunAction(input: { await tx .update(reminders) .set({ - status: reminder.scheduleKind === "recurring" ? "active" : "ended", + status: reminder.scheduleKind === "recurring" ? "active" : "inactive", updatedAt: new Date(), }) .where(eq(reminders.id, reminder.id)); diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index b3fdc65..abff01e 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -182,9 +182,9 @@ export default async function DashboardPage() { /> diff --git a/apps/web/src/app/reminders/[id]/actions-bar.test.tsx b/apps/web/src/app/reminders/[id]/actions-bar.test.tsx index b453360..31eb349 100644 --- a/apps/web/src/app/reminders/[id]/actions-bar.test.tsx +++ b/apps/web/src/app/reminders/[id]/actions-bar.test.tsx @@ -41,9 +41,9 @@ describe("ActionsBar — card visibility by status", () => { expect(html).not.toMatch(/aria-label="Pause"/); }); - it("ended: shows Restart and Delete (no Pause)", () => { + it("inactive: shows Restart and Delete (no Pause)", () => { const html = renderToStaticMarkup( - , + , ); expect(html).toMatch(/aria-label="Restart"/); expect(html).toMatch(/aria-label="Delete"/); diff --git a/apps/web/src/app/reminders/[id]/actions-bar.tsx b/apps/web/src/app/reminders/[id]/actions-bar.tsx index 2cf9e20..0096f0b 100644 --- a/apps/web/src/app/reminders/[id]/actions-bar.tsx +++ b/apps/web/src/app/reminders/[id]/actions-bar.tsx @@ -38,7 +38,7 @@ interface ActionsBarProps { * on desktop, stacked on mobile: * * - Pause — only when status === "active" - * - Restart — when status is "paused" or "ended" + * - Restart — when status is "paused" or "inactive" * - Delete — always available (terminal) * * Each Dialog confirms before firing the corresponding server action. @@ -46,7 +46,7 @@ interface ActionsBarProps { */ export function ActionsBar({ reminderId, status, isRecurring }: ActionsBarProps) { const canPause = status === "active"; - const canRestart = status === "paused" || status === "ended"; + const canRestart = status === "paused" || status === "inactive"; return (
diff --git a/apps/web/src/app/reminders/[id]/page.tsx b/apps/web/src/app/reminders/[id]/page.tsx index a2bbfac..be9769b 100644 --- a/apps/web/src/app/reminders/[id]/page.tsx +++ b/apps/web/src/app/reminders/[id]/page.tsx @@ -48,7 +48,7 @@ function formatWhen(date: Date | null, tz: string): string { const STATUS_STYLES: Record = { active: "bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border-transparent", - ended: + inactive: "bg-slate-200/60 text-slate-500 dark:bg-slate-700/40 dark:text-slate-400 border-transparent", paused: "bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 border-transparent", diff --git a/apps/web/src/app/reminders/page.tsx b/apps/web/src/app/reminders/page.tsx index 3e5aab1..f92e27d 100644 --- a/apps/web/src/app/reminders/page.tsx +++ b/apps/web/src/app/reminders/page.tsx @@ -32,7 +32,7 @@ import { restartReminderAction, } from "@/actions/reminders"; -type FilterValue = "all" | "active" | "ended" | "paused"; +type FilterValue = "all" | "active" | "inactive" | "paused"; function formatWhen(date: Date | null, tz: string): string { if (!date) return "—"; @@ -48,7 +48,7 @@ function formatWhen(date: Date | null, tz: string): string { const STATUS_STYLES: Record = { active: "bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border-transparent", - ended: + inactive: "bg-slate-200/60 text-slate-500 dark:bg-slate-700/40 dark:text-slate-400 border-transparent", paused: "bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 border-transparent", @@ -104,7 +104,7 @@ function StatusPill({ status }: { status: string }) { const FILTER_TABS: { value: FilterValue; label: string }[] = [ { value: "all", label: "All" }, { value: "active", label: "Active" }, - { value: "ended", label: "Ended" }, + { value: "inactive", label: "Inactive" }, { value: "paused", label: "Paused" }, ]; @@ -127,7 +127,7 @@ interface PageProps { export default async function RemindersPage({ searchParams }: PageProps) { const sp = await searchParams; const status: FilterValue = - sp.filter === "active" || sp.filter === "ended" || sp.filter === "paused" + sp.filter === "active" || sp.filter === "inactive" || sp.filter === "paused" ? sp.filter : "all"; // Sort is now fixed to `created_desc`. Reordering on every status flip @@ -225,7 +225,7 @@ export default async function RemindersPage({ searchParams }: PageProps) { {visible.map((reminder) => { const canPause = reminder.status === "active"; const canRestart = - reminder.status === "paused" || reminder.status === "ended"; + reminder.status === "paused" || reminder.status === "inactive"; const cardBody = ( -
-