This sandbox ships with no Python/Node/Docker/browser and no sudo, so
the bulk of this is setup.sh: bootstrap Python via uv, then get
Playwright's Chromium (and tmux, also missing) working by extracting
their .deb dependencies non-root instead of apt-get install. driver.py
is a small Playwright REPL standing in for chromium-cli, which isn't
available here either.
Also carves out .claude/skills/ from the blanket .claude/ gitignore --
skills are shared project tooling, not personal/local state.
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.
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.
- version.txt + esp_app_desc_t version reporting (X-Frame-Version header);
new ota_update.c checks the server's advertised version against the
running one and streams+applies an update via esp_https_ota, gated by
bootloader rollback (marks the image valid only after a full successful
cycle, so a bad update can't brick a wall-mounted frame).
- Dual-OTA partition tables: partitions.csv (8MB dev board, 2MB slots) and
new partitions_xiao.csv (4MB XIAO, 1.875MB slots -- the dev board's
table doesn't fit the XIAO's flash). New build_for_board.sh gives each
board its own build dir + generated sdkconfig via SDKCONFIG_DEFAULTS
layering, so switching boards never clobbers the other's config.
- fetch_photo_info()/fetch_face_labels() were using the short
reachability-check timeout even though the manage-menu path can be the
first (cold, TLS-handshake-paying) request of a wake cycle -- switched
to the longer fetch timeout to stop spurious ESP_ERR_HTTP_CONNECT
failures.
- XIAO: the RF switch that selects onboard vs. external antenna
(GPIO3/14) isn't initialized by plain ESP-IDF the way Seeed's Arduino
package does it, leaving WiFi unable to reliably reach the antenna at
all -- new board_antenna.c powers the switch and selects the onboard
antenna, gated behind FRAME_XIAO_ANTENNA_INIT (on by default in
sdkconfig.xiao). Also remaps the EPD DC/RST/BUSY pins, since the dev
board's defaults (GPIO9/10/11) aren't physically exposed on the XIAO.
docker-compose.yml is tracked in a repo meant for publishing, so it can't
hold a real API key. Renamed it to docker-compose.yml.example (placeholder
values, safe to commit) and gitignored the real docker-compose.yml --
deploying is now "cp the example, fill in real values, docker compose up",
no .env file needed.
config.load() now reads IMMICH_URL/IMMICH_API_KEY from the environment
and applies them on top of whatever's in config.json, so setting them in
the compose file's environment: block takes effect without ever touching
the web UI. Env vars always win over the UI-saved values when both are
present -- verified they survive a save() with different UI-entered
values still in place.
Moves the existing ESP-IDF captive_portal example into firmware/ to make
room for the new FastAPI server and project docs, ahead of building out the
full ESPresso Frame project (ESP32-C6 + Immich-backed e-ink photo frame).