Fix 413 upload limit + add /api/kds/settings to kitchen backend

- nginx client_max_body_size raised to 800m (was 20m) to allow 577MB backup
  upload; proxy_read_timeout raised to 600s for long restore operations
- Add api/kds_settings.py: GET+PATCH /api/kds/settings served by kitchen
  backend (kitchen Settings page configures KDS timers/GraphQL/course order;
  all config lives in kitchen_settings so kitchen owns these endpoints)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-12 17:47:28 +00:00
parent 14903e80a6
commit e7e1fda9f6
3 changed files with 141 additions and 3 deletions

137
backend/api/kds_settings.py Normal file
View file

@ -0,0 +1,137 @@
"""
KDS settings endpoints served by the kitchen backend.
The kitchen Settings page configures KDS (timers, GraphQL URL, course order, etc.)
via /kitchen/api/kds/settings. All KDS config lives in kitchen_settings so these
endpoints belong here, not in the KDS backend.
"""
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from auth import get_current_user, require_cap
from database import get_db
from models.settings import KitchenSettings
from models.user import User
router = APIRouter()
class KDSSettingsResponse(BaseModel):
kds_enabled: bool = False
kds_graphql_url: Optional[str] = None
kds_graphql_username: Optional[str] = None
kds_graphql_password_set: bool = False
kds_graphql_client_id: Optional[str] = None
kds_poll_interval_seconds: int = 6000
kds_timer_green_seconds: int = 300
kds_timer_amber_seconds: int = 600
kds_timer_red_seconds: int = 900
kds_away_timer_green_seconds: int = 600
kds_away_timer_amber_seconds: int = 900
kds_away_timer_red_seconds: int = 1200
kds_course_order: list = ["Starters", "Mains", "Desserts"]
kds_show_completed_for_seconds: int = 30
kds_bookings_refresh_seconds: int = 60
class Config:
from_attributes = True
class KDSSettingsUpdate(BaseModel):
kds_enabled: Optional[bool] = None
kds_graphql_url: Optional[str] = None
kds_graphql_username: Optional[str] = None
kds_graphql_password: Optional[str] = None
kds_graphql_client_id: Optional[str] = None
kds_poll_interval_seconds: Optional[int] = None
kds_timer_green_seconds: Optional[int] = None
kds_timer_amber_seconds: Optional[int] = None
kds_timer_red_seconds: Optional[int] = None
kds_away_timer_green_seconds: Optional[int] = None
kds_away_timer_amber_seconds: Optional[int] = None
kds_away_timer_red_seconds: Optional[int] = None
kds_course_order: Optional[list] = None
kds_show_completed_for_seconds: Optional[int] = None
kds_bookings_refresh_seconds: Optional[int] = None
def _normalize_course_config(course_order: list, s: KitchenSettings) -> list:
result = []
for entry in course_order:
if isinstance(entry, str):
result.append({
"name": entry,
"prep_green": s.kds_timer_green_seconds or 300,
"prep_amber": s.kds_timer_amber_seconds or 600,
"prep_red": s.kds_timer_red_seconds or 900,
"away_green": s.kds_away_timer_green_seconds or 600,
"away_amber": s.kds_away_timer_amber_seconds or 900,
"away_red": s.kds_away_timer_red_seconds or 1200,
})
elif isinstance(entry, dict) and "name" in entry:
result.append(entry)
return result
async def _get_settings(db: AsyncSession, kitchen_id: int):
result = await db.execute(
select(KitchenSettings).where(KitchenSettings.kitchen_id == kitchen_id)
)
return result.scalar_one_or_none()
def _to_response(s: KitchenSettings) -> KDSSettingsResponse:
raw = s.kds_course_order or ["Starters", "Mains", "Desserts"]
return KDSSettingsResponse(
kds_enabled=s.kds_enabled or False,
kds_graphql_url=s.kds_graphql_url,
kds_graphql_username=s.kds_graphql_username,
kds_graphql_password_set=bool(s.kds_graphql_password),
kds_graphql_client_id=s.kds_graphql_client_id,
kds_poll_interval_seconds=s.kds_poll_interval_seconds or 6000,
kds_timer_green_seconds=s.kds_timer_green_seconds or 300,
kds_timer_amber_seconds=s.kds_timer_amber_seconds or 600,
kds_timer_red_seconds=s.kds_timer_red_seconds or 900,
kds_away_timer_green_seconds=s.kds_away_timer_green_seconds or 600,
kds_away_timer_amber_seconds=s.kds_away_timer_amber_seconds or 900,
kds_away_timer_red_seconds=s.kds_away_timer_red_seconds or 1200,
kds_course_order=_normalize_course_config(raw, s),
kds_show_completed_for_seconds=s.kds_show_completed_for_seconds or 30,
kds_bookings_refresh_seconds=s.kds_bookings_refresh_seconds or 60,
)
@router.get("/settings", response_model=KDSSettingsResponse)
async def get_kds_settings(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
s = await _get_settings(db, current_user.kitchen_id)
if not s:
return KDSSettingsResponse()
return _to_response(s)
@router.patch("/settings", response_model=KDSSettingsResponse)
async def update_kds_settings(
update: KDSSettingsUpdate,
current_user: User = Depends(require_cap("settings")),
db: AsyncSession = Depends(get_db),
):
s = await _get_settings(db, current_user.kitchen_id)
if not s:
raise HTTPException(status_code=404, detail="Kitchen settings not found")
for field, value in update.model_dump(exclude_unset=True).items():
if hasattr(s, field):
setattr(s, field, value)
s.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(s)
return _to_response(s)