QR / pairing - Replace the per-QR 30 s countdown with a single pairing-window timer matching the bot's PAIR_TIMEOUT (5 minutes). Baileys naturally rotates QR images every ~5 s — the previous 30 s bar reset on every rotation, which felt like a constantly-cycling timer to the user. - The new timer starts on the first QR and ticks down once; later QR rotations refresh the displayed image but leave the countdown alone. - Added a hint: "The QR rotates automatically every few seconds — scan whichever one is showing." Format switches to MM:SS. - countdownRender's danger threshold scales: 10 s for short windows (≤ 60 s), 30 s for the multi-minute pairing window, so the warning flash appears while the user can still react. Reminder filter tabs - Tabs are now: All / Active / Ended / Paused. "Failed" is dropped — reminder.status doesn't carry "failed" (run statuses do; that view belongs in /activity?filter=failed). Tests (+4 = 84 passing total) - qr-dedupe.test.ts: extended with a "pairing-window scaling" suite covering pct/danger/expired at 5-minute scale and the threshold split between short and long windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
/**
|
|
* Per-account dedupe of inbound QR strings from Baileys. Pure logic so the
|
|
* pair-handler stays thin and the dedupe is unit-testable.
|
|
*
|
|
* Invariant: for a given accountId, identical QR payloads are dropped.
|
|
* A different payload (or the first ever for that account) returns true,
|
|
* meaning "yes, emit this one to the web".
|
|
*/
|
|
export function makeQrDedupe() {
|
|
const last = new Map<string, string>();
|
|
|
|
return {
|
|
/** Returns true if this payload should be forwarded; false if it's a dup. */
|
|
shouldEmit(accountId: string, payload: string): boolean {
|
|
if (last.get(accountId) === payload) return false;
|
|
last.set(accountId, payload);
|
|
return true;
|
|
},
|
|
/** Forget the last payload — call when the session ends or pairing restarts. */
|
|
reset(accountId: string): void {
|
|
last.delete(accountId);
|
|
},
|
|
/** Test helper — current cache size. */
|
|
size(): number {
|
|
return last.size;
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Pure logic for the pair page's countdown bar. Given a remaining-seconds
|
|
* value and the total window, return the rendering primitives. Extracted
|
|
* so the visual behaviour is unit-testable.
|
|
*/
|
|
export interface CountdownRender {
|
|
/** Width % for the progress bar [0..100]. */
|
|
pct: number;
|
|
/** True when the remaining time crosses the danger threshold — UI flips
|
|
* to destructive colours. Threshold scales with `total`: 10 s for short
|
|
* per-QR timers, 30 s for the multi-minute pairing window. */
|
|
danger: boolean;
|
|
/** True when the QR has expired (≤ 0). */
|
|
expired: boolean;
|
|
}
|
|
|
|
export function countdownRender(seconds: number, total: number): CountdownRender {
|
|
const safeTotal = total > 0 ? total : 1;
|
|
const clamped = Math.max(0, Math.min(seconds, safeTotal));
|
|
const pct = Math.round((clamped / safeTotal) * 100);
|
|
// Short windows (≤60 s) use a 10 s warning; longer ones (the 5-minute
|
|
// pairing window) use 30 s so it shows up while still actionable.
|
|
const dangerThreshold = safeTotal <= 60 ? 10 : 30;
|
|
return {
|
|
pct,
|
|
danger: clamped > 0 && clamped <= dangerThreshold,
|
|
expired: clamped <= 0,
|
|
};
|
|
}
|