Files
espresso_frame/CLAUDE.md
T
tfaour b15747a604
Build and push server image / test (push) Has been cancelled
Build and push server image / build-and-push (push) Has been cancelled
Build and push server image / deploy (push) Has been cancelled
Add per-widget border option (style, thickness, palette color)
A Widget-level property (border_style/border_thickness/border_color_index),
not a per-type config field, since every widget type can have one -- drawn
once centrally in device.py's _render_widgets before compositing, using
an exact panel palette color so it never dithers. Styles: solid, dashed,
dotted, and a fancy double-line picture-frame-mat look. Configurable from
a shared "Border" card in every widget's gear-icon dialog.
2026-07-27 19:51:04 +00:00

5.8 KiB

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 -add more actions for buttons (i.e. change widget/layout) -Fix spurious button assignment stuff (probably but buttons on widget config with sane defaults) -battery life widget -sharing layouts with linked users -a "coming up this week" widget -scan to download for non-immich photos too? -switch button reset action? and on reset dismiss the menu.

Start here, don't re-derive from scratch:

  • docs/architecture.md -- how firmware and server talk (sequence diagram, boot flow).
  • docs/widgets.md -- the server-side widget system (data model, grid placement, compositor, button-action dispatch). Notes a known gap at the bottom (legacy Frame columns not yet dropped).
  • docs/hardware.md -- wiring.
  • server/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 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.