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:
12
Dockerfile
Normal file
12
Dockerfile
Normal 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
24
docker-compose.prod.yml
Normal 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
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -19,3 +19,6 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["lightsync"]
|
||||||
|
|||||||
Reference in New Issue
Block a user