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)
This commit is contained in:
Claude
2026-04-05 18:59:34 +00:00
parent 9f3a2fc7a1
commit a21962cbcf
7 changed files with 45 additions and 6 deletions

12
Dockerfile Normal file
View File

@@ -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"]

24
docker-compose.prod.yml Normal file
View File

@@ -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

View File

@@ -5,12 +5,12 @@ import lightsync.main as state # access registry via module-level ref
router = APIRouter(tags=["devices"]) router = APIRouter(tags=["devices"])
@router.get("/") @router.get("")
async def list_devices() -> list[DeviceConfig]: async def list_devices() -> list[DeviceConfig]:
return state.registry.list_all() return state.registry.list_all()
@router.post("/", status_code=201) @router.post("", status_code=201)
async def add_device(config: DeviceConfig) -> DeviceConfig: async def add_device(config: DeviceConfig) -> DeviceConfig:
state.registry.add(config) state.registry.add(config)
await state.registry.save() await state.registry.save()

View File

@@ -5,7 +5,7 @@ import lightsync.main as state # access show_store via module-level ref
router = APIRouter(tags=["shows"]) router = APIRouter(tags=["shows"])
@router.get("/") @router.get("")
async def list_shows(): async def list_shows():
"""Return summary list of all shows.""" """Return summary list of all shows."""
summaries = [] summaries = []
@@ -20,7 +20,7 @@ async def list_shows():
return summaries return summaries
@router.post("/", status_code=201) @router.post("", status_code=201)
async def create_show(show: ShowModel) -> ShowModel: async def create_show(show: ShowModel) -> ShowModel:
await state.show_store.save(show) await state.show_store.save(show)
return show return show

View File

@@ -58,7 +58,7 @@ client.connect();
// Load device list on startup // Load device list on startup
async function loadDevices() { async function loadDevices() {
try { try {
const res = await fetch("/api/devices/"); const res = await fetch("/api/devices");
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const devices = await res.json(); const devices = await res.json();
const list = document.getElementById("device-list"); const list = document.getElementById("device-list");

View File

@@ -25,7 +25,7 @@ async def lifespan(app: FastAPI):
def create_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) # API routes (registered before static mount — see Pattern 7)
from lightsync.api import shows, devices, ws from lightsync.api import shows, devices, ws

View File

@@ -19,3 +19,6 @@ dependencies = [
dev = [ dev = [
"httpx>=0.27.0", "httpx>=0.27.0",
] ]
[tool.hatch.build.targets.wheel]
packages = ["lightsync"]