feat: per-event reminders + default reminder setting (server)

local_events gains a `reminders` TEXT column (comma-separated minutes-before-
start, like exdate); EventCreate/EventUpdate accept a `reminders: [int]` list
and build_local_event_dict emits it back as a list. user_settings gains
`default_reminder_minutes` (nullable int, null = off), exposed/updatable via
/api/settings (explicit null persists as off). Migrations added in _migrate().
Clients (iOS/Android) schedule the OS notifications locally from these.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-06 16:07:19 +02:00
parent fc00bf9114
commit bff9a244e7
5 changed files with 29 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
import uuid
from datetime import datetime, timezone
from typing import Optional
from typing import List, Optional
from fastapi import APIRouter, Depends, Form, HTTPException, Query, UploadFile, File
from fastapi.responses import Response
@@ -43,6 +43,7 @@ class EventCreate(BaseModel):
color: Optional[str] = None
rrule: Optional[str] = None
private: bool = False
reminders: Optional[List[int]] = None # minutes before start (0 = at start)
class EventUpdate(BaseModel):
@@ -56,6 +57,7 @@ class EventUpdate(BaseModel):
rrule: Optional[str] = None
exdate: Optional[str] = None
private: Optional[bool] = None
reminders: Optional[List[int]] = None
class ShareCreate(BaseModel):
@@ -247,6 +249,7 @@ def create_event(
color=data.color,
rrule=data.rrule,
is_private=data.private,
reminders=(",".join(str(m) for m in data.reminders) if data.reminders else None),
creator_id=current_user.id, # server-side, never from the client
)
db.add(ev)
@@ -296,6 +299,8 @@ def update_event(
if data.exdate not in dates:
dates.append(data.exdate)
ev.exdate = ",".join(dates)
if data.reminders is not None:
ev.reminders = ",".join(str(m) for m in data.reminders) if data.reminders else None
db.commit()
return {"ok": True}

View File

@@ -29,6 +29,7 @@ class SettingsUpdate(BaseModel):
bg_color: Optional[str] = None
private_event_visibility: Optional[str] = None
group_visible_calendar_id: Optional[int] = None
default_reminder_minutes: Optional[int] = None # null = off
def _settings_dict(s: models.UserSettings) -> dict:
@@ -50,6 +51,7 @@ def _settings_dict(s: models.UserSettings) -> dict:
"bg_color": s.bg_color,
"private_event_visibility": s.private_event_visibility or "busy",
"group_visible_calendar_id": s.group_visible_calendar_id,
"default_reminder_minutes": s.default_reminder_minutes,
}
@@ -92,7 +94,7 @@ def update_settings(
# For these three override colours, an explicit null is meaningful
# ("reset to default") and must be persisted as NULL. All other fields
# keep the previous behaviour where a null/missing value is ignored.
NULLABLE_OVERRIDES = {"text_color", "line_color", "bg_color", "group_visible_calendar_id"}
NULLABLE_OVERRIDES = {"text_color", "line_color", "bg_color", "group_visible_calendar_id", "default_reminder_minutes"}
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
if field in NULLABLE_OVERRIDES: