From b7251b72c7bba543469677114c28062388034020 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Tue, 24 Feb 2026 01:02:19 +0800 Subject: [PATCH] Address remaining QA items: index, validation, accessibility, guard - S1: Add composite index (is_active, is_dismissed, remind_at) for /due query performance with multi-user scaling - W3: Snooze endpoint rejects dismissed/inactive reminders (409) - W4: Custom field_validator on ReminderSnooze for clear error message - S2: aria-label on all snooze/dismiss buttons in banner and toasts Co-Authored-By: Claude Opus 4.6 --- .../alembic/versions/017_add_reminder_snoozed_until.py | 6 ++++++ backend/app/routers/reminders.py | 3 +++ backend/app/schemas/reminder.py | 9 ++++++++- frontend/src/components/dashboard/AlertBanner.tsx | 2 ++ frontend/src/hooks/useAlerts.tsx | 2 ++ 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/alembic/versions/017_add_reminder_snoozed_until.py b/backend/alembic/versions/017_add_reminder_snoozed_until.py index c04aad8..85d35e9 100644 --- a/backend/alembic/versions/017_add_reminder_snoozed_until.py +++ b/backend/alembic/versions/017_add_reminder_snoozed_until.py @@ -18,7 +18,13 @@ depends_on = None def upgrade() -> None: op.add_column("reminders", sa.Column("snoozed_until", sa.DateTime(), nullable=True)) + op.create_index( + "ix_reminders_due_lookup", + "reminders", + ["is_active", "is_dismissed", "remind_at"], + ) def downgrade() -> None: + op.drop_index("ix_reminders_due_lookup", table_name="reminders") op.drop_column("reminders", "snoozed_until") diff --git a/backend/app/routers/reminders.py b/backend/app/routers/reminders.py index 26d3fc6..2192ffb 100644 --- a/backend/app/routers/reminders.py +++ b/backend/app/routers/reminders.py @@ -79,6 +79,9 @@ async def snooze_reminder( if not reminder: raise HTTPException(status_code=404, detail="Reminder not found") + 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) await db.commit() diff --git a/backend/app/schemas/reminder.py b/backend/app/schemas/reminder.py index c2bbc67..f537281 100644 --- a/backend/app/schemas/reminder.py +++ b/backend/app/schemas/reminder.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, field_validator from datetime import datetime from typing import Literal, Optional @@ -23,6 +23,13 @@ class ReminderUpdate(BaseModel): class ReminderSnooze(BaseModel): minutes: Literal[5, 10, 15] + @field_validator('minutes', mode='before') + @classmethod + def validate_minutes(cls, v: int) -> int: + if v not in (5, 10, 15): + raise ValueError('Snooze duration must be 5, 10, or 15 minutes') + return v + class ReminderResponse(BaseModel): id: int diff --git a/frontend/src/components/dashboard/AlertBanner.tsx b/frontend/src/components/dashboard/AlertBanner.tsx index 2ea3f06..bd86cc1 100644 --- a/frontend/src/components/dashboard/AlertBanner.tsx +++ b/frontend/src/components/dashboard/AlertBanner.tsx @@ -38,6 +38,7 @@ export default function AlertBanner({ alerts, onDismiss, onSnooze }: AlertBanner