diff --git a/apps/bot/src/ipc/pair-handler.ts b/apps/bot/src/ipc/pair-handler.ts index ad4d31f..966163b 100644 --- a/apps/bot/src/ipc/pair-handler.ts +++ b/apps/bot/src/ipc/pair-handler.ts @@ -37,8 +37,14 @@ async function abandonPair(accountId: string): Promise<{ existed: boolean; label if (sessionManager.hasSession(accountId)) { await sessionManager.stop(accountId); } + // Throw away the partial Baileys session files so the next pair + // attempt starts clean — but KEEP the account row so the operator + // sees it on the list with a "Re-pair" affordance. await rm(join(env.SESSIONS_DIR, accountId), { recursive: true, force: true }); - await db.delete(whatsappAccounts).where(eq(whatsappAccounts.id, accountId)); + await db + .update(whatsappAccounts) + .set({ status: "unpaired", lastQrPng: null }) + .where(eq(whatsappAccounts.id, accountId)); return { existed: true, label: account.label }; } @@ -132,8 +138,9 @@ export async function handleStartPairing(accountId: string): Promise { // During the pairing window, ANY close means the QR window // ended without a successful link — Baileys' default is to // close after exhausting QR refs (~2.5 min). Surface this to - // the UI so the user gets a "pairing timed out" screen and a - // chance to retry, instead of staring at a stale QR forever. + // the UI so the user gets a "pairing timed out" screen, and + // park the row in a stable state so it shows up cleanly on + // the accounts list with a "Re-pair" affordance. const t = pairTimeouts.get(id); if (t) { clearTimeout(t); @@ -141,6 +148,10 @@ export async function handleStartPairing(accountId: string): Promise { } lastQrPayload.delete(id); offByAccount.delete(id); + await db + .update(whatsappAccounts) + .set({ status: "unpaired", lastQrPng: null }) + .where(eq(whatsappAccounts.id, id)); await pgNotifyWeb({ type: "session.timeout", accountId: id }); off(); } @@ -175,7 +186,12 @@ export async function handleStartPairing(accountId: string): Promise { pairTimeouts.set(accountId, timeoutId); } -/** Sweep stale pending accounts on bot startup. */ +/** + * Sweep stale `pending` accounts on bot startup. The bot was probably + * restarted mid-pair (or the operator never finished scanning) — the + * row is parked as `unpaired` so the operator sees it on the list and + * can hit Re-pair, instead of silently disappearing. + */ export async function sweepStalePendingAccounts(): Promise { const cutoff = new Date(Date.now() - 60 * 60 * 1000); const stale = await db @@ -184,7 +200,10 @@ export async function sweepStalePendingAccounts(): Promise { .where(and(eq(whatsappAccounts.status, "pending"), lt(whatsappAccounts.createdAt, cutoff))); for (const row of stale) { await rm(join(env.SESSIONS_DIR, row.id), { recursive: true, force: true }); - await db.delete(whatsappAccounts).where(eq(whatsappAccounts.id, row.id)); - logger.info({ accountId: row.id, label: row.label }, "sweep: removed stale pending account"); + await db + .update(whatsappAccounts) + .set({ status: "unpaired", lastQrPng: null }) + .where(eq(whatsappAccounts.id, row.id)); + logger.info({ accountId: row.id, label: row.label }, "sweep: parked stale pending account as unpaired"); } } diff --git a/apps/web/src/lib/queries.ts b/apps/web/src/lib/queries.ts index 34ccba8..58dd900 100644 --- a/apps/web/src/lib/queries.ts +++ b/apps/web/src/lib/queries.ts @@ -44,13 +44,14 @@ export async function getDashboardStats(operatorId: string) { } export async function listAccounts(operatorId: string) { - // Show all accounts except those in the transient `pending` state - // (active QR scan in progress — they're surfaced via the pairing page, - // and abandoned ones are swept by the bot). Operators see unpaired, - // connected, disconnected, and banned accounts so they can manage them. + // Show every account the operator owns, regardless of status. The + // status badge tells the user what state each row is in (Pending / + // Unpaired / Connected / Disconnected / etc), and the detail page + // exposes Pair / Re-pair / Delete actions accordingly. Hiding rows + // by status produced phantom "I created an account but it's gone" + // bug reports. return db.query.whatsappAccounts.findMany({ - where: (a, { eq, and, ne }) => - and(eq(a.operatorId, operatorId), ne(a.status, "pending")), + where: (a, { eq }) => eq(a.operatorId, operatorId), orderBy: (a, { asc }) => [asc(a.label)], }); }