Commit Graph
6 Commits
Author SHA1 Message Date
tfaour 4e8c6e534b Install Node.js from Debian's own repo, drop NodeSource dependency
Build and push server image / test (push) Successful in 30s
Build and push server image / build-and-push (push) Successful in 2m34s
Build and push server image / deploy (push) Successful in 53s
deb.nodesource.com started intermittently 403ing today on both its
setup_*.x scripts and its GPG key (confirmed directly, not just via CI --
some setup_NN.x paths 403, others 200, no consistent pattern), and the
curl-piped-into-bash install pattern silently swallowed that failure
instead of breaking the build loudly: curl -f exits non-zero on a 403,
but bash then runs on empty stdin and exits 0, so the RUN kept going
into a broken fallback (Debian's own split nodejs package with no
bundled npm) rather than stopping.

This base image now tracks Debian trixie, whose own nodejs package
(20.19.2) is inside jsdom 29's engines range and clears express/
resvg-js's much lower floors -- the version gap that originally required
routing through NodeSource is gone, so this drops that whole external
dependency (and the curl/gnupg install-then-purge dance) rather than
just swapping to a different NodeSource script.
2026-07-27 20:40:30 +00:00
tfaour c171047adf Split server Dockerfile into smaller layers
Build and push server image / build-and-push (push) Successful in 2m2s
The 413 from git.thumeit.com was on a single blob PUT, not the whole
image -- split the Node.js apt install and the render-service npm
install into several smaller RUN layers (purging curl/gnupg in the
same layer they're installed in, cleaning npm's cache after each
package) so no single pushed blob is as large as before.
2026-07-23 17:23:20 -04:00
tfaour 644fdefa66 Add whiteboard frame mode (Nextcloud Whiteboard / Excalidraw over WebDAV)
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.
2026-07-23 17:02:08 -04:00
tfaour fdcb444d57 Server web UI: modern themeable layout with dark mode, timezone moved into config
Replaces the ad-hoc inline styling with a shared base template driven by
CSS custom properties (light/dark palettes), a card-based two-column
layout, and a persistent dark-mode toggle. Also moves quiet-hours'
timezone from the container's TZ env var into a proper web UI setting
(zoneinfo-backed), so it no longer needs a docker-compose.yml edit and
restart to change.
2026-07-20 23:48:41 -04:00
tfaour fd9516f5d1 Server: quiet hours (no wake overnight), zero firmware changes needed
Build and push server image / build-and-push (push) Successful in 36s
Purely a server-side decision: GET /frame/config hands back a longer
refresh_interval_s while quiet hours are in effect (exactly the seconds
until they end), and clamps the normal interval so the device's next
wake lands at the boundary instead of wandering into the window, when
outside it but approaching. A device already mid-sleep when quiet hours
begin can still land one wake inside the window -- unavoidable without
touching the firmware, since it has no wall-clock awareness -- but from
that wake on it sleeps straight through to the end.

Window is "HH:MM"-"HH:MM", wrap-past-midnight aware (e.g. 22:00-07:00),
in the server's local timezone -- added tzdata to the Dockerfile since
python:3.12-slim doesn't include it and TZ would otherwise silently
resolve to nothing and fall back to UTC.

Also fixed the "overdue" device-status check to account for quiet hours:
without this it would falsely flag a device sleeping through a long
quiet window as unreachable.
2026-07-20 23:25:43 -04:00
tfaour 5e7c47e2eb Add FastAPI server: pulls from Immich, pre-processes for the panel
Implements the server side of the architecture decided on: the ESP32-C6
has no PSRAM and a tight RAM budget, so all the heavy lifting (JPEG
decode, resize, Floyd-Steinberg dithering, 6-color quantization, 4bpp
packing) happens here instead of on-device. The frame just does a single
GET and streams the response straight to SPI.

- GET /frame/image: looks up the current cursor's asset in the configured
  Immich album, downloads its preview thumbnail, and returns it packed
  into the panel's exact 800x480/4bpp/2px-per-byte format
  (application/octet-stream, always exactly 192,000 bytes).
- GET / + POST /api/config + GET /api/albums: a small web UI for entering
  the Immich URL/API key and picking an album, rather than cramming that
  into the ESP32's captive portal form.
- Config (Immich creds, selected album, cursor) persists to a JSON file
  via a docker-compose volume mount.

Verified locally with a venv (Docker isn't available in this environment):
unit-tested image_pipeline against a synthetic image (exact byte count,
valid panel color codes only), and ran a full end-to-end pass against a
mock Immich HTTP server exercising the real /frame/image path.

Pinned dependency versions in requirements.txt after hitting a real bug
with unpinned floors: the latest starlette (1.3.1) resolved by `pip
install fastapi` breaks Jinja2Templates outright.

Not yet wired to the ESP32 side (task 6) or authenticated -- /frame/image
is unauthenticated for now, fine on a trusted LAN but worth revisiting
once the firmware sends a shared device token.
2026-07-18 14:35:14 -04:00