From fdcb444d57dac546cd2f527f725001c56e0b82b6 Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Mon, 20 Jul 2026 23:48:41 -0400 Subject: [PATCH] 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. --- server/Dockerfile | 8 +- server/README.md | 11 +- server/app/config.py | 17 +- server/app/main.py | 26 +- server/app/templates/base.html | 329 +++++++++++++++++++++++++ server/app/templates/index.html | 297 +++++++++++----------- server/app/templates/token_prompt.html | 50 ++-- server/docker-compose.yml.example | 4 - 8 files changed, 540 insertions(+), 202 deletions(-) create mode 100644 server/app/templates/base.html diff --git a/server/Dockerfile b/server/Dockerfile index 95bb660..1c8c4f2 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -2,10 +2,10 @@ FROM python:3.12-slim WORKDIR /app -# tzdata: python:3.12-slim doesn't include it by default, so a TZ -# environment variable (see docker-compose.yml.example -- used by the -# "Quiet hours" setting) would silently fail to resolve and fall back to -# UTC without this. +# tzdata: python:3.12-slim doesn't include it by default, so the zoneinfo +# database backing the web UI's "Timezone" setting (used by "Quiet hours") +# would have no named zones to resolve without this -- ZoneInfo() would +# raise for anything other than "UTC". RUN apt-get update && apt-get install -y --no-install-recommends tzdata \ && rm -rf /var/lib/apt/lists/* diff --git a/server/README.md b/server/README.md index 133dc7f..c294764 100644 --- a/server/README.md +++ b/server/README.md @@ -52,7 +52,7 @@ algorithm itself -- it just streams the response straight to the panel. upcoming grid -- not Immich URL/API key, see Setup above) - `GET /api/albums` -- lists Immich albums (used by the config UI) - `POST /api/config` -- saves - album/order/orientation/refresh_interval_s/smart_crop_faces/queue_target_len/quiet_hours_*. + album/order/orientation/refresh_interval_s/smart_crop_faces/queue_target_len/quiet_hours_*/timezone. `orientation` (`landscape`, `portrait`, `landscape_flipped`, `portrait_flipped`) matches how the frame is physically hung: photos are composed/cropped for that shape (portrait crops at 480x800), then @@ -66,10 +66,11 @@ algorithm itself -- it just streams the response straight to the panel. (`"HH:MM"`, may wrap past midnight, e.g. `22:00`-`07:00`) don't touch the device at all -- purely a server decision about what `refresh_interval_s` to hand back from `GET /frame/config` below, - computed in `_effective_refresh_interval_s`. Uses the server's local - timezone (`TZ` in `docker-compose.yml.example` -- the image needs - `tzdata` for a named zone to actually resolve, already installed in - the provided `Dockerfile`). The device can still land one wake right + computed in `_effective_refresh_interval_s`. Interpreted in the + `timezone` set from the web UI's "Timezone" dropdown (an IANA zone + name, e.g. `America/New_York`; defaults to `UTC`) -- no + docker-compose.yml edit or container restart needed to change it. The + device can still land one wake right at the start of the window (nothing server-side can prevent that without touching the firmware, since the device doesn't know wall-clock time), but from that wake on it's told to sleep exactly until the diff --git a/server/app/config.py b/server/app/config.py index b4ad416..0e4a168 100644 --- a/server/app/config.py +++ b/server/app/config.py @@ -27,15 +27,20 @@ class FrameConfig(BaseModel): order: str = "sequential" # or "shuffle" refresh_interval_s: int = 3600 # Quiet hours: no point waking the device overnight just to swap a - # photo nobody's looking at. Times are "HH:MM" in the server's local - # timezone (see docker-compose.yml.example's TZ note) and may wrap - # past midnight (e.g. start=22:00, end=07:00). Purely a server-side - # decision -- the device is unaware, it just gets told a longer - # refresh_interval_s by GET /frame/config while quiet hours are in - # effect (see main.py's _effective_refresh_interval_s). + # photo nobody's looking at. Times are "HH:MM" interpreted in + # `timezone` below and may wrap past midnight (e.g. start=22:00, + # end=07:00). Purely a server-side decision -- the device is unaware, + # it just gets told a longer refresh_interval_s by GET /frame/config + # while quiet hours are in effect (see main.py's + # _effective_refresh_interval_s). quiet_hours_enabled: bool = False quiet_hours_start: str = "22:00" quiet_hours_end: str = "07:00" + # IANA zone name (e.g. "America/New_York") quiet_hours_start/end are + # interpreted in. Set from the web UI rather than the container's TZ + # environment variable, so it survives container recreation and + # doesn't need a docker-compose.yml edit to change. + timezone: str = "UTC" smart_crop_faces: bool = True # How the physical frame is hung: landscape (native), portrait, # landscape_flipped, portrait_flipped. Purely a server-side render diff --git a/server/app/main.py b/server/app/main.py index 119fe97..87d6625 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -7,6 +7,7 @@ import io import logging import time from datetime import datetime, timedelta +from zoneinfo import ZoneInfo, available_timezones import httpx from fastapi import Depends, FastAPI, File, HTTPException, Form, Request, UploadFile @@ -32,6 +33,11 @@ MAX_QUEUE_TARGET_LEN = 5000 ORIENTATIONS = ("landscape", "portrait", "landscape_flipped", "portrait_flipped") +# Populated once from the OS's zoneinfo database (installed via the +# `tzdata` apt package in the Dockerfile) and offered as a - {% if cfg.album_id %}{% endif %} - - - - - -
- - -
-
- - -
- - -

