Web frontend (packages/web)
July 21, 2026 · View on GitHub
React single-page app (Vite + TypeScript + React Router + TanStack Query) with a
bento-style design system, served as static files by the API service. It
replaced the earlier NiceGUI UI service; the /media image proxy that service
hosted now lives in the API (packages/api/.../routes/media.py).
Sections
The top nav has five sections:
| Path | Purpose |
|---|---|
/images | Library: browse, search, upload, edit, delete. Filter by source, orientation, grid pool, or rotation status (excluded images get a badge); real pagination ("x–y of N" via the API's X-Total-Count); multi-select mode for bulk delete (with a 7-second Undo window before anything is sent) / bulk grid assignment — bulk failures name the affected images. The Groups overview lists every image group (panel spreads): generated ones read-only + delete, curated ones fully editable (rename, grid, per-image panel assignment with a mini layout preview); select images → Group… creates a spread with panels auto-assigned round-robin. Image detail has an e-ink preview toggle (server-simulated Spectra 6 dither), a per-image hold time, an exclude-from-rotation switch, and a send dialog that offers every device — non-matching panels are sent a server-side cover-crop ("cropped to fit") instead of being hidden |
/displays | Upcoming refresh schedule, online device wall with per-device controls (next, pin/unpin, "don't show again" for the current image, schedule, clear), and grid management. Refresh health distinguishes "failed — retrying" (controller self-heals) from "failing — check power" (failure outlived the backoff; likely needs a physical power cycle); failing devices also surface as alerts on the landing page |
/grids/{id} | Grid detail: proportional canvas preview with drag-to-reposition placements (numeric cm inputs remain for precision), confirmation before removing a placement, the "Up next" queue card (playback order with reorder arrows, per-entry show-now, Next/Release), the "Display schedule" card (cron preset editor with weekday/time builders and timezone, duration or "until released"), per-grid image pool with resolution traffic-light badges and an honest truncation note |
/jobs | Overview of upcoming runs and recent executions (in progress with spinner, waiting-for-worker, success/error with result stats) above a tabbed list of Immich sync, Gemini and display jobs (?tab=gemini|display) with per-job run history, "Run now", pause switch and a quick schedule dialog for display jobs. Create/edit forms live at /sync-jobs/*, /gemini-jobs/* and /display-jobs/* |
/display-jobs/{id} | Display-job editor: story prompt + source mode + image preset, generation schedule (cron presets, manual-only supported), target grid with one content part per slot, generate/display actions, and a 7-day story history with per-story preview and redisplay (see motd.md). Session control lives on the grid page |
/genai | Tabbed GenAI hub (?tab=prompts; old ?tab=motd|jobs links redirect to /jobs?tab=display). Images: on-demand generation form and a "Recent generations" status list (backed by GET /api/genai/tasks, shared with job generations). Prompt library: the block/preset library image prompts are built from |
/settings | Global default refresh interval, quiet hours (daily window pausing automatic rotation), guest invites |
Legacy deep links (/generate, /prompts, bare /sync-jobs,
/gemini-jobs) redirect to their new homes. A dark mode toggle in the nav persists per browser
and defaults to the OS preference. A global error boundary catches render
crashes with a reload prompt instead of a blank page, and the image
detail/upload forms guard unsaved changes on navigation (data-router
blocker + beforeunload).
Architecture
Browser ──HTTP──► API container ──S3──► MinIO / Garage (writer credentials)
▲ │
└── static React └── MQTT ──► devices
bundle + /media proxy
- The browser talks same-origin only:
/api/*for data,/media/*for images, everything else falls back to the SPA'sindex.html. /media/{object_key}streams originals with ETag/Cache-Control;?w=240|480|960serves a downscaled JPEG generated lazily on first request and cached in the bucket underthumbs/w{width}/…. Gallery and device-card views request?w=480; detail views load originals.- Auth is optional (see auth.md): unconfigured, the app is open
(trusted LAN); with OIDC configured the SPA shows a sign-in gate, learns
its role from
GET /api/auth/me, and renders a reduced UI (Images + GenAI) for guests arriving via invite links. The browser never holds tokens — only an HttpOnly session cookie, which also authenticates/media/*image loads.
Development
cd packages/web
npm install
npm run dev # Vite dev server on :5173, proxying /api and /media to localhost:8000
npm run build # typecheck + production bundle into dist/
Point a locally running API at the build output with
API_WEB_DIST_PATH=$(pwd)/packages/web/dist. The API container image builds
and bundles the frontend automatically.