Fix QA findings: bound queries, error handlers, snooze clamp
C-01: Add 30-day lower bound on overdue todo/reminder queries to prevent fetching entire history. C-02: Remove dead include_past query param — past-event filtering is handled client-side. W-01: Add onError toast handlers to all three inline mutations. W-02: Snooze dropdown opens upward (bottom-full) to avoid clipping inside the ScrollArea overflow container. S-06: Clamp getMinutesUntilTomorrowMorning() to max 1440 to stay within ReminderSnooze schema bounds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
99161f1b47
commit
847372643b
@ -156,26 +156,27 @@ async def get_dashboard(
|
|||||||
async def get_upcoming(
|
async def get_upcoming(
|
||||||
days: int = Query(default=7, ge=1, le=90),
|
days: int = Query(default=7, ge=1, le=90),
|
||||||
client_date: Optional[date] = Query(None, ge=date(2020, 1, 1), le=date(2099, 12, 31)),
|
client_date: Optional[date] = Query(None, ge=date(2020, 1, 1), le=date(2099, 12, 31)),
|
||||||
include_past: bool = Query(default=True),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
current_settings: Settings = Depends(get_current_settings),
|
current_settings: Settings = Depends(get_current_settings),
|
||||||
):
|
):
|
||||||
"""Get unified list of upcoming items (todos, events, reminders) sorted by date."""
|
"""Get unified list of upcoming items (todos, events, reminders) sorted by date."""
|
||||||
today = client_date or date.today()
|
today = client_date or date.today()
|
||||||
now = datetime.now()
|
|
||||||
cutoff_date = today + timedelta(days=days)
|
cutoff_date = today + timedelta(days=days)
|
||||||
cutoff_datetime = datetime.combine(cutoff_date, datetime.max.time())
|
cutoff_datetime = datetime.combine(cutoff_date, datetime.max.time())
|
||||||
today_start = datetime.combine(today, datetime.min.time())
|
today_start = datetime.combine(today, datetime.min.time())
|
||||||
|
overdue_floor = today - timedelta(days=30)
|
||||||
|
overdue_floor_dt = datetime.combine(overdue_floor, datetime.min.time())
|
||||||
|
|
||||||
# Subquery: calendar IDs belonging to this user
|
# Subquery: calendar IDs belonging to this user
|
||||||
user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id)
|
user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id)
|
||||||
|
|
||||||
# Build queries — include overdue todos and snoozed reminders
|
# Build queries — include overdue todos (up to 30 days back) and snoozed reminders
|
||||||
todos_query = select(Todo).where(
|
todos_query = select(Todo).where(
|
||||||
Todo.user_id == current_user.id,
|
Todo.user_id == current_user.id,
|
||||||
Todo.completed == False,
|
Todo.completed == False,
|
||||||
Todo.due_date.isnot(None),
|
Todo.due_date.isnot(None),
|
||||||
|
Todo.due_date >= overdue_floor,
|
||||||
Todo.due_date <= cutoff_date
|
Todo.due_date <= cutoff_date
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -190,6 +191,7 @@ async def get_upcoming(
|
|||||||
Reminder.user_id == current_user.id,
|
Reminder.user_id == current_user.id,
|
||||||
Reminder.is_active == True,
|
Reminder.is_active == True,
|
||||||
Reminder.is_dismissed == False,
|
Reminder.is_dismissed == False,
|
||||||
|
Reminder.remind_at >= overdue_floor_dt,
|
||||||
Reminder.remind_at <= cutoff_datetime
|
Reminder.remind_at <= cutoff_datetime
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -220,11 +222,6 @@ async def get_upcoming(
|
|||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
end_dt = event.end_datetime
|
end_dt = event.end_datetime
|
||||||
# When include_past=False, filter out past events
|
|
||||||
if not include_past and end_dt:
|
|
||||||
if end_dt < now:
|
|
||||||
continue
|
|
||||||
|
|
||||||
upcoming_items.append({
|
upcoming_items.append({
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"id": event.id,
|
"id": event.id,
|
||||||
|
|||||||
@ -35,7 +35,7 @@ const typeConfig: Record<string, { hoverGlow: string; pillBg: string; pillText:
|
|||||||
function getMinutesUntilTomorrowMorning(): number {
|
function getMinutesUntilTomorrowMorning(): number {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 9, 0, 0);
|
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 9, 0, 0);
|
||||||
return Math.max(1, Math.round((tomorrow.getTime() - now.getTime()) / 60000));
|
return Math.min(1440, Math.max(1, Math.round((tomorrow.getTime() - now.getTime()) / 60000)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDayLabel(dateStr: string): string {
|
function getDayLabel(dateStr: string): string {
|
||||||
@ -77,6 +77,7 @@ export default function UpcomingWidget({ items }: UpcomingWidgetProps) {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
||||||
toast.success('Todo completed');
|
toast.success('Todo completed');
|
||||||
},
|
},
|
||||||
|
onError: () => toast.error('Failed to complete todo'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Snooze reminder
|
// Snooze reminder
|
||||||
@ -87,6 +88,7 @@ export default function UpcomingWidget({ items }: UpcomingWidgetProps) {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['upcoming'] });
|
queryClient.invalidateQueries({ queryKey: ['upcoming'] });
|
||||||
toast.success('Reminder snoozed');
|
toast.success('Reminder snoozed');
|
||||||
},
|
},
|
||||||
|
onError: () => toast.error('Failed to snooze reminder'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dismiss reminder
|
// Dismiss reminder
|
||||||
@ -97,6 +99,7 @@ export default function UpcomingWidget({ items }: UpcomingWidgetProps) {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
||||||
toast.success('Reminder dismissed');
|
toast.success('Reminder dismissed');
|
||||||
},
|
},
|
||||||
|
onError: () => toast.error('Failed to dismiss reminder'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter and group items
|
// Filter and group items
|
||||||
@ -325,7 +328,7 @@ export default function UpcomingWidget({ items }: UpcomingWidgetProps) {
|
|||||||
<Clock className="h-3.5 w-3.5" />
|
<Clock className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
{snoozeOpen === itemKey && (
|
{snoozeOpen === itemKey && (
|
||||||
<div className="absolute right-0 top-full mt-1 w-28 rounded-md border bg-popover shadow-lg z-50 py-1 animate-fade-in">
|
<div className="absolute right-0 bottom-full mb-1 w-28 rounded-md border bg-popover shadow-lg z-50 py-1 animate-fade-in">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => handleSnooze(item.id, 60, e)}
|
onClick={(e) => handleSnooze(item.id, 60, e)}
|
||||||
className="flex items-center w-full px-3 py-1.5 text-xs hover:bg-card-elevated transition-colors text-muted-foreground hover:text-foreground"
|
className="flex items-center w-full px-3 py-1.5 text-xs hover:bg-card-elevated transition-colors text-muted-foreground hover:text-foreground"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user