Uses the server's local timezone - (see TZ in docker-compose.yml.example). The - device may still wake once right at the start of quiet hours -- it - can't know ahead of time -- but goes right back to sleep until they - end.

- - - - -
+
+
+

Settings

+
+ + + + +
+ + +
+
+ + +
+ + + +

Quiet hours times above are + interpreted in this timezone. The device may still wake once right + at the start of quiet hours -- it can't know ahead of time -- but + goes right back to sleep until they end.

+ + + +
+
+
-

Now displaying

-

Loading...

+
+
+

Now displaying

+

Loading...

+
-

Device

-

Loading...

+
+

Device

+

Loading...

+
-

Battery history

-

Loading...

+
+

Battery history

+

Loading...

+
-

Firmware update

-

- {% if cfg.firmware_available_version %}Uploaded: v{{ cfg.firmware_available_version }} -- the frame - updates itself on its next wake if it's running something else.{% else %}No firmware uploaded yet.{% endif %} -

- - +
+

Firmware update

+

+ {% if cfg.firmware_available_version %}Uploaded: v{{ cfg.firmware_available_version }} -- the frame + updates itself on its next wake if it's running something else.{% else %}No firmware uploaded yet.{% endif %} +

+ + +
+
-

Upcoming

-

Drag a photo to reorder (on touch, hold briefly first so a - normal scroll still works), "Show next" to jump it to the front, or - the × to remove it from rotation entirely.

-
+
+

Upcoming

+

Drag a photo to reorder (on touch, hold briefly first so a + normal scroll still works), "Show next" to jump it to the front, or + the × to remove it from rotation entirely.

+
+
+{% endblock %} +{% block scripts %} - - +{% endblock %} diff --git a/server/app/templates/token_prompt.html b/server/app/templates/token_prompt.html index fb0f87d..09d7a93 100644 --- a/server/app/templates/token_prompt.html +++ b/server/app/templates/token_prompt.html @@ -1,30 +1,20 @@ - - - - - - ESPresso Frame - - - -

ESPresso Frame

-

This management page needs an access token.

- {% if wrong %} -
Invalid token.
- {% endif %} -
- - - -
- - +{% extends "base.html" %} + +{% block page_class %}page-narrow{% endblock %} + +{% block subtitle %} +

This management page needs an access token.

+{% endblock %} + +{% block content %} +
+ {% if wrong %} +
Invalid token.
+ {% endif %} +
+ + + +
+
+{% endblock %} diff --git a/server/docker-compose.yml.example b/server/docker-compose.yml.example index 9ff50db..362b073 100644 --- a/server/docker-compose.yml.example +++ b/server/docker-compose.yml.example @@ -10,10 +10,6 @@ services: - CONFIG_PATH=/data/config.json - IMMICH_URL=http://your-immich-host:2283 - IMMICH_API_KEY=your-immich-api-key-here - # Set this to your local timezone (e.g. America/New_York) if you use - # the "Quiet hours" setting -- without it, the container defaults to - # UTC, and quiet hours would run on UTC clock time instead of yours. - - TZ=UTC # Optional: gates the entire server -- the web UI (/, /api/*) AND # every device-facing /frame/* endpoint -- behind this shared secret. # Leave unset to keep it all open on a trusted LAN, same as before.