From fa583cd279ec24d0272715ae93a5f27bde88e7a6 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 23 Apr 2026 13:58:53 +0000 Subject: [PATCH] fix(timer): dismiss actually cancels on server + shorter retention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: после перезагрузки страницы оверлей «Таймер прозвенел» открывался снова и снова. Две причины: - dismissTimer в TimerWidget удалял таймер только из локального useState, но /data/tablet-timers.json оставался нетронутым. После reload таймер возвращался в список и firedRef (которая пустая после reload) снова триггерила alarm. - lib/timers.ts держал просроченные таймеры 30 минут, давая им шанс повторно сработать при каждом reload в этом окне. Фикс: - dismissTimer теперь POST /api/voice/timer {action:cancel, id} через cookie auth (endpoint с прошлого коммита принимает и cookie, и bearer). - Retention в listActive снижена до 30 секунд — этого хватает чтобы клиент увидел свежий звонок; старше = самоудаление. - TimerWidget клиентский фильтр тоже 30 секунд. --- components/TimerWidget.tsx | 22 ++++++++++++++-------- lib/timers.ts | 6 ++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/components/TimerWidget.tsx b/components/TimerWidget.tsx index d78ce2a..b911d67 100644 --- a/components/TimerWidget.tsx +++ b/components/TimerWidget.tsx @@ -120,21 +120,27 @@ export default function TimerWidget() { }, [timers, tick]) const dismissTimer = async (id: string) => { + firedRef.current.delete(id) + setFiredIds(new Set(firedRef.current)) + setTimers(ts => ts.filter(t => t.id !== id)) try { - firedRef.current.delete(id) - setFiredIds(new Set(firedRef.current)) - // We use POST with bearer — but widget runs with cookie auth. - // Cancel endpoint only accepts bearer; for user-dismissal we use DELETE-style via... hmm. - // For simplicity, tell server to cancel via a plain GET-less POST flow — skip server call here. - // (The timer will be cleaned up on next listActive when expired >30min ago.) - setTimers(ts => ts.filter(t => t.id !== id)) + // Fire-and-forget server cancel. API теперь принимает cookie auth, так что + // браузерный запрос проходит middleware. + await fetch('/api/voice/timer', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'cancel', id }), + credentials: 'include', + }) } catch {} } const now = Date.now() const active = timers.filter(t => { const remain = new Date(t.endsAt).getTime() - now - return remain > -30 * 60 * 1000 // keep expired ones visible for 30 min max + // Keep expired for only 30 sec — иначе при перезагрузке давно прошедший + // таймер снова начнёт трезвонить. + return remain > -30 * 1000 }) if (active.length === 0) return null diff --git a/lib/timers.ts b/lib/timers.ts index 6392315..fe29f6e 100644 --- a/lib/timers.ts +++ b/lib/timers.ts @@ -28,8 +28,10 @@ function save(list: Timer[]) { } function cleanup(list: Timer[]): Timer[] { - // Drop items expired more than 30 min ago - const cutoff = Date.now() - 30 * 60 * 1000 + // Drop items expired more than 30 seconds ago. Достаточно чтобы показать + // «звенит» при активной странице, но не воскрешать alarm после перезагрузки + // через час. + const cutoff = Date.now() - 30 * 1000 return list.filter(t => new Date(t.endsAt).getTime() > cutoff) }