# espresso_frame A DIY e-ink photo frame: an ESP32-C6 (`firmware/`, ESP-IDF) driving a Waveshare 7.3" E Ink Spectra 6 panel (800x480, 6-color, SPI), paired with a self-hosted FastAPI server (`server/`) that pulls from Immich, does all image processing (crop/dither/quantize/pack), and serves a placeable photos/calendar/whiteboard/weather widget system to the device. CURRENT TODO -sharing layouts with linked users -a "coming up this week" widget -on reset dismiss the menu. Start here, don't re-derive from scratch: - [`docs/architecture.md`](docs/architecture.md) -- how firmware and server talk (sequence diagram, boot flow). - [`docs/widgets.md`](docs/widgets.md) -- the server-side widget system (data model, grid placement, compositor, button-action dispatch). - [`docs/hardware.md`](docs/hardware.md) -- wiring. - [`server/README.md`](server/README.md), [`firmware/README.md`](firmware/README.md) -- per-component setup, config, and a lot of accumulated gotchas (Immich API shape, TLS trust-anchor details, button GPIO wakeup quirks, etc.) -- check these before assuming something is a new bug. ## Conventions specific to this repo - **No `Co-Authored-By: Claude` trailers in commits.** Attribution lives in the root [`README.md`](README.md) instead (see its last line) -- the maintainer's explicit preference, not the default. - **Copyleft dependencies need an explicit flag, not a silent decision.** Before adding anything LGPL/GPL/AGPL (or unclear), verify the actual license via `pip show`/package metadata -- including transitive deps, not just the top-level package -- and present the finding and tradeoff in plain text rather than picking an approach unilaterally (hand-rolling an alternative, swapping packages, silently accepting it). This project has knowingly accepted AGPL-3.0-or-later exposure once already (`icalendar-searcher`, a transitive dep of `caldav`) as a deliberate, explicit call -- not a precedent for skipping the check next time. - **Scope new auth/access-control broadly, not just to the literal endpoint named.** When a request changes the trust model (e.g. adding public-internet exposure), apply the new gate to every endpoint serving real data or performing a real action, and call out anything you're tempted to exclude and why. This repo shipped a token gate once that covered `/api/*` but left `/frame/image` -- the actual photo bytes -- open; caught immediately in production. - **Commit and push once a task is verified working, without waiting to be asked separately.** Once tests pass (and, for UI changes, the browser check has been done), stage the relevant files, write a normal commit message, and push to the current branch -- the maintainer's standing authorization for the commit/push step itself. This doesn't relax anything else: still run `git status`/review the diff before staging, still never force-push/amend a pushed commit/skip hooks, and still surface anything that looks like it needs a real decision (e.g. a change that would trigger `main`'s deploy workflow, see below) instead of pushing through it silently. ## Working in this repo - **Server tests**: `cd server && pytest` (SQLite, fixtures wipe/reseed between tests -- see `tests/conftest.py`). Migration changes need a matching test in `tests/test_migrations.py`; anything touching `require_frame_view`/`require_frame_control` boundaries needs a same-shape permission test (see `tests/test_permission_boundaries.py` and `tests/test_button_actions.py` for the pattern: owner, linked user, unrelated user, logged out). - **UI changes**: verify in a real browser (Playwright), not just by reading the JS -- this project has hit multiple bugs that only showed up live (mobile viewport CSS collapse, a dialog's status message landing behind its own backdrop, a JSON/form-urlencoded body mismatch). Spin up `uvicorn app.main:app` against a scratch `DATABASE_URL`/`CONFIG_PATH` sqlite file, don't touch the real deployment's data. `.claude/skills/run-server/` (`/run-server`) has a driver for exactly this. - **New/changed UI must work at both desktop and mobile widths -- screenshot both, don't assume one implies the other.** The layout genuinely forks at the 860px breakpoint (`theme.css`): the sidebar goes off-canvas behind a hamburger below it. A dialog, header control, or new widget that looks right at a wide viewport can overflow, overlap the mobile bar, or mis-center at phone widths. `run-server`'s driver has a `viewport` command for exactly this (defaults to a phone size; switch to `1280 900` for desktop). - **Deploy**: Gitea Actions at `git.thumeit.com/tfaour/espresso_frame` (`.gitea/workflows/server-docker-build.yml`: `test` -> `build-and-push` -> `deploy` on any push to `main` touching `server/**`; `deploy` SSHes into the host as `espressoframe_deployer` and runs `docker compose pull && docker compose up -d`). A separate workflow (`firmware-release-build.yml`) builds+publishes firmware binaries as Gitea release assets when `firmware/version.txt` changes. Poll CI status with `curl https://git.thumeit.com/api/v1/repos/tfaour/espresso_frame/actions/tasks` rather than asking the user to check. - **Device-facing paths are frozen.** `/frame/image`, `/frame/advance`, `/frame/back`, `/frame/config`, `/frame/battery`, `/frame/firmware` and their exact JSON key names (`refresh_interval_s`, `firmware_version`, etc.) are baked into deployed firmware -- never rename or restructure these without a firmware-side migration story to match.