Add a whiteboard file picker and force-refresh
Build and push server image / build-and-push (push) Successful in 1m57s

File picker: an optional WebDAV browse root in Settings
(User.webdav_base_url) plus a plain-PROPFIND directory listing
(webdav_client.list_directory) power a "Browse..." panel on a frame's
Whiteboard tab, so a file can be clicked into rather than typing its
exact WebDAV URL. Manual URL entry still works unchanged either way.

Force-refresh: get_or_refresh_whiteboard takes a force flag that skips
the fetch throttle entirely; the Whiteboard tab's refresh button now
passes it, so clicking it always re-fetches and re-renders instead of
possibly just re-showing the same cached image from within the last
~20 minutes.
This commit is contained in:
2026-07-23 18:20:39 -04:00
parent 8556221b08
commit b4ca795003
9 changed files with 237 additions and 13 deletions
+41 -4
View File
@@ -25,7 +25,7 @@ from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from .. import calendar_render, gitea_releases, photo_queue, quiet_hours, weather
from .. import calendar_render, gitea_releases, photo_queue, quiet_hours, weather, webdav_client
from ..auth import require_frame_control, require_frame_view, require_user_api
from ..db import frame_locked, get_db
from ..image_pipeline import (
@@ -52,6 +52,7 @@ from .common import (
list_assets,
require_configured,
valid_http_url,
webdav_creds_for,
)
logger = logging.getLogger(__name__)
@@ -650,15 +651,51 @@ def api_whiteboard_source(
return {"status": "saved", "url": body.url}
@router.get("/api/frames/{frame_id}/whiteboard-browse")
def api_whiteboard_browse(
request: Request,
url: str | None = None,
frame: Frame = Depends(require_frame_view),
db: Session = Depends(get_db),
):
"""One level of a WebDAV directory listing, using the calling user's
own credentials (never the frame's saved whiteboard_user_id -- this
is "help me find a file in MY account", same person as whoever would
go on to Save it, before that's even happened) -- powers the file
picker on the Whiteboard tab as an alternative to pasting a URL.
`url` omitted/None starts from the user's webdav_base_url (see
models.py's User docstring); passing back a previous response's
`entries[].url` (for a folder) descends into it."""
user = require_user_api(request, db)
creds = webdav_creds_for(user)
if creds is None:
raise HTTPException(400, "Set up WebDAV credentials in Settings first")
target = url or user.webdav_base_url
if not target:
raise HTTPException(400, "Set a WebDAV browse root in Settings first, or paste the file URL directly")
if not valid_http_url(target):
raise HTTPException(400, "Browse URL must be a plain http:// or https:// URL")
try:
entries = webdav_client.list_directory(target, creds[0], creds[1])
except webdav_client.WebDavError as e:
raise HTTPException(502, f"Could not browse: {e}")
base = user.webdav_base_url or target
parent_url = webdav_client.parent_directory_url(base, target)
return {"current_url": target, "parent_url": parent_url, "entries": entries}
@router.get("/api/frames/{frame_id}/preview/whiteboard")
def api_preview_whiteboard(frame: Frame = Depends(require_frame_view), db: Session = Depends(get_db)):
def api_preview_whiteboard(
force: bool = False, frame: Frame = Depends(require_frame_view), db: Session = Depends(get_db)
):
"""The same throttled fetch/render cache a live device request would
use, run through the same panel composition/quantization pipeline
(see routers/device.py's _render_whiteboard_mode) -- "how it will
look on the frame" (dithered, letterboxed), not just the raw
Excalidraw export, same convention as preview/rendered and
preview/calendar."""
png_bytes = get_or_refresh_whiteboard(db, frame)
preview/calendar. force=True (the "Refresh now" button, as opposed
to just reopening this tab) bypasses the fetch throttle."""
png_bytes = get_or_refresh_whiteboard(db, frame, force=force)
if png_bytes is None:
if not frame.whiteboard_url:
raise HTTPException(400, "No whiteboard configured on this frame yet")