Snapshot of in-progress work across local_backend, manager_dashboard, and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM, inventory, permissions), plus the nginx/docker-compose deploy fixes for the Unraid + NPM reverse-proxy setup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
65 lines
2.1 KiB
Python
65 lines
2.1 KiB
Python
"""
|
|
Phone call event receiver + WebSocket broadcaster.
|
|
|
|
The Grandstream UCM sends a simple HTTP GET to /api/phone/call-event
|
|
with caller ID and extension details. We broadcast that to all connected
|
|
WebSocket clients (manager dashboard Phone page).
|
|
|
|
The call-event endpoint is intentionally unauthenticated so the UCM device
|
|
(which has no JWT) can POST to it directly on the local LAN.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
|
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# In-memory list of active WebSocket connections to the Phone page.
|
|
_connections: list[WebSocket] = []
|
|
|
|
|
|
@router.websocket("/ws")
|
|
async def phone_ws(websocket: WebSocket):
|
|
await websocket.accept()
|
|
_connections.append(websocket)
|
|
logger.debug("phone_ws: client connected (%d total)", len(_connections))
|
|
try:
|
|
# Keep the connection alive; client drives nothing, server pushes events.
|
|
while True:
|
|
await asyncio.sleep(25)
|
|
await websocket.send_text(json.dumps({"type": "ping"}))
|
|
except (WebSocketDisconnect, Exception):
|
|
pass
|
|
finally:
|
|
_connections.remove(websocket)
|
|
logger.debug("phone_ws: client disconnected (%d total)", len(_connections))
|
|
|
|
|
|
@router.get("/call-event")
|
|
async def call_event(
|
|
caller: str = Query(..., description="Caller ID number from UCM"),
|
|
ext: str = Query("", description="Dialled extension"),
|
|
):
|
|
"""
|
|
Called by the Grandstream UCM Action URL on every incoming call.
|
|
Example: GET /api/phone/call-event?caller=2106001234&ext=100
|
|
"""
|
|
payload = json.dumps({"type": "incoming_call", "caller": caller, "ext": ext})
|
|
dead = []
|
|
for ws in list(_connections):
|
|
try:
|
|
await ws.send_text(payload)
|
|
except Exception:
|
|
dead.append(ws)
|
|
for ws in dead:
|
|
try:
|
|
_connections.remove(ws)
|
|
except ValueError:
|
|
pass
|
|
logger.info("phone call-event: caller=%s ext=%s (notified %d clients)", caller, ext, len(_connections) - len(dead))
|
|
return {"status": "ok", "notified": len(_connections) - len(dead)}
|