Move mode picker below device status bar; fix invisible calendar lines; add CalDAV calendar support
Build and push server image / build-and-push (push) Successful in 50s

Calendar rule/grid lines were light gray, which dithers away to
near-invisible on the 6-color e-ink palette -- now black.

CalDAV accounts (Nextcloud, Fastmail, iCloud, ...) can now be linked
alongside the existing single ICS subscription, since one account can
expose several calendars. A frame's Calendar tab now lists calendars
per person rather than one opt-in per person: your own row shows every
calendar you have available with a full add/remove toggle, while other
linked users' rows show only calendars they've included, toggleable
off (mute) but not on -- only a calendar's owner can add it to a
shared frame. FrameCalendar replaces the old single-boolean
UserFrame.calendar_included; existing opt-ins are migrated forward.
This commit is contained in:
2026-07-22 22:01:05 -04:00
parent 95d69a5512
commit acdb929a99
21 changed files with 525 additions and 109 deletions
+35 -15
View File
@@ -36,7 +36,7 @@ from ..image_pipeline import (
render_preview_png,
)
from ..firmware import firmware_path, parse_app_version
from ..models import BatteryLog, Frame, UserFrame
from ..models import BatteryLog, Frame, FrameCalendar
from .common import (
FRAME_MODES,
OVERDUE_FACTOR,
@@ -409,33 +409,53 @@ def api_preview_rendered(frame: Frame = Depends(require_frame_view), db: Session
return Response(content=png, media_type="image/png")
class CalendarIncludedRequest(BaseModel):
class CalendarSelectRequest(BaseModel):
user_id: int
calendar_key: str
calendar_label: str = ""
included: bool
@router.post("/api/frames/{frame_id}/calendar-included")
def api_calendar_included(
body: CalendarIncludedRequest,
@router.post("/api/frames/{frame_id}/calendar-select")
def api_calendar_select(
body: CalendarSelectRequest,
request: Request,
frame: Frame = Depends(require_frame_view), # view access only -- NOT require_frame_control
db: Session = Depends(get_db),
):
"""A user's own opt-in into this frame's merged calendar (see
UserFrame.calendar_included). Deliberately not require_frame_control:
this is the toggling user's own data-sharing preference about their
own calendar, not a frame setting its controller manages on someone
else's behalf -- there's no target user_id in the request body by
design, it always toggles the calling session's own row."""
"""Include/exclude one calendar (calendar_key "ics" or
"caldav:<href>", see FrameCalendar) on this frame. Deliberately not
require_frame_control: adding your own calendar, or muting anyone's
(including your own), is each viewer's own call, not something a
frame's controller manages on someone else's behalf. The one-sided
permission split lives here: turning a calendar ON requires being its
owner (nobody can add someone else's calendar to a shared frame for
them); turning one OFF only requires being linked to the frame at
all, so anyone sharing the display can mute a calendar they'd rather
not see there even if they don't own it."""
user = require_user_api(request, db)
row = db.get(UserFrame, (user.id, frame.id))
if body.included and body.user_id != user.id:
raise HTTPException(403, "Only a calendar's owner can add it to a frame")
row = db.execute(
select(FrameCalendar).where(
FrameCalendar.frame_id == frame.id,
FrameCalendar.user_id == body.user_id,
FrameCalendar.calendar_key == body.calendar_key,
)
).scalar_one_or_none()
if row is None:
raise HTTPException(404, "Not linked to this frame")
row.calendar_included = body.included
if not body.included:
raise HTTPException(404, "Not currently included on this frame")
row = FrameCalendar(frame_id=frame.id, user_id=body.user_id, calendar_key=body.calendar_key)
db.add(row)
row.included = body.included
if body.calendar_label:
row.calendar_label = body.calendar_label
# Force this frame's merged cache to pick up the change promptly
# rather than waiting out the throttle.
frame.calendar_checked_at = 0.0
db.commit()
return {"status": "saved", "included": row.calendar_included}
return {"status": "saved", "included": row.included}
def _calendar_photo_inlay(frame: Frame, db: Session):