Add calendar frame mode data model (migration 7)

New columns, all ADD COLUMN with inert defaults -- no existing frame's
behavior changes until mode is explicitly switched to "calendar":

- users.calendar_ics_url: one personal iCal/CalDAV subscription per
  user, same shape as the existing per-user immich_url/immich_api_key.
- user_frames.calendar_included: explicit per-(user,frame) opt-in,
  default off. Being linked to a frame does not by itself contribute
  your calendar to it -- each person's calendar is their own data to
  share, not something a frame's controller decides on their behalf.
- frames.calendar_view/calendar_photo_inlay/calendar_browse_offset:
  per-frame display settings and NEXT/BACK navigation state.
- frames.calendar_checked_at/calendar_cached_events/calendar_fetch_summary:
  the throttled merge-fetch cache, same shape as the existing
  firmware_update_checked_at/firmware_gitea_latest_version pattern.
This commit is contained in:
2026-07-22 19:05:02 -04:00
parent fc65b19cf2
commit 1fa1c68478
4 changed files with 60 additions and 417 deletions
+39
View File
@@ -48,6 +48,12 @@ class User(Base):
# email -- see routers/device.py's frame_battery) go here; blank = no
# email configured, both features silently no-op for this user.
email: Mapped[str] = mapped_column(String, default="")
# Personal iCal/CalDAV .ics subscription URL (no OAuth) for calendar
# frame mode -- see calendar_feed.py. Setting this alone shows up
# nowhere: a linked frame only pulls this user's events in once
# they've also opted in on that frame's own Configuration -> Calendar
# card (UserFrame.calendar_included below).
calendar_ics_url: Mapped[str] = mapped_column(String, default="")
created_at: Mapped[float] = mapped_column(Float, default=time.time)
__table_args__ = (
@@ -139,6 +145,31 @@ class Frame(Base):
# original always-on full-strength Floyd-Steinberg dithering.
dither_strength: Mapped[float] = mapped_column(Float, default=1.0)
# -- calendar mode (see calendar_feed.py, calendar_render.py,
# routers/device.py's RENDERERS["calendar"]) --
calendar_view: Mapped[str] = mapped_column(String, default="agenda") # "agenda" | "week" | "month"
# Agenda view only; reuses this frame's existing photos-mode album/
# queue, not a separate photo setup.
calendar_photo_inlay: Mapped[bool] = mapped_column(Boolean, default=False)
# How many periods (unit depends on calendar_view: days/weeks/months)
# NEXT/BACK have browsed from "today". Reset to 0 by the next normal
# (non-button) /frame/image request, and whenever calendar_view
# itself changes -- a stale offset means something different in a
# different view's units.
calendar_browse_offset: Mapped[int] = mapped_column(Integer, default=0)
# Throttled merge-fetch cache (see routers/common.py's
# get_or_refresh_calendar_events) -- same shape as the
# firmware_update_checked_at/firmware_gitea_latest_version pattern
# below. One shared cache for every included user's merged events,
# not per-user.
calendar_checked_at: Mapped[float] = mapped_column(Float, default=0.0)
calendar_cached_events: Mapped[list | None] = mapped_column(JSON, nullable=True, default=None)
# "" when the last merge-fetch fully succeeded, else e.g. "1 of 2
# calendars unavailable" -- never names which user's feed failed, a
# shared household display shouldn't call out a specific person's
# outage to everyone who looks at it.
calendar_fetch_summary: Mapped[str] = mapped_column(String, default="")
# -- state --
current_asset_id: Mapped[str] = mapped_column(String, default="")
current_asset_set_at: Mapped[float] = mapped_column(Float, default=0.0)
@@ -200,6 +231,14 @@ class UserFrame(Base):
ForeignKey("frames.id", ondelete="CASCADE"), primary_key=True
)
created_at: Mapped[float] = mapped_column(Float, default=time.time)
# Explicit per-(user,frame) opt-in for calendar frame mode -- being
# linked to a frame does NOT by itself contribute this user's
# calendar to it (deliberate choice, not an oversight: each person's
# calendar is their own data to share or not, not something a
# frame's controller decides on their behalf). Meaningless if the
# user has no calendar_ics_url set. See routers/api_frames.py's
# api_calendar_included.
calendar_included: Mapped[bool] = mapped_column(Boolean, default=False)
class PendingClaim(Base):