From a21962cbcf1e463ab9b3a828a29f243954094928 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Apr 2026 18:59:34 +0000 Subject: [PATCH] feat(01-03): integration wiring, Docker deployment, lightsync.groll.cloud - Fix API routing: redirect_slashes=False + strip trailing slashes from route paths - Fix StaticFiles catch-all intercepting /api/* before FastAPI redirect - Add [tool.hatch.build.targets.wheel] to pyproject.toml for Docker builds - Install docker-compose plugin (v2.34.0) as CLI plugin - Create Dockerfile (python:3.11-alpine, CMD python -m lightsync) - Create docker-compose.prod.yml (Traefik + Authelia, edge network) - Deploy lightsync container to lightsync.groll.cloud - Add LightSync entry to start/projects.json - Update frontend app.js to use /api/devices (no trailing slash) --- Dockerfile | 12 ++++++++++++ docker-compose.prod.yml | 24 ++++++++++++++++++++++++ lightsync/api/devices.py | 4 ++-- lightsync/api/shows.py | 4 ++-- lightsync/frontend/app.js | 2 +- lightsync/main.py | 2 +- pyproject.toml | 3 +++ 7 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.prod.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..df1af7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-alpine + +WORKDIR /app + +COPY pyproject.toml . +RUN pip install --no-cache-dir . + +COPY lightsync/ lightsync/ + +EXPOSE 8000 + +CMD ["python", "-m", "lightsync"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..d6d6c87 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,24 @@ +services: + lightsync: + build: . + container_name: lightsync + restart: unless-stopped + environment: + HOST: 0.0.0.0 + PORT: "8000" + volumes: + - ./shows:/app/shows + - ./devices.json:/app/devices.json + networks: + - edge + labels: + - "traefik.enable=true" + - "traefik.http.routers.lightsync.rule=Host(`lightsync.groll.cloud`)" + - "traefik.http.routers.lightsync.entrypoints=websecure" + - "traefik.http.routers.lightsync.tls.certresolver=letsencrypt" + - "traefik.http.routers.lightsync.middlewares=authelia@docker" + - "traefik.http.services.lightsync.loadbalancer.server.port=8000" + +networks: + edge: + external: true diff --git a/lightsync/api/devices.py b/lightsync/api/devices.py index 2130254..fa45131 100644 --- a/lightsync/api/devices.py +++ b/lightsync/api/devices.py @@ -5,12 +5,12 @@ import lightsync.main as state # access registry via module-level ref router = APIRouter(tags=["devices"]) -@router.get("/") +@router.get("") async def list_devices() -> list[DeviceConfig]: return state.registry.list_all() -@router.post("/", status_code=201) +@router.post("", status_code=201) async def add_device(config: DeviceConfig) -> DeviceConfig: state.registry.add(config) await state.registry.save() diff --git a/lightsync/api/shows.py b/lightsync/api/shows.py index 2f67f32..250673a 100644 --- a/lightsync/api/shows.py +++ b/lightsync/api/shows.py @@ -5,7 +5,7 @@ import lightsync.main as state # access show_store via module-level ref router = APIRouter(tags=["shows"]) -@router.get("/") +@router.get("") async def list_shows(): """Return summary list of all shows.""" summaries = [] @@ -20,7 +20,7 @@ async def list_shows(): return summaries -@router.post("/", status_code=201) +@router.post("", status_code=201) async def create_show(show: ShowModel) -> ShowModel: await state.show_store.save(show) return show diff --git a/lightsync/frontend/app.js b/lightsync/frontend/app.js index f81ffc5..3e726ff 100644 --- a/lightsync/frontend/app.js +++ b/lightsync/frontend/app.js @@ -58,7 +58,7 @@ client.connect(); // Load device list on startup async function loadDevices() { try { - const res = await fetch("/api/devices/"); + const res = await fetch("/api/devices"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const devices = await res.json(); const list = document.getElementById("device-list"); diff --git a/lightsync/main.py b/lightsync/main.py index 50f2a78..6bf8f88 100644 --- a/lightsync/main.py +++ b/lightsync/main.py @@ -25,7 +25,7 @@ async def lifespan(app: FastAPI): def create_app() -> FastAPI: - app = FastAPI(title="LightSync", lifespan=lifespan) + app = FastAPI(title="LightSync", lifespan=lifespan, redirect_slashes=False) # API routes (registered before static mount — see Pattern 7) from lightsync.api import shows, devices, ws diff --git a/pyproject.toml b/pyproject.toml index 9b9584c..9dea7fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,6 @@ dependencies = [ dev = [ "httpx>=0.27.0", ] + +[tool.hatch.build.targets.wheel] +packages = ["lightsync"]