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:
parent
42e0fff40c
commit
daf2a4d5f1
@ -40,11 +40,12 @@ async def get_reminders(
|
|||||||
|
|
||||||
@router.get("/due", response_model=List[ReminderResponse])
|
@router.get("/due", response_model=List[ReminderResponse])
|
||||||
async def get_due_reminders(
|
async def get_due_reminders(
|
||||||
|
client_now: Optional[datetime] = Query(None),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: Settings = Depends(get_current_session)
|
current_user: Settings = Depends(get_current_session)
|
||||||
):
|
):
|
||||||
"""Get reminders that are currently due for alerting."""
|
"""Get reminders that are currently due for alerting."""
|
||||||
now = datetime.now()
|
now = client_now or datetime.now()
|
||||||
query = select(Reminder).where(
|
query = select(Reminder).where(
|
||||||
and_(
|
and_(
|
||||||
Reminder.remind_at <= now,
|
Reminder.remind_at <= now,
|
||||||
@ -82,7 +83,8 @@ async def snooze_reminder(
|
|||||||
if reminder.is_dismissed or not reminder.is_active:
|
if reminder.is_dismissed or not reminder.is_active:
|
||||||
raise HTTPException(status_code=409, detail="Cannot snooze a dismissed or inactive reminder")
|
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.commit()
|
||||||
await db.refresh(reminder)
|
await db.refresh(reminder)
|
||||||
|
|||||||
@ -22,6 +22,7 @@ class ReminderUpdate(BaseModel):
|
|||||||
|
|
||||||
class ReminderSnooze(BaseModel):
|
class ReminderSnooze(BaseModel):
|
||||||
minutes: Literal[5, 10, 15]
|
minutes: Literal[5, 10, 15]
|
||||||
|
client_now: Optional[datetime] = None
|
||||||
|
|
||||||
@field_validator('minutes', mode='before')
|
@field_validator('minutes', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Bell } from 'lucide-react';
|
import { Bell } from 'lucide-react';
|
||||||
import api from '@/lib/api';
|
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 SnoozeDropdown from '@/components/reminders/SnoozeDropdown';
|
||||||
import type { Reminder } from '@/types';
|
import type { Reminder } from '@/types';
|
||||||
|
|
||||||
@ -36,7 +36,9 @@ export function AlertsProvider({ children }: { children: ReactNode }) {
|
|||||||
const { data: alerts = [] } = useQuery<Reminder[]>({
|
const { data: alerts = [] } = useQuery<Reminder[]>({
|
||||||
queryKey: ['reminders', 'due'],
|
queryKey: ['reminders', 'due'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await api.get<Reminder[]>('/reminders/due');
|
const { data } = await api.get<Reminder[]>('/reminders/due', {
|
||||||
|
params: { client_now: toLocalDatetime() },
|
||||||
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
@ -66,7 +68,7 @@ export function AlertsProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const snoozeMutation = useMutation({
|
const snoozeMutation = useMutation({
|
||||||
mutationFn: ({ id, minutes }: { id: number; minutes: 5 | 10 | 15 }) =>
|
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: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['reminders'] });
|
queryClient.invalidateQueries({ queryKey: ['reminders'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
||||||
|
|||||||
@ -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 {
|
export function getRelativeTime(dateStr: string): string {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user