From bff9a244e764cb9dd71510d43ea0961034fcb0b3 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 6 Jun 2026 16:07:19 +0200 Subject: [PATCH] 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 --- backend/local_events_util.py | 1 + backend/main.py | 14 ++++++++++++++ backend/models.py | 5 +++++ backend/routers/local_router.py | 7 ++++++- backend/routers/settings_router.py | 4 +++- 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/backend/local_events_util.py b/backend/local_events_util.py index 1028f2c..edca593 100644 --- a/backend/local_events_util.py +++ b/backend/local_events_util.py @@ -128,6 +128,7 @@ def build_local_event_dict( "type": "local", "creator": creator, "private": bool(ev.is_private), + "reminders": [int(x) for x in (ev.reminders or "").split(",") if x.strip().lstrip("-").isdigit()], } if owner is not None: d["owner"] = owner diff --git a/backend/main.py b/backend/main.py index 013c9d4..b931917 100644 --- a/backend/main.py +++ b/backend/main.py @@ -102,6 +102,20 @@ def _migrate(): except Exception: pass + try: + conn.execute(text("ALTER TABLE local_events ADD COLUMN reminders TEXT")) + conn.commit() + logging.info("Migration: added reminders to local_events") + except Exception: + pass + + try: + conn.execute(text("ALTER TABLE user_settings ADD COLUMN default_reminder_minutes INTEGER")) + conn.commit() + logging.info("Migration: added default_reminder_minutes to user_settings") + except Exception: + pass + try: conn.execute(text("ALTER TABLE user_settings ADD COLUMN month_divider_color VARCHAR(7) DEFAULT '#7090c0'")) conn.commit() diff --git a/backend/models.py b/backend/models.py index 74184a5..e09d976 100644 --- a/backend/models.py +++ b/backend/models.py @@ -101,6 +101,9 @@ class UserSettings(Base): # The single local calendar this user shares into all their groups # (combined view shows only this calendar per member). NULL = share nothing. group_visible_calendar_id = Column(Integer, nullable=True) + # Default reminder in minutes-before-start applied to all events client-side + # (0 = at start time). NULL = no default reminder. + default_reminder_minutes = Column(Integer, nullable=True) user = relationship("User", back_populates="settings") @@ -133,6 +136,8 @@ class LocalEvent(Base): color = Column(String(7), nullable=True) rrule = Column(Text, nullable=True) exdate = Column(Text, nullable=True) # Comma-separated YYYYMMDD dates to exclude + # Comma-separated minutes-before-start for reminders, e.g. "10,60" (0 = at start). + reminders = Column(Text, nullable=True) # Creator: set server-side from the auth token on create, never from the client. creator_id = Column(Integer, ForeignKey("users.id"), nullable=True) # For imported events without a local user (from the .ics ORGANIZER field). diff --git a/backend/routers/local_router.py b/backend/routers/local_router.py index b8bb981..5f4c980 100644 --- a/backend/routers/local_router.py +++ b/backend/routers/local_router.py @@ -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} diff --git a/backend/routers/settings_router.py b/backend/routers/settings_router.py index cc140b0..59a418b 100644 --- a/backend/routers/settings_router.py +++ b/backend/routers/settings_router.py @@ -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: