Build and push server image / build-and-push (push) Successful in 31s
ESP32 side can now reach the tools server over HTTPS: the Tools Server field accepts an https:// address for a TLS-terminating reverse proxy in front of the server (which still only ever speaks plain HTTP itself), trusting Cloudflare's Origin CA root (embedded at build time) since that's the common way to get a real cert on a private origin. Every URL the device builds -- image fetch, config check, manage-menu data, the QR codes' own links -- goes through one build_url() helper that picks the scheme from what's configured. Also adds an optional MANAGEMENT_TOKEN (docker-compose.yml) that gates the web UI (/, /api/*) behind a shared secret -- unset by default, so existing trusted-LAN deployments are unaffected. The same token is entered once during the ESP32's captive-portal setup and gets baked into the manage-menu's QR code (?token=...), so scanning it just works; visiting the page without a valid token shows a plain entry prompt instead of the config UI, and a valid query-param hit sets a cookie so the page's own fetch()/<img> calls stay authorized for the rest of the visit. Device-facing /frame/* endpoints are unaffected -- a separate, already-documented trust boundary.
65 lines
2.1 KiB
Python
65 lines
2.1 KiB
Python
"""JSON-file-backed config: Immich connection, selected album, and cursor
|
|
state (which photo /frame/image serves next)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from threading import Lock
|
|
|
|
from pydantic import BaseModel
|
|
|
|
CONFIG_PATH = Path(os.environ.get("CONFIG_PATH", "/data/config.json"))
|
|
|
|
_lock = Lock()
|
|
|
|
|
|
class FrameConfig(BaseModel):
|
|
immich_url: str = ""
|
|
immich_api_key: str = ""
|
|
management_token: str = "" # gates the web UI (see main.py); empty = no gate, open on trusted LAN
|
|
album_id: str = ""
|
|
order: str = "sequential" # or "shuffle"
|
|
refresh_interval_s: int = 3600
|
|
smart_crop_faces: bool = True
|
|
|
|
# Current photo + upcoming queue (see app/photo_queue.py). current_asset_set_at
|
|
# is what lets the server decide "has it been long enough to advance" on its
|
|
# own clock, independent of how/why the device asked for a photo.
|
|
current_asset_id: str = ""
|
|
current_asset_set_at: float = 0.0
|
|
queue: list[str] = []
|
|
queue_cursor: int = 0 # internal bookkeeping for sequential queue top-up; not user-facing
|
|
queue_target_len: int = 20 # how many upcoming photos to keep queued/shown in the web UI
|
|
|
|
|
|
def load() -> FrameConfig:
|
|
with _lock:
|
|
if not CONFIG_PATH.exists():
|
|
cfg = FrameConfig()
|
|
else:
|
|
cfg = FrameConfig(**json.loads(CONFIG_PATH.read_text()))
|
|
|
|
# IMMICH_URL/IMMICH_API_KEY/MANAGEMENT_TOKEN set in the environment
|
|
# (e.g. docker-compose.yml, see docker-compose.yml.example) take
|
|
# precedence over whatever's saved in CONFIG_PATH, so credentials never
|
|
# need to go through the web UI.
|
|
env_url = os.environ.get("IMMICH_URL")
|
|
env_key = os.environ.get("IMMICH_API_KEY")
|
|
env_token = os.environ.get("MANAGEMENT_TOKEN")
|
|
if env_url:
|
|
cfg.immich_url = env_url
|
|
if env_key:
|
|
cfg.immich_api_key = env_key
|
|
if env_token:
|
|
cfg.management_token = env_token
|
|
|
|
return cfg
|
|
|
|
|
|
def save(cfg: FrameConfig) -> None:
|
|
with _lock:
|
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
CONFIG_PATH.write_text(cfg.model_dump_json(indent=2))
|