Add whiteboard frame mode (Nextcloud Whiteboard / Excalidraw over WebDAV)
Build and push server image / build-and-push (push) Failing after 1m10s
Build and push server image / build-and-push (push) Failing after 1m10s
New third mode alongside photos/calendar: fetches a .whiteboard file over plain WebDAV (Basic auth -- generic, not Nextcloud-specific) and renders it via a small Node.js sidecar using Excalidraw's own real export code (@excalidraw/utils + @resvg/resvg-js, no headless browser), since a .whiteboard file turns out to be Excalidraw scene JSON, not an image. The sidecar runs as a second process inside this same container (Dockerfile installs Node, start.sh backgrounds it before exec'ing uvicorn) rather than a separate docker-compose service -- lightweight, stateless, reachable only at 127.0.0.1 from the Python process, nothing worth independently scaling. The rendered PNG is treated exactly like a photo from there on -- composed/quantized through the existing image_pipeline (letterboxed, never cropped) rather than a second parallel rendering pipeline. WebDAV credentials support the common "it's actually the same Nextcloud account as my CalDAV" case (an explicit opt-in checkbox, not silently inferred) while still working with any WebDAV server generically. Frame-level source (URL + owning account) follows the same owner- controls-their-own-data permission split as calendar sources and the week view's task list: only the account owner can point a frame at it, anyone linked can clear it. Honest limitation: this environment has no Node.js/npm, so render-service/ is written carefully against each library's documented API (verified via the npm registry, including transitive dependency licenses after the CalDAV/AGPL surprise earlier this session) but has never actually been executed. First real docker build is the first true test -- see render-service/README.md.
This commit is contained in:
+37
-3
@@ -19,7 +19,7 @@ from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Float, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy import JSON, Boolean, Float, ForeignKey, Index, Integer, LargeBinary, String
|
||||
from sqlalchemy.ext.mutable import MutableList
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
@@ -68,6 +68,18 @@ class User(Base):
|
||||
# add, without hitting the CalDAV server on every page load.
|
||||
calendar_caldav_calendars: Mapped[list | None] = mapped_column(JSON, nullable=True, default=None)
|
||||
calendar_caldav_checked_at: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
# WebDAV credentials for whiteboard frame mode (see webdav_client.py,
|
||||
# whiteboard.py) -- generic WebDAV, not Nextcloud-specific, but
|
||||
# webdav_reuse_caldav_creds is a convenience for the common case
|
||||
# where it IS the same Nextcloud account as calendar_caldav_*: skip
|
||||
# re-entering the same username/password, since Nextcloud's CalDAV
|
||||
# and general-file-WebDAV both sit under the one account. Doesn't
|
||||
# try to be clever and derive the reuse automatically -- an explicit
|
||||
# opt-in, same as everywhere else in this project defaults features
|
||||
# off rather than silently inferring them.
|
||||
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)
|
||||
created_at: Mapped[float] = mapped_column(Float, default=time.time)
|
||||
|
||||
__table_args__ = (
|
||||
@@ -102,8 +114,9 @@ class Frame(Base):
|
||||
# the migrated legacy frame until its device first reports an id.
|
||||
device_id: Mapped[str | None] = mapped_column(String, unique=True, nullable=True)
|
||||
name: Mapped[str] = mapped_column(String, default="")
|
||||
# Renderer dispatch seam for future calendar/canva modes -- only
|
||||
# "photos" is registered today (see routers/device.py RENDERERS).
|
||||
# Renderer dispatch seam -- "photos", "calendar", or "whiteboard"
|
||||
# (see routers/device.py RENDERERS/ADVANCE_RENDERERS/BACK_RENDERERS
|
||||
# and routers/common.py FRAME_MODES).
|
||||
mode: Mapped[str] = mapped_column(String, default="photos")
|
||||
# Whose Immich library this frame pulls from; NULL = unclaimed.
|
||||
owner_user_id: Mapped[int | None] = mapped_column(
|
||||
@@ -238,6 +251,27 @@ class Frame(Base):
|
||||
# by due date -- see caldav_client.fetch_tasks.
|
||||
calendar_tasks_cached: Mapped[list | None] = mapped_column(JSON, nullable=True, default=None)
|
||||
|
||||
# -- whiteboard mode (see webdav_client.py, whiteboard.py,
|
||||
# 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: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
# The specific .whiteboard file's WebDAV URL -- pasted directly, same
|
||||
# idiom as calendar_ics_url, not discovered/browsed (unlike CalDAV's
|
||||
# account-has-several-calendars case, a WebDAV account doesn't need
|
||||
# a picker step here since the user already knows which one file).
|
||||
whiteboard_url: Mapped[str] = mapped_column(String, default="")
|
||||
whiteboard_checked_at: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
# Cached rendered PNG bytes (see whiteboard.fetch_and_render) --
|
||||
# BLOB rather than the JSON columns the rest of this cache-pattern
|
||||
# family uses, since this is binary image data, not JSON-shaped.
|
||||
whiteboard_cached_image: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
|
||||
|
||||
# -- state --
|
||||
current_asset_id: Mapped[str] = mapped_column(String, default="")
|
||||
current_asset_set_at: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
|
||||
Reference in New Issue
Block a user