Widget system Phase 4b: per-widget gear-icon config dialogs
Replaces the Photos/Calendar/Whiteboard tabs with a single Layout page
(now the frame's landing route) where each widget gets a gear icon
opening a dialog scoped to that specific widget's own settings. This
was the missing piece for genuinely independent same-type widgets --
"the Calendar tab" never made sense once a frame could hold more than
one calendar widget with different settings.
Data layer: FrameCalendar re-keyed from frame_id to widget_id, so each
calendar widget has its own independent included-calendars set. The
rekey runs as an unconditional post-startup step (like the existing
widget backfill), not a numbered migration -- it depends on calendar
widgets already existing, which themselves come from that same
backfill step, not from schema migration. Registering it as a numbered
migration would have run it first during a real upgrade, silently
dropping every row; caught by a new test that exercises the raw-SQL
upgrade path instead of the fresh-install create_all() shortcut every
other migration test takes.
API layer: every endpoint that used to assume "the frame's widget of
this type" (photo queue/thumbnail/preview, calendar select/color/
tasks/weather, whiteboard source/browse/preview) moved into
api_widgets.py under /api/frames/{id}/widgets/{widget_id}/..., with a
new require_widget_view/control dependency pair mirroring the existing
frame-level ones. Device status (battery/last-seen/firmware) got its
own frame-level /status endpoint, split out of the old photo-specific
/queue it used to piggyback on -- fixes the status bar going silently
blank on any frame without a photo widget.
UI layer: each widget type's existing settings markup/JS was ported
into a dialog partial + an explicit init/close function pair (the
content is now fetched and injected on demand, not loaded at page load
time). window.FRAME_API is repointed to the open dialog's widget-scoped
API base for its duration and restored on close; a separate
window.FRAME_BASE_API stays stable for the always-present header/
status-bar scripts.
Caught during manual browser testing: the consolidated config-save
endpoint initially expected a JSON body while the copied-over dialog JS
posts form-urlencoded data (the old convention) -- fixed to match, with
new HTTP-level test coverage that would have caught it immediately.
This commit is contained in:
+20
-15
@@ -80,9 +80,10 @@ class User(Base):
|
||||
webdav_username: Mapped[str] = mapped_column(String, default="")
|
||||
webdav_password: Mapped[str] = mapped_column(String, default="")
|
||||
webdav_reuse_caldav_creds: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
# Optional starting folder for the file-picker on a frame's Whiteboard
|
||||
# tab (see routers/api_frames.py's whiteboard-browse) -- purely a
|
||||
# convenience for browsing to a file rather than typing its full URL.
|
||||
# Optional starting folder for the whiteboard dialog's file-picker
|
||||
# (see routers/api_widgets.py's api_widget_whiteboard_browse) --
|
||||
# purely a convenience for browsing to a file rather than typing its
|
||||
# full URL.
|
||||
# Never used for fetching/rendering itself, which always uses the
|
||||
# frame's own saved whiteboard_url regardless of whether this is set.
|
||||
webdav_base_url: Mapped[str] = mapped_column(String, default="")
|
||||
@@ -241,8 +242,8 @@ class Frame(Base):
|
||||
# _draw_tasks). CalDAV only (a task list is a VTODO collection, not
|
||||
# something a plain ICS subscription meaningfully has); source is
|
||||
# one specific linked user's own CalDAV calendar, same
|
||||
# owner-controls-their-own-data permission split as FrameCalendar --
|
||||
# see routers/api_frames.py's api_tasks_source. calendar_tasks_user_id
|
||||
# owner-controls-their-own-data permission split as FrameCalendar.
|
||||
# calendar_tasks_user_id
|
||||
# SET NULL on the user's deletion clears the source rather than
|
||||
# leaving a dangling reference (checked_at isn't reset by that, but
|
||||
# the next refresh attempt finds no source and just returns []).
|
||||
@@ -261,9 +262,8 @@ class Frame(Base):
|
||||
# routers/device.py's RENDERERS["whiteboard"]) -- a frame-wide
|
||||
# setting like calendar mode's own frame_calendars source, not
|
||||
# personal data, but still owner-gated the same way: only
|
||||
# whiteboard_user_id may point the frame at their own account (see
|
||||
# routers/api_frames.py's api_whiteboard_source), since it's their
|
||||
# credentials being used to fetch it. --
|
||||
# whiteboard_user_id may point the frame at their own account, since
|
||||
# it's their credentials being used to fetch it. --
|
||||
whiteboard_user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
@@ -355,12 +355,18 @@ class FrameCalendar(Base):
|
||||
ANY user linked to the frame may flip included back to False, muting
|
||||
a calendar they'd rather not see on a shared display even though
|
||||
they don't own it. Only the owner may flip it back to True. See
|
||||
routers/api_frames.py's api_calendar_select."""
|
||||
routers/api_widgets.py's api_widget_calendar_select.
|
||||
|
||||
Keyed by widget_id, not frame_id -- a frame can hold more than one
|
||||
independent calendar widget (see Widget), each with its own included-
|
||||
calendars set; "included on this frame" stopped being unambiguous
|
||||
the moment that became possible (see migration.py's _migration_17,
|
||||
which re-keyed this table)."""
|
||||
|
||||
__tablename__ = "frame_calendars"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
frame_id: Mapped[int] = mapped_column(ForeignKey("frames.id", ondelete="CASCADE"))
|
||||
widget_id: Mapped[int] = mapped_column(ForeignKey("widgets.id", ondelete="CASCADE"))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
calendar_key: Mapped[str] = mapped_column(String)
|
||||
# Snapshot label for display -- so the list still reads sensibly even
|
||||
@@ -372,11 +378,11 @@ class FrameCalendar(Base):
|
||||
# text/background) pinning this calendar's events to a specific
|
||||
# panel color rather than calendar_render.py's old owner-name
|
||||
# auto-cycle. NULL keeps the auto-cycle behavior. Only the calendar's
|
||||
# owner may set this -- see routers/api_frames.py's api_calendar_color.
|
||||
# owner may set this -- see routers/api_widgets.py's api_widget_calendar_color.
|
||||
color_index: Mapped[int | None] = mapped_column(Integer, nullable=True, default=None)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_frame_calendars_unique", "frame_id", "user_id", "calendar_key", unique=True),
|
||||
Index("ix_frame_calendars_unique", "widget_id", "user_id", "calendar_key", unique=True),
|
||||
)
|
||||
|
||||
|
||||
@@ -445,9 +451,8 @@ class CalendarWidgetConfig(Base):
|
||||
minus calendar_photo_inlay (dropped: arbitrary widget placement
|
||||
subsumes what a fixed 50/50 inlay split did, so it's not a special
|
||||
case anymore, just place a photo widget alongside). "Included
|
||||
calendars" stays on FrameCalendar (frame_id-keyed for now; re-keyed
|
||||
to widget_id in a later phase once more than one calendar widget per
|
||||
frame is actually supported end to end)."""
|
||||
calendars" is its own table (FrameCalendar), widget_id-keyed so each
|
||||
calendar widget on a frame has its own independent set."""
|
||||
|
||||
__tablename__ = "calendar_widget_configs"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user