Includes all work to date: - local_backend: FastAPI backend with products, orders, tables, shifts, cloud sync - manager_dashboard: React manager UI with product/category management, reports, settings - waiter_pwa: React PWA for waiter devices - Category reparent endpoint and UI - Waiter domain: local_ip sent on heartbeat, waiter_domain persisted from cloud response - QR code modal in AppInfoTab for waiter domain - Product form: number input spinners removed, category pre-selected on new product - Category row: count badge moved to far right Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
61 lines
1.8 KiB
Python
61 lines
1.8 KiB
Python
"""
|
|
SSE stream endpoint — one long-lived GET per connected phone.
|
|
|
|
Authentication: token passed as query param ?token=<jwt>
|
|
(EventSource API in browsers cannot set custom headers, so query param is the standard pattern.)
|
|
|
|
The client receives a stream of JSON lines:
|
|
data: {"type": "...", "data": {...}}\n\n
|
|
|
|
A keepalive comment (": ping") is sent every 25 seconds to prevent proxy timeouts.
|
|
"""
|
|
|
|
import asyncio
|
|
from fastapi import APIRouter, Query
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
from routers.deps import decode_token
|
|
from services.sse_bus import subscribe, unsubscribe
|
|
|
|
router = APIRouter()
|
|
|
|
KEEPALIVE_INTERVAL = 25 # seconds
|
|
|
|
|
|
async def _event_stream(user_id: int):
|
|
q = await subscribe(user_id)
|
|
try:
|
|
while True:
|
|
try:
|
|
payload = await asyncio.wait_for(q.get(), timeout=KEEPALIVE_INTERVAL)
|
|
yield f"data: {payload}\n\n"
|
|
except asyncio.TimeoutError:
|
|
# keepalive — prevents nginx/proxies from closing idle connections
|
|
yield ": ping\n\n"
|
|
except asyncio.CancelledError:
|
|
pass
|
|
finally:
|
|
await unsubscribe(user_id, q)
|
|
|
|
|
|
@router.get("/stream")
|
|
async def sse_stream(token: str = Query(...)):
|
|
"""
|
|
Open an SSE stream for the authenticated user.
|
|
The phone connects once on login and stays connected.
|
|
On reconnect (after network drop) it does a full GET first, then reconnects here.
|
|
"""
|
|
# decode_token raises HTTPException on invalid/expired — no manual check needed
|
|
payload = decode_token(token)
|
|
user_id: int = int(payload["sub"])
|
|
|
|
return StreamingResponse(
|
|
_event_stream(user_id),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"X-Accel-Buffering": "no", # disable nginx buffering
|
|
"Connection": "keep-alive",
|
|
},
|
|
)
|