6 Commits

Author SHA1 Message Date
6cb387bf59 feat: per-section reminder edit, activity tab, more tests
Per-section reminder editing
- Replace the wizard-redirect edit shell with four focused single-form
  pages: /reminders/[id]/edit/{account,message,when,groups}.
- Each click on a section card on the detail page goes straight to the
  matching focused editor — no stepper, no other sections, no
  wizard chrome. Save returns to the detail page.
- New form components live under components/reminder-edit/:
  EditMessageForm, EditWhenForm (full recurrence builder reused),
  EditGroupsForm, EditAccountForm. All submit via updateReminderAction
  with the existing values for untouched fields. Switching account
  clears group targets (groups are scoped per account; the form warns
  and the user re-picks groups afterwards).

Activity tab
- New "Activity" item in the bottom nav + sidebar (between Reminders
  and Settings).
- /activity page: full run history (last 200), filter tabs (All /
  Success / Partial / Failed / Skipped), clickable rows that open the
  underlying reminder, and a Clear history dialog. Mirrors the
  dashboard's Recent Activity widget but with deeper data and its own
  empty-state messaging.

Tests (+20 — 80 passing total)
- qr-dedupe.test.ts: 14 tests covering the makeQrDedupe factory (per-
  account, fresh QRs always emit, reset/scope) and countdownRender
  (the QR-expired timer logic — danger threshold, expired flag,
  clamping). The dedupe + countdown logic is now used by pair-handler
  and pair-live.
- reminder-edit/edit-message-form.test.tsx: 6 tests verifying the form
  pre-fills, hides/shows the caption based on attachment, renders the
  Save (not "Schedule reminder") action, and the action receives the
  expected payload shape for both text-only and media-attached paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:35:17 +08:00
f19ea03e0d feat: edit reminders, mature recurrence, QR throttle, more tests
Reminders
- Reminder list / detail show recurrence summary ("Every Mon, Wed, Fri",
  "Every 2 weeks until 2027-01-01", etc).
- Detail page reorganised: each section (Account / Message / When /
  Groups) is itself a clickable card that deep-links into the wizard
  step in edit mode (editReminderId URL param). No standalone Edit
  button. Run history stays read-only.
- New /reminders/[id]/edit shell loads the row, encodes its state into
  wizard URL params, and forwards to /reminders/new. The wizard
  threads editReminderId through every step.
- updateReminderAction: validates ownership of both the existing
  reminder and the (possibly changed) target account, replaces targets
  + messages wholesale, re-arms the pg-boss job (singleton key picks
  up the new fire time).
- Wizard submit branches to updateReminderAction when editReminderId
  is set; button reads "Save changes" / "Saving…".
- Wizard default first-fire is now the current minute in the operator
  zone (not now+1h). Same-minute clicks bump silently to next minute
  via a 60 s grace window so the user isn't punished.
- /reminders empty state is filter-aware: "No failed reminders yet."
  when ?filter=failed and there are reminders in other states.

Recurrence
- Spec is now a structured object: { kind, interval, weeklyDays,
  monthDay, end }. Builder produces RRULEs with INTERVAL, BYDAY,
  BYMONTHDAY, COUNT, UNTIL as appropriate. specFromRrule round-trips
  for resuming/edit.
- When-step UI: frequency pills, "Every N days/weeks/…" interval,
  weekday picker (weekly), day-of-month input (monthly), end picker
  (Never / After N occurrences / On date), live human-readable
  summary preview.

QR pairing
- Throttle QR refresh to once per 25 s and detach the previous
  per-account session listener on Re-pair so listeners can't
  accumulate. The UI countdown was flicking every ~5 s because each
  Re-pair attached an extra listener — every Baileys QR event then
  triggered a fresh DB write + NOTIFY.

Tests (60 green total, +33 in this batch)
- recurrence.test.ts: extended to 25 tests covering interval,
  monthday, end conditions (COUNT/UNTIL), and round-trip parsing.
- date-picker.test.ts: 14 tests for splitDateTime / combineDateTime /
  validateScheduledAt (incl. the "click-too-fast" same-minute grace)
  and defaultFirstFireIso.
- /api/qr/[accountId] route.test.ts: 4 tests — 404 when no QR yet,
  404 on missing row, 200 with image/png + no-store + correct PNG
  bytes, and verifies the where-clause queries by accountId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:22:22 +08:00
86f2fe0124 fix(web): reminder wizard date/time picker, reorder, optional groups
- Fix "Invalid datetime" error: createReminderAction's Zod schema rejected
  offset-suffixed ISO strings (luxon's `toISO()` produces +08:00 form).
  Switched to `.datetime({ offset: true })`.

- Replace the single datetime-local input with separate native date + time
  inputs (proper UI pickers on both desktop and mobile). Default value is
  now computed server-side ("now + 1h") and passed in as a prop, so first
  render is fully populated and there's no SSR/client hydration mismatch
  from `Date.now()` inside the client component. Removed the quick-pick
  shortcuts.

- Reorder wizard steps: Account → Compose → When → Groups → Review.
  Groups is now the last and optional step (Continue button reads
  "Skip groups" when empty); the action accepts an empty array and
  inserts no reminder_targets in that case.

- Account list: card is the link target. Removed inline Pair / Open /
  Delete quick-action buttons; lifecycle actions stay on the detail page.

- Account detail: removed the "Sync Groups Now" card. The bot already
  auto-syncs on `groups.upsert` / `groups.update` events. The Groups card
  itself is now a clickable link instead of carrying an inline View
  button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:45:19 +08:00
9437df74ee feat(web): split Add Account from Pair; add Unpair/Re-pair/Delete actions
Reshape the account lifecycle to match how operators actually want to
work the system:

- Add Account → creates a row with status='unpaired'. No QR yet; the
  operator lands on the detail page.
- Pair / Re-pair → transitions an unpaired account to status='pending'
  and opens the live QR flow. Works for first-time pair AND for re-pair
  of an account that was previously unpaired.
- Unpair → asks the bot to stop the live Baileys session and clean
  session files; sets status='unpaired' but KEEPS the row (and its
  reminders) so the operator can re-pair without retyping anything.
- Delete → permanently removes the account and cascades to its groups,
  reminders, run history.

Schema:
- whatsapp_groups.account_id and reminders.account_id now have
  ON DELETE CASCADE so deleting an account fans out cleanly.

UI:
- /accounts list shows everything except the transient 'pending' state.
- /accounts/[id] shows state-aware buttons: Pair (when unpaired/banned/
  disconnected), Sync + Unpair (when connected), Delete (always).
- /accounts/new is now an "Add Account" form (label only).

Other fixes:
- next.config.ts: allowedDevOrigins includes 192.168.0.253 +
  test/rexwa subdomains so Server Actions work across the LAN.
- packages/shared/src/rrule.ts: rrule@2.8.1 has no exports field and
  ships ESM that some bundlers can't resolve via default OR named
  import. Use createRequire to bridge — works under both NodeNext
  (bot runtime) and Turbopack (web SSR).
2026-05-10 00:27:33 +08:00
6916f5a0ed feat(web): delete reminder server action wired to detail page 2026-05-09 23:46:23 +08:00
8fd5468e3a feat(web): reminders list + detail pages with run history 2026-05-09 23:36:18 +08:00