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 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-24 03:12:31 +08:00
parent 42e0fff40c
commit daf2a4d5f1
4 changed files with 16 additions and 5 deletions

View File

@ -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)

View File

@ -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

View File

@ -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<Reminder[]>({
queryKey: ['reminders', 'due'],
queryFn: async () => {
const { data } = await api.get<Reminder[]>('/reminders/due');
const { data } = await api.get<Reminder[]>('/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'] });

View File

@ -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();