From daf2a4d5f144562aa72e1dd3f29c278e82696b8c Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Tue, 24 Feb 2026 03:12:31 +0800 Subject: [PATCH] Fix snooze/due using container UTC instead of client local time Docker container datetime.now() returns UTC, but all stored datetimes are naive local time from the browser. Both /due and /snooze now accept client_now from the frontend, ensuring snooze computes from the user's actual current time, not the container's clock. Co-Authored-By: Claude Opus 4.6 --- backend/app/routers/reminders.py | 6 ++++-- backend/app/schemas/reminder.py | 1 + frontend/src/hooks/useAlerts.tsx | 8 +++++--- frontend/src/lib/date-utils.ts | 6 ++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/backend/app/routers/reminders.py b/backend/app/routers/reminders.py index 2192ffb..a554ab9 100644 --- a/backend/app/routers/reminders.py +++ b/backend/app/routers/reminders.py @@ -40,11 +40,12 @@ async def get_reminders( @router.get("/due", response_model=List[ReminderResponse]) async def get_due_reminders( + client_now: Optional[datetime] = Query(None), db: AsyncSession = Depends(get_db), current_user: Settings = Depends(get_current_session) ): """Get reminders that are currently due for alerting.""" - now = datetime.now() + now = client_now or datetime.now() query = select(Reminder).where( and_( Reminder.remind_at <= now, @@ -82,7 +83,8 @@ async def snooze_reminder( if reminder.is_dismissed or not reminder.is_active: raise HTTPException(status_code=409, detail="Cannot snooze a dismissed or inactive reminder") - reminder.snoozed_until = datetime.now() + timedelta(minutes=body.minutes) + base_time = body.client_now or datetime.now() + reminder.snoozed_until = base_time + timedelta(minutes=body.minutes) await db.commit() await db.refresh(reminder) diff --git a/backend/app/schemas/reminder.py b/backend/app/schemas/reminder.py index f537281..f37a3bc 100644 --- a/backend/app/schemas/reminder.py +++ b/backend/app/schemas/reminder.py @@ -22,6 +22,7 @@ class ReminderUpdate(BaseModel): class ReminderSnooze(BaseModel): minutes: Literal[5, 10, 15] + client_now: Optional[datetime] = None @field_validator('minutes', mode='before') @classmethod diff --git a/frontend/src/hooks/useAlerts.tsx b/frontend/src/hooks/useAlerts.tsx index 5705b0e..a2d0b40 100644 --- a/frontend/src/hooks/useAlerts.tsx +++ b/frontend/src/hooks/useAlerts.tsx @@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom'; import { toast } from 'sonner'; import { Bell } from 'lucide-react'; import api from '@/lib/api'; -import { getRelativeTime } from '@/lib/date-utils'; +import { getRelativeTime, toLocalDatetime } from '@/lib/date-utils'; import SnoozeDropdown from '@/components/reminders/SnoozeDropdown'; import type { Reminder } from '@/types'; @@ -36,7 +36,9 @@ export function AlertsProvider({ children }: { children: ReactNode }) { const { data: alerts = [] } = useQuery({ queryKey: ['reminders', 'due'], queryFn: async () => { - const { data } = await api.get('/reminders/due'); + const { data } = await api.get('/reminders/due', { + params: { client_now: toLocalDatetime() }, + }); return data; }, refetchInterval: 30_000, @@ -66,7 +68,7 @@ export function AlertsProvider({ children }: { children: ReactNode }) { const snoozeMutation = useMutation({ mutationFn: ({ id, minutes }: { id: number; minutes: 5 | 10 | 15 }) => - api.patch(`/reminders/${id}/snooze`, { minutes }), + api.patch(`/reminders/${id}/snooze`, { minutes, client_now: toLocalDatetime() }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['reminders'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); diff --git a/frontend/src/lib/date-utils.ts b/frontend/src/lib/date-utils.ts index 48fcea8..190e361 100644 --- a/frontend/src/lib/date-utils.ts +++ b/frontend/src/lib/date-utils.ts @@ -1,3 +1,9 @@ +/** Format a Date as a naive local datetime string (no Z suffix). */ +export function toLocalDatetime(d: Date = new Date()): string { + const pad = (n: number) => n.toString().padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + export function getRelativeTime(dateStr: string): string { const date = new Date(dateStr); const now = new Date();