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>
31 lines
1.3 KiB
TypeScript
31 lines
1.3 KiB
TypeScript
import { sql } from "drizzle-orm";
|
|
import { db } from "../db.js";
|
|
import { logger } from "../logger.js";
|
|
|
|
export type WebEvent =
|
|
// QR PNG bytes live in `whatsapp_accounts.last_qr_png` so this NOTIFY
|
|
// payload stays under Postgres' 8000-byte limit. Web fetches the PNG
|
|
// from /api/qr/[accountId] when it sees this event.
|
|
| { type: "session.qr"; accountId: string; ts: number }
|
|
| { type: "session.connected"; accountId: string; phoneNumber: string | null }
|
|
| { type: "session.disconnected"; accountId: string }
|
|
| { type: "session.timeout"; accountId: string }
|
|
| { type: "groups.synced"; accountId: string; count: number }
|
|
| { type: "reminder.fired"; reminderId: string; runId: string; status: string }
|
|
| { type: "reminder.failed"; reminderId: string; error: string }
|
|
// The web action enqueues a send_test via pg_notify and shows
|
|
// "Sending…" optimistically. This event closes the loop.
|
|
| {
|
|
type: "send_test.done";
|
|
groupId: string;
|
|
ok: boolean;
|
|
error: string | null;
|
|
};
|
|
|
|
export async function pgNotifyWeb(event: WebEvent): Promise<void> {
|
|
const json = JSON.stringify(event);
|
|
// pg_notify takes a literal channel name as 1st arg.
|
|
await db.execute(sql`SELECT pg_notify('web.event', ${json})`);
|
|
logger.debug({ event: event.type }, "ipc: web.event published");
|
|
}
|