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:
|
def upgrade() -> None:
|
||||||
op.add_column("reminders", sa.Column("snoozed_until", sa.DateTime(), nullable=True))
|
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:
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_reminders_due_lookup", table_name="reminders")
|
||||||
op.drop_column("reminders", "snoozed_until")
|
op.drop_column("reminders", "snoozed_until")
|
||||||
|
|||||||
@ -79,6 +79,9 @@ async def snooze_reminder(
|
|||||||
if not reminder:
|
if not reminder:
|
||||||
raise HTTPException(status_code=404, detail="Reminder not found")
|
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)
|
reminder.snoozed_until = datetime.now() + timedelta(minutes=body.minutes)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict, field_validator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
@ -23,6 +23,13 @@ class ReminderUpdate(BaseModel):
|
|||||||
class ReminderSnooze(BaseModel):
|
class ReminderSnooze(BaseModel):
|
||||||
minutes: Literal[5, 10, 15]
|
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):
|
class ReminderResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export default function AlertBanner({ alerts, onDismiss, onSnooze }: AlertBanner
|
|||||||
<button
|
<button
|
||||||
key={m}
|
key={m}
|
||||||
onClick={() => onSnooze(alert.id, 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"
|
className="px-1.5 py-0.5 rounded bg-secondary hover:bg-accent/10 hover:text-accent text-muted-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{m}m
|
{m}m
|
||||||
@ -46,6 +47,7 @@ export default function AlertBanner({ alerts, onDismiss, onSnooze }: AlertBanner
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => onDismiss(alert.id)}
|
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"
|
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" />
|
<X className="h-3.5 w-3.5" />
|
||||||
|
|||||||
@ -108,6 +108,7 @@ export function AlertsProvider({ children }: { children: ReactNode }) {
|
|||||||
<button
|
<button
|
||||||
key={m}
|
key={m}
|
||||||
onClick={() => snoozeRef.current(reminder.id, m as 5 | 10 | 15)}
|
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"
|
className="px-1.5 py-0.5 rounded bg-secondary hover:bg-card-elevated text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{m}m
|
{m}m
|
||||||
@ -116,6 +117,7 @@ export function AlertsProvider({ children }: { children: ReactNode }) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => dismissRef.current(reminder.id)}
|
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"
|
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
|
Dismiss
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user