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 <noreply@anthropic.com>
This commit is contained in:
parent
5b1b9cc5b7
commit
b7251b72c7
@ -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")
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -38,6 +38,7 @@ export default function AlertBanner({ alerts, onDismiss, onSnooze }: AlertBanner
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => onSnooze(alert.id, m)}
|
||||
aria-label={`Snooze "${alert.title}" for ${m} minutes`}
|
||||
className="px-1.5 py-0.5 rounded bg-secondary hover:bg-accent/10 hover:text-accent text-muted-foreground transition-colors"
|
||||
>
|
||||
{m}m
|
||||
@ -46,6 +47,7 @@ export default function AlertBanner({ alerts, onDismiss, onSnooze }: AlertBanner
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDismiss(alert.id)}
|
||||
aria-label={`Dismiss "${alert.title}"`}
|
||||
className="p-1 rounded hover:bg-accent/10 hover:text-accent text-muted-foreground transition-colors shrink-0"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
|
||||
@ -108,6 +108,7 @@ export function AlertsProvider({ children }: { children: ReactNode }) {
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => snoozeRef.current(reminder.id, m as 5 | 10 | 15)}
|
||||
aria-label={`Snooze "${reminder.title}" for ${m} minutes`}
|
||||
className="px-1.5 py-0.5 rounded bg-secondary hover:bg-card-elevated text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{m}m
|
||||
@ -116,6 +117,7 @@ export function AlertsProvider({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => dismissRef.current(reminder.id)}
|
||||
aria-label={`Dismiss "${reminder.title}"`}
|
||||
className="ml-auto px-2 py-0.5 rounded text-[11px] bg-secondary hover:bg-card-elevated text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Dismiss
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user