37bd657299c6eb17aa26e91e6bb2f6af63c83500
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
37bd657299 |
Widget system Phase 2: full cutover to widget-based rendering
device.py's mode-keyed dispatch is replaced by a real compositor: load a frame's widgets, compute pixel rects via app/grid.py, render each through its widget module, and composite with render_panel. Physical NEXT/BACK buttons now execute each frame's assigned FrameButtonAction rows instead of one hardcoded per-mode action. api_frames.py, manage.py, and common.py's build_manage_content are repointed to read/write the frame's widget config rows instead of the old Frame columns, and every settings page (Photos/Calendar/ Whiteboard tabs) now pre-fills its form from the same widget config the write endpoints actually save to -- previously the read and write sides would have silently diverged. The old mode selector and photo-inlay checkbox are removed along with their now-inert wiring; arbitrary widget placement subsumes what the fixed inlay split did. Ships together with Phase 1 (per-type render/action modules) since splitting the read/write cutover across deploys would have left settings changes with no visible effect. |
||
|
|
f48daa71c8 |
Widget system Phase 1: per-type render/action modules
New app/widgets/ package (photos.py, calendar.py, whiteboard.py, plus the WIDGET_TYPES registry) -- the widget-system analogue of routers/device.py's old RENDERERS/ADVANCE_RENDERERS/BACK_RENDERERS, generalized from "one mode owns the whole panel" to "each widget renders into its own region and responds to named button actions." Each module exposes render(db, frame, widget, target_w, target_h) -> Image.Image (never raises -- a widget's own fetch hiccup falls back to a small placeholder rather than taking the whole panel's render down) and an ACTIONS registry for NEXT/BACK button assignment. Supporting changes needed to give the widget modules something to call, all mechanical/behavior-preserving for every existing caller: - image_pipeline.py: render_panel(regions, ...) generalizes render_frame's tail (paste, enhance once, overlay once, quantize once, pack once) from one photo to N regions -- not a restructuring, since calendar mode's photo-inlay feature already pastes a second composed image onto the canvas before that single shared pipeline runs. - photo_queue.py: advance_forced/back_forced/remove_from_rotation/ get_current take an explicit `frame` param now that `cfg` won't always be the Frame itself once photo-queue state moves to PhotoWidgetConfig -- caught a real latent bug while doing this: get_current was reading refresh_interval_s off `cfg`, but that's a frame-level wake-cadence setting, not something that becomes per-widget, so it now reads that off `frame` explicitly instead. - routers/common.py: list_assets/fetch_source_and_faces take album_id/ display_mode directly instead of a whole Frame (both only ever read that one attribute off it); new get_or_refresh_*_for_widget siblings of the existing calendar/weather/tasks/whiteboard cache helpers, read/ writing the new per-widget config tables -- the Frame-scoped originals are untouched and still what routers/device.py's actual dispatch calls until the Phase 2 cutover. 26 new tests (95 total): render_panel size/placement/orientation coverage, and per-widget-type render/action tests (unconfigured -> placeholder, a fetch failure -> placeholder not a crash, actions mutate the right state). Full suite passes; diff-reviewed to confirm device.py's actual RENDERERS dispatch and the old Frame-scoped get_or_refresh_* bodies are unchanged, so this is safe to deploy on its own despite being step 1 of a two-step cutover (see the project plan on why the *next* step, not this one, has to ship atomically). |
||
|
|
8bc0749b42 |
Widget system Phase 0: data model + migration
First step of replacing Frame.mode (one renderer owns the whole panel) with an Android-home-screen-style widget system -- a frame will hold N independently placed/sized widgets (photos/calendar/whiteboard), each with its own config/state, plus fully user-assignable NEXT/BACK button actions. Full plan at .claude/plans/prancy-snacking-iverson.md. This phase is additive only and changes no existing behavior -- nothing reads these new tables yet: - models.py: Widget (placement) + PhotoWidgetConfig/CalendarWidgetConfig/ WhiteboardWidgetConfig (per-type 1:1 extension tables, matching this codebase's existing convention of dedicated tables for naturally-scoped state rather than one wide table) + FrameButtonAction (ordered (widget, action) bindings per physical button). - grid.py: pure snap-to-grid placement math, defined relative to the panel's long/short axis so it stays valid across logical_render_size(orientation)'s genuine width/height swap for portrait, not just a rotation applied at the end. - db.py: widget_locked(), the widget-scoped equivalent of frame_locked() -- deliberately still locks at frame granularity (not a new per-widget lock) to avoid a new class of multi-lock deadlock bugs. - migration.py: _migration_16 creates the new tables; a separate _ensure_widgets_backfilled() (ORM-based, not raw SQL -- much less error-prone for this much per-mode branching) gives every existing frame a widget reproducing its exact current mode/settings, so upgrading changes nothing about what a frame displays or what its buttons do. calendar_photo_inlay frames specifically get two widgets (calendar + photo, split like the old inlay did) rather than silently losing the photo half. 10 new tests covering fresh-install backfill, re-run idempotency, the photo-inlay two-widget case, whiteboard's check_now button mapping, and migration_16's actual CREATE TABLE path against a simulated pre-existing database (not just the fresh-install create_all() shortcut). Full suite (69 tests) passes. |
||
|
|
31adc34a19 |
Add a real pytest suite, gating the CI build
64 tests covering: auth/setup and the CSRF gate, the "owner adds their own data, anyone linked can mute it" permission pattern shared across calendar-select/tasks-source/whiteboard-source, migration correctness (fresh install, idempotent re-run, expected columns), battery estimate outlier rejection, calendar_feed's fetch/merge/partial-failure handling, webdav_client's fetch/list-directory, the whiteboard force-refresh throttle bypass and browse endpoint, and render-size invariants across calendar views/orientations. No DB/HTTP fixtures need Docker, Node, or a real Immich/CalDAV/WebDAV server -- a fresh temp SQLite file plus a couple of small local HTTP servers as test doubles cover it all. Table data is wiped and reseeded between tests rather than relying on SQLAlchemy's transaction-rollback isolation pattern, which needs a pysqlite event-listener workaround app/db.py's engine doesn't have and has no reason to gain just for tests. Wired into .gitea/workflows/server-docker-build.yml as its own job that build-and-push now depends on, so a failing suite blocks the image push rather than just running alongside it for show. |