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:
+63
-1
@@ -15,7 +15,7 @@ import secrets
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy import inspect, select, text
|
||||
|
||||
from . import config, grid
|
||||
from .db import SessionLocal, engine
|
||||
@@ -378,6 +378,7 @@ def run_migrations() -> None:
|
||||
_ensure_frame_one()
|
||||
_ensure_server_settings()
|
||||
_ensure_widgets_backfilled()
|
||||
_ensure_frame_calendars_rekeyed()
|
||||
|
||||
|
||||
def new_device_token() -> str:
|
||||
@@ -604,3 +605,64 @@ def _ensure_widgets_backfilled() -> None:
|
||||
continue
|
||||
_backfill_frame_widgets(db, frame)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _ensure_frame_calendars_rekeyed() -> None:
|
||||
"""Re-keys frame_calendars from frame_id to widget_id -- a frame can
|
||||
hold more than one independent calendar widget (see the widget
|
||||
system), each with its own included-calendars set, so "included on
|
||||
this frame" no longer means anything unambiguous (see
|
||||
models.FrameCalendar). Existing rows attach to their frame's calendar
|
||||
widget if it has one; rows for a frame with no calendar widget at all
|
||||
are dropped -- they were already-dormant settings for content
|
||||
nothing ever actually displayed (the Calendar tab stayed reachable
|
||||
and savable even while a frame's old `mode` was "photos"), not real
|
||||
live configuration.
|
||||
|
||||
Deliberately NOT a numbered migration: this needs each frame's
|
||||
calendar widget to already exist to know what to re-key against, and
|
||||
those widget rows aren't created by a schema migration at all --
|
||||
they come from _ensure_widgets_backfilled() above, which (like this
|
||||
function) runs unconditionally after every startup rather than being
|
||||
tracked by schema_version. Running this as a numbered migration
|
||||
would execute it *before* that backfill during a real upgrade (the
|
||||
numbered-migration loop runs first, see run_migrations), silently
|
||||
dropping every row -- caught by test_migrations.py actually exercising
|
||||
the raw-SQL upgrade path instead of the fresh-install create_all()
|
||||
shortcut every other test in that file takes.
|
||||
|
||||
Runs unconditionally after every startup, like _ensure_widgets_
|
||||
backfilled; a no-op the moment frame_calendars is already
|
||||
widget_id-shaped (every fresh install, and any existing install
|
||||
after its first run past this code) -- SQLite can't ALTER a column's
|
||||
FK target or drop a column that's part of an index/FK constraint, so
|
||||
when it isn't a no-op this is the standard SQLite "rebuild" pattern:
|
||||
create the new-shape table, copy matching rows across (joining to
|
||||
find each row's calendar widget), drop the old table, rename the new
|
||||
one into place."""
|
||||
inspector = inspect(engine)
|
||||
columns = {c["name"] for c in inspector.get_columns("frame_calendars")}
|
||||
if "widget_id" in columns:
|
||||
return
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text(
|
||||
"CREATE TABLE frame_calendars_new ("
|
||||
"id INTEGER PRIMARY KEY, "
|
||||
"widget_id INTEGER NOT NULL REFERENCES widgets(id) ON DELETE CASCADE, "
|
||||
"user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, "
|
||||
"calendar_key TEXT NOT NULL, "
|
||||
"calendar_label TEXT NOT NULL DEFAULT '', "
|
||||
"included INTEGER NOT NULL DEFAULT 1, "
|
||||
"color_index INTEGER)"
|
||||
))
|
||||
conn.execute(text(
|
||||
"INSERT INTO frame_calendars_new (widget_id, user_id, calendar_key, calendar_label, included, color_index) "
|
||||
"SELECT w.id, fc.user_id, fc.calendar_key, fc.calendar_label, fc.included, fc.color_index "
|
||||
"FROM frame_calendars fc "
|
||||
"JOIN widgets w ON w.frame_id = fc.frame_id AND w.widget_type = 'calendar'"
|
||||
))
|
||||
conn.execute(text("DROP TABLE frame_calendars"))
|
||||
conn.execute(text("ALTER TABLE frame_calendars_new RENAME TO frame_calendars"))
|
||||
conn.execute(text(
|
||||
"CREATE UNIQUE INDEX ix_frame_calendars_unique ON frame_calendars (widget_id, user_id, calendar_key)"
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user