Three small bugs the user surfaced after the swipe rollout:
1. Editing a paused/ended one-off reminder threw "Time is in the past"
----------------------------------------------------------------------
The four edit-section pages (account / message / groups / when) all
POST through `updateReminderAction`. The action's "scheduledAt
must be in the future" check fires on every submit — including the
three section pages that don't change the time and just pass the
original `scheduledAt` straight through. So a user editing the
message body of a reminder they paused yesterday saw their save
rejected with "Time is in the past".
New pure helper `validateUpdateScheduledAt` in lib/reminder-update.ts
keeps the future-time check in place for active reminders that are
actually changing the time, but allows past timestamps when:
- the reminder is paused or ended (won't fire while in those
states regardless of what the row says about scheduledAt), OR
- the submitted timestamp matches the existing one within a
second of rounding (the form is a passthrough).
Tests: 10 cases in `lib/reminder-update.test.ts` covering active
future, active past, paused passthrough, ended passthrough, paused
with deliberate change, sub-second drift tolerance, exact-NOW edge,
null existing scheduledAt, malformed ISO.
Also (drive-by, related): `updateReminderAction` no longer force-
sets `status: "active"` on save. Editing a paused reminder's
message shouldn't silently un-pause it. The user uses Restart for
that.
2. Reminder list reshuffled after Pause/Restart
----------------------------------------------------------------------
The list defaulted to `sort=scheduled_desc`, so clicking Restart on
row N (which moves scheduledAt forward to the next occurrence) flipped
the row to row 0. Felt like the wrong action ran. Fixed:
- Page now hard-codes `sort = "created_desc"` (created_at never
changes, so a row stays where it is).
- Sort dropdown removed from `<ReminderFilterBar>` since it has
nothing to drive anymore. Account + Group filters and the
search box stay.
3. Swipe shelf stayed open after the action ran
----------------------------------------------------------------------
`SwipeableRow` keeps its offset in component state. When a shelf
button submits the form, the page revalidates and re-renders, but
React keeps the same row instance (matched by `key={reminder.id}`),
so the open offset stuck around. Now both row sites encode the
"row state" into the key:
- reminders: `key={\`${reminder.id}-${reminder.status}\`}`
- activity: `key={\`${run.id}-${run.archivedAt ? "1" : "0"}\`}`
Status flip → key change → React unmounts/remounts → offset back
to 0 → shelf closed. Costs nothing (these rows are cheap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>