3 Commits

Author SHA1 Message Date
ec57a78853 feat(send-test): close the loop — bot reports done back to the form
The send-test form was stuck on "Sending to <Group>…" because the
server action returns the moment it publishes the IPC NOTIFY; the bot
processed the actual WhatsApp send out-of-band and the form had no way
to learn whether it succeeded.

Round-trip now wired end-to-end:

- New WebEvent variant `send_test.done` { groupId, ok, error }.
- bot/src/ipc/send-test-handler emits it on every exit path:
  - missing group   → ok=false, "Group not found"
  - account offline → ok=false, "Account not connected — re-pair first"
  - send threw      → ok=false, error message
  - send succeeded  → ok=true,  null
- web/src/hooks/use-events declares the new event in its type map.
- web SendTestForm subscribes via useEvents, filters by its own
  groupId so a parallel send-test on another group can't move our
  state, and renders one of three pills:
    * Sending…           (in-flight — Loader2 spinner)
    * Sent ✓             (success — emerald CheckCircle2)
    * <error message>    (failure — destructive AlertCircle)
  The "Send Test" button stays disabled while in-flight.

Tests (+5; 110 web tests total):
  send-test-form.test.tsx
  - SSR markup: textarea, submit button, hidden groupId, no premature
    pill on first render.
  - useEvents wiring: form registers a `send_test.done` handler.
  - Handler safely accepts:
    * matching success event
    * matching failure event
    * mismatched groupId (must not throw)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:04:33 +08:00
2b738383e4 feat: recurring reminders, fix QR pairing, account UX polish, tests
Reminders
- Add recurrence to wizard step 3 (None / Daily / Weekly+weekday picker /
  Monthly / Yearly). Build the RRULE client-side and thread it through
  the wizard URL state.
- Action stores rrule + scheduleKind="recurring" on insert.
- Bot reschedules the next occurrence after firing a recurring reminder
  using the existing rrule helpers in @cmbot/shared. One-off behavior
  unchanged.
- Add reminders.last_fired_at column to track last fire.

Pairing
- Move QR PNG out of the pg_notify payload (the 8000-byte limit was
  silently truncating it; QR never reached the web → "QR hang"). PNG
  now lives on whatsapp_accounts.last_qr_png; NOTIFY just signals
  {type: session.qr, accountId, ts}. Web fetches the bytes from a new
  read-only /api/qr/[accountId] route (allowed via middleware).
- handleStartPairing now stops any in-flight session before starting a
  fresh one — fixes Re-pair where session.start was a silent no-op and
  Baileys never re-emitted QR.
- Pair-live: countdown moved out from over the QR (it was overlapping
  the scan area); shown as a discrete progress bar above the QR.
- Add a "Save QR" download button.

Account detail page
- Pair / Unpair / Delete cards are themselves the trigger (form submit
  or DialogTrigger) — no inline buttons, whole card is clickable.
- Sync Groups Now card removed earlier; bot already auto-syncs.

Account list page
- Cards are the link target. A small floating Delete trigger (top-right
  trash icon) opens the destructive confirm dialog without blocking
  navigation on the rest of the card.

Tests
- recurrence.test.ts: 10 tests for buildRrule / kindFromRrule /
  describeRecurrence (incl. weekly day combos and BYDAY ordering).
- reminders.schema.test.ts: regression for the "Invalid datetime" bug —
  proves strict Zod .datetime() rejected luxon's offset ISO and the
  { offset: true } option accepts both forms.

Migration: 0004_next_prowler.sql
- whatsapp_accounts.last_qr_png (text)
- reminders.last_fired_at (timestamptz)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:01:31 +08:00
1fe674c70e feat(web): SSE endpoint + useEvents hook 2026-05-09 23:11:25 +08:00