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) <noreply@anthropic.com>
One component now owns the icon / heading / helper / action stack
that the dashboard, accounts list, reminders list, and activity tab
were each rendering inline. The four duplicated 'flex-col items-center
py-12 text-center' Card blocks collapse to one shared surface so the
empty experience reads the same wherever the user lands.
Covered by 4 SSR tests (icon + title + description, omitted helper,
action slot pass-through, centring).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single component now owns the page chrome — wrapper width, padding,
vertical rhythm, and the page-header row (hidden-on-mobile H1 + an
optional right-aligned action slot). Dashboard, Accounts, Reminders,
Activity, and Settings all use it, replacing five copies of the same
\`<div className=\"max-w-5xl mx-auto px-4 ...\">\` markup.
Settings was previously \`max-w-2xl\` and \`container mx-auto\`; it
now matches the other tabs at 5xl so the chrome stays consistent.
Covered by 5 SSR tests (header order, responsive justify utilities,
wrapper class, action-optional path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wizard When-step and the per-section Edit When page now expose an
optional "Pause sending by" hour. Fire time IS the implicit start, so
the deadline is the only thing the operator sets. When the bot's
fan-out hasn't finished by that hour (in the reminder's timezone) the
run pauses for resume — that runtime gating lands in a later phase;
this commit just persists the hour and threads it through the wizard.
HourSelect splits hour and AM/PM into two side-by-side <select>s and
emits a single 0..23 value. to12Hour / from12Hour are pure helpers
covered by 11 round-trip tests.
Dashboard adjustments:
* "WhatsApp accounts" card now reads Connected / Unpaired / Total.
* "Reminders" card reads Active / Paused / Ended / Total.
* "Recent runs" stat card removed (the Recent activity section below
shows the same info).
* Activity rows show absolute timestamp with AM/PM and relative time
in tandem.
Accounts list:
* The page-level <h1>Accounts</h1> is hidden on mobile (the top bar
already shows it), matching the Dashboard pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dashboard:
* "Active reminders" card retitled to "Reminders" and now shows
active / total in the same X / Y format as the Accounts card
(mirroring 2 / 3 connected / total).
Accounts list:
* The page-level <h1>Accounts</h1> is now hidden on mobile (the top
bar already shows it), matching the Dashboard pattern. The
"Add Account" button still shows on every breakpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile header strip carries the current section title in its centre
slot ("Dashboard" / "Reminders" / etc.). The top-level pages were
ALSO rendering the same string in an H1 right below — duplicate
labelling, wasted vertical space, and the H1 was the first thing
that overlapped the header on tight viewports.
Switched the four duplicates to `hidden sm:block`:
- / (Dashboard)
- /reminders
- /activity
- /settings
Desktop sidebar has no per-page title chip, so the H1 stays visible
sm: and up. Sub-pages (account detail, group detail, reminder
detail, "New Reminder", "Add Account") have dynamic H1s that don't
duplicate the header — those keep their visibility unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DialogFooter with showCloseButton now lays out the auto-rendered Close
and the caller's primary action (typically a <form>-wrapped Submit) in
a 2-column grid that's identical at every viewport. Both buttons are
sized "sm" with w-full so they fill their column and match in height.
The trick to making this transparent for callers: \`[&>form]:contents\`
collapses the form box so its <Button> child becomes a real grid item
sibling of the Close button, not a single grid cell containing the
button. \`[&>form>button]:w-full\` then sizes the submit button to
match the Close button's column width.
Five existing call sites pick this up automatically — no changes
needed at the call site:
- reminder pause/restart/delete (actions-bar)
- account unpair / delete
- dashboard "Clear history"
- activity tab "Clear history"
Also: dashboard "Recent activity" now shows the 3 most recent runs
instead of 10. The "Recent runs" stat card description updates to
match ("3 most recent runs"), points to /activity, and a "View all"
ghost link sits beside the section heading so you can jump to the
full history without hunting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dashboard
- Stat cards are now clickable: Accounts → /accounts, Active reminders →
/reminders?filter=active, Recent runs → /reminders.
- Recent activity rows link to the underlying reminder when it still
exists. Runs whose reminder has been deleted render with a "(deleted)"
marker and stay non-clickable.
- New "Clear history" action wipes all run rows the operator owns plus
any orphan rows (reminderId=NULL).
Run history persists after reminder delete
- reminder_runs.reminder_id is now nullable with ON DELETE SET NULL, so
deleting a reminder no longer cascade-erases its history.
- New reminder_runs.reminder_name column snapshots the name at fire
time so history rows stay readable even after the reminder is gone.
- Fire-reminder records the snapshot.
- Dashboard query LEFT JOINs and COALESCEs name from the live reminder,
the snapshot, or "(deleted reminder)" as last resort.
QR
- Drop the 25 s server-side throttle. With listener accumulation already
fixed (previous commit), the payload-equality dedupe is enough.
Symptom: after the first QR expired the throttle blocked the next
emit, and the QR never refreshed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